Skip to main content
Glama
by Coder-RL
cache-manager.ts17.5 kB
import { EventEmitter } from 'events'; import * as crypto from 'crypto'; export interface CacheEntry<T = any> { key: string; value: T; ttl: number; createdAt: number; lastAccessed: number; accessCount: number; size: number; tags: string[]; metadata?: Record<string, any>; } export interface CacheConfig { maxSize: number; // bytes maxEntries: number; defaultTTL: number; // seconds cleanupInterval: number; // seconds strategy: 'lru' | 'lfu' | 'fifo' | 'random'; compression?: { enabled: boolean; algorithm: 'gzip' | 'brotli'; threshold: number; // bytes }; persistence?: { enabled: boolean; path: string; interval: number; // seconds }; distribution?: { enabled: boolean; nodes: string[]; replicationFactor: number; consistentHashing: boolean; }; } export interface CacheStats { hits: number; misses: number; evictions: number; size: number; entries: number; hitRatio: number; avgAccessTime: number; memoryUsage: number; } export interface CacheLayer { name: string; priority: number; config: CacheConfig; enabled: boolean; } export class CacheManager extends EventEmitter { private layers = new Map<string, Map<string, CacheEntry>>(); private layerConfigs = new Map<string, CacheConfig>(); private layerStats = new Map<string, CacheStats>(); private cleanupTimers = new Map<string, NodeJS.Timeout>(); private persistenceTimers = new Map<string, NodeJS.Timeout>(); private compressionCache = new Map<string, Buffer>(); private accessPatterns = new Map<string, number[]>(); private hotKeys = new Set<string>(); constructor() { super(); this.setupDefaultLayer(); this.startHotKeyDetection(); } private setupDefaultLayer(): void { const defaultConfig: CacheConfig = { maxSize: 100 * 1024 * 1024, // 100MB maxEntries: 10000, defaultTTL: 3600, // 1 hour cleanupInterval: 300, // 5 minutes strategy: 'lru' }; this.addLayer('default', defaultConfig); } addLayer(name: string, config: CacheConfig): void { this.layers.set(name, new Map()); this.layerConfigs.set(name, config); this.layerStats.set(name, { hits: 0, misses: 0, evictions: 0, size: 0, entries: 0, hitRatio: 0, avgAccessTime: 0, memoryUsage: 0 }); this.setupLayerCleanup(name); if (config.persistence?.enabled) { this.setupLayerPersistence(name); } this.emit('layer-added', { name, config }); } removeLayer(name: string): void { if (name === 'default') { throw new Error('Cannot remove default layer'); } const layer = this.layers.get(name); if (layer) { layer.clear(); this.layers.delete(name); this.layerConfigs.delete(name); this.layerStats.delete(name); const cleanupTimer = this.cleanupTimers.get(name); if (cleanupTimer) { clearInterval(cleanupTimer); this.cleanupTimers.delete(name); } const persistenceTimer = this.persistenceTimers.get(name); if (persistenceTimer) { clearInterval(persistenceTimer); this.persistenceTimers.delete(name); } this.emit('layer-removed', { name }); } } async set<T>( key: string, value: T, options: { ttl?: number; layer?: string; tags?: string[]; metadata?: Record<string, any>; } = {} ): Promise<void> { const layerName = options.layer || 'default'; const layer = this.layers.get(layerName); const config = this.layerConfigs.get(layerName); if (!layer || !config) { throw new Error(`Layer not found: ${layerName}`); } const now = Date.now(); const ttl = (options.ttl || config.defaultTTL) * 1000; // Convert to ms const serializedValue = JSON.stringify(value); let processedValue: any = serializedValue; let size = Buffer.byteLength(serializedValue, 'utf8'); // Apply compression if configured if (config.compression?.enabled && size > config.compression.threshold) { processedValue = await this.compress(serializedValue, config.compression.algorithm); size = processedValue.length; } const entry: CacheEntry<T> = { key, value: processedValue, ttl, createdAt: now, lastAccessed: now, accessCount: 1, size, tags: options.tags || [], metadata: options.metadata }; // Check if we need to evict entries await this.ensureCapacity(layerName, size); layer.set(key, entry as CacheEntry); const stats = this.layerStats.get(layerName)!; stats.entries = layer.size; stats.size += size; stats.memoryUsage = this.calculateMemoryUsage(layerName); this.emit('cache-set', { layer: layerName, key, size, ttl }); } async get<T>( key: string, options: { layer?: string; updateAccessTime?: boolean; } = {} ): Promise<T | null> { const layerName = options.layer || 'default'; const updateAccessTime = options.updateAccessTime !== false; // Try all layers if no specific layer requested const layersToCheck = layerName === 'default' ? Array.from(this.layers.keys()) : [layerName]; for (const currentLayer of layersToCheck) { const layer = this.layers.get(currentLayer); const config = this.layerConfigs.get(currentLayer); const stats = this.layerStats.get(currentLayer); if (!layer || !config || !stats) continue; const entry = layer.get(key); if (!entry) { stats.misses++; continue; } const now = Date.now(); // Check if entry has expired if (entry.createdAt + entry.ttl < now) { layer.delete(key); stats.size -= entry.size; stats.entries = layer.size; stats.evictions++; stats.misses++; this.emit('cache-expired', { layer: currentLayer, key }); continue; } // Update access information if (updateAccessTime) { entry.lastAccessed = now; entry.accessCount++; } stats.hits++; stats.hitRatio = stats.hits / (stats.hits + stats.misses); // Track access patterns this.trackAccess(key); let value: T; // Decompress if needed if (config.compression?.enabled && Buffer.isBuffer(entry.value)) { const decompressed = await this.decompress(entry.value, config.compression.algorithm); value = JSON.parse(decompressed); } else { value = typeof entry.value === 'string' ? JSON.parse(entry.value) : entry.value; } this.emit('cache-hit', { layer: currentLayer, key, accessCount: entry.accessCount }); return value; } // Cache miss const stats = this.layerStats.get(layerName)!; stats.misses++; stats.hitRatio = stats.hits / (stats.hits + stats.misses); this.emit('cache-miss', { layer: layerName, key }); return null; } async delete(key: string, layer?: string): Promise<boolean> { const layersToCheck = layer ? [layer] : Array.from(this.layers.keys()); let deleted = false; for (const layerName of layersToCheck) { const layerMap = this.layers.get(layerName); const stats = this.layerStats.get(layerName); if (!layerMap || !stats) continue; const entry = layerMap.get(key); if (entry) { layerMap.delete(key); stats.size -= entry.size; stats.entries = layerMap.size; deleted = true; this.emit('cache-delete', { layer: layerName, key, size: entry.size }); } } return deleted; } async invalidateByTag(tag: string, layer?: string): Promise<number> { const layersToCheck = layer ? [layer] : Array.from(this.layers.keys()); let invalidated = 0; for (const layerName of layersToCheck) { const layerMap = this.layers.get(layerName); const stats = this.layerStats.get(layerName); if (!layerMap || !stats) continue; const keysToDelete: string[] = []; for (const [key, entry] of layerMap) { if (entry.tags.includes(tag)) { keysToDelete.push(key); } } for (const key of keysToDelete) { const entry = layerMap.get(key)!; layerMap.delete(key); stats.size -= entry.size; stats.entries = layerMap.size; invalidated++; } } this.emit('cache-invalidated', { tag, count: invalidated }); return invalidated; } async clear(layer?: string): Promise<void> { const layersToClear = layer ? [layer] : Array.from(this.layers.keys()); for (const layerName of layersToClear) { const layerMap = this.layers.get(layerName); const stats = this.layerStats.get(layerName); if (!layerMap || !stats) continue; const entryCount = layerMap.size; layerMap.clear(); stats.size = 0; stats.entries = 0; stats.memoryUsage = 0; this.emit('cache-cleared', { layer: layerName, entriesRemoved: entryCount }); } } private async ensureCapacity(layerName: string, newEntrySize: number): Promise<void> { const layer = this.layers.get(layerName); const config = this.layerConfigs.get(layerName); const stats = this.layerStats.get(layerName); if (!layer || !config || !stats) return; // Check size limit if (stats.size + newEntrySize > config.maxSize) { await this.evictEntries(layerName, (stats.size + newEntrySize) - config.maxSize); } // Check entry count limit if (layer.size >= config.maxEntries) { await this.evictEntries(layerName, 0, layer.size - config.maxEntries + 1); } } private async evictEntries(layerName: string, sizeToFree: number, countToFree: number = 0): Promise<void> { const layer = this.layers.get(layerName); const config = this.layerConfigs.get(layerName); const stats = this.layerStats.get(layerName); if (!layer || !config || !stats) return; const entries = Array.from(layer.entries()).map(([key, entry]) => ({ key, entry })); let evictionCandidates: Array<{ key: string; entry: CacheEntry }> = []; switch (config.strategy) { case 'lru': evictionCandidates = entries.sort((a, b) => a.entry.lastAccessed - b.entry.lastAccessed); break; case 'lfu': evictionCandidates = entries.sort((a, b) => a.entry.accessCount - b.entry.accessCount); break; case 'fifo': evictionCandidates = entries.sort((a, b) => a.entry.createdAt - b.entry.createdAt); break; case 'random': evictionCandidates = entries.sort(() => Math.random() - 0.5); break; } let freedSize = 0; let freedCount = 0; for (const { key, entry } of evictionCandidates) { if ((sizeToFree > 0 && freedSize >= sizeToFree) && (countToFree === 0 || freedCount >= countToFree)) { break; } layer.delete(key); freedSize += entry.size; freedCount++; stats.evictions++; this.emit('cache-evicted', { layer: layerName, key, size: entry.size, strategy: config.strategy }); } stats.size -= freedSize; stats.entries = layer.size; } private setupLayerCleanup(layerName: string): void { const config = this.layerConfigs.get(layerName); if (!config) return; const timer = setInterval(() => { this.cleanupExpiredEntries(layerName); }, config.cleanupInterval * 1000); this.cleanupTimers.set(layerName, timer); } private setupLayerPersistence(layerName: string): void { const config = this.layerConfigs.get(layerName); if (!config?.persistence?.enabled) return; const timer = setInterval(() => { this.persistLayer(layerName); }, config.persistence.interval * 1000); this.persistenceTimers.set(layerName, timer); } private cleanupExpiredEntries(layerName: string): void { const layer = this.layers.get(layerName); const stats = this.layerStats.get(layerName); if (!layer || !stats) return; const now = Date.now(); const expiredKeys: string[] = []; for (const [key, entry] of layer) { if (entry.createdAt + entry.ttl < now) { expiredKeys.push(key); } } for (const key of expiredKeys) { const entry = layer.get(key)!; layer.delete(key); stats.size -= entry.size; stats.evictions++; } stats.entries = layer.size; if (expiredKeys.length > 0) { this.emit('cleanup-completed', { layer: layerName, expired: expiredKeys.length }); } } private async persistLayer(layerName: string): Promise<void> { const layer = this.layers.get(layerName); const config = this.layerConfigs.get(layerName); if (!layer || !config?.persistence?.enabled) return; try { const entries = Array.from(layer.entries()); const data = JSON.stringify(entries); // In production, write to actual file system this.emit('persistence-completed', { layer: layerName, entries: entries.length }); } catch (error) { this.emit('persistence-failed', { layer: layerName, error: (error as Error).message }); } } private async compress(data: string, algorithm: 'gzip' | 'brotli'): Promise<Buffer> { const key = crypto.createHash('md5').update(data).digest('hex'); if (this.compressionCache.has(key)) { return this.compressionCache.get(key)!; } let compressed: Buffer; if (algorithm === 'gzip') { const zlib = require('zlib'); compressed = zlib.gzipSync(data); } else { const zlib = require('zlib'); compressed = zlib.brotliCompressSync(data); } this.compressionCache.set(key, compressed); return compressed; } private async decompress(data: Buffer, algorithm: 'gzip' | 'brotli'): Promise<string> { if (algorithm === 'gzip') { const zlib = require('zlib'); return zlib.gunzipSync(data).toString(); } else { const zlib = require('zlib'); return zlib.brotliDecompressSync(data).toString(); } } private trackAccess(key: string): void { const now = Date.now(); const pattern = this.accessPatterns.get(key) || []; // Keep only recent accesses (last hour) const recentAccesses = pattern.filter(time => now - time < 3600000); recentAccesses.push(now); this.accessPatterns.set(key, recentAccesses); // Mark as hot key if accessed frequently if (recentAccesses.length > 10) { this.hotKeys.add(key); } } private startHotKeyDetection(): void { setInterval(() => { const now = Date.now(); // Clean up old access patterns for (const [key, pattern] of this.accessPatterns) { const recentAccesses = pattern.filter(time => now - time < 3600000); if (recentAccesses.length === 0) { this.accessPatterns.delete(key); this.hotKeys.delete(key); } else { this.accessPatterns.set(key, recentAccesses); if (recentAccesses.length < 5) { this.hotKeys.delete(key); } } } this.emit('hot-keys-updated', { hotKeys: Array.from(this.hotKeys) }); }, 300000); // Every 5 minutes } private calculateMemoryUsage(layerName: string): number { const layer = this.layers.get(layerName); if (!layer) return 0; let usage = 0; for (const entry of layer.values()) { usage += entry.size + 200; // Approximate overhead per entry } return usage; } getStats(layer?: string): CacheStats | Record<string, CacheStats> { if (layer) { const stats = this.layerStats.get(layer); if (!stats) { throw new Error(`Layer not found: ${layer}`); } return { ...stats }; } const allStats: Record<string, CacheStats> = {}; for (const [layerName, stats] of this.layerStats) { allStats[layerName] = { ...stats }; } return allStats; } getHotKeys(): string[] { return Array.from(this.hotKeys); } getLayers(): string[] { return Array.from(this.layers.keys()); } getLayerConfig(layer: string): CacheConfig | null { return this.layerConfigs.get(layer) || null; } updateLayerConfig(layer: string, config: Partial<CacheConfig>): void { const currentConfig = this.layerConfigs.get(layer); if (!currentConfig) { throw new Error(`Layer not found: ${layer}`); } const newConfig = { ...currentConfig, ...config }; this.layerConfigs.set(layer, newConfig); // Restart timers if intervals changed if (config.cleanupInterval) { const timer = this.cleanupTimers.get(layer); if (timer) { clearInterval(timer); this.setupLayerCleanup(layer); } } this.emit('layer-config-updated', { layer, config: newConfig }); } destroy(): void { // Clear all timers for (const timer of this.cleanupTimers.values()) { clearInterval(timer); } for (const timer of this.persistenceTimers.values()) { clearInterval(timer); } // Clear all caches this.layers.clear(); this.layerConfigs.clear(); this.layerStats.clear(); this.cleanupTimers.clear(); this.persistenceTimers.clear(); this.compressionCache.clear(); this.accessPatterns.clear(); this.hotKeys.clear(); this.removeAllListeners(); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Coder-RL/Claude_MCPServer_Dev1'

If you have feedback or need assistance with the MCP directory API, please join our Discord server