@twin.org/core 0.9.3-next.1 → 0.9.3-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/factories/facadeFactory.js +9 -0
- package/dist/es/factories/facadeFactory.js.map +1 -0
- package/dist/es/factories/factory.js +137 -6
- package/dist/es/factories/factory.js.map +1 -1
- package/dist/es/helpers/timeoutHelper.js +46 -0
- package/dist/es/helpers/timeoutHelper.js.map +1 -0
- package/dist/es/index.js +4 -0
- package/dist/es/index.js.map +1 -1
- package/dist/es/models/IFacade.js +4 -0
- package/dist/es/models/IFacade.js.map +1 -0
- package/dist/es/models/IMutexWaiter.js +4 -0
- package/dist/es/models/IMutexWaiter.js.map +1 -0
- package/dist/es/utils/lfuCache.js +91 -13
- package/dist/es/utils/lfuCache.js.map +1 -1
- package/dist/es/utils/lruCache.js +83 -10
- package/dist/es/utils/lruCache.js.map +1 -1
- package/dist/es/utils/mutex.js +170 -23
- package/dist/es/utils/mutex.js.map +1 -1
- package/dist/types/factories/facadeFactory.d.ts +6 -0
- package/dist/types/factories/factory.d.ts +22 -2
- package/dist/types/helpers/timeoutHelper.d.ts +17 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/models/IFacade.d.ts +12 -0
- package/dist/types/models/IMutexWaiter.d.ts +17 -0
- package/dist/types/utils/lfuCache.d.ts +8 -2
- package/dist/types/utils/lruCache.d.ts +8 -2
- package/dist/types/utils/mutex.d.ts +9 -0
- package/docs/changelog.md +170 -0
- package/docs/examples.md +38 -1
- package/docs/reference/classes/Factory.md +80 -2
- package/docs/reference/classes/LfuCache.md +18 -2
- package/docs/reference/classes/LruCache.md +18 -2
- package/docs/reference/classes/Mutex.md +9 -0
- package/docs/reference/classes/TimeoutHelper.md +57 -0
- package/docs/reference/index.md +4 -0
- package/docs/reference/interfaces/IFacade.md +32 -0
- package/docs/reference/interfaces/IMutexWaiter.md +37 -0
- package/docs/reference/variables/FacadeFactory.md +5 -0
- package/locales/en.json +5 -2
- package/package.json +2 -2
|
@@ -14,6 +14,8 @@ import { RandomHelper } from "../helpers/randomHelper.js";
|
|
|
14
14
|
* The timer only runs while there are entries; it stops automatically when the cache empties.
|
|
15
15
|
*
|
|
16
16
|
* `get` and `set` increment an entry's access frequency and reset its idle timer.
|
|
17
|
+
* `set` and `getOrSet` accept an optional hard expiry timestamp; the entry is removed once that
|
|
18
|
+
* time is reached however recently it was used, and the TTI still applies alongside it.
|
|
17
19
|
* `has` and `keys` are pure peeks they evict idle entries but do not affect frequency or TTI.
|
|
18
20
|
* Call `destroy` when the cache is no longer needed to stop the background timer.
|
|
19
21
|
*/
|
|
@@ -65,6 +67,16 @@ export class LfuCache {
|
|
|
65
67
|
* @internal
|
|
66
68
|
*/
|
|
67
69
|
_minFreq;
|
|
70
|
+
/**
|
|
71
|
+
* The earliest hard expiry timestamp among the live entries, used to pace the sweep timer.
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
_nextExpires;
|
|
75
|
+
/**
|
|
76
|
+
* The timestamp the pending sweep is due to run at.
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
_scheduledDueAt;
|
|
68
80
|
/**
|
|
69
81
|
* Handle for the pending idle-sweep timeout, or undefined if no timer is scheduled.
|
|
70
82
|
* @internal
|
|
@@ -103,6 +115,8 @@ export class LfuCache {
|
|
|
103
115
|
this._keyMap = new Map();
|
|
104
116
|
this._freqMap = new Map();
|
|
105
117
|
this._minFreq = 0;
|
|
118
|
+
this._nextExpires = undefined;
|
|
119
|
+
this._scheduledDueAt = 0;
|
|
106
120
|
this._sweepTimer = undefined;
|
|
107
121
|
}
|
|
108
122
|
/**
|
|
@@ -120,11 +134,12 @@ export class LfuCache {
|
|
|
120
134
|
* @returns The cached value, or undefined on a miss or idle eviction.
|
|
121
135
|
*/
|
|
122
136
|
get(key) {
|
|
137
|
+
Guards.stringValue(LfuCache.CLASS_NAME, "key", key);
|
|
123
138
|
const entry = this._keyMap.get(key);
|
|
124
|
-
if (entry
|
|
139
|
+
if (Is.empty(entry)) {
|
|
125
140
|
return undefined;
|
|
126
141
|
}
|
|
127
|
-
if (
|
|
142
|
+
if (this.isExpired(entry, Date.now())) {
|
|
128
143
|
this.removeEntry(key);
|
|
129
144
|
return undefined;
|
|
130
145
|
}
|
|
@@ -138,17 +153,26 @@ export class LfuCache {
|
|
|
138
153
|
* least-frequently-used entry is evicted (LRU among ties).
|
|
139
154
|
* @param key The key to store.
|
|
140
155
|
* @param value The value to cache.
|
|
156
|
+
* @param expires Hard expiry timestamp in milliseconds since the epoch. The entry is removed
|
|
157
|
+
* once this time is reached regardless of how recently it was used. Must be an integer.
|
|
141
158
|
*/
|
|
142
|
-
set(key, value) {
|
|
159
|
+
set(key, value, expires) {
|
|
160
|
+
Guards.stringValue(LfuCache.CLASS_NAME, "key", key);
|
|
161
|
+
if (!Is.empty(expires)) {
|
|
162
|
+
Guards.integer(LfuCache.CLASS_NAME, "expires", expires);
|
|
163
|
+
}
|
|
143
164
|
const existing = this._keyMap.get(key);
|
|
144
165
|
if (existing !== undefined) {
|
|
145
|
-
if (
|
|
146
|
-
// Idle: evict and fall through to add as a fresh entry
|
|
166
|
+
if (this.isExpired(existing, Date.now())) {
|
|
167
|
+
// Idle or expired: evict and fall through to add as a fresh entry
|
|
147
168
|
this.removeEntry(key);
|
|
148
169
|
}
|
|
149
170
|
else {
|
|
150
171
|
existing.value = value;
|
|
172
|
+
existing.expires = expires;
|
|
173
|
+
this.trackExpires(expires);
|
|
151
174
|
this.promote(key, existing);
|
|
175
|
+
this.startTimer();
|
|
152
176
|
return;
|
|
153
177
|
}
|
|
154
178
|
}
|
|
@@ -158,7 +182,7 @@ export class LfuCache {
|
|
|
158
182
|
if (this._keyMap.size >= this._capacity) {
|
|
159
183
|
this.evictLfu();
|
|
160
184
|
}
|
|
161
|
-
const entry = { value, freq: 1, lastAccessed: Date.now() };
|
|
185
|
+
const entry = { value, freq: 1, lastAccessed: Date.now(), expires };
|
|
162
186
|
this._keyMap.set(key, entry);
|
|
163
187
|
let bucket = this._freqMap.get(1);
|
|
164
188
|
if (bucket === undefined) {
|
|
@@ -167,6 +191,7 @@ export class LfuCache {
|
|
|
167
191
|
}
|
|
168
192
|
bucket.add(key);
|
|
169
193
|
this._minFreq = 1;
|
|
194
|
+
this.trackExpires(expires);
|
|
170
195
|
this.startTimer();
|
|
171
196
|
}
|
|
172
197
|
/**
|
|
@@ -174,11 +199,16 @@ export class LfuCache {
|
|
|
174
199
|
* Concurrent calls for the same key are serialized via a mutex.
|
|
175
200
|
* @param key The key to get or create.
|
|
176
201
|
* @param valueFactory Async callback used to build a value when the key is absent.
|
|
202
|
+
* @param expires Hard expiry timestamp in milliseconds since the epoch, applied to the entry
|
|
203
|
+
* when one is created. Must be an integer.
|
|
177
204
|
* @returns The existing or newly created value.
|
|
178
205
|
*/
|
|
179
|
-
async getOrSet(key, valueFactory) {
|
|
206
|
+
async getOrSet(key, valueFactory, expires) {
|
|
180
207
|
Guards.stringValue(LfuCache.CLASS_NAME, "key", key);
|
|
181
208
|
Guards.function(LfuCache.CLASS_NAME, "valueFactory", valueFactory);
|
|
209
|
+
if (!Is.empty(expires)) {
|
|
210
|
+
Guards.integer(LfuCache.CLASS_NAME, "expires", expires);
|
|
211
|
+
}
|
|
182
212
|
const mutexKey = `${this._mutexScope}:${key}`;
|
|
183
213
|
await Mutex.lock(mutexKey, {
|
|
184
214
|
timeoutMs: this._mutexTimeoutMs,
|
|
@@ -189,7 +219,7 @@ export class LfuCache {
|
|
|
189
219
|
return this.get(key);
|
|
190
220
|
}
|
|
191
221
|
const value = await valueFactory();
|
|
192
|
-
this.set(key, value);
|
|
222
|
+
this.set(key, value, expires);
|
|
193
223
|
return value;
|
|
194
224
|
}
|
|
195
225
|
finally {
|
|
@@ -203,11 +233,12 @@ export class LfuCache {
|
|
|
203
233
|
* @returns True if the key is present and not idle.
|
|
204
234
|
*/
|
|
205
235
|
has(key) {
|
|
236
|
+
Guards.stringValue(LfuCache.CLASS_NAME, "key", key);
|
|
206
237
|
const entry = this._keyMap.get(key);
|
|
207
238
|
if (entry === undefined) {
|
|
208
239
|
return false;
|
|
209
240
|
}
|
|
210
|
-
if (
|
|
241
|
+
if (this.isExpired(entry, Date.now())) {
|
|
211
242
|
this.removeEntry(key);
|
|
212
243
|
return false;
|
|
213
244
|
}
|
|
@@ -222,7 +253,7 @@ export class LfuCache {
|
|
|
222
253
|
keys() {
|
|
223
254
|
const now = Date.now();
|
|
224
255
|
for (const [k, entry] of this._keyMap) {
|
|
225
|
-
if (
|
|
256
|
+
if (this.isExpired(entry, now)) {
|
|
226
257
|
this.removeEntry(k);
|
|
227
258
|
}
|
|
228
259
|
}
|
|
@@ -244,6 +275,7 @@ export class LfuCache {
|
|
|
244
275
|
* @param key The key to remove.
|
|
245
276
|
*/
|
|
246
277
|
delete(key) {
|
|
278
|
+
Guards.stringValue(LfuCache.CLASS_NAME, "key", key);
|
|
247
279
|
this.removeEntry(key);
|
|
248
280
|
if (this._keyMap.size === 0) {
|
|
249
281
|
this.cancelTimer();
|
|
@@ -257,6 +289,7 @@ export class LfuCache {
|
|
|
257
289
|
this._keyMap.clear();
|
|
258
290
|
this._freqMap.clear();
|
|
259
291
|
this._minFreq = 0;
|
|
292
|
+
this._nextExpires = undefined;
|
|
260
293
|
}
|
|
261
294
|
/**
|
|
262
295
|
* Stop the background idle-sweep timer and release all entries.
|
|
@@ -266,6 +299,8 @@ export class LfuCache {
|
|
|
266
299
|
this.cancelTimer();
|
|
267
300
|
this._keyMap.clear();
|
|
268
301
|
this._freqMap.clear();
|
|
302
|
+
this._minFreq = 0;
|
|
303
|
+
this._nextExpires = undefined;
|
|
269
304
|
}
|
|
270
305
|
/**
|
|
271
306
|
* Increment the frequency of an entry and move it to the correct frequency bucket.
|
|
@@ -275,6 +310,7 @@ export class LfuCache {
|
|
|
275
310
|
* @param entry.value The cached value.
|
|
276
311
|
* @param entry.freq The current access frequency.
|
|
277
312
|
* @param entry.lastAccessed The last-accessed timestamp in milliseconds.
|
|
313
|
+
* @param entry.expires The hard expiry timestamp in milliseconds, or undefined for none.
|
|
278
314
|
* @internal
|
|
279
315
|
*/
|
|
280
316
|
promote(key, entry) {
|
|
@@ -350,21 +386,63 @@ export class LfuCache {
|
|
|
350
386
|
sweepIdle() {
|
|
351
387
|
this.cancelTimer();
|
|
352
388
|
const now = Date.now();
|
|
389
|
+
let nextExpires;
|
|
353
390
|
for (const [k, entry] of this._keyMap) {
|
|
354
|
-
if (
|
|
391
|
+
if (this.isExpired(entry, now)) {
|
|
355
392
|
this.removeEntry(k);
|
|
356
393
|
}
|
|
394
|
+
else if (Is.notEmpty(entry.expires) &&
|
|
395
|
+
(Is.empty(nextExpires) || entry.expires < nextExpires)) {
|
|
396
|
+
nextExpires = entry.expires;
|
|
397
|
+
}
|
|
357
398
|
}
|
|
399
|
+
this._nextExpires = nextExpires;
|
|
358
400
|
if (this._keyMap.size > 0) {
|
|
359
401
|
this.startTimer();
|
|
360
402
|
}
|
|
361
403
|
}
|
|
362
404
|
/**
|
|
363
|
-
*
|
|
405
|
+
* Record an entry expiry timestamp if it is earlier than the currently tracked one.
|
|
406
|
+
* @param expires The expiry timestamp in milliseconds, or undefined for none.
|
|
407
|
+
* @internal
|
|
408
|
+
*/
|
|
409
|
+
trackExpires(expires) {
|
|
410
|
+
if (Is.notEmpty(expires) && (Is.empty(this._nextExpires) || expires < this._nextExpires)) {
|
|
411
|
+
this._nextExpires = expires;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Determine whether an entry has idled out or reached its hard expiry timestamp.
|
|
416
|
+
* @param entry The entry to test.
|
|
417
|
+
* @param entry.lastAccessed The last-accessed timestamp in milliseconds.
|
|
418
|
+
* @param entry.expires The hard expiry timestamp in milliseconds, or undefined for none.
|
|
419
|
+
* @param now The current time in milliseconds.
|
|
420
|
+
* @returns True if the entry should be removed.
|
|
421
|
+
* @internal
|
|
422
|
+
*/
|
|
423
|
+
isExpired(entry, now) {
|
|
424
|
+
return (now - entry.lastAccessed >= this._ttiMs ||
|
|
425
|
+
(Is.notEmpty(entry.expires) && now >= entry.expires));
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Schedule the next sweep if no timer is already pending, bringing a pending one forward
|
|
429
|
+
* when an entry with an earlier hard expiry has since been added.
|
|
364
430
|
* @internal
|
|
365
431
|
*/
|
|
366
432
|
startTimer() {
|
|
367
|
-
|
|
433
|
+
const now = Date.now();
|
|
434
|
+
let delay = this._ttiMs;
|
|
435
|
+
if (Is.notEmpty(this._nextExpires)) {
|
|
436
|
+
delay = Math.min(delay, Math.max(0, this._nextExpires - now));
|
|
437
|
+
}
|
|
438
|
+
if (Is.empty(this._sweepTimer)) {
|
|
439
|
+
this._scheduledDueAt = now + delay;
|
|
440
|
+
this._sweepTimer = setTimeout(() => this.sweepIdle(), delay);
|
|
441
|
+
}
|
|
442
|
+
else if (now + delay < this._scheduledDueAt) {
|
|
443
|
+
this.cancelTimer();
|
|
444
|
+
this.startTimer();
|
|
445
|
+
}
|
|
368
446
|
}
|
|
369
447
|
/**
|
|
370
448
|
* Cancel the pending idle-sweep timer.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lfuCache.js","sourceRoot":"","sources":["../../../src/utils/lfuCache.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { nameof } from \"@twin.org/nameof\";\nimport { Guards } from \"./guards.js\";\nimport { Is } from \"./is.js\";\nimport { Mutex } from \"./mutex.js\";\nimport { Validation } from \"./validation.js\";\nimport { RandomHelper } from \"../helpers/randomHelper.js\";\nimport type { IValidationFailure } from \"../models/IValidationFailure.js\";\n\n/**\n * A fixed-capacity LFU cache with time-to-idle eviction.\n *\n * Entries are removed in two ways:\n * - Capacity eviction: when the cache is full the least-frequently-used entry is removed first.\n * Ties in frequency are broken by recency the least-recently-used entry among those with the\n * minimum frequency is evicted.\n * - TTI eviction: a background timer sweeps idle entries every ttiMs milliseconds.\n * The timer only runs while there are entries; it stops automatically when the cache empties.\n *\n * `get` and `set` increment an entry's access frequency and reset its idle timer.\n * `has` and `keys` are pure peeks they evict idle entries but do not affect frequency or TTI.\n * Call `destroy` when the cache is no longer needed to stop the background timer.\n */\nexport class LfuCache<T> {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<LfuCache<unknown>>();\n\n\t/**\n\t * Default capacity.\n\t */\n\tpublic static readonly DEFAULT_CAPACITY = 1000;\n\n\t/**\n\t * Default time-to-idle in milliseconds.\n\t */\n\tpublic static readonly DEFAULT_TTI_MS = 10000;\n\n\t/**\n\t * The maximum number of entries the cache will hold.\n\t * @internal\n\t */\n\tprivate readonly _capacity: number;\n\n\t/**\n\t * The idle duration in milliseconds after which an untouched entry is evicted.\n\t * @internal\n\t */\n\tprivate readonly _ttiMs: number;\n\n\t/**\n\t * Optional timeout in milliseconds for mutex acquisition.\n\t * @internal\n\t */\n\tprivate readonly _mutexTimeoutMs: number | undefined;\n\n\t/**\n\t * Per-instance namespace prefix for mutex keys.\n\t * @internal\n\t */\n\tprivate readonly _mutexScope: string;\n\n\t/**\n\t * Maps each key to its cached value, access frequency, and last-accessed timestamp.\n\t * @internal\n\t */\n\tprivate readonly _keyMap: Map<string, { value: T; freq: number; lastAccessed: number }>;\n\n\t/**\n\t * Maps each frequency to the ordered set of keys at that frequency (insertion order = LRU).\n\t * @internal\n\t */\n\tprivate readonly _freqMap: Map<number, Set<string>>;\n\n\t/**\n\t * The lowest frequency among all live entries. Maintained to make eviction O(1).\n\t * @internal\n\t */\n\tprivate _minFreq: number;\n\n\t/**\n\t * Handle for the pending idle-sweep timeout, or undefined if no timer is scheduled.\n\t * @internal\n\t */\n\tprivate _sweepTimer: ReturnType<typeof setTimeout> | undefined;\n\n\t/**\n\t * Create a new instance of LfuCache.\n\t * @param options The cache options.\n\t * @param options.capacity Maximum number of entries. Defaults to 1000. Must be a positive integer.\n\t * @param options.ttiMs Time-to-idle in milliseconds. Defaults to 10000. Must be a positive integer.\n\t * @param options.mutexTimeoutMs Maximum time in milliseconds to wait for getOrSet mutex acquisition.\n\t * @throws ValidationError if capacity or ttiMs is not a positive integer.\n\t */\n\tconstructor(options?: { capacity?: number; ttiMs?: number; mutexTimeoutMs?: number }) {\n\t\tconst capacity = options?.capacity ?? LfuCache.DEFAULT_CAPACITY;\n\t\tconst ttiMs = options?.ttiMs ?? LfuCache.DEFAULT_TTI_MS;\n\t\tconst mutexTimeoutMs = options?.mutexTimeoutMs;\n\n\t\tGuards.integer(LfuCache.CLASS_NAME, nameof(capacity), capacity);\n\t\tGuards.integer(LfuCache.CLASS_NAME, nameof(ttiMs), ttiMs);\n\t\tif (Is.notEmpty(mutexTimeoutMs)) {\n\t\t\tGuards.integer(LfuCache.CLASS_NAME, nameof(mutexTimeoutMs), mutexTimeoutMs);\n\t\t}\n\n\t\tconst failures: IValidationFailure[] = [];\n\t\tValidation.integer(nameof(capacity), capacity, failures, undefined, { minValue: 1 });\n\t\tValidation.integer(nameof(ttiMs), ttiMs, failures, undefined, { minValue: 1 });\n\t\tif (Is.notEmpty(mutexTimeoutMs)) {\n\t\t\tValidation.integer(nameof(mutexTimeoutMs), mutexTimeoutMs, failures, undefined, {\n\t\t\t\tminValue: 0\n\t\t\t});\n\t\t}\n\t\tValidation.asValidationError(LfuCache.CLASS_NAME, nameof<LfuCache<unknown>>(), failures);\n\n\t\tthis._capacity = capacity;\n\t\tthis._ttiMs = ttiMs;\n\t\tthis._mutexTimeoutMs = mutexTimeoutMs;\n\t\tthis._mutexScope = `${LfuCache.CLASS_NAME}:${RandomHelper.generateUuidV7()}`;\n\t\tthis._keyMap = new Map();\n\t\tthis._freqMap = new Map();\n\t\tthis._minFreq = 0;\n\t\tthis._sweepTimer = undefined;\n\t}\n\n\t/**\n\t * The number of entries currently held in the cache.\n\t * @returns The number of entries in the cache.\n\t */\n\tpublic count(): number {\n\t\treturn this._keyMap.size;\n\t}\n\n\t/**\n\t * Get a value from the cache.\n\t * Returns undefined if the key is absent or the entry has idled out.\n\t * A successful hit increments the entry's frequency and resets its idle timer.\n\t * @param key The key to retrieve.\n\t * @returns The cached value, or undefined on a miss or idle eviction.\n\t */\n\tpublic get(key: string): T | undefined {\n\t\tconst entry = this._keyMap.get(key);\n\t\tif (entry === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (Date.now() - entry.lastAccessed >= this._ttiMs) {\n\t\t\tthis.removeEntry(key);\n\t\t\treturn undefined;\n\t\t}\n\t\tthis.promote(key, entry);\n\t\treturn entry.value;\n\t}\n\n\t/**\n\t * Store a value in the cache.\n\t * If the key already exists its value and frequency are updated.\n\t * When the cache is at capacity, idle entries are swept first; if it is still full the\n\t * least-frequently-used entry is evicted (LRU among ties).\n\t * @param key The key to store.\n\t * @param value The value to cache.\n\t */\n\tpublic set(key: string, value: T): void {\n\t\tconst existing = this._keyMap.get(key);\n\t\tif (existing !== undefined) {\n\t\t\tif (Date.now() - existing.lastAccessed >= this._ttiMs) {\n\t\t\t\t// Idle: evict and fall through to add as a fresh entry\n\t\t\t\tthis.removeEntry(key);\n\t\t\t} else {\n\t\t\t\texisting.value = value;\n\t\t\t\tthis.promote(key, existing);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (this._keyMap.size >= this._capacity) {\n\t\t\tthis.sweepIdle();\n\t\t}\n\t\tif (this._keyMap.size >= this._capacity) {\n\t\t\tthis.evictLfu();\n\t\t}\n\t\tconst entry = { value, freq: 1, lastAccessed: Date.now() };\n\t\tthis._keyMap.set(key, entry);\n\t\tlet bucket = this._freqMap.get(1);\n\t\tif (bucket === undefined) {\n\t\t\tbucket = new Set<string>();\n\t\t\tthis._freqMap.set(1, bucket);\n\t\t}\n\t\tbucket.add(key);\n\t\tthis._minFreq = 1;\n\t\tthis.startTimer();\n\t}\n\n\t/**\n\t * Atomically get an existing value or create and store it once using an async factory.\n\t * Concurrent calls for the same key are serialized via a mutex.\n\t * @param key The key to get or create.\n\t * @param valueFactory Async callback used to build a value when the key is absent.\n\t * @returns The existing or newly created value.\n\t */\n\tpublic async getOrSet(key: string, valueFactory: () => Promise<T>): Promise<T> {\n\t\tGuards.stringValue(LfuCache.CLASS_NAME, nameof(key), key);\n\t\tGuards.function(LfuCache.CLASS_NAME, nameof(valueFactory), valueFactory);\n\n\t\tconst mutexKey = `${this._mutexScope}:${key}`;\n\t\tawait Mutex.lock(mutexKey, {\n\t\t\ttimeoutMs: this._mutexTimeoutMs,\n\t\t\tthrowOnTimeout: true\n\t\t});\n\n\t\ttry {\n\t\t\tif (this.has(key)) {\n\t\t\t\treturn this.get(key) as T;\n\t\t\t}\n\n\t\t\tconst value = await valueFactory();\n\t\t\tthis.set(key, value);\n\t\t\treturn value;\n\t\t} finally {\n\t\t\tMutex.unlock(mutexKey);\n\t\t}\n\t}\n\n\t/**\n\t * Check whether a key exists in the cache and has not idled out.\n\t * Idle entries are evicted on peek, but a live entry's frequency and TTI are not updated.\n\t * @param key The key to test.\n\t * @returns True if the key is present and not idle.\n\t */\n\tpublic has(key: string): boolean {\n\t\tconst entry = this._keyMap.get(key);\n\t\tif (entry === undefined) {\n\t\t\treturn false;\n\t\t}\n\t\tif (Date.now() - entry.lastAccessed >= this._ttiMs) {\n\t\t\tthis.removeEntry(key);\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * Return all keys for entries that have not idled out.\n\t * Idle entries encountered during iteration are evicted.\n\t * Keys are returned in ascending frequency order; within the same frequency, LRU first.\n\t * @returns An array of live keys ordered from least-frequently-used to most-frequently-used.\n\t */\n\tpublic keys(): string[] {\n\t\tconst now = Date.now();\n\t\tfor (const [k, entry] of this._keyMap) {\n\t\t\tif (now - entry.lastAccessed >= this._ttiMs) {\n\t\t\t\tthis.removeEntry(k);\n\t\t\t}\n\t\t}\n\t\tconst result: string[] = [];\n\t\tconst sortedFreqs = [...this._freqMap.keys()].sort((a, b) => a - b);\n\t\tfor (const freq of sortedFreqs) {\n\t\t\tconst bucket = this._freqMap.get(freq);\n\t\t\tif (bucket !== undefined) {\n\t\t\t\tfor (const k of bucket) {\n\t\t\t\t\tresult.push(k);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Remove an entry from the cache.\n\t * Cancels the background timer if the cache becomes empty.\n\t * @param key The key to remove.\n\t */\n\tpublic delete(key: string): void {\n\t\tthis.removeEntry(key);\n\t\tif (this._keyMap.size === 0) {\n\t\t\tthis.cancelTimer();\n\t\t}\n\t}\n\n\t/**\n\t * Remove all entries from the cache and cancel the background timer.\n\t */\n\tpublic clear(): void {\n\t\tthis.cancelTimer();\n\t\tthis._keyMap.clear();\n\t\tthis._freqMap.clear();\n\t\tthis._minFreq = 0;\n\t}\n\n\t/**\n\t * Stop the background idle-sweep timer and release all entries.\n\t * The cache must not be used after this call.\n\t */\n\tpublic destroy(): void {\n\t\tthis.cancelTimer();\n\t\tthis._keyMap.clear();\n\t\tthis._freqMap.clear();\n\t}\n\n\t/**\n\t * Increment the frequency of an entry and move it to the correct frequency bucket.\n\t * Updates lastAccessed to now.\n\t * @param key The key to promote.\n\t * @param entry The entry object to update in place.\n\t * @param entry.value The cached value.\n\t * @param entry.freq The current access frequency.\n\t * @param entry.lastAccessed The last-accessed timestamp in milliseconds.\n\t * @internal\n\t */\n\tprivate promote(key: string, entry: { value: T; freq: number; lastAccessed: number }): void {\n\t\tconst oldFreq = entry.freq;\n\t\tconst oldBucket = this._freqMap.get(oldFreq);\n\t\tif (oldBucket !== undefined) {\n\t\t\toldBucket.delete(key);\n\t\t\tif (oldBucket.size === 0) {\n\t\t\t\tthis._freqMap.delete(oldFreq);\n\t\t\t\tif (oldFreq === this._minFreq) {\n\t\t\t\t\tthis._minFreq = oldFreq + 1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tentry.freq += 1;\n\t\tentry.lastAccessed = Date.now();\n\t\tlet newBucket = this._freqMap.get(entry.freq);\n\t\tif (newBucket === undefined) {\n\t\t\tnewBucket = new Set<string>();\n\t\t\tthis._freqMap.set(entry.freq, newBucket);\n\t\t}\n\t\tnewBucket.add(key);\n\t}\n\n\t/**\n\t * Evict the least-frequently-used entry, breaking ties by recency.\n\t * @internal\n\t */\n\tprivate evictLfu(): void {\n\t\tconst bucket = this._freqMap.get(this._minFreq);\n\t\tif (bucket === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tconst evictKey = bucket.values().next().value as string;\n\t\tbucket.delete(evictKey);\n\t\tif (bucket.size === 0) {\n\t\t\tthis._freqMap.delete(this._minFreq);\n\t\t}\n\t\tthis._keyMap.delete(evictKey);\n\t}\n\n\t/**\n\t * Remove an entry from both the key map and its frequency bucket.\n\t * Recalculates _minFreq if the removed entry was the last one at _minFreq.\n\t * @param key The key to remove.\n\t * @internal\n\t */\n\tprivate removeEntry(key: string): void {\n\t\tconst entry = this._keyMap.get(key);\n\t\tif (entry === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tthis._keyMap.delete(key);\n\t\tconst bucket = this._freqMap.get(entry.freq);\n\t\tif (bucket !== undefined) {\n\t\t\tbucket.delete(key);\n\t\t\tif (bucket.size === 0) {\n\t\t\t\tthis._freqMap.delete(entry.freq);\n\t\t\t\tif (entry.freq === this._minFreq) {\n\t\t\t\t\tlet newMin = Number.MAX_SAFE_INTEGER;\n\t\t\t\t\tfor (const f of this._freqMap.keys()) {\n\t\t\t\t\t\tif (f < newMin) {\n\t\t\t\t\t\t\tnewMin = f;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis._minFreq = newMin === Number.MAX_SAFE_INTEGER ? 1 : newMin;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Cancel the pending timer, sweep idle entries, then restart the timer if entries remain.\n\t * @internal\n\t */\n\tprivate sweepIdle(): void {\n\t\tthis.cancelTimer();\n\t\tconst now = Date.now();\n\t\tfor (const [k, entry] of this._keyMap) {\n\t\t\tif (now - entry.lastAccessed >= this._ttiMs) {\n\t\t\t\tthis.removeEntry(k);\n\t\t\t}\n\t\t}\n\t\tif (this._keyMap.size > 0) {\n\t\t\tthis.startTimer();\n\t\t}\n\t}\n\n\t/**\n\t * Schedule the next idle sweep if no timer is already pending.\n\t * @internal\n\t */\n\tprivate startTimer(): void {\n\t\tthis._sweepTimer ??= setTimeout(() => this.sweepIdle(), this._ttiMs);\n\t}\n\n\t/**\n\t * Cancel the pending idle-sweep timer.\n\t * @internal\n\t */\n\tprivate cancelTimer(): void {\n\t\tif (Is.notEmpty(this._sweepTimer)) {\n\t\t\tclearTimeout(this._sweepTimer);\n\t\t\tthis._sweepTimer = undefined;\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"lfuCache.js","sourceRoot":"","sources":["../../../src/utils/lfuCache.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { nameof } from \"@twin.org/nameof\";\nimport { Guards } from \"./guards.js\";\nimport { Is } from \"./is.js\";\nimport { Mutex } from \"./mutex.js\";\nimport { Validation } from \"./validation.js\";\nimport { RandomHelper } from \"../helpers/randomHelper.js\";\nimport type { IValidationFailure } from \"../models/IValidationFailure.js\";\n\n/**\n * A fixed-capacity LFU cache with time-to-idle eviction.\n *\n * Entries are removed in two ways:\n * - Capacity eviction: when the cache is full the least-frequently-used entry is removed first.\n * Ties in frequency are broken by recency the least-recently-used entry among those with the\n * minimum frequency is evicted.\n * - TTI eviction: a background timer sweeps idle entries every ttiMs milliseconds.\n * The timer only runs while there are entries; it stops automatically when the cache empties.\n *\n * `get` and `set` increment an entry's access frequency and reset its idle timer.\n * `set` and `getOrSet` accept an optional hard expiry timestamp; the entry is removed once that\n * time is reached however recently it was used, and the TTI still applies alongside it.\n * `has` and `keys` are pure peeks they evict idle entries but do not affect frequency or TTI.\n * Call `destroy` when the cache is no longer needed to stop the background timer.\n */\nexport class LfuCache<T> {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<LfuCache<unknown>>();\n\n\t/**\n\t * Default capacity.\n\t */\n\tpublic static readonly DEFAULT_CAPACITY = 1000;\n\n\t/**\n\t * Default time-to-idle in milliseconds.\n\t */\n\tpublic static readonly DEFAULT_TTI_MS = 10000;\n\n\t/**\n\t * The maximum number of entries the cache will hold.\n\t * @internal\n\t */\n\tprivate readonly _capacity: number;\n\n\t/**\n\t * The idle duration in milliseconds after which an untouched entry is evicted.\n\t * @internal\n\t */\n\tprivate readonly _ttiMs: number;\n\n\t/**\n\t * Optional timeout in milliseconds for mutex acquisition.\n\t * @internal\n\t */\n\tprivate readonly _mutexTimeoutMs: number | undefined;\n\n\t/**\n\t * Per-instance namespace prefix for mutex keys.\n\t * @internal\n\t */\n\tprivate readonly _mutexScope: string;\n\n\t/**\n\t * Maps each key to its cached value, access frequency, and last-accessed timestamp.\n\t * @internal\n\t */\n\tprivate readonly _keyMap: Map<\n\t\tstring,\n\t\t{ value: T; freq: number; lastAccessed: number; expires: number | undefined }\n\t>;\n\n\t/**\n\t * Maps each frequency to the ordered set of keys at that frequency (insertion order = LRU).\n\t * @internal\n\t */\n\tprivate readonly _freqMap: Map<number, Set<string>>;\n\n\t/**\n\t * The lowest frequency among all live entries. Maintained to make eviction O(1).\n\t * @internal\n\t */\n\tprivate _minFreq: number;\n\n\t/**\n\t * The earliest hard expiry timestamp among the live entries, used to pace the sweep timer.\n\t * @internal\n\t */\n\tprivate _nextExpires: number | undefined;\n\n\t/**\n\t * The timestamp the pending sweep is due to run at.\n\t * @internal\n\t */\n\tprivate _scheduledDueAt: number;\n\n\t/**\n\t * Handle for the pending idle-sweep timeout, or undefined if no timer is scheduled.\n\t * @internal\n\t */\n\tprivate _sweepTimer: ReturnType<typeof setTimeout> | undefined;\n\n\t/**\n\t * Create a new instance of LfuCache.\n\t * @param options The cache options.\n\t * @param options.capacity Maximum number of entries. Defaults to 1000. Must be a positive integer.\n\t * @param options.ttiMs Time-to-idle in milliseconds. Defaults to 10000. Must be a positive integer.\n\t * @param options.mutexTimeoutMs Maximum time in milliseconds to wait for getOrSet mutex acquisition.\n\t * @throws ValidationError if capacity or ttiMs is not a positive integer.\n\t */\n\tconstructor(options?: { capacity?: number; ttiMs?: number; mutexTimeoutMs?: number }) {\n\t\tconst capacity = options?.capacity ?? LfuCache.DEFAULT_CAPACITY;\n\t\tconst ttiMs = options?.ttiMs ?? LfuCache.DEFAULT_TTI_MS;\n\t\tconst mutexTimeoutMs = options?.mutexTimeoutMs;\n\n\t\tGuards.integer(LfuCache.CLASS_NAME, nameof(capacity), capacity);\n\t\tGuards.integer(LfuCache.CLASS_NAME, nameof(ttiMs), ttiMs);\n\t\tif (Is.notEmpty(mutexTimeoutMs)) {\n\t\t\tGuards.integer(LfuCache.CLASS_NAME, nameof(mutexTimeoutMs), mutexTimeoutMs);\n\t\t}\n\n\t\tconst failures: IValidationFailure[] = [];\n\t\tValidation.integer(nameof(capacity), capacity, failures, undefined, { minValue: 1 });\n\t\tValidation.integer(nameof(ttiMs), ttiMs, failures, undefined, { minValue: 1 });\n\t\tif (Is.notEmpty(mutexTimeoutMs)) {\n\t\t\tValidation.integer(nameof(mutexTimeoutMs), mutexTimeoutMs, failures, undefined, {\n\t\t\t\tminValue: 0\n\t\t\t});\n\t\t}\n\t\tValidation.asValidationError(LfuCache.CLASS_NAME, nameof<LfuCache<unknown>>(), failures);\n\n\t\tthis._capacity = capacity;\n\t\tthis._ttiMs = ttiMs;\n\t\tthis._mutexTimeoutMs = mutexTimeoutMs;\n\t\tthis._mutexScope = `${LfuCache.CLASS_NAME}:${RandomHelper.generateUuidV7()}`;\n\t\tthis._keyMap = new Map();\n\t\tthis._freqMap = new Map();\n\t\tthis._minFreq = 0;\n\t\tthis._nextExpires = undefined;\n\t\tthis._scheduledDueAt = 0;\n\t\tthis._sweepTimer = undefined;\n\t}\n\n\t/**\n\t * The number of entries currently held in the cache.\n\t * @returns The number of entries in the cache.\n\t */\n\tpublic count(): number {\n\t\treturn this._keyMap.size;\n\t}\n\n\t/**\n\t * Get a value from the cache.\n\t * Returns undefined if the key is absent or the entry has idled out.\n\t * A successful hit increments the entry's frequency and resets its idle timer.\n\t * @param key The key to retrieve.\n\t * @returns The cached value, or undefined on a miss or idle eviction.\n\t */\n\tpublic get(key: string): T | undefined {\n\t\tGuards.stringValue(LfuCache.CLASS_NAME, nameof(key), key);\n\n\t\tconst entry = this._keyMap.get(key);\n\t\tif (Is.empty(entry)) {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (this.isExpired(entry, Date.now())) {\n\t\t\tthis.removeEntry(key);\n\t\t\treturn undefined;\n\t\t}\n\t\tthis.promote(key, entry);\n\n\t\treturn entry.value;\n\t}\n\n\t/**\n\t * Store a value in the cache.\n\t * If the key already exists its value and frequency are updated.\n\t * When the cache is at capacity, idle entries are swept first; if it is still full the\n\t * least-frequently-used entry is evicted (LRU among ties).\n\t * @param key The key to store.\n\t * @param value The value to cache.\n\t * @param expires Hard expiry timestamp in milliseconds since the epoch. The entry is removed\n\t * once this time is reached regardless of how recently it was used. Must be an integer.\n\t */\n\tpublic set(key: string, value: T, expires?: number): void {\n\t\tGuards.stringValue(LfuCache.CLASS_NAME, nameof(key), key);\n\t\tif (!Is.empty(expires)) {\n\t\t\tGuards.integer(LfuCache.CLASS_NAME, nameof(expires), expires);\n\t\t}\n\n\t\tconst existing = this._keyMap.get(key);\n\t\tif (existing !== undefined) {\n\t\t\tif (this.isExpired(existing, Date.now())) {\n\t\t\t\t// Idle or expired: evict and fall through to add as a fresh entry\n\t\t\t\tthis.removeEntry(key);\n\t\t\t} else {\n\t\t\t\texisting.value = value;\n\t\t\t\texisting.expires = expires;\n\t\t\t\tthis.trackExpires(expires);\n\t\t\t\tthis.promote(key, existing);\n\t\t\t\tthis.startTimer();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (this._keyMap.size >= this._capacity) {\n\t\t\tthis.sweepIdle();\n\t\t}\n\t\tif (this._keyMap.size >= this._capacity) {\n\t\t\tthis.evictLfu();\n\t\t}\n\t\tconst entry = { value, freq: 1, lastAccessed: Date.now(), expires };\n\t\tthis._keyMap.set(key, entry);\n\t\tlet bucket = this._freqMap.get(1);\n\t\tif (bucket === undefined) {\n\t\t\tbucket = new Set<string>();\n\t\t\tthis._freqMap.set(1, bucket);\n\t\t}\n\t\tbucket.add(key);\n\t\tthis._minFreq = 1;\n\t\tthis.trackExpires(expires);\n\t\tthis.startTimer();\n\t}\n\n\t/**\n\t * Atomically get an existing value or create and store it once using an async factory.\n\t * Concurrent calls for the same key are serialized via a mutex.\n\t * @param key The key to get or create.\n\t * @param valueFactory Async callback used to build a value when the key is absent.\n\t * @param expires Hard expiry timestamp in milliseconds since the epoch, applied to the entry\n\t * when one is created. Must be an integer.\n\t * @returns The existing or newly created value.\n\t */\n\tpublic async getOrSet(key: string, valueFactory: () => Promise<T>, expires?: number): Promise<T> {\n\t\tGuards.stringValue(LfuCache.CLASS_NAME, nameof(key), key);\n\t\tGuards.function(LfuCache.CLASS_NAME, nameof(valueFactory), valueFactory);\n\t\tif (!Is.empty(expires)) {\n\t\t\tGuards.integer(LfuCache.CLASS_NAME, nameof(expires), expires);\n\t\t}\n\n\t\tconst mutexKey = `${this._mutexScope}:${key}`;\n\t\tawait Mutex.lock(mutexKey, {\n\t\t\ttimeoutMs: this._mutexTimeoutMs,\n\t\t\tthrowOnTimeout: true\n\t\t});\n\n\t\ttry {\n\t\t\tif (this.has(key)) {\n\t\t\t\treturn this.get(key) as T;\n\t\t\t}\n\n\t\t\tconst value = await valueFactory();\n\t\t\tthis.set(key, value, expires);\n\t\t\treturn value;\n\t\t} finally {\n\t\t\tMutex.unlock(mutexKey);\n\t\t}\n\t}\n\n\t/**\n\t * Check whether a key exists in the cache and has not idled out.\n\t * Idle entries are evicted on peek, but a live entry's frequency and TTI are not updated.\n\t * @param key The key to test.\n\t * @returns True if the key is present and not idle.\n\t */\n\tpublic has(key: string): boolean {\n\t\tGuards.stringValue(LfuCache.CLASS_NAME, nameof(key), key);\n\t\tconst entry = this._keyMap.get(key);\n\t\tif (entry === undefined) {\n\t\t\treturn false;\n\t\t}\n\t\tif (this.isExpired(entry, Date.now())) {\n\t\t\tthis.removeEntry(key);\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * Return all keys for entries that have not idled out.\n\t * Idle entries encountered during iteration are evicted.\n\t * Keys are returned in ascending frequency order; within the same frequency, LRU first.\n\t * @returns An array of live keys ordered from least-frequently-used to most-frequently-used.\n\t */\n\tpublic keys(): string[] {\n\t\tconst now = Date.now();\n\t\tfor (const [k, entry] of this._keyMap) {\n\t\t\tif (this.isExpired(entry, now)) {\n\t\t\t\tthis.removeEntry(k);\n\t\t\t}\n\t\t}\n\t\tconst result: string[] = [];\n\t\tconst sortedFreqs = [...this._freqMap.keys()].sort((a, b) => a - b);\n\t\tfor (const freq of sortedFreqs) {\n\t\t\tconst bucket = this._freqMap.get(freq);\n\t\t\tif (bucket !== undefined) {\n\t\t\t\tfor (const k of bucket) {\n\t\t\t\t\tresult.push(k);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Remove an entry from the cache.\n\t * Cancels the background timer if the cache becomes empty.\n\t * @param key The key to remove.\n\t */\n\tpublic delete(key: string): void {\n\t\tGuards.stringValue(LfuCache.CLASS_NAME, nameof(key), key);\n\t\tthis.removeEntry(key);\n\t\tif (this._keyMap.size === 0) {\n\t\t\tthis.cancelTimer();\n\t\t}\n\t}\n\n\t/**\n\t * Remove all entries from the cache and cancel the background timer.\n\t */\n\tpublic clear(): void {\n\t\tthis.cancelTimer();\n\t\tthis._keyMap.clear();\n\t\tthis._freqMap.clear();\n\t\tthis._minFreq = 0;\n\t\tthis._nextExpires = undefined;\n\t}\n\n\t/**\n\t * Stop the background idle-sweep timer and release all entries.\n\t * The cache must not be used after this call.\n\t */\n\tpublic destroy(): void {\n\t\tthis.cancelTimer();\n\t\tthis._keyMap.clear();\n\t\tthis._freqMap.clear();\n\t\tthis._minFreq = 0;\n\t\tthis._nextExpires = undefined;\n\t}\n\n\t/**\n\t * Increment the frequency of an entry and move it to the correct frequency bucket.\n\t * Updates lastAccessed to now.\n\t * @param key The key to promote.\n\t * @param entry The entry object to update in place.\n\t * @param entry.value The cached value.\n\t * @param entry.freq The current access frequency.\n\t * @param entry.lastAccessed The last-accessed timestamp in milliseconds.\n\t * @param entry.expires The hard expiry timestamp in milliseconds, or undefined for none.\n\t * @internal\n\t */\n\tprivate promote(\n\t\tkey: string,\n\t\tentry: { value: T; freq: number; lastAccessed: number; expires: number | undefined }\n\t): void {\n\t\tconst oldFreq = entry.freq;\n\t\tconst oldBucket = this._freqMap.get(oldFreq);\n\t\tif (oldBucket !== undefined) {\n\t\t\toldBucket.delete(key);\n\t\t\tif (oldBucket.size === 0) {\n\t\t\t\tthis._freqMap.delete(oldFreq);\n\t\t\t\tif (oldFreq === this._minFreq) {\n\t\t\t\t\tthis._minFreq = oldFreq + 1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tentry.freq += 1;\n\t\tentry.lastAccessed = Date.now();\n\t\tlet newBucket = this._freqMap.get(entry.freq);\n\t\tif (newBucket === undefined) {\n\t\t\tnewBucket = new Set<string>();\n\t\t\tthis._freqMap.set(entry.freq, newBucket);\n\t\t}\n\t\tnewBucket.add(key);\n\t}\n\n\t/**\n\t * Evict the least-frequently-used entry, breaking ties by recency.\n\t * @internal\n\t */\n\tprivate evictLfu(): void {\n\t\tconst bucket = this._freqMap.get(this._minFreq);\n\t\tif (bucket === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tconst evictKey = bucket.values().next().value as string;\n\t\tbucket.delete(evictKey);\n\t\tif (bucket.size === 0) {\n\t\t\tthis._freqMap.delete(this._minFreq);\n\t\t}\n\t\tthis._keyMap.delete(evictKey);\n\t}\n\n\t/**\n\t * Remove an entry from both the key map and its frequency bucket.\n\t * Recalculates _minFreq if the removed entry was the last one at _minFreq.\n\t * @param key The key to remove.\n\t * @internal\n\t */\n\tprivate removeEntry(key: string): void {\n\t\tconst entry = this._keyMap.get(key);\n\t\tif (entry === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tthis._keyMap.delete(key);\n\t\tconst bucket = this._freqMap.get(entry.freq);\n\t\tif (bucket !== undefined) {\n\t\t\tbucket.delete(key);\n\t\t\tif (bucket.size === 0) {\n\t\t\t\tthis._freqMap.delete(entry.freq);\n\t\t\t\tif (entry.freq === this._minFreq) {\n\t\t\t\t\tlet newMin = Number.MAX_SAFE_INTEGER;\n\t\t\t\t\tfor (const f of this._freqMap.keys()) {\n\t\t\t\t\t\tif (f < newMin) {\n\t\t\t\t\t\t\tnewMin = f;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis._minFreq = newMin === Number.MAX_SAFE_INTEGER ? 1 : newMin;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Cancel the pending timer, sweep idle entries, then restart the timer if entries remain.\n\t * @internal\n\t */\n\tprivate sweepIdle(): void {\n\t\tthis.cancelTimer();\n\t\tconst now = Date.now();\n\t\tlet nextExpires: number | undefined;\n\t\tfor (const [k, entry] of this._keyMap) {\n\t\t\tif (this.isExpired(entry, now)) {\n\t\t\t\tthis.removeEntry(k);\n\t\t\t} else if (\n\t\t\t\tIs.notEmpty(entry.expires) &&\n\t\t\t\t(Is.empty(nextExpires) || entry.expires < nextExpires)\n\t\t\t) {\n\t\t\t\tnextExpires = entry.expires;\n\t\t\t}\n\t\t}\n\t\tthis._nextExpires = nextExpires;\n\t\tif (this._keyMap.size > 0) {\n\t\t\tthis.startTimer();\n\t\t}\n\t}\n\n\t/**\n\t * Record an entry expiry timestamp if it is earlier than the currently tracked one.\n\t * @param expires The expiry timestamp in milliseconds, or undefined for none.\n\t * @internal\n\t */\n\tprivate trackExpires(expires: number | undefined): void {\n\t\tif (Is.notEmpty(expires) && (Is.empty(this._nextExpires) || expires < this._nextExpires)) {\n\t\t\tthis._nextExpires = expires;\n\t\t}\n\t}\n\n\t/**\n\t * Determine whether an entry has idled out or reached its hard expiry timestamp.\n\t * @param entry The entry to test.\n\t * @param entry.lastAccessed The last-accessed timestamp in milliseconds.\n\t * @param entry.expires The hard expiry timestamp in milliseconds, or undefined for none.\n\t * @param now The current time in milliseconds.\n\t * @returns True if the entry should be removed.\n\t * @internal\n\t */\n\tprivate isExpired(\n\t\tentry: { lastAccessed: number; expires: number | undefined },\n\t\tnow: number\n\t): boolean {\n\t\treturn (\n\t\t\tnow - entry.lastAccessed >= this._ttiMs ||\n\t\t\t(Is.notEmpty(entry.expires) && now >= entry.expires)\n\t\t);\n\t}\n\n\t/**\n\t * Schedule the next sweep if no timer is already pending, bringing a pending one forward\n\t * when an entry with an earlier hard expiry has since been added.\n\t * @internal\n\t */\n\tprivate startTimer(): void {\n\t\tconst now = Date.now();\n\t\tlet delay = this._ttiMs;\n\t\tif (Is.notEmpty(this._nextExpires)) {\n\t\t\tdelay = Math.min(delay, Math.max(0, this._nextExpires - now));\n\t\t}\n\t\tif (Is.empty(this._sweepTimer)) {\n\t\t\tthis._scheduledDueAt = now + delay;\n\t\t\tthis._sweepTimer = setTimeout(() => this.sweepIdle(), delay);\n\t\t} else if (now + delay < this._scheduledDueAt) {\n\t\t\tthis.cancelTimer();\n\t\t\tthis.startTimer();\n\t\t}\n\t}\n\n\t/**\n\t * Cancel the pending idle-sweep timer.\n\t * @internal\n\t */\n\tprivate cancelTimer(): void {\n\t\tif (Is.notEmpty(this._sweepTimer)) {\n\t\t\tclearTimeout(this._sweepTimer);\n\t\t\tthis._sweepTimer = undefined;\n\t\t}\n\t}\n}\n"]}
|
|
@@ -12,6 +12,8 @@ import { RandomHelper } from "../helpers/randomHelper.js";
|
|
|
12
12
|
* The timer only runs while there are entries; it stops automatically when the cache empties.
|
|
13
13
|
*
|
|
14
14
|
* `get` and `set` both update an entry's LRU position and reset its idle timer.
|
|
15
|
+
* `set` and `getOrSet` accept an optional hard expiry timestamp; the entry is removed once that
|
|
16
|
+
* time is reached however recently it was used, and the TTI still applies alongside it.
|
|
15
17
|
* `has` is a pure peek it evicts idle entries but does not refresh a live entry's TTI.
|
|
16
18
|
* Call `destroy` when the cache is no longer needed to stop the background timer.
|
|
17
19
|
*/
|
|
@@ -53,6 +55,16 @@ export class LruCache {
|
|
|
53
55
|
* @internal
|
|
54
56
|
*/
|
|
55
57
|
_cache;
|
|
58
|
+
/**
|
|
59
|
+
* The earliest hard expiry timestamp among the live entries, used to pace the sweep timer.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
_nextExpires;
|
|
63
|
+
/**
|
|
64
|
+
* The timestamp the pending sweep is due to run at.
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
_scheduledDueAt;
|
|
56
68
|
/**
|
|
57
69
|
* Handle for the pending idle-sweep timeout, or undefined if no timer is scheduled.
|
|
58
70
|
* @internal
|
|
@@ -89,6 +101,8 @@ export class LruCache {
|
|
|
89
101
|
this._mutexTimeoutMs = mutexTimeoutMs;
|
|
90
102
|
this._mutexScope = `${LruCache.CLASS_NAME}:${RandomHelper.generateUuidV7()}`;
|
|
91
103
|
this._cache = new Map();
|
|
104
|
+
this._nextExpires = undefined;
|
|
105
|
+
this._scheduledDueAt = 0;
|
|
92
106
|
this._sweepTimer = undefined;
|
|
93
107
|
}
|
|
94
108
|
/**
|
|
@@ -106,12 +120,13 @@ export class LruCache {
|
|
|
106
120
|
* @returns The cached value, or undefined on a miss or idle eviction.
|
|
107
121
|
*/
|
|
108
122
|
get(key) {
|
|
123
|
+
Guards.stringValue(LruCache.CLASS_NAME, "key", key);
|
|
109
124
|
const entry = this._cache.get(key);
|
|
110
125
|
if (entry === undefined) {
|
|
111
126
|
return undefined;
|
|
112
127
|
}
|
|
113
128
|
const now = Date.now();
|
|
114
|
-
if (
|
|
129
|
+
if (this.isExpired(entry, now)) {
|
|
115
130
|
this._cache.delete(key);
|
|
116
131
|
return undefined;
|
|
117
132
|
}
|
|
@@ -128,8 +143,14 @@ export class LruCache {
|
|
|
128
143
|
* least-recently-used entry is evicted.
|
|
129
144
|
* @param key The key to store.
|
|
130
145
|
* @param value The value to cache.
|
|
146
|
+
* @param expires Hard expiry timestamp in milliseconds since the epoch. The entry is removed
|
|
147
|
+
* once this time is reached regardless of how recently it was used. Must be an integer.
|
|
131
148
|
*/
|
|
132
|
-
set(key, value) {
|
|
149
|
+
set(key, value, expires) {
|
|
150
|
+
Guards.stringValue(LruCache.CLASS_NAME, "key", key);
|
|
151
|
+
if (!Is.empty(expires)) {
|
|
152
|
+
Guards.integer(LruCache.CLASS_NAME, "expires", expires);
|
|
153
|
+
}
|
|
133
154
|
const now = Date.now();
|
|
134
155
|
// Remove any existing entry so the refreshed version is inserted at the end
|
|
135
156
|
this._cache.delete(key);
|
|
@@ -142,7 +163,8 @@ export class LruCache {
|
|
|
142
163
|
this._cache.delete(lruKey);
|
|
143
164
|
}
|
|
144
165
|
}
|
|
145
|
-
this._cache.set(key, { value, lastAccessed: now });
|
|
166
|
+
this._cache.set(key, { value, lastAccessed: now, expires });
|
|
167
|
+
this.trackExpires(expires);
|
|
146
168
|
this.startTimer();
|
|
147
169
|
}
|
|
148
170
|
/**
|
|
@@ -150,11 +172,16 @@ export class LruCache {
|
|
|
150
172
|
* Concurrent calls for the same key are serialized via a mutex.
|
|
151
173
|
* @param key The key to get or create.
|
|
152
174
|
* @param valueFactory Async callback used to build a value when the key is absent.
|
|
175
|
+
* @param expires Hard expiry timestamp in milliseconds since the epoch, applied to the entry
|
|
176
|
+
* when one is created. Must be an integer.
|
|
153
177
|
* @returns The existing or newly created value.
|
|
154
178
|
*/
|
|
155
|
-
async getOrSet(key, valueFactory) {
|
|
179
|
+
async getOrSet(key, valueFactory, expires) {
|
|
156
180
|
Guards.stringValue(LruCache.CLASS_NAME, "key", key);
|
|
157
181
|
Guards.function(LruCache.CLASS_NAME, "valueFactory", valueFactory);
|
|
182
|
+
if (!Is.empty(expires)) {
|
|
183
|
+
Guards.integer(LruCache.CLASS_NAME, "expires", expires);
|
|
184
|
+
}
|
|
158
185
|
const mutexKey = `${this._mutexScope}:${key}`;
|
|
159
186
|
await Mutex.lock(mutexKey, {
|
|
160
187
|
timeoutMs: this._mutexTimeoutMs,
|
|
@@ -165,7 +192,7 @@ export class LruCache {
|
|
|
165
192
|
return this.get(key);
|
|
166
193
|
}
|
|
167
194
|
const value = await valueFactory();
|
|
168
|
-
this.set(key, value);
|
|
195
|
+
this.set(key, value, expires);
|
|
169
196
|
return value;
|
|
170
197
|
}
|
|
171
198
|
finally {
|
|
@@ -179,11 +206,12 @@ export class LruCache {
|
|
|
179
206
|
* @returns True if the key is present and not idle.
|
|
180
207
|
*/
|
|
181
208
|
has(key) {
|
|
209
|
+
Guards.stringValue(LruCache.CLASS_NAME, "key", key);
|
|
182
210
|
const entry = this._cache.get(key);
|
|
183
211
|
if (entry === undefined) {
|
|
184
212
|
return false;
|
|
185
213
|
}
|
|
186
|
-
if (
|
|
214
|
+
if (this.isExpired(entry, Date.now())) {
|
|
187
215
|
this._cache.delete(key);
|
|
188
216
|
return false;
|
|
189
217
|
}
|
|
@@ -198,7 +226,7 @@ export class LruCache {
|
|
|
198
226
|
const now = Date.now();
|
|
199
227
|
const result = [];
|
|
200
228
|
for (const [k, entry] of this._cache) {
|
|
201
|
-
if (
|
|
229
|
+
if (this.isExpired(entry, now)) {
|
|
202
230
|
this._cache.delete(k);
|
|
203
231
|
}
|
|
204
232
|
else {
|
|
@@ -213,6 +241,7 @@ export class LruCache {
|
|
|
213
241
|
* @param key The key to remove.
|
|
214
242
|
*/
|
|
215
243
|
delete(key) {
|
|
244
|
+
Guards.stringValue(LruCache.CLASS_NAME, "key", key);
|
|
216
245
|
this._cache.delete(key);
|
|
217
246
|
if (this._cache.size === 0) {
|
|
218
247
|
this.cancelTimer();
|
|
@@ -224,6 +253,7 @@ export class LruCache {
|
|
|
224
253
|
clear() {
|
|
225
254
|
this.cancelTimer();
|
|
226
255
|
this._cache.clear();
|
|
256
|
+
this._nextExpires = undefined;
|
|
227
257
|
}
|
|
228
258
|
/**
|
|
229
259
|
* Stop the background idle-sweep timer and release all entries.
|
|
@@ -232,6 +262,7 @@ export class LruCache {
|
|
|
232
262
|
destroy() {
|
|
233
263
|
this.cancelTimer();
|
|
234
264
|
this._cache.clear();
|
|
265
|
+
this._nextExpires = undefined;
|
|
235
266
|
}
|
|
236
267
|
/**
|
|
237
268
|
* Delete all entries whose idle time has been exceeded, then restart the timer
|
|
@@ -241,21 +272,63 @@ export class LruCache {
|
|
|
241
272
|
sweepIdle() {
|
|
242
273
|
this.cancelTimer();
|
|
243
274
|
const now = Date.now();
|
|
275
|
+
let nextExpires;
|
|
244
276
|
for (const [k, entry] of this._cache) {
|
|
245
|
-
if (
|
|
277
|
+
if (this.isExpired(entry, now)) {
|
|
246
278
|
this._cache.delete(k);
|
|
247
279
|
}
|
|
280
|
+
else if (Is.notEmpty(entry.expires) &&
|
|
281
|
+
(Is.empty(nextExpires) || entry.expires < nextExpires)) {
|
|
282
|
+
nextExpires = entry.expires;
|
|
283
|
+
}
|
|
248
284
|
}
|
|
285
|
+
this._nextExpires = nextExpires;
|
|
249
286
|
if (this._cache.size > 0) {
|
|
250
287
|
this.startTimer();
|
|
251
288
|
}
|
|
252
289
|
}
|
|
253
290
|
/**
|
|
254
|
-
*
|
|
291
|
+
* Record an entry expiry timestamp if it is earlier than the currently tracked one.
|
|
292
|
+
* @param expires The expiry timestamp in milliseconds, or undefined for none.
|
|
293
|
+
* @internal
|
|
294
|
+
*/
|
|
295
|
+
trackExpires(expires) {
|
|
296
|
+
if (Is.notEmpty(expires) && (Is.empty(this._nextExpires) || expires < this._nextExpires)) {
|
|
297
|
+
this._nextExpires = expires;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Determine whether an entry has idled out or reached its hard expiry timestamp.
|
|
302
|
+
* @param entry The entry to test.
|
|
303
|
+
* @param entry.lastAccessed The last-accessed timestamp in milliseconds.
|
|
304
|
+
* @param entry.expires The hard expiry timestamp in milliseconds, or undefined for none.
|
|
305
|
+
* @param now The current time in milliseconds.
|
|
306
|
+
* @returns True if the entry should be removed.
|
|
307
|
+
* @internal
|
|
308
|
+
*/
|
|
309
|
+
isExpired(entry, now) {
|
|
310
|
+
return (now - entry.lastAccessed >= this._ttiMs ||
|
|
311
|
+
(Is.notEmpty(entry.expires) && now >= entry.expires));
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Schedule the next sweep if no timer is already pending, bringing a pending one forward
|
|
315
|
+
* when an entry with an earlier hard expiry has since been added.
|
|
255
316
|
* @internal
|
|
256
317
|
*/
|
|
257
318
|
startTimer() {
|
|
258
|
-
|
|
319
|
+
const now = Date.now();
|
|
320
|
+
let delay = this._ttiMs;
|
|
321
|
+
if (Is.notEmpty(this._nextExpires)) {
|
|
322
|
+
delay = Math.min(delay, Math.max(0, this._nextExpires - now));
|
|
323
|
+
}
|
|
324
|
+
if (Is.empty(this._sweepTimer)) {
|
|
325
|
+
this._scheduledDueAt = now + delay;
|
|
326
|
+
this._sweepTimer = setTimeout(() => this.sweepIdle(), delay);
|
|
327
|
+
}
|
|
328
|
+
else if (now + delay < this._scheduledDueAt) {
|
|
329
|
+
this.cancelTimer();
|
|
330
|
+
this.startTimer();
|
|
331
|
+
}
|
|
259
332
|
}
|
|
260
333
|
/**
|
|
261
334
|
* Cancel the pending idle-sweep timer.
|