import { isDid } from "@atcute/lexicons/syntax"; type Entry = { value: Promise; expires: number; tags: Set }; export interface LookupStats { hits: number; misses: number; evictions: number; clears: number; } const MAX_ENTRIES = 10_000; export const didTag = (did: string) => `did:${did}`; export const handleTag = (handle: string) => `handle:${handle.toLowerCase()}`; export const identifierTag = (identifier: string) => isDid(identifier) ? didTag(identifier) : handleTag(identifier); // shared by every request on the server. does nothing until configure() sets a ttl export class LookupCache { #ttlMs: number; readonly #entries = new Map(); readonly #tagged = new Map>(); stats: LookupStats = { hits: 0, misses: 0, evictions: 0, clears: 0 }; constructor(ttlMs = 0) { this.#ttlMs = ttlMs; } get size(): number { return this.#entries.size; } configure(ttlMs: number): void { this.#ttlMs = ttlMs; this.clear(); } // tagsOf adds more tags once the value has loaded. undefined drops the entry lookup( key: string, tags: readonly string[], load: () => Promise, tagsOf: (value: T) => readonly string[] | undefined ): Promise { if (this.#ttlMs <= 0) return load(); const now = Date.now(); const hit = this.#entries.get(key); if (hit && hit.expires > now) { this.stats.hits++; return hit.value as Promise; } if (hit) this.#drop(key); this.stats.misses++; const value = load(); const entry: Entry = { value, expires: now + this.#ttlMs, tags: new Set() }; this.#entries.set(key, entry); for (const tag of tags) this.#tag(key, entry, tag); if (this.#entries.size > MAX_ENTRIES) { const oldest = this.#entries.keys().next().value; if (oldest !== undefined) this.#drop(oldest); } value.then( (loaded) => { if (this.#entries.get(key) !== entry) return; const more = tagsOf(loaded); if (more === undefined) this.#drop(key); else for (const tag of more) this.#tag(key, entry, tag); }, () => { if (this.#entries.get(key) === entry) this.#drop(key); } ); return value; } evict(tags: readonly string[]): number { let evicted = 0; for (const tag of tags) { for (const key of [...(this.#tagged.get(tag) ?? [])]) { this.#drop(key); evicted++; } } this.stats.evictions += evicted; return evicted; } clear(): void { this.#entries.clear(); this.#tagged.clear(); this.stats.clears++; } #tag(key: string, entry: Entry, tag: string): void { entry.tags.add(tag); let keys = this.#tagged.get(tag); if (!keys) this.#tagged.set(tag, (keys = new Set())); keys.add(key); } #drop(key: string): void { const entry = this.#entries.get(key); if (!entry) return; this.#entries.delete(key); for (const tag of entry.tags) { const keys = this.#tagged.get(tag); keys?.delete(key); if (keys?.size === 0) this.#tagged.delete(tag); } } } export const lookups = new LookupCache();