Skip to main content
Glama
manager.ts•5.54 kB
import { createHash } from "crypto"; import { getCacheConfig, type CacheConfig } from "./config.js"; export interface CacheEntry { data: any; timestamp: Date; expiresAt: Date; source: string; lastAccessed: Date; } export interface CacheStats { totalEntries: number; hits: number; misses: number; hitRate: number; missRate: number; memoryUsageEstimate: number; // bytes oldestEntry: Date | null; newestEntry: Date | null; } class CacheManager { private cache: Map<string, CacheEntry>; private config: CacheConfig; private hits: number; private misses: number; private cleanupInterval: NodeJS.Timeout | null; constructor() { this.cache = new Map(); this.config = getCacheConfig(); this.hits = 0; this.misses = 0; this.cleanupInterval = null; // Start auto-cleanup if enabled if (this.config.enabled) { this.startCleanup(); } } /** * Generate a cache key from source, operation, and parameters */ generateKey(source: string, operation: string, params: any): string { // Create a stable string representation of params const paramsStr = JSON.stringify(params, Object.keys(params || {}).sort()); const hash = createHash("sha256").update(paramsStr).digest("hex"); return `${source}:${operation}:${hash.substring(0, 16)}`; } /** * Get a cached entry if it exists and is not expired */ get(key: string): CacheEntry | null { if (!this.config.enabled) { return null; } const entry = this.cache.get(key); if (!entry) { this.misses++; return null; } // Check if expired if (new Date() > entry.expiresAt) { this.cache.delete(key); this.misses++; return null; } // Update last accessed time for LRU entry.lastAccessed = new Date(); this.hits++; return entry; } /** * Store data in cache with TTL */ set(key: string, data: any, ttlSeconds: number, source: string): void { if (!this.config.enabled) { return; } const now = new Date(); const expiresAt = new Date(now.getTime() + ttlSeconds * 1000); const entry: CacheEntry = { data, timestamp: now, expiresAt, source, lastAccessed: now, }; // Check if we need to evict entries (LRU) if (this.cache.size >= this.config.maxSize) { this.evictLRU(); } this.cache.set(key, entry); } /** * Remove a specific cache entry */ invalidate(key: string): void { this.cache.delete(key); } /** * Clear all cache entries */ clear(): void { this.cache.clear(); this.hits = 0; this.misses = 0; } /** * Get cache statistics */ getStats(): CacheStats { const totalEntries = this.cache.size; const totalRequests = this.hits + this.misses; const hitRate = totalRequests > 0 ? (this.hits / totalRequests) * 100 : 0; const missRate = totalRequests > 0 ? (this.misses / totalRequests) * 100 : 0; // Estimate memory usage (rough calculation) let memoryUsageEstimate = 0; let oldestEntry: Date | null = null; let newestEntry: Date | null = null; for (const entry of this.cache.values()) { // Rough estimate: JSON stringify size memoryUsageEstimate += JSON.stringify(entry.data).length; memoryUsageEstimate += 200; // Overhead for entry metadata if (!oldestEntry || entry.timestamp < oldestEntry) { oldestEntry = entry.timestamp; } if (!newestEntry || entry.timestamp > newestEntry) { newestEntry = entry.timestamp; } } return { totalEntries, hits: this.hits, misses: this.misses, hitRate: Math.round(hitRate * 100) / 100, missRate: Math.round(missRate * 100) / 100, memoryUsageEstimate, oldestEntry, newestEntry, }; } /** * Evict least recently used entries when cache is full */ private evictLRU(): void { if (this.cache.size === 0) return; // Sort entries by lastAccessed (oldest first) const entries = Array.from(this.cache.entries()).sort( (a, b) => a[1].lastAccessed.getTime() - b[1].lastAccessed.getTime(), ); // Remove oldest 10% or enough to get below max size const toRemove = Math.max( Math.ceil(this.cache.size * 0.1), this.cache.size - this.config.maxSize + 1, ); for (let i = 0; i < toRemove; i++) { this.cache.delete(entries[i][0]); } } /** * Start automatic cleanup of expired entries */ private startCleanup(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } this.cleanupInterval = setInterval(() => { this.cleanup(); }, this.config.cleanupInterval); } /** * Remove all expired entries */ private cleanup(): void { const now = new Date(); 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.log( `đź§ą Cache cleanup: Removed ${keysToDelete.length} expired entries`, ); } } /** * Stop cleanup interval (useful for testing) */ stopCleanup(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } } } // Export singleton instance export const cacheManager = new CacheManager();

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/JamesANZ/medical-mcp'

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