From 72c169e375b5d91b36efcaf268206aeac9339a8c Mon Sep 17 00:00:00 2001 From: clover caruso Date: Thu, 4 Sep 2025 23:41:22 -0700 Subject: [PATCH] lib: add a least recently used cache --- framework/lib/lru.ts | 139 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 framework/lib/lru.ts diff --git a/framework/lib/lru.ts b/framework/lib/lru.ts new file mode 100644 index 0000000000000000000000000000000000000000..f11d96db969a372cb13e1a268257b4b5f09d4000 --- /dev/null +++ b/framework/lib/lru.ts @@ -0,0 +1,139 @@ +interface Options { + capacity: number; + /** + * Decide how many units of size each entry takes. + * For example, accessing `.byteLength` of a Buffer. + */ + sizeFn?: (value: V, key: K) => number; + /** Called when `set` or `ensureUnusedCapacity` removes items. */ + evict?: (value: V, key: K) => void; + /** Called when anything removes items. */ + delete?: (value: V, key: K) => void; +} + +/** Trivial Least-recently-used cache data structure. */ +export class Lru extends Map { + #order = new Map>(); + /** The most recently used */ + #head: K | none = none; + /** The least recently used */ + #tail: K | none = none; + #used: number = 0; + #capacity: number; + #sizeFn: (value: V, key: K) => number; + #deleteFn: null | ((value: V, key: K) => void); + #evictFn: null | ((value: V, key: K) => void); + + constructor(opts: Options) { + super(); + const { capacity, sizeFn, delete: deleteFn, evict } = opts; + this.#capacity = capacity; + this.#sizeFn = sizeFn ?? (() => 1); + this.#deleteFn = deleteFn ?? null; + this.#evictFn = evict ?? null; + } + + override clear() { + this.clear(); + this.#order.clear(); + this.#head = none; + this.#tail = none; + this.#used = 0; + } + override delete(key: K) { + if (!this.#evict(key)) return false; + ASSERT(this.#order.delete(key)); + ASSERT(super.delete(key)); + return true; + } + override get(key: K) { + if (this.#head !== key) { + const entry = this.#evict(key); + if (!entry) return undefined; + entry[1] = none, entry[2] = this.#head; + UNWRAP(this.#order.get(this.#head as K))[1] = key; + this.#head = key; + } + return super.get(key) as V; + } + override set(key: K, value: V) { + const entry = this.#evict(key); + const valueSize = this.#sizeFn(value, key); + if (entry) { + // relocate + this.#used -= entry[0]; + entry[0] = valueSize, entry[1] = none, entry[2] = this.#head; + UNWRAP(this.#order.get(this.#head as K))[1] = key; + } else { + // insertion + const { size } = this; + const order: Order = [valueSize, none, size > 0 ? this.#head : none]; + this.#order.set(key, order); + if (size === 0) this.#tail = key; + else UNWRAP(this.#order.get(this.#head as K))[1] = key; + } + this.#head = key; + this.#used += valueSize; + this.ensureUnusedCapacity(0); + super.set(key, value); + return this; + } + + /** Evict items until there are `unused` capacity slots. */ + ensureUnusedCapacity(unused: number) { + const needed = this.#used - this.#capacity + unused; + let remain = needed; + const evictFn = this.#evictFn; + const deleteFn = this.#deleteFn; + while (remain > 0) { + const key = this.#tail as K; + const tail = UNWRAP(this.#evict(key)); + if (evictFn) evictFn(super.get(key) as V, key); + if (deleteFn) deleteFn(super.get(key) as V, key); + ASSERT(this.#order.delete(key)); + ASSERT(super.delete(key)); + remain -= tail[0]; + } + this.#used -= needed - remain; + } + + /** Unlink an entry without removing it. */ + #evict(key: K) { + const entry = this.#order.get(key); + if (!entry) return null; + const [, before, after] = entry; + if (before !== none) { + UNWRAP(this.#order.get(before as K))[2] = after; + if (after === none) this.#tail = before; + } + if (after !== none) { + UNWRAP(this.#order.get(after as K))[1] = before; + if (before === none) this.#head = after; + } + return entry; + } + + get capacity() { + return this.#capacity; + } + set capacity(capacity: number) { + this.#capacity = capacity; + this.ensureUnusedCapacity(0); + } +} + +/** + * To indicate the start or end of the list, set the key to + * itself. Otherwise, it is ambiguous with null or undefined keys. + */ +type Order = [ + /** Number of units computed at insertion time. */ + size: number, + /** Towards most recently used */ + before: K | none, + /** Towards least recently used */ + after: K | none, +]; + +const none: unique symbol = Symbol(); +type none = typeof none; -- 2.54.0