// A small in-memory LRU with a TTL, a size cap and in-flight // de-duplication, for the resolve and site answers. Pure TypeScript (no // Nitro imports), so `node --test` drives it directly. // // clear() (the site cache purge) bumps a generation: a load that started // before it still answers its caller, but its value is never stored, and a // load after it never joins the older one. So no request after a purge sees // an answer fetched before it. export interface LruOptions { /** Most entries kept; the least recently used goes first. */ max: number /** How long an entry is fresh, in milliseconds; 0 or less stores nothing. */ ttlMs: number /** The clock (Date.now); tests pass their own. */ now?: () => number } export interface Lru { get(key: string): V | undefined set(key: string, value: V): void delete(key: string): void /** Empties the cache and detaches the loads in flight. */ clear(): void readonly size: number /** * The cached value for `key`, else the loader's, shared by concurrent * callers of the same key. The value is stored when `cacheable` accepts * it (default: always); a rejected loader stores nothing. */ load(key: string, loader: () => Promise, cacheable?: (value: V) => boolean): Promise } export function createLru({ max, ttlMs, now = Date.now }: LruOptions): Lru { const entries = new Map() const inflight = new Map>() let generation = 0 const limit = Math.max(1, Math.floor(max)) const get = (key: string): V | undefined => { const hit = entries.get(key) if (!hit) return undefined if (hit.expires <= now()) { entries.delete(key) return undefined } // Map keeps insertion order: re-inserting marks the key most recent. entries.delete(key) entries.set(key, hit) return hit.value } const set = (key: string, value: V) => { if (ttlMs <= 0) return entries.delete(key) entries.set(key, { value, expires: now() + ttlMs }) while (entries.size > limit) { const oldest = entries.keys().next().value if (oldest === undefined) break entries.delete(oldest) } } return { get, set, delete: (key) => void entries.delete(key), clear() { entries.clear() inflight.clear() generation++ }, get size() { for (const [key, e] of entries) if (e.expires <= now()) entries.delete(key) return entries.size }, load(key, loader, cacheable = () => true) { const hit = get(key) if (hit !== undefined) return Promise.resolve(hit) const pending = inflight.get(key) if (pending) return pending const started = generation // The loader starts at once; one that throws at once becomes a // rejection, which settles after `p` is assigned and leaves no entry // in flight. let first: Promise try { first = Promise.resolve(loader()) } catch (err) { first = Promise.reject(err) } const p: Promise = first .then((value) => { if (generation === started && cacheable(value)) set(key, value) return value }) .finally(() => { if (inflight.get(key) === p) inflight.delete(key) }) inflight.set(key, p) return p }, } }