Skip to main content
Glama
cache.ts3.8 kB
/** * In-memory cache för API-data */ import { log } from './logger.js'; import { CacheError } from './errors.js'; // Constants const DEFAULT_CACHE_TTL_MS = 3600000; // 1 hour const DEFAULT_PRUNE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes interface CacheEntry<T> { data: T; expires: number; } export class Cache { private store = new Map<string, CacheEntry<any>>(); private hits = 0; private misses = 0; private pruneInterval?: NodeJS.Timeout; /** * Hämta data från cache */ get<T>(key: string): T | null { const entry = this.store.get(key); if (!entry) { this.misses++; log.debug('Cache miss', { key }); return null; } // Kolla om expired if (entry.expires < Date.now()) { this.store.delete(key); this.misses++; log.debug('Cache expired', { key }); return null; } this.hits++; log.debug('Cache hit', { key }); return entry.data as T; } /** * Sätt data i cache */ set<T>(key: string, data: T, ttl: number = DEFAULT_CACHE_TTL_MS): void { const expires = Date.now() + ttl; this.store.set(key, { data, expires }); log.debug('Cache set', { key, ttl, expires }); } /** * Ta bort specifik nyckel från cache */ delete(key: string): boolean { const deleted = this.store.delete(key); if (deleted) { log.debug('Cache deleted', { key }); } return deleted; } /** * Rensa hela cachen */ clear(): void { const size = this.store.size; this.store.clear(); this.hits = 0; this.misses = 0; log.info('Cache cleared', { entriesRemoved: size }); } /** * Starta auto-prune interval */ startAutoPrune(intervalMs: number = DEFAULT_PRUNE_INTERVAL_MS): void { if (this.pruneInterval) { log.warn('Auto-prune already running'); return; } this.pruneInterval = setInterval(() => { this.prune(); }, intervalMs); log.info('Auto-prune started', { intervalMs }); } /** * Stoppa auto-prune interval */ stopAutoPrune(): void { if (this.pruneInterval) { clearInterval(this.pruneInterval); this.pruneInterval = undefined; log.info('Auto-prune stopped'); } } /** * Rensa utgångna entries */ prune(): number { const now = Date.now(); let pruned = 0; for (const [key, entry] of this.store.entries()) { if (entry.expires < now) { this.store.delete(key); pruned++; } } if (pruned > 0) { log.info('Cache pruned', { entriesRemoved: pruned }); } return pruned; } /** * Hämta cache-statistik */ getStats() { return { size: this.store.size, hits: this.hits, misses: this.misses, hitRate: this.hits + this.misses > 0 ? (this.hits / (this.hits + this.misses) * 100).toFixed(2) + '%' : '0%' }; } /** * Kör en funktion med caching */ async getOrFetch<T>( key: string, fetchFn: () => Promise<T>, ttl: number = DEFAULT_CACHE_TTL_MS ): Promise<T> { // Försök hämta från cache const cached = this.get<T>(key); if (cached !== null) { return cached; } // Fetch och cache try { const data = await fetchFn(); this.set(key, data, ttl); return data; } catch (error) { log.error('Cache fetch failed', { key, error }); throw new CacheError(`Failed to fetch data for key: ${key}`); } } } // Singleton cache instance export const cache = new Cache(); // Starta auto-prune cache.startAutoPrune(); // Cleanup vid process exit process.on('exit', () => { cache.stopAutoPrune(); }); process.on('SIGTERM', () => { cache.stopAutoPrune(); }); process.on('SIGINT', () => { cache.stopAutoPrune(); }); export default cache;

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/isakskogstad/skolverket-syllabus-mcp'

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