| ... | ... | @@ -0,0 +1,139 @@ |
| 1 | interface 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. */ |
| 15 | export 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 | */ |
| 129 | type 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 | |
| 138 | const none: unique symbol = Symbol(); |
| 139 | type none = typeof none; |