UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

301 lines 8.96 kB
/** * In-Memory Cache Implementation * * L1 cache layer with LRU eviction policy and size limits. * Provides ultra-fast access for frequently used queries. */ import { getLogger } from '../../../logging/Logger.js'; export class InMemoryCache { logger = getLogger(); cache = new Map(); accessOrder = []; // For LRU tracking // Configuration maxSizeBytes; maxEntries; defaultTTL; checkInterval; // Statistics stats = { hits: 0, misses: 0, evictions: 0, expirations: 0, }; currentSizeBytes = 0; cleanupTimer; constructor(options = {}) { this.maxSizeBytes = (options.maxSizeMB || 100) * 1024 * 1024; // Default 100MB this.maxEntries = options.maxEntries || 10000; this.defaultTTL = options.defaultTTL || 300000; // Default 5 minutes this.checkInterval = options.checkInterval || 60000; // Default 1 minute // Start cleanup timer this.startCleanupTimer(); this.logger.info({ maxSizeMB: this.maxSizeBytes / 1024 / 1024, maxEntries: this.maxEntries, defaultTTL: this.defaultTTL, }, 'InMemoryCache initialized'); } /** * Get value from cache */ async get(key) { const entry = this.cache.get(key); if (!entry) { this.stats.misses++; this.logger.debug({ key }, 'Cache miss'); return null; } // Check expiration if (Date.now() > entry.expiresAt) { this.stats.expirations++; this.logger.debug({ key }, 'Cache entry expired'); this.delete(key); return null; } // Update access tracking this.updateAccessOrder(key); entry.lastAccessed = Date.now(); entry.hits++; this.stats.hits++; this.logger.debug({ key, hits: entry.hits, ttlRemaining: entry.expiresAt - Date.now() }, 'Cache hit'); // Return cached result with updated metadata return { data: entry.value, metadata: { ...entry.metadata, hits: entry.hits, } }; } /** * Set value in cache */ async set(key, value, ttl, metadata) { const effectiveTTL = ttl || this.defaultTTL; const size = this.estimateSize(value); const now = Date.now(); // Check if we need to evict entries if (this.needsEviction(size)) { await this.evictEntries(size); } // Create cache entry const entry = { key, value, metadata: { cached: true, cachedAt: now, ttl: effectiveTTL, cacheKey: key, source: 'memory', hits: 0, ...metadata }, expiresAt: now + effectiveTTL, size, lastAccessed: now, hits: 0, }; // Remove old entry if exists if (this.cache.has(key)) { this.delete(key); } // Add new entry this.cache.set(key, entry); this.accessOrder.push(key); this.currentSizeBytes += size; this.logger.debug({ key, size, ttl: effectiveTTL, expiresAt: new Date(entry.expiresAt), currentSizeMB: this.currentSizeBytes / 1024 / 1024, }, 'Cache entry added'); } /** * Delete entry from cache */ delete(key) { const entry = this.cache.get(key); if (!entry) return false; this.cache.delete(key); this.currentSizeBytes -= entry.size; // Remove from access order const index = this.accessOrder.indexOf(key); if (index > -1) { this.accessOrder.splice(index, 1); } this.logger.debug({ key, size: entry.size }, 'Cache entry deleted'); return true; } /** * Clear all entries */ clear() { const entriesCleared = this.cache.size; this.cache.clear(); this.accessOrder.length = 0; this.currentSizeBytes = 0; this.logger.info({ entriesCleared }, 'Cache cleared'); } /** * Invalidate entries matching pattern */ async invalidate(patterns) { let invalidated = 0; for (const pattern of patterns) { const regex = this.patternToRegex(pattern); for (const key of this.cache.keys()) { if (regex.test(key)) { this.delete(key); invalidated++; } } } this.logger.info({ patterns, invalidated }, 'Cache entries invalidated'); return invalidated; } /** * Get cache statistics */ getStats() { const entries = this.cache.size; const sizeBytes = this.currentSizeBytes; const sizeMB = sizeBytes / 1024 / 1024; const total = this.stats.hits + this.stats.misses; const hitRate = total > 0 ? this.stats.hits / total : 0; return { entries, sizeBytes, sizeMB, hits: this.stats.hits, misses: this.stats.misses, evictions: this.stats.evictions, expirations: this.stats.expirations, hitRate, }; } /** * Get all keys (for debugging) */ keys() { return Array.from(this.cache.keys()); } /** * Check if needs eviction */ needsEviction(additionalSize) { return (this.currentSizeBytes + additionalSize > this.maxSizeBytes || this.cache.size >= this.maxEntries); } /** * Evict entries using LRU policy */ async evictEntries(requiredSize) { const targetSize = this.maxSizeBytes * 0.9; // Free up to 90% capacity let evicted = 0; while ((this.currentSizeBytes + requiredSize > targetSize || this.cache.size >= this.maxEntries) && this.accessOrder.length > 0) { // Get least recently used key const lruKey = this.accessOrder[0]; if (this.delete(lruKey)) { evicted++; this.stats.evictions++; } } if (evicted > 0) { this.logger.info({ evicted, currentSizeMB: this.currentSizeBytes / 1024 / 1024 }, 'Evicted LRU entries'); } } /** * Update access order for LRU tracking */ updateAccessOrder(key) { const index = this.accessOrder.indexOf(key); if (index > -1) { // Move to end (most recently used) this.accessOrder.splice(index, 1); this.accessOrder.push(key); } } /** * Estimate size of value in bytes */ estimateSize(value) { if (value === null || value === undefined) return 8; switch (typeof value) { case 'boolean': return 4; case 'number': return 8; case 'string': return value.length * 2; // UTF-16 case 'object': if (Buffer.isBuffer(value)) { return value.length; } // Rough estimate for objects const json = JSON.stringify(value); return json.length * 2; default: return 64; // Default estimate } } /** * Convert wildcard pattern to regex */ patternToRegex(pattern) { // Escape special regex characters except * const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); // Replace * with .* const regexPattern = escaped.replace(/\*/g, '.*'); return new RegExp(`^${regexPattern}$`); } /** * Start cleanup timer for expired entries */ startCleanupTimer() { this.cleanupTimer = setInterval(() => { this.cleanupExpired(); }, this.checkInterval); } /** * Clean up expired entries */ cleanupExpired() { const now = Date.now(); let cleaned = 0; for (const [key, entry] of this.cache.entries()) { if (now > entry.expiresAt) { this.delete(key); cleaned++; this.stats.expirations++; } } if (cleaned > 0) { this.logger.debug({ cleaned }, 'Cleaned expired entries'); } } /** * Shutdown cache and stop timers */ shutdown() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } this.clear(); this.logger.info('InMemoryCache shutdown'); } } //# sourceMappingURL=InMemoryCache.js.map