Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117import { isDid } from "@atcute/lexicons/syntax";
type Entry = { value: Promise<unknown>; expires: number; tags: Set<string> };
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 ttlexport class LookupCache { #ttlMs: number; readonly #entries = new Map<string, Entry>(); readonly #tagged = new Map<string, Set<string>>(); 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<T>( key: string, tags: readonly string[], load: () => Promise<T>, tagsOf: (value: T) => readonly string[] | undefined ): Promise<T> { 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<T>; } 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();