| ... | ... | @@ -19,6 +19,7 @@ export class Lru<K, V> extends Map<K, V> { |
| 19 | 19 | /** The least recently used */ |
| 20 | 20 | #tail: K | none = none; |
| 21 | 21 | #used: number = 0; |
| 22 | #locked: number = 0; |
| 22 | 23 | #capacity: number; |
| 23 | 24 | #sizeFn: (value: V, key: K) => number; |
| 24 | 25 | #deleteFn: null | ((value: V, key: K) => void); |
| ... | ... | @@ -34,14 +35,16 @@ export class Lru<K, V> extends Map<K, V> { |
| 34 | 35 | } |
| 35 | 36 | |
| 36 | 37 | override clear() { |
| 37 | | this.clear(); |
| 38 | super.clear(); |
| 38 | 39 | this.#order.clear(); |
| 39 | 40 | this.#head = none; |
| 40 | 41 | this.#tail = none; |
| 41 | 42 | this.#used = 0; |
| 42 | 43 | } |
| 43 | 44 | override delete(key: K) { |
| 44 | | if (!this.#evict(key)) return false; |
| 45 | const entry = this.#evict(key); |
| 46 | if (!entry) return false; |
| 47 | this.#used -= entry[0]; |
| 45 | 48 | ASSERT(this.#order.delete(key)); |
| 46 | 49 | ASSERT(super.delete(key)); |
| 47 | 50 | return true; |
| ... | ... | @@ -57,46 +60,103 @@ export class Lru<K, V> extends Map<K, V> { |
| 57 | 60 | return super.get(key) as V; |
| 58 | 61 | } |
| 59 | 62 | override set(key: K, value: V) { |
| 63 | const size = this.#sizeFn(value, key); |
| 64 | { |
| 65 | const uc = this.unlockedCapacity; |
| 66 | if (size > uc) { |
| 67 | const label = uc === this.#capacity ? " " : " unlocked "; |
| 68 | const msg = `Object of size ${size} exceeds${label}capacity ${uc}`; |
| 69 | throw new RangeError(msg); |
| 70 | } |
| 71 | } |
| 72 | |
| 60 | 73 | const entry = this.#evict(key); |
| 61 | | const valueSize = this.#sizeFn(value, key); |
| 62 | 74 | if (entry) { |
| 63 | 75 | // relocate |
| 64 | 76 | 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; |
| 77 | const head = this.#head; |
| 78 | ASSERT(head !== key); |
| 79 | entry[0] = size, entry[1] = none, entry[2] = this.#head; |
| 80 | if (head !== none) UNWRAP(this.#order.get(this.#head as K))[1] = key; |
| 81 | if (this.#tail === none) this.#tail = key; |
| 67 | 82 | } else { |
| 68 | 83 | // insertion |
| 69 | | const { size } = this; |
| 70 | | const order: Order<K> = [valueSize, none, size > 0 ? this.#head : none]; |
| 84 | const { size: mapSize } = this; |
| 85 | const order: Order<K> = [size, none, mapSize > 0 ? this.#head : none, 0]; |
| 71 | 86 | this.#order.set(key, order); |
| 72 | | if (size === 0) this.#tail = key; |
| 73 | | else UNWRAP(this.#order.get(this.#head as K))[1] = key; |
| 87 | if (mapSize === 0) this.#tail = key; |
| 88 | else if (this.#head !== none) { |
| 89 | UNWRAP(this.#order.get(this.#head as K))[1] = key; |
| 90 | } |
| 74 | 91 | } |
| 92 | |
| 75 | 93 | this.#head = key; |
| 76 | | this.#used += valueSize; |
| 77 | | this.ensureUnusedCapacity(0); |
| 94 | this.#used += size; |
| 95 | try { |
| 96 | this.ensureUnusedCapacity(0); |
| 97 | } catch { |
| 98 | ASSERT(false, "capacity is already verified"); |
| 99 | } |
| 78 | 100 | super.set(key, value); |
| 79 | 101 | return this; |
| 80 | 102 | } |
| 81 | 103 | |
| 104 | /** Prevent an item from being automatically evicted */ |
| 105 | lock(key: K): Lock { |
| 106 | const order = this.#order.get(key); |
| 107 | if (!order) throw new Error(`Key ${key} not in Lru`); |
| 108 | const count = order[3] += 1; |
| 109 | if (count === 1) this.#locked += order[0]; |
| 110 | const unlock = () => { |
| 111 | const count = order![3] -= 1; |
| 112 | if (count === 0) this.#locked -= order[0]; |
| 113 | }; |
| 114 | unlock[Symbol.dispose] = unlock; |
| 115 | return unlock; |
| 116 | } |
| 117 | |
| 82 | 118 | /** Evict items until there are `unused` capacity slots. */ |
| 83 | 119 | ensureUnusedCapacity(unused: number) { |
| 120 | const uc = this.unlockedCapacity; |
| 121 | if (unused > uc) { |
| 122 | const label = uc === this.#capacity ? "capacity" : "unlocked capacity"; |
| 123 | const msg = `Can't ensure ${unused} unused capacity with ${label} ${uc}`; |
| 124 | throw new RangeError(msg); |
| 125 | } |
| 84 | 126 | const needed = this.#used - this.#capacity + unused; |
| 127 | if (needed <= 0) return; |
| 85 | 128 | let remain = needed; |
| 86 | 129 | const evictFn = this.#evictFn; |
| 87 | 130 | 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]; |
| 131 | const order = this.#order; |
| 132 | let key = this.#tail; |
| 133 | loop: { |
| 134 | let hadLocks = false; |
| 135 | while (key !== none) { |
| 136 | const entry = UNWRAP(order.get(key)); |
| 137 | if (entry[3] > 0) { |
| 138 | key = entry[1]; |
| 139 | hadLocks = true; |
| 140 | continue; |
| 141 | } |
| 142 | ASSERT(this.#evict(key) === entry); |
| 143 | if (evictFn) evictFn(super.get(key) as V, key); |
| 144 | if (deleteFn) deleteFn(super.get(key) as V, key); |
| 145 | ASSERT(this.#order.delete(key)); |
| 146 | ASSERT(super.delete(key)); |
| 147 | remain -= entry[0]; |
| 148 | if (remain <= 0) break loop; |
| 149 | key = entry[1]; |
| 150 | } |
| 151 | ASSERT(hadLocks); |
| 152 | const msg = `Too many locked objects, cannot free ${unused} capacity`; |
| 153 | throw new RangeError(msg); |
| 96 | 154 | } |
| 97 | 155 | this.#used -= needed - remain; |
| 98 | 156 | } |
| 99 | 157 | |
| 158 | hasEvictableCapacity() {} |
| 159 | |
| 100 | 160 | /** Unlink an entry without removing it. */ |
| 101 | 161 | #evict(key: K) { |
| 102 | 162 | const entry = this.#order.get(key); |
| ... | ... | @@ -104,12 +164,10 @@ export class Lru<K, V> extends Map<K, V> { |
| 104 | 164 | const [, before, after] = entry; |
| 105 | 165 | if (before !== none) { |
| 106 | 166 | UNWRAP(this.#order.get(before as K))[2] = after; |
| 107 | | if (after === none) this.#tail = before; |
| 108 | | } |
| 167 | } else this.#head = after; |
| 109 | 168 | if (after !== none) { |
| 110 | 169 | UNWRAP(this.#order.get(after as K))[1] = before; |
| 111 | | if (before === none) this.#head = after; |
| 112 | | } |
| 170 | } else this.#tail = before; |
| 113 | 171 | return entry; |
| 114 | 172 | } |
| 115 | 173 | |
| ... | ... | @@ -120,6 +178,58 @@ export class Lru<K, V> extends Map<K, V> { |
| 120 | 178 | this.#capacity = capacity; |
| 121 | 179 | this.ensureUnusedCapacity(0); |
| 122 | 180 | } |
| 181 | get remaining() { |
| 182 | return this.#capacity - this.#used; |
| 183 | } |
| 184 | get unlockedCapacity() { |
| 185 | return this.#capacity - this.#locked; |
| 186 | } |
| 187 | get used() { |
| 188 | return this.#used; |
| 189 | } |
| 190 | |
| 191 | serialize(): Serialized<K, V>; |
| 192 | serialize<OK, OV>(o: SerializeOptions<K, V, OK, OV>): Serialized<OK, OV>; |
| 193 | serialize<OK, OV>( |
| 194 | opts: SerializeOptions<K, V, OK, OV> = {}, |
| 195 | ): Serialized<OK, OV> { |
| 196 | const { map = (k, v) => [k as unknown as OK, v as unknown as OV] } = opts; |
| 197 | const result: Serialized<OK, OV> = []; |
| 198 | const order = this.#order; |
| 199 | let head = this.#head; |
| 200 | while (head !== none) { |
| 201 | const entry = UNWRAP(order.get(head), head); |
| 202 | const [key, value] = map(head, super.get(head) as V); |
| 203 | result.push([key, value, entry[0]]); |
| 204 | head = entry[2]; |
| 205 | } |
| 206 | return result; |
| 207 | } |
| 208 | revive<IK, IV>( |
| 209 | entries: Serialized<IK, IV>, |
| 210 | opts: SerializeOptions<IK, IV, K, V> = {}, |
| 211 | ) { |
| 212 | const { map = (k, v) => [k as unknown as K, v as unknown as V] } = opts; |
| 213 | const order = this.#order; |
| 214 | let prev: Order<K> | null = null; |
| 215 | let pk!: K; |
| 216 | for (let i = entries.length - 1; i > 0; i -= 1) { |
| 217 | const [ik, iv, size] = UNWRAP(entries[i]); |
| 218 | const [k, v] = map(ik, iv); |
| 219 | super.set(k, v); |
| 220 | const current: Order<K> = [size, none, prev ? pk : none, 0]; |
| 221 | if (prev) prev[1] = k; |
| 222 | order.set(k, prev = current); |
| 223 | pk = k; |
| 224 | } |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | /** The first item is the most recently used. */ |
| 229 | export type Serialized<K, V> = Array<[K, V, number]>; |
| 230 | |
| 231 | interface SerializeOptions<IK, IV, OK = IK, OV = IV> { |
| 232 | map?: (key: IK, value: IV) => [OK, OV]; |
| 123 | 233 | } |
| 124 | 234 | |
| 125 | 235 | /** |
| ... | ... | @@ -133,7 +243,13 @@ type Order<K> = [ |
| 133 | 243 | before: K | none, |
| 134 | 244 | /** Towards least recently used */ |
| 135 | 245 | after: K | none, |
| 246 | /** Locking prevents automatic eviction */ |
| 247 | locks: number, |
| 136 | 248 | ]; |
| 137 | 249 | |
| 250 | interface Lock extends Disposable { |
| 251 | (): void; |
| 252 | } |
| 253 | |
| 138 | 254 | const none: unique symbol = Symbol(); |
| 139 | 255 | type none = typeof none; |