export interface SenderCacheOptions { readonly ttlMs?: number; readonly maxSize?: number; } interface SenderCacheEntry { readonly name: string; readonly expiresAt: number; } const DEFAULT_TTL_MS = 600_000; const DEFAULT_MAX_SIZE = 2_048; export class SenderNameCache { private readonly ttlMs: number; private readonly maxSize: number; private readonly entries = new Map(); constructor(options: SenderCacheOptions = {}) { this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS; this.maxSize = Math.max(0, options.maxSize ?? DEFAULT_MAX_SIZE); } get(openId: string): string | null { const entry = this.entries.get(openId); if (entry === undefined) return null; if (entry.expiresAt <= Date.now()) { this.entries.delete(openId); return null; } this.entries.delete(openId); this.entries.set(openId, entry); return entry.name; } set(openId: string, name: string): void { if (this.maxSize === 0) return; this.entries.delete(openId); this.entries.set(openId, { name, expiresAt: Date.now() + this.ttlMs }); while (this.entries.size > this.maxSize) { const oldest = this.entries.keys().next().value; if (oldest === undefined) break; this.entries.delete(oldest); } } async getOrFetch( openId: string, fetchFn: (openId: string) => Promise, ): Promise { const cached = this.get(openId); if (cached !== null) return cached; const fetched = await fetchFn(openId); if (fetched !== null) { this.set(openId, fetched); } return fetched; } clear(): void { this.entries.clear(); } size(): number { this.pruneExpired(); return this.entries.size; } private pruneExpired(): void { const now = Date.now(); for (const [openId, entry] of this.entries) { if (entry.expiresAt <= now) { this.entries.delete(openId); } } } }