type ScopedCacheOptions = { maxEntries?: number ttlMs?: number dispose?: (value: T, key: string) => void now?: () => number } type Entry = { value: T touchedAt: number } export function createScopedCache(createValue: (key: string) => T, options: ScopedCacheOptions = {}) { const store = new Map>() const now = options.now ?? Date.now const dispose = (key: string, entry: Entry) => { options.dispose?.(entry.value, key) } const expired = (entry: Entry) => { if (options.ttlMs === undefined) return false return now() - entry.touchedAt >= options.ttlMs } const sweep = () => { if (options.ttlMs === undefined) return for (const [key, entry] of store) { if (!expired(entry)) continue store.delete(key) dispose(key, entry) } } const touch = (key: string, entry: Entry) => { entry.touchedAt = now() store.delete(key) store.set(key, entry) } const prune = () => { if (options.maxEntries === undefined) return while (store.size > options.maxEntries) { const key = store.keys().next().value if (!key) return const entry = store.get(key) store.delete(key) if (!entry) continue dispose(key, entry) } } const remove = (key: string) => { const entry = store.get(key) if (!entry) return store.delete(key) dispose(key, entry) return entry.value } const peek = (key: string) => { sweep() const entry = store.get(key) if (!entry) return if (!expired(entry)) return entry.value store.delete(key) dispose(key, entry) } const get = (key: string) => { sweep() const entry = store.get(key) if (entry && !expired(entry)) { touch(key, entry) return entry.value } if (entry) { store.delete(key) dispose(key, entry) } const created = { value: createValue(key), touchedAt: now(), } store.set(key, created) prune() return created.value } const clear = () => { for (const [key, entry] of store) { dispose(key, entry) } store.clear() } return { get, peek, delete: remove, clear, } }