/** * Keeps what the server fetched from another site for a while, by key, so a * page or a feed asking again doesn't refetch it: Bandcamp tracklists, YouTube * playlists, and link previews. Each holder sets how long an entry lives and * how many are kept; past that count, the oldest written goes first. * * @module */ /** * A map whose entries expire `ttlMs` after they are written, holding * `maxEntries` at most. * * @example * const playlists = new TtlCache(6 * 3_600_000, 200); * const hit = playlists.get(listId) ?? playlists.set(listId, await fetch()); */ export class TtlCache { #entries = new Map(); constructor(readonly ttlMs: number, readonly maxEntries: number) {} /** Returns the value under `key` unless it is missing or expired. */ get(key: string): V | undefined { const hit = this.#entries.get(key); if (!hit) return undefined; if (Date.now() - hit.at >= this.ttlMs) { this.#entries.delete(key); return undefined; } return hit.value; } /** Stores `value` under `key` from now on, and returns it. */ set(key: string, value: V): V { this.#entries.delete(key); // A Map keeps insertion order, so the first key is the oldest write. if (this.#entries.size >= this.maxEntries) { const oldest = this.#entries.keys().next().value; if (oldest !== undefined) this.#entries.delete(oldest); } this.#entries.set(key, { value, at: Date.now() }); return value; } /** * Removes `key`, or only when `value` is still what it holds, so a late * failure can't remove the entry that replaced its own. */ delete(key: string, value?: V): void { if (value === undefined || this.#entries.get(key)?.value === value) { this.#entries.delete(key); } } /** Empties the cache, for the tests' reset between cases. */ clear(): void { this.#entries.clear(); } }