| ... | @@ -19,6 +19,7 @@ export class Lru<K, V> extends Map<K, V> { | ... | @@ -19,6 +19,7 @@ export class Lru<K, V> extends Map<K, V> { |
| 19 | /** The least recently used */ | 19 | /** The least recently used */ |
| 20 | #tail: K | none = none; | 20 | #tail: K | none = none; |
| 21 | #used: number = 0; | 21 | #used: number = 0; |
| | 22 | #locked: number = 0; |
| 22 | #capacity: number; | 23 | #capacity: number; |
| 23 | #sizeFn: (value: V, key: K) => number; | 24 | #sizeFn: (value: V, key: K) => number; |
| 24 | #deleteFn: null | ((value: V, key: K) => void); | 25 | #deleteFn: null | ((value: V, key: K) => void); |
| ... | @@ -34,14 +35,16 @@ export class Lru<K, V> extends Map<K, V> { | ... | @@ -34,14 +35,16 @@ export class Lru<K, V> extends Map<K, V> { |
| 34 | } | 35 | } |
| 35 | | 36 | |
| 36 | override clear() { | 37 | override clear() { |
| 37 | this.clear(); | 38 | super.clear(); |
| 38 | this.#order.clear(); | 39 | this.#order.clear(); |
| 39 | this.#head = none; | 40 | this.#head = none; |
| 40 | this.#tail = none; | 41 | this.#tail = none; |
| 41 | this.#used = 0; | 42 | this.#used = 0; |
| 42 | } | 43 | } |
| 43 | override delete(key: K) { | 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 | ASSERT(this.#order.delete(key)); | 48 | ASSERT(this.#order.delete(key)); |
| 46 | ASSERT(super.delete(key)); | 49 | ASSERT(super.delete(key)); |
| 47 | return true; | 50 | return true; |
| ... | @@ -57,46 +60,103 @@ export class Lru<K, V> extends Map<K, V> { | ... | @@ -57,46 +60,103 @@ export class Lru<K, V> extends Map<K, V> { |
| 57 | return super.get(key) as V; | 60 | return super.get(key) as V; |
| 58 | } | 61 | } |
| 59 | override set(key: K, value: V) { | 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 | const entry = this.#evict(key); | 73 | const entry = this.#evict(key); |
| 61 | const valueSize = this.#sizeFn(value, key); | | |
| 62 | if (entry) { | 74 | if (entry) { |
| 63 | // relocate | 75 | // relocate |
| 64 | this.#used -= entry[0]; | 76 | this.#used -= entry[0]; |
| 65 | entry[0] = valueSize, entry[1] = none, entry[2] = this.#head; | 77 | const head = this.#head; |
| 66 | UNWRAP(this.#order.get(this.#head as K))[1] = key; | 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 | } else { | 82 | } else { |
| 68 | // insertion | 83 | // insertion |
| 69 | const { size } = this; | 84 | const { size: mapSize } = this; |
| 70 | const order: Order<K> = [valueSize, none, size > 0 ? this.#head : none]; | 85 | const order: Order<K> = [size, none, mapSize > 0 ? this.#head : none, 0]; |
| 71 | this.#order.set(key, order); | 86 | this.#order.set(key, order); |
| 72 | if (size === 0) this.#tail = key; | 87 | if (mapSize === 0) this.#tail = key; |
| 73 | else UNWRAP(this.#order.get(this.#head as K))[1] = key; | 88 | else if (this.#head !== none) { |
| | 89 | UNWRAP(this.#order.get(this.#head as K))[1] = key; |
| | 90 | } |
| 74 | } | 91 | } |
| | 92 | |
| 75 | this.#head = key; | 93 | this.#head = key; |
| 76 | this.#used += valueSize; | 94 | this.#used += size; |
| 77 | this.ensureUnusedCapacity(0); | 95 | try { |
| | 96 | this.ensureUnusedCapacity(0); |
| | 97 | } catch { |
| | 98 | ASSERT(false, "capacity is already verified"); |
| | 99 | } |
| 78 | super.set(key, value); | 100 | super.set(key, value); |
| 79 | return this; | 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 | /** Evict items until there are `unused` capacity slots. */ | 118 | /** Evict items until there are `unused` capacity slots. */ |
| 83 | ensureUnusedCapacity(unused: number) { | 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 | const needed = this.#used - this.#capacity + unused; | 126 | const needed = this.#used - this.#capacity + unused; |
| | 127 | if (needed <= 0) return; |
| 85 | let remain = needed; | 128 | let remain = needed; |
| 86 | const evictFn = this.#evictFn; | 129 | const evictFn = this.#evictFn; |
| 87 | const deleteFn = this.#deleteFn; | 130 | const deleteFn = this.#deleteFn; |
| 88 | while (remain > 0) { | 131 | const order = this.#order; |
| 89 | const key = this.#tail as K; | 132 | let key = this.#tail; |
| 90 | const tail = UNWRAP(this.#evict(key)); | 133 | loop: { |
| 91 | if (evictFn) evictFn(super.get(key) as V, key); | 134 | let hadLocks = false; |
| 92 | if (deleteFn) deleteFn(super.get(key) as V, key); | 135 | while (key !== none) { |
| 93 | ASSERT(this.#order.delete(key)); | 136 | const entry = UNWRAP(order.get(key)); |
| 94 | ASSERT(super.delete(key)); | 137 | if (entry[3] > 0) { |
| 95 | remain -= tail[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 | this.#used -= needed - remain; | 155 | this.#used -= needed - remain; |
| 98 | } | 156 | } |
| 99 | | 157 | |
| | 158 | hasEvictableCapacity() {} |
| | 159 | |
| 100 | /** Unlink an entry without removing it. */ | 160 | /** Unlink an entry without removing it. */ |
| 101 | #evict(key: K) { | 161 | #evict(key: K) { |
| 102 | const entry = this.#order.get(key); | 162 | const entry = this.#order.get(key); |
| ... | @@ -104,12 +164,10 @@ export class Lru<K, V> extends Map<K, V> { | ... | @@ -104,12 +164,10 @@ export class Lru<K, V> extends Map<K, V> { |
| 104 | const [, before, after] = entry; | 164 | const [, before, after] = entry; |
| 105 | if (before !== none) { | 165 | if (before !== none) { |
| 106 | UNWRAP(this.#order.get(before as K))[2] = after; | 166 | UNWRAP(this.#order.get(before as K))[2] = after; |
| 107 | if (after === none) this.#tail = before; | 167 | } else this.#head = after; |
| 108 | } | | |
| 109 | if (after !== none) { | 168 | if (after !== none) { |
| 110 | UNWRAP(this.#order.get(after as K))[1] = before; | 169 | UNWRAP(this.#order.get(after as K))[1] = before; |
| 111 | if (before === none) this.#head = after; | 170 | } else this.#tail = before; |
| 112 | } | | |
| 113 | return entry; | 171 | return entry; |
| 114 | } | 172 | } |
| 115 | | 173 | |
| ... | @@ -120,6 +178,58 @@ export class Lru<K, V> extends Map<K, V> { | ... | @@ -120,6 +178,58 @@ export class Lru<K, V> extends Map<K, V> { |
| 120 | this.#capacity = capacity; | 178 | this.#capacity = capacity; |
| 121 | this.ensureUnusedCapacity(0); | 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,7 +243,13 @@ type Order<K> = [ |
| 133 | before: K | none, | 243 | before: K | none, |
| 134 | /** Towards least recently used */ | 244 | /** Towards least recently used */ |
| 135 | after: K | none, | 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 | const none: unique symbol = Symbol(); | 254 | const none: unique symbol = Symbol(); |
| 139 | type none = typeof none; | 255 | type none = typeof none; |