UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

737 lines 24.8 kB
/** * Enterprise Caching System * Implements multi-layer caching with Redis, memory, and intelligent invalidation */ import crypto from 'crypto'; import { EventEmitter } from 'events'; import { logger } from '../logger.js'; /** * LRU Cache implementation */ class LRUCache { maxSize; cache = new Map(); accessOrder = new Map(); currentTime = 0; constructor(maxSize) { this.maxSize = maxSize; } get(key) { const entry = this.cache.get(key); if (!entry) return null; // Check expiration if (entry.expiresAt > 0 && Date.now() > entry.expiresAt) { this.delete(key); return null; } // Update access tracking entry.lastAccessed = Date.now(); entry.accessCount++; this.accessOrder.set(key, ++this.currentTime); return entry; } set(key, entry) { // Remove existing entry if (this.cache.has(key)) { this.cache.delete(key); this.accessOrder.delete(key); } // Evict if necessary while (this.cache.size >= this.maxSize) { this.evictLRU(); } // Add new entry this.cache.set(key, entry); this.accessOrder.set(key, ++this.currentTime); } delete(key) { this.accessOrder.delete(key); return this.cache.delete(key); } clear() { this.cache.clear(); this.accessOrder.clear(); } size() { return this.cache.size; } keys() { return Array.from(this.cache.keys()); } entries() { return Array.from(this.cache.entries()); } evictLRU() { let oldestKey = null; let oldestTime = Infinity; for (const [key, time] of this.accessOrder.entries()) { if (time < oldestTime) { oldestTime = time; oldestKey = key; } } if (oldestKey) { this.delete(oldestKey); } } } /** * Redis Cache Layer - Real Redis implementation with fallback */ class RedisCache { client = null; mockStorage = new Map(); config; isConnected = false; useRealRedis = false; constructor(config) { this.config = config; this.initializeRedis().catch(() => { console.warn('Redis connection failed, using in-memory fallback'); }); } async initializeRedis() { try { const { createClient } = await import('redis'); const redisUrl = `redis://${this.config.host || 'localhost'}:${this.config.port || 6379}`; this.client = createClient({ url: redisUrl, socket: { connectTimeout: 5000, }, }); this.client.on('error', (err) => { console.warn('Redis connection error, falling back to in-memory cache:', err.message); this.isConnected = false; this.useRealRedis = false; }); this.client.on('connect', () => { this.isConnected = true; this.useRealRedis = true; }); await this.client.connect(); } catch (error) { console.warn('Redis initialization failed, using in-memory fallback:', error); this.useRealRedis = false; } } async get(key) { const fullKey = `${this.config.keyPrefix}${key}`; if (this.useRealRedis && this.isConnected && this.client) { try { return await this.client.get(fullKey); } catch (error) { console.warn('Redis get failed, using fallback:', error); this.useRealRedis = false; } } // Fallback to in-memory storage return this.mockStorage.get(fullKey) || null; } async set(key, value, ttl) { const fullKey = `${this.config.keyPrefix}${key}`; if (this.useRealRedis && this.isConnected && this.client) { try { if (ttl) { await this.client.setEx(fullKey, ttl, value); } else { await this.client.set(fullKey, value); } return; } catch (error) { console.warn('Redis set failed, using fallback:', error); this.useRealRedis = false; } } // Fallback to in-memory storage this.mockStorage.set(fullKey, value); if (ttl) { setTimeout(() => { this.mockStorage.delete(fullKey); }, ttl * 1000); } } async delete(key) { const fullKey = `${this.config.keyPrefix}${key}`; if (this.useRealRedis && this.isConnected && this.client) { try { const result = await this.client.del(fullKey); return result > 0; } catch (error) { console.warn('Redis delete failed, using fallback:', error); this.useRealRedis = false; } } // Fallback to in-memory storage return this.mockStorage.delete(fullKey); } async clear() { if (this.useRealRedis && this.isConnected && this.client) { try { const keys = await this.client.keys(`${this.config.keyPrefix}*`); if (keys.length > 0) { await this.client.del(keys); } return; } catch (error) { console.warn('Redis clear failed, using fallback:', error); this.useRealRedis = false; } } // Fallback to in-memory storage this.mockStorage.clear(); } async keys(pattern) { if (this.useRealRedis && this.isConnected && this.client) { try { const fullPattern = `${this.config.keyPrefix}${pattern}`; return await this.client.keys(fullPattern); } catch (error) { console.warn('Redis keys failed, using fallback:', error); this.useRealRedis = false; } } // Fallback to in-memory storage const regex = new RegExp(pattern.replace('*', '.*')); return Array.from(this.mockStorage.keys()).filter(key => regex.test(key)); } async disconnect() { if (this.client && this.isConnected) { try { await this.client.disconnect(); } catch (error) { console.warn('Error disconnecting from Redis:', error); } } this.isConnected = false; this.useRealRedis = false; } } /** * Main Cache Manager */ export class CacheManager extends EventEmitter { config; memoryCache; redisCache; stats; cleanupInterval; encryptionKey; constructor(config = {}) { super(); this.config = { maxSize: 1000, defaultTTL: 3600, // 1 hour checkInterval: 60000, // 1 minute enableCompression: true, enableEncryption: false, layers: { memory: { enabled: true, maxSize: 1000, algorithm: 'lru', }, redis: { enabled: false, host: 'localhost', port: 6379, db: 0, keyPrefix: 'codecrucible:', }, disk: { enabled: false, path: './cache', maxSize: '1GB', }, }, ...config, }; this.memoryCache = new LRUCache(this.config.layers.memory.maxSize); if (this.config.layers.redis.enabled) { this.redisCache = new RedisCache(this.config.layers.redis); } this.stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0, hitRate: 0, memoryUsage: 0, keyCount: 0, lastCleanup: new Date(), }; if (this.config.enableEncryption && this.config.encryptionKey) { this.encryptionKey = Buffer.from(this.config.encryptionKey, 'hex'); } this.startCleanupTimer(); } /** * Get value from cache */ async get(key) { try { const cacheKey = this.generateCacheKey(key); // Try memory cache first if (this.config.layers.memory.enabled) { const memoryEntry = this.memoryCache.get(cacheKey); if (memoryEntry) { this.stats.hits++; this.updateHitRate(); logger.debug('Cache hit (memory)', { key: cacheKey }); this.emit('cache-hit', { key: cacheKey, layer: 'memory' }); return this.deserializeValue(memoryEntry.value); } } // Try Redis cache if (this.config.layers.redis.enabled && this.redisCache) { const redisValue = await this.redisCache.get(cacheKey); if (redisValue) { const entry = JSON.parse(redisValue); // Check expiration if (entry.expiresAt === 0 || Date.now() < entry.expiresAt) { this.stats.hits++; this.updateHitRate(); // Promote to memory cache if (this.config.layers.memory.enabled) { this.memoryCache.set(cacheKey, entry); } logger.debug('Cache hit (redis)', { key: cacheKey }); this.emit('cache-hit', { key: cacheKey, layer: 'redis' }); return this.deserializeValue(entry.value); } else { // Expired, remove from Redis await this.redisCache.delete(cacheKey); } } } // Cache miss this.stats.misses++; this.updateHitRate(); logger.debug('Cache miss', { key: cacheKey }); this.emit('cache-miss', { key: cacheKey }); return null; } catch (error) { logger.error('Cache get error', error, { key }); this.emit('cache-error', { operation: 'get', key, error }); return null; } } /** * Set value in cache */ async set(key, value, options = {}) { try { const cacheKey = this.generateCacheKey(key); const now = Date.now(); const ttl = options.ttl || this.config.defaultTTL; const expiresAt = ttl > 0 ? now + ttl * 1000 : 0; const entry = { key: cacheKey, value: await this.serializeValue(value, options), expiresAt, createdAt: now, lastAccessed: now, accessCount: 0, tags: options.tags || [], metadata: options.metadata, }; // Store in memory cache if (this.config.layers.memory.enabled) { this.memoryCache.set(cacheKey, entry); } // Store in Redis cache if (this.config.layers.redis.enabled && this.redisCache) { const serialized = JSON.stringify(entry); await this.redisCache.set(cacheKey, serialized, ttl); } this.stats.sets++; this.updateStats(); logger.debug('Cache set', { key: cacheKey, ttl, tags: options.tags, compressed: options.compress, encrypted: options.encrypt, }); this.emit('cache-set', { key: cacheKey, ttl, options }); } catch (error) { logger.error('Cache set error', error, { key }); this.emit('cache-error', { operation: 'set', key, error }); throw error; } } /** * Delete value from cache */ async delete(key) { try { const cacheKey = this.generateCacheKey(key); let deleted = false; // Delete from memory cache if (this.config.layers.memory.enabled) { deleted = this.memoryCache.delete(cacheKey) || deleted; } // Delete from Redis cache if (this.config.layers.redis.enabled && this.redisCache) { deleted = (await this.redisCache.delete(cacheKey)) || deleted; } if (deleted) { this.stats.deletes++; this.updateStats(); logger.debug('Cache delete', { key: cacheKey }); this.emit('cache-delete', { key: cacheKey }); } return deleted; } catch (error) { logger.error('Cache delete error', error, { key }); this.emit('cache-error', { operation: 'delete', key, error }); return false; } } /** * Clear cache by tags */ async deleteByTags(tags) { try { let deletedCount = 0; // Clear from memory cache if (this.config.layers.memory.enabled) { const entries = this.memoryCache.entries(); for (const [key, entry] of entries) { if (entry.tags.some(tag => tags.includes(tag))) { this.memoryCache.delete(key); deletedCount++; } } } // Clear from Redis cache if (this.config.layers.redis.enabled && this.redisCache) { const keys = await this.redisCache.keys('*'); for (const key of keys) { const value = await this.redisCache.get(key); if (value) { try { const entry = JSON.parse(value); if (entry.tags && entry.tags.some((tag) => tags.includes(tag))) { await this.redisCache.delete(key); deletedCount++; } } catch { // Invalid JSON, skip } } } } logger.info('Cache cleared by tags', { tags, deletedCount }); this.emit('cache-clear-tags', { tags, deletedCount }); return deletedCount; } catch (error) { logger.error('Cache clear by tags error', error, { tags }); this.emit('cache-error', { operation: 'clear-tags', tags, error }); return 0; } } /** * Clear all cache */ async clear() { try { // Clear memory cache if (this.config.layers.memory.enabled) { this.memoryCache.clear(); } // Clear Redis cache if (this.config.layers.redis.enabled && this.redisCache) { await this.redisCache.clear(); } // Reset stats this.stats = { ...this.stats, hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0, hitRate: 0, keyCount: 0, }; logger.info('Cache cleared'); this.emit('cache-clear'); } catch (error) { logger.error('Cache clear error', error); this.emit('cache-error', { operation: 'clear', error }); throw error; } } /** * Get cache statistics */ getStats() { this.updateStats(); return { ...this.stats }; } /** * Get or set value with function */ async getOrSet(key, factory, options = {}) { try { // Try to get from cache first const cached = await this.get(key); if (cached !== null) { return cached; } // Generate value const value = await factory(); // Store in cache await this.set(key, value, options); return value; } catch (error) { logger.error('Cache getOrSet error', error, { key }); throw error; } } /** * Warm up cache with data */ async warmUp(data) { try { const promises = data.map(({ key, value, options }) => this.set(key, value, options)); await Promise.allSettled(promises); logger.info('Cache warmed up', { entries: data.length }); this.emit('cache-warmup', { entries: data.length }); } catch (error) { logger.error('Cache warmup error', error); throw error; } } /** * Generate cache key with namespace */ generateCacheKey(key) { // Create hash for very long keys if (key.length > 250) { return crypto.createHash('sha256').update(key).digest('hex'); } return key.replace(/[^a-zA-Z0-9_:-]/g, '_'); } /** * Serialize value for storage */ async serializeValue(value, options) { let serialized = value; // JSON serialize non-primitives if (typeof value === 'object' && value !== null) { serialized = JSON.stringify(value); } // Compress if enabled if (this.config.enableCompression && options.compress !== false) { // In production, use gzip compression serialized = `compressed:${serialized}`; } // Encrypt if enabled if (this.config.enableEncryption && options.encrypt !== false && this.encryptionKey) { serialized = this.encrypt(serialized); } return serialized; } /** * Deserialize value from storage */ async deserializeValue(value) { let deserialized = value; // Decrypt if encrypted if (typeof value === 'string' && value.startsWith('encrypted:')) { deserialized = this.decrypt(value); } // Decompress if compressed if (typeof deserialized === 'string' && deserialized.startsWith('compressed:')) { deserialized = deserialized.substring(11); // Remove 'compressed:' prefix // In production, use gzip decompression } // Parse JSON if it looks like JSON if (typeof deserialized === 'string') { try { if (deserialized.startsWith('{') || deserialized.startsWith('[')) { deserialized = JSON.parse(deserialized); } } catch { // Not JSON, return as string } } return deserialized; } /** * Encrypt value */ encrypt(value) { if (!this.encryptionKey) return value; const iv = crypto.randomBytes(16); // Use secure AES-256-CBC with proper IV instead of deprecated createCipher const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(this.encryptionKey).subarray(0, 32), iv); let encrypted = cipher.update(value, 'utf8', 'hex'); encrypted += cipher.final('hex'); return `encrypted:${iv.toString('hex')}:${encrypted}`; } /** * Decrypt value */ decrypt(encryptedValue) { if (!this.encryptionKey) return encryptedValue; const parts = encryptedValue.split(':'); if (parts.length !== 3) return encryptedValue; const iv = Buffer.from(parts[1], 'hex'); const encrypted = parts[2]; // Use secure AES-256-CBC with proper IV instead of deprecated createDecipher const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this.encryptionKey).subarray(0, 32), iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } /** * Update cache statistics */ updateStats() { this.stats.keyCount = this.memoryCache.size(); this.stats.memoryUsage = this.estimateMemoryUsage(); this.updateHitRate(); } /** * Update hit rate */ updateHitRate() { const total = this.stats.hits + this.stats.misses; this.stats.hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0; } /** * Estimate memory usage */ estimateMemoryUsage() { let total = 0; for (const [key, entry] of this.memoryCache.entries()) { total += key.length * 2; // Approximate string size total += JSON.stringify(entry).length * 2; // Approximate entry size } return total; } /** * Start cleanup timer */ startCleanupTimer() { this.cleanupInterval = setInterval(() => { // TODO: Store interval ID and call clearInterval in cleanup this.cleanup(); }, this.config.checkInterval); } /** * Clean up expired entries */ cleanup() { try { let cleaned = 0; const now = Date.now(); // Clean memory cache if (this.config.layers.memory.enabled) { const entries = this.memoryCache.entries(); for (const [key, entry] of entries) { if (entry.expiresAt > 0 && now > entry.expiresAt) { this.memoryCache.delete(key); cleaned++; } } } this.stats.evictions += cleaned; this.stats.lastCleanup = new Date(); this.updateStats(); if (cleaned > 0) { logger.debug('Cache cleanup completed', { cleanedEntries: cleaned }); this.emit('cache-cleanup', { cleanedEntries: cleaned }); } } catch (error) { logger.error('Cache cleanup error', error); } } /** * Create Express caching middleware */ middleware(options = {}) { return async (req, res, next) => { try { // Skip caching if specified if (options.skipIf && options.skipIf(req)) { return next(); } // Generate cache key const keyGenerator = options.keyGenerator || this.defaultKeyGenerator; const cacheKey = keyGenerator(req); // Try to get from cache const cached = await this.get(cacheKey); if (cached) { logger.debug('Serving from cache', { key: cacheKey }); return res.json(cached); } // Intercept response const originalJson = res.json; res.json = async (data) => { // Cache the response try { await this.set(cacheKey, data, { ttl: options.ttl, tags: options.tags, }); } catch (error) { logger.error('Failed to cache response', error, { key: cacheKey }); } return originalJson.call(res, data); }; next(); } catch (error) { logger.error('Cache middleware error', error); next(); // Continue without caching } }; } /** * Default key generator for middleware */ defaultKeyGenerator(req) { return `${req.method}:${req.originalUrl}:${JSON.stringify(req.query)}`; } /** * Stop cache manager and cleanup */ async stop() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } await this.clear(); logger.info('Cache manager stopped'); this.emit('cache-stop'); } } //# sourceMappingURL=cache-manager.js.map