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
JavaScript
/**
* 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