Skip to main content
Glama
cache-manager.ts6.57 kB
/** * Cache Manager - In-memory caching with multiple eviction strategies */ export interface CacheEntry<T> { value: T; createdAt: number; expiresAt: number; hits: number; lastAccessedAt: number; } export interface CacheOptions { ttl: number; // Time to live in milliseconds maxSize?: number; // Maximum number of entries strategy?: 'LRU' | 'LFU' | 'FIFO'; } export interface CacheStats { size: number; maxSize: number; hits: number; misses: number; hitRate: number; oldestEntry?: number; newestEntry?: number; } export class CacheManager<T> { private cache: Map<string, CacheEntry<T>>; private options: Required<CacheOptions>; private stats: { hits: number; misses: number }; private cleanupInterval: NodeJS.Timeout | null = null; constructor(options: CacheOptions) { this.cache = new Map(); this.options = { ttl: options.ttl, maxSize: options.maxSize || 1000, strategy: options.strategy || 'LRU', }; this.stats = { hits: 0, misses: 0 }; // Start automatic cleanup this.startCleanup(); } /** * Set a value in the cache */ set(key: string, value: T, ttl?: number): void { const now = Date.now(); const expiresAt = now + (ttl || this.options.ttl); // Check if we need to evict if (this.cache.size >= this.options.maxSize && !this.cache.has(key)) { this.evict(); } this.cache.set(key, { value, createdAt: now, expiresAt, hits: 0, lastAccessedAt: now, }); } /** * Get a value from the cache */ get(key: string): T | undefined { const entry = this.cache.get(key); if (!entry) { this.stats.misses++; return undefined; } // Check if expired const now = Date.now(); if (now > entry.expiresAt) { this.cache.delete(key); this.stats.misses++; return undefined; } // Update access statistics entry.hits++; entry.lastAccessedAt = now; this.stats.hits++; return entry.value; } /** * Check if a key exists in the cache */ has(key: string): boolean { const entry = this.cache.get(key); if (!entry) { return false; } // Check if expired if (Date.now() > entry.expiresAt) { this.cache.delete(key); return false; } return true; } /** * Delete a key from the cache */ delete(key: string): boolean { return this.cache.delete(key); } /** * Clear all entries from the cache */ clear(): void { this.cache.clear(); this.stats = { hits: 0, misses: 0 }; } /** * Get cache statistics */ getStats(): CacheStats { const entries = Array.from(this.cache.values()); const totalRequests = this.stats.hits + this.stats.misses; let oldestEntry: number | undefined; let newestEntry: number | undefined; if (entries.length > 0) { oldestEntry = Math.min(...entries.map(e => e.createdAt)); newestEntry = Math.max(...entries.map(e => e.createdAt)); } return { size: this.cache.size, maxSize: this.options.maxSize, hits: this.stats.hits, misses: this.stats.misses, hitRate: totalRequests > 0 ? this.stats.hits / totalRequests : 0, oldestEntry, newestEntry, }; } /** * Get all keys in the cache */ keys(): string[] { return Array.from(this.cache.keys()); } /** * Get the size of the cache */ size(): number { return this.cache.size; } /** * Start automatic cleanup of expired entries */ private startCleanup(): void { // Run cleanup every minute this.cleanupInterval = setInterval(() => { this.cleanup(); }, 60000); } /** * Stop automatic cleanup */ stopCleanup(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } } /** * Clean up expired entries */ private cleanup(): void { const now = Date.now(); const keysToDelete: string[] = []; for (const [key, entry] of this.cache.entries()) { if (now > entry.expiresAt) { keysToDelete.push(key); } } for (const key of keysToDelete) { this.cache.delete(key); } if (keysToDelete.length > 0) { console.error(`Cache cleanup: removed ${keysToDelete.length} expired entries`); } } /** * Evict an entry based on the configured strategy */ private evict(): void { if (this.cache.size === 0) { return; } let keyToEvict: string | undefined; switch (this.options.strategy) { case 'LRU': // Least Recently Used keyToEvict = this.findLRUKey(); break; case 'LFU': // Least Frequently Used keyToEvict = this.findLFUKey(); break; case 'FIFO': // First In First Out keyToEvict = this.cache.keys().next().value; break; } if (keyToEvict) { this.cache.delete(keyToEvict); } } /** * Find the least recently used key */ private findLRUKey(): string | undefined { let lruKey: string | undefined; let oldestAccess = Infinity; for (const [key, entry] of this.cache.entries()) { if (entry.lastAccessedAt < oldestAccess) { oldestAccess = entry.lastAccessedAt; lruKey = key; } } return lruKey; } /** * Find the least frequently used key */ private findLFUKey(): string | undefined { let lfuKey: string | undefined; let leastHits = Infinity; for (const [key, entry] of this.cache.entries()) { if (entry.hits < leastHits) { leastHits = entry.hits; lfuKey = key; } } return lfuKey; } /** * Get a value or compute it if not in cache */ async getOrCompute( key: string, compute: () => Promise<T>, ttl?: number ): Promise<T> { const cached = this.get(key); if (cached !== undefined) { return cached; } const value = await compute(); this.set(key, value, ttl); return value; } /** * Warm up the cache with pre-computed values */ warmUp(entries: Map<string, T>, ttl?: number): void { for (const [key, value] of entries) { this.set(key, value, ttl); } } /** * Export cache contents */ export(): Map<string, T> { const result = new Map<string, T>(); const now = Date.now(); for (const [key, entry] of this.cache.entries()) { if (now <= entry.expiresAt) { result.set(key, entry.value); } } return result; } }

Latest Blog Posts

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/krtw00/search-mcp'

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