chore: refactoring and tests (#12468)

This commit is contained in:
Adam
2026-02-06 09:37:49 -06:00
committed by GitHub
parent c07077f96c
commit a4bc883595
39 changed files with 3804 additions and 1494 deletions

View File

@@ -0,0 +1,69 @@
import { describe, expect, test } from "bun:test"
import { createScopedCache } from "./scoped-cache"
describe("createScopedCache", () => {
test("evicts least-recently-used entry when max is reached", () => {
const disposed: string[] = []
const cache = createScopedCache((key) => ({ key }), {
maxEntries: 2,
dispose: (value) => disposed.push(value.key),
})
const a = cache.get("a")
const b = cache.get("b")
expect(a.key).toBe("a")
expect(b.key).toBe("b")
cache.get("a")
const c = cache.get("c")
expect(c.key).toBe("c")
expect(cache.peek("a")?.key).toBe("a")
expect(cache.peek("b")).toBeUndefined()
expect(cache.peek("c")?.key).toBe("c")
expect(disposed).toEqual(["b"])
})
test("disposes entries on delete and clear", () => {
const disposed: string[] = []
const cache = createScopedCache((key) => ({ key }), {
dispose: (value) => disposed.push(value.key),
})
cache.get("a")
cache.get("b")
const removed = cache.delete("a")
expect(removed?.key).toBe("a")
expect(cache.peek("a")).toBeUndefined()
cache.clear()
expect(cache.peek("b")).toBeUndefined()
expect(disposed).toEqual(["a", "b"])
})
test("expires stale entries with ttl and recreates on get", () => {
let clock = 0
let count = 0
const disposed: string[] = []
const cache = createScopedCache((key) => ({ key, count: ++count }), {
ttlMs: 10,
now: () => clock,
dispose: (value) => disposed.push(`${value.key}:${value.count}`),
})
const first = cache.get("a")
expect(first.count).toBe(1)
clock = 9
expect(cache.peek("a")?.count).toBe(1)
clock = 11
expect(cache.peek("a")).toBeUndefined()
expect(disposed).toEqual(["a:1"])
const second = cache.get("a")
expect(second.count).toBe(2)
expect(disposed).toEqual(["a:1"])
})
})

View File

@@ -0,0 +1,104 @@
type ScopedCacheOptions<T> = {
maxEntries?: number
ttlMs?: number
dispose?: (value: T, key: string) => void
now?: () => number
}
type Entry<T> = {
value: T
touchedAt: number
}
export function createScopedCache<T>(createValue: (key: string) => T, options: ScopedCacheOptions<T> = {}) {
const store = new Map<string, Entry<T>>()
const now = options.now ?? Date.now
const dispose = (key: string, entry: Entry<T>) => {
options.dispose?.(entry.value, key)
}
const expired = (entry: Entry<T>) => {
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<T>) => {
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,
}
}