/** * Tiny in-process TTL cache with request coalescing (spec §59). Used for aggregates only * (stats, trending, pulse, radar, rankings) — never for the live feed itself. */ const store = new Map }>(); export async function cached(key: string, ttlMs: number, fn: () => Promise): Promise { const now = Date.now(); const hit = store.get(key); if (hit && now - hit.at < hit.ttl) return hit.value as T; if (hit?.pending) return hit.pending as Promise; const pending = fn() .then((value) => { store.set(key, { at: Date.now(), ttl: ttlMs, value }); return value; }) .catch((e) => { // serve stale on failure when we have anything at all if (hit) { store.set(key, { ...hit, pending: undefined }); return hit.value as T; } store.delete(key); throw e; }); store.set(key, { at: hit?.at ?? 0, ttl: hit?.ttl ?? ttlMs, value: hit?.value, pending }); return pending; } export function cacheStats(): { entries: number; keys: string[] } { return { entries: store.size, keys: [...store.keys()].slice(0, 50) }; } export function invalidate(prefix: string): void { for (const k of store.keys()) if (k.startsWith(prefix)) store.delete(k); }