authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-09-04 23:41:22-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-10-14 02:40:46-07:00
log72c169e375b5d91b36efcaf268206aeac9339a8c
tree007ac89bdece4c9415c447db868b739c042741d2
parentb0e85067049af72206d83b1abf8763a8cd510d18
signature Commit is signed but in an unrecognized format.

lib: add a least recently used cache


1 files changed, 139 insertions(+), 0 deletions(-)

framework/lib/lru.ts created+139
......@@ -0,0 +1,139 @@
1interface Options<K, V> {
2 capacity: number;
3 /**
4 * Decide how many units of size each entry takes.
5 * For example, accessing `.byteLength` of a Buffer.
6 */
7 sizeFn?: (value: V, key: K) => number;
8 /** Called when `set` or `ensureUnusedCapacity` removes items. */
9 evict?: (value: V, key: K) => void;
10 /** Called when anything removes items. */
11 delete?: (value: V, key: K) => void;
12}
13
14/** Trivial Least-recently-used cache data structure. */
15export class Lru<K, V> extends Map<K, V> {
16 #order = new Map<K, Order<K>>();
17 /** The most recently used */
18 #head: K | none = none;
19 /** The least recently used */
20 #tail: K | none = none;
21 #used: number = 0;
22 #capacity: number;
23 #sizeFn: (value: V, key: K) => number;
24 #deleteFn: null | ((value: V, key: K) => void);
25 #evictFn: null | ((value: V, key: K) => void);
26
27 constructor(opts: Options<K, V>) {
28 super();
29 const { capacity, sizeFn, delete: deleteFn, evict } = opts;
30 this.#capacity = capacity;
31 this.#sizeFn = sizeFn ?? (() => 1);
32 this.#deleteFn = deleteFn ?? null;
33 this.#evictFn = evict ?? null;
34 }
35
36 override clear() {
37 this.clear();
38 this.#order.clear();
39 this.#head = none;
40 this.#tail = none;
41 this.#used = 0;
42 }
43 override delete(key: K) {
44 if (!this.#evict(key)) return false;
45 ASSERT(this.#order.delete(key));
46 ASSERT(super.delete(key));
47 return true;
48 }
49 override get(key: K) {
50 if (this.#head !== key) {
51 const entry = this.#evict(key);
52 if (!entry) return undefined;
53 entry[1] = none, entry[2] = this.#head;
54 UNWRAP(this.#order.get(this.#head as K))[1] = key;
55 this.#head = key;
56 }
57 return super.get(key) as V;
58 }
59 override set(key: K, value: V) {
60 const entry = this.#evict(key);
61 const valueSize = this.#sizeFn(value, key);
62 if (entry) {
63 // relocate
64 this.#used -= entry[0];
65 entry[0] = valueSize, entry[1] = none, entry[2] = this.#head;
66 UNWRAP(this.#order.get(this.#head as K))[1] = key;
67 } else {
68 // insertion
69 const { size } = this;
70 const order: Order<K> = [valueSize, none, size > 0 ? this.#head : none];
71 this.#order.set(key, order);
72 if (size === 0) this.#tail = key;
73 else UNWRAP(this.#order.get(this.#head as K))[1] = key;
74 }
75 this.#head = key;
76 this.#used += valueSize;
77 this.ensureUnusedCapacity(0);
78 super.set(key, value);
79 return this;
80 }
81
82 /** Evict items until there are `unused` capacity slots. */
83 ensureUnusedCapacity(unused: number) {
84 const needed = this.#used - this.#capacity + unused;
85 let remain = needed;
86 const evictFn = this.#evictFn;
87 const deleteFn = this.#deleteFn;
88 while (remain > 0) {
89 const key = this.#tail as K;
90 const tail = UNWRAP(this.#evict(key));
91 if (evictFn) evictFn(super.get(key) as V, key);
92 if (deleteFn) deleteFn(super.get(key) as V, key);
93 ASSERT(this.#order.delete(key));
94 ASSERT(super.delete(key));
95 remain -= tail[0];
96 }
97 this.#used -= needed - remain;
98 }
99
100 /** Unlink an entry without removing it. */
101 #evict(key: K) {
102 const entry = this.#order.get(key);
103 if (!entry) return null;
104 const [, before, after] = entry;
105 if (before !== none) {
106 UNWRAP(this.#order.get(before as K))[2] = after;
107 if (after === none) this.#tail = before;
108 }
109 if (after !== none) {
110 UNWRAP(this.#order.get(after as K))[1] = before;
111 if (before === none) this.#head = after;
112 }
113 return entry;
114 }
115
116 get capacity() {
117 return this.#capacity;
118 }
119 set capacity(capacity: number) {
120 this.#capacity = capacity;
121 this.ensureUnusedCapacity(0);
122 }
123}
124
125/**
126 * To indicate the start or end of the list, set the key to
127 * itself. Otherwise, it is ambiguous with null or undefined keys.
128 */
129type Order<K> = [
130 /** Number of units computed at insertion time. */
131 size: number,
132 /** Towards most recently used */
133 before: K | none,
134 /** Towards least recently used */
135 after: K | none,
136];
137
138const none: unique symbol = Symbol();
139type none = typeof none;