interface CacheEntry { data: T; expiresAt: number; } export class TtlCache { private store = new Map>(); private defaultTtlMs: number; private maxSize: number; constructor(ttlSeconds = 60, maxSize = 1000) { this.defaultTtlMs = ttlSeconds * 1000; this.maxSize = maxSize; } get(key: string): T | undefined { const entry = this.store.get(key) as CacheEntry | undefined; if (!entry) return undefined; if (Date.now() > entry.expiresAt) { this.store.delete(key); return undefined; } // LRU: move to end on access this.store.delete(key); this.store.set(key, entry); return entry.data; } set(key: string, data: T, ttlMs?: number): void { const effectiveTtl = ttlMs ?? this.defaultTtlMs; // If key already exists, delete first to refresh insertion order if (this.store.has(key)) { this.store.delete(key); } else if (this.store.size >= this.maxSize) { // Evict oldest entry (first in Map insertion order) const oldestKey = this.store.keys().next().value; if (oldestKey !== undefined) { this.store.delete(oldestKey); } } this.store.set(key, { data, expiresAt: Date.now() + effectiveTtl }); } /** Delete all keys that start with the given prefix */ invalidate(prefix: string): void { for (const key of Array.from(this.store.keys())) { if (key.startsWith(prefix)) { this.store.delete(key); } } } /** Alias for invalidate — delete all keys starting with prefix */ invalidatePrefix(prefix: string): void { this.invalidate(prefix); } /** Remove a single key */ delete(key: string): void { this.store.delete(key); } /** Completely clear the cache */ clear(): void { this.store.clear(); } /** Current number of entries (including possibly expired ones) */ get size(): number { return this.store.size; } } export const formsCache = new TtlCache(60); export const tasksMinimalCache = new TtlCache(60);