@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.
Files changed (40) hide show
  1. package/dist/es/factories/facadeFactory.js +9 -0
  2. package/dist/es/factories/facadeFactory.js.map +1 -0
  3. package/dist/es/factories/factory.js +137 -6
  4. package/dist/es/factories/factory.js.map +1 -1
  5. package/dist/es/helpers/timeoutHelper.js +46 -0
  6. package/dist/es/helpers/timeoutHelper.js.map +1 -0
  7. package/dist/es/index.js +4 -0
  8. package/dist/es/index.js.map +1 -1
  9. package/dist/es/models/IFacade.js +4 -0
  10. package/dist/es/models/IFacade.js.map +1 -0
  11. package/dist/es/models/IMutexWaiter.js +4 -0
  12. package/dist/es/models/IMutexWaiter.js.map +1 -0
  13. package/dist/es/utils/lfuCache.js +91 -13
  14. package/dist/es/utils/lfuCache.js.map +1 -1
  15. package/dist/es/utils/lruCache.js +83 -10
  16. package/dist/es/utils/lruCache.js.map +1 -1
  17. package/dist/es/utils/mutex.js +170 -23
  18. package/dist/es/utils/mutex.js.map +1 -1
  19. package/dist/types/factories/facadeFactory.d.ts +6 -0
  20. package/dist/types/factories/factory.d.ts +22 -2
  21. package/dist/types/helpers/timeoutHelper.d.ts +17 -0
  22. package/dist/types/index.d.ts +4 -0
  23. package/dist/types/models/IFacade.d.ts +12 -0
  24. package/dist/types/models/IMutexWaiter.d.ts +17 -0
  25. package/dist/types/utils/lfuCache.d.ts +8 -2
  26. package/dist/types/utils/lruCache.d.ts +8 -2
  27. package/dist/types/utils/mutex.d.ts +9 -0
  28. package/docs/changelog.md +170 -0
  29. package/docs/examples.md +38 -1
  30. package/docs/reference/classes/Factory.md +80 -2
  31. package/docs/reference/classes/LfuCache.md +18 -2
  32. package/docs/reference/classes/LruCache.md +18 -2
  33. package/docs/reference/classes/Mutex.md +9 -0
  34. package/docs/reference/classes/TimeoutHelper.md +57 -0
  35. package/docs/reference/index.md +4 -0
  36. package/docs/reference/interfaces/IFacade.md +32 -0
  37. package/docs/reference/interfaces/IMutexWaiter.md +37 -0
  38. package/docs/reference/variables/FacadeFactory.md +5 -0
  39. package/locales/en.json +5 -2
  40. 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 === undefined) {
139
+ if (Is.empty(entry)) {
125
140
  return undefined;
126
141
  }
127
- if (Date.now() - entry.lastAccessed >= this._ttiMs) {
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 (Date.now() - existing.lastAccessed >= this._ttiMs) {
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 (Date.now() - entry.lastAccessed >= this._ttiMs) {
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 (now - entry.lastAccessed >= this._ttiMs) {
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 (now - entry.lastAccessed >= this._ttiMs) {
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
- * Schedule the next idle sweep if no timer is already pending.
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
- this._sweepTimer ??= setTimeout(() => this.sweepIdle(), this._ttiMs);
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 (now - entry.lastAccessed >= this._ttiMs) {
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 (Date.now() - entry.lastAccessed >= this._ttiMs) {
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 (now - entry.lastAccessed >= this._ttiMs) {
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 (now - entry.lastAccessed >= this._ttiMs) {
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
- * Schedule the next idle sweep if no timer is already pending.
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
- this._sweepTimer ??= setTimeout(() => this.sweepIdle(), this._ttiMs);
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.