@twin.org/core 0.9.3-next.2 → 0.9.3-next.4

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.
@@ -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.
@@ -1 +1 @@
1
- {"version":3,"file":"lruCache.js","sourceRoot":"","sources":["../../../src/utils/lruCache.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 LRU cache with time-to-idle eviction.\n *\n * Entries are removed in two ways:\n * - Capacity eviction: when the cache is full the least-recently-used entry is removed first.\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` both update an entry's LRU position and reset its idle timer.\n * `has` is a pure peek it evicts idle entries but does not refresh a live entry's TTI.\n * Call `destroy` when the cache is no longer needed to stop the background timer.\n */\nexport class LruCache<T = unknown> {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<LruCache>();\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 * Underlying storage; Map iteration order tracks LRU position (first = oldest).\n\t * @internal\n\t */\n\tprivate readonly _cache: Map<string, { value: T; lastAccessed: 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 LruCache.\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 ?? LruCache.DEFAULT_CAPACITY;\n\t\tconst ttiMs = options?.ttiMs ?? LruCache.DEFAULT_TTI_MS;\n\t\tconst mutexTimeoutMs = options?.mutexTimeoutMs;\n\n\t\tGuards.integer(LruCache.CLASS_NAME, nameof(capacity), capacity);\n\t\tGuards.integer(LruCache.CLASS_NAME, nameof(ttiMs), ttiMs);\n\t\tif (Is.notEmpty(mutexTimeoutMs)) {\n\t\t\tGuards.integer(LruCache.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(LruCache.CLASS_NAME, nameof<LruCache>(), failures);\n\n\t\tthis._capacity = capacity;\n\t\tthis._ttiMs = ttiMs;\n\t\tthis._mutexTimeoutMs = mutexTimeoutMs;\n\t\tthis._mutexScope = `${LruCache.CLASS_NAME}:${RandomHelper.generateUuidV7()}`;\n\t\tthis._cache = new Map();\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._cache.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 resets the entry's idle timer and moves it to most-recently-used.\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._cache.get(key);\n\t\tif (entry === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst now = Date.now();\n\t\tif (now - entry.lastAccessed >= this._ttiMs) {\n\t\t\tthis._cache.delete(key);\n\t\t\treturn undefined;\n\t\t}\n\t\t// Move to end of Map (most-recently-used) via delete + re-insert\n\t\tthis._cache.delete(key);\n\t\tentry.lastAccessed = now;\n\t\tthis._cache.set(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 idle timer are refreshed.\n\t * When the cache is at capacity, idle entries are swept first; if it is still full the\n\t * least-recently-used entry is evicted.\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 now = Date.now();\n\t\t// Remove any existing entry so the refreshed version is inserted at the end\n\t\tthis._cache.delete(key);\n\t\tif (this._cache.size >= this._capacity) {\n\t\t\tthis.sweepIdle();\n\t\t}\n\t\tif (this._cache.size >= this._capacity) {\n\t\t\tconst lruKey = this._cache.keys().next().value;\n\t\t\tif (lruKey !== undefined) {\n\t\t\t\tthis._cache.delete(lruKey);\n\t\t\t}\n\t\t}\n\t\tthis._cache.set(key, { value, lastAccessed: now });\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(LruCache.CLASS_NAME, nameof(key), key);\n\t\tGuards.function(LruCache.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 TTI is not reset.\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._cache.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._cache.delete(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 * @returns An array of live keys in least-recently-used to most-recently-used order.\n\t */\n\tpublic keys(): string[] {\n\t\tconst now = Date.now();\n\t\tconst result: string[] = [];\n\t\tfor (const [k, entry] of this._cache) {\n\t\t\tif (now - entry.lastAccessed >= this._ttiMs) {\n\t\t\t\tthis._cache.delete(k);\n\t\t\t} else {\n\t\t\t\tresult.push(k);\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._cache.delete(key);\n\t\tif (this._cache.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._cache.clear();\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._cache.clear();\n\t}\n\n\t/**\n\t * Delete all entries whose idle time has been exceeded, then restart the timer\n\t * if any 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._cache) {\n\t\t\tif (now - entry.lastAccessed >= this._ttiMs) {\n\t\t\t\tthis._cache.delete(k);\n\t\t\t}\n\t\t}\n\t\tif (this._cache.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":"lruCache.js","sourceRoot":"","sources":["../../../src/utils/lruCache.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 LRU cache with time-to-idle eviction.\n *\n * Entries are removed in two ways:\n * - Capacity eviction: when the cache is full the least-recently-used entry is removed first.\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` both update an entry's LRU position 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` is a pure peek it evicts idle entries but does not refresh a live entry's TTI.\n * Call `destroy` when the cache is no longer needed to stop the background timer.\n */\nexport class LruCache<T = unknown> {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<LruCache>();\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 * Underlying storage; Map iteration order tracks LRU position (first = oldest).\n\t * @internal\n\t */\n\tprivate readonly _cache: Map<\n\t\tstring,\n\t\t{ value: T; lastAccessed: number; expires: number | undefined }\n\t>;\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 LruCache.\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 ?? LruCache.DEFAULT_CAPACITY;\n\t\tconst ttiMs = options?.ttiMs ?? LruCache.DEFAULT_TTI_MS;\n\t\tconst mutexTimeoutMs = options?.mutexTimeoutMs;\n\n\t\tGuards.integer(LruCache.CLASS_NAME, nameof(capacity), capacity);\n\t\tGuards.integer(LruCache.CLASS_NAME, nameof(ttiMs), ttiMs);\n\t\tif (Is.notEmpty(mutexTimeoutMs)) {\n\t\t\tGuards.integer(LruCache.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(LruCache.CLASS_NAME, nameof<LruCache>(), failures);\n\n\t\tthis._capacity = capacity;\n\t\tthis._ttiMs = ttiMs;\n\t\tthis._mutexTimeoutMs = mutexTimeoutMs;\n\t\tthis._mutexScope = `${LruCache.CLASS_NAME}:${RandomHelper.generateUuidV7()}`;\n\t\tthis._cache = new Map();\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._cache.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 resets the entry's idle timer and moves it to most-recently-used.\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(LruCache.CLASS_NAME, nameof(key), key);\n\n\t\tconst entry = this._cache.get(key);\n\t\tif (entry === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst now = Date.now();\n\t\tif (this.isExpired(entry, now)) {\n\t\t\tthis._cache.delete(key);\n\t\t\treturn undefined;\n\t\t}\n\t\t// Move to end of Map (most-recently-used) via delete + re-insert\n\t\tthis._cache.delete(key);\n\t\tentry.lastAccessed = now;\n\t\tthis._cache.set(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 idle timer are refreshed.\n\t * When the cache is at capacity, idle entries are swept first; if it is still full the\n\t * least-recently-used entry is evicted.\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(LruCache.CLASS_NAME, nameof(key), key);\n\n\t\tif (!Is.empty(expires)) {\n\t\t\tGuards.integer(LruCache.CLASS_NAME, nameof(expires), expires);\n\t\t}\n\n\t\tconst now = Date.now();\n\t\t// Remove any existing entry so the refreshed version is inserted at the end\n\t\tthis._cache.delete(key);\n\t\tif (this._cache.size >= this._capacity) {\n\t\t\tthis.sweepIdle();\n\t\t}\n\t\tif (this._cache.size >= this._capacity) {\n\t\t\tconst lruKey = this._cache.keys().next().value;\n\t\t\tif (lruKey !== undefined) {\n\t\t\t\tthis._cache.delete(lruKey);\n\t\t\t}\n\t\t}\n\t\tthis._cache.set(key, { value, lastAccessed: now, expires });\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(LruCache.CLASS_NAME, nameof(key), key);\n\t\tGuards.function(LruCache.CLASS_NAME, nameof(valueFactory), valueFactory);\n\t\tif (!Is.empty(expires)) {\n\t\t\tGuards.integer(LruCache.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 TTI is not reset.\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(LruCache.CLASS_NAME, nameof(key), key);\n\t\tconst entry = this._cache.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._cache.delete(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 * @returns An array of live keys in least-recently-used to most-recently-used order.\n\t */\n\tpublic keys(): string[] {\n\t\tconst now = Date.now();\n\t\tconst result: string[] = [];\n\t\tfor (const [k, entry] of this._cache) {\n\t\t\tif (this.isExpired(entry, now)) {\n\t\t\t\tthis._cache.delete(k);\n\t\t\t} else {\n\t\t\t\tresult.push(k);\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(LruCache.CLASS_NAME, nameof(key), key);\n\t\tthis._cache.delete(key);\n\t\tif (this._cache.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._cache.clear();\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._cache.clear();\n\t\tthis._nextExpires = undefined;\n\t}\n\n\t/**\n\t * Delete all entries whose idle time has been exceeded, then restart the timer\n\t * if any 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._cache) {\n\t\t\tif (this.isExpired(entry, now)) {\n\t\t\t\tthis._cache.delete(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._cache.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"]}
@@ -0,0 +1,6 @@
1
+ import { Factory } from "./factory.js";
2
+ import type { IFacade } from "../models/IFacade.js";
3
+ /**
4
+ * Factory for creating implementation of facade types.
5
+ */
6
+ export declare const FacadeFactory: Factory<IFacade<unknown>>;
@@ -92,18 +92,36 @@ export declare class Factory<T> {
92
92
  * Remove all the instances and the generators.
93
93
  */
94
94
  clear(): void;
95
+ /**
96
+ * Activate a facade for this factory, so every instance it produces is wrapped by it.
97
+ * An instance is passed through the facades in the order they were activated, so the facade
98
+ * activated first is the outermost. Activating a facade which is already active does nothing.
99
+ * @param name The name of the facade, as registered with the facade factory.
100
+ * @throws GuardError if the parameters are invalid.
101
+ * @throws GeneralError if no facade is registered with the name, or the factory is the facade
102
+ * factory itself.
103
+ */
104
+ useFacade(name: string): void;
105
+ /**
106
+ * Deactivate a facade for this factory. Deactivating a facade which is not active does nothing.
107
+ * @param name The name of the facade to deactivate.
108
+ * @throws GuardError if the parameters are invalid.
109
+ */
110
+ unuseFacade(name: string): void;
95
111
  /**
96
112
  * Get all the instances as a map.
113
+ * @param withFacade Return the instances with the active facades applied, defaults to false.
97
114
  * @returns The instances as a map.
98
115
  */
99
- instancesMap(): {
116
+ instancesMap(withFacade?: boolean): {
100
117
  [name: string]: T;
101
118
  };
102
119
  /**
103
120
  * Get all the instances as a list in the order they were registered.
121
+ * @param withFacade Return the instances with the active facades applied, defaults to false.
104
122
  * @returns The instances as a list in the order they were registered.
105
123
  */
106
- instancesList(): T[];
124
+ instancesList(withFacade?: boolean): T[];
107
125
  /**
108
126
  * Get all the generator names in the order they were registered.
109
127
  * @returns The ordered generator names.
@@ -14,6 +14,7 @@ export * from "./errors/unauthorizedError.js";
14
14
  export * from "./errors/unprocessableError.js";
15
15
  export * from "./errors/validationError.js";
16
16
  export * from "./factories/componentFactory.js";
17
+ export * from "./factories/facadeFactory.js";
17
18
  export * from "./factories/factory.js";
18
19
  export * from "./helpers/arrayHelper.js";
19
20
  export * from "./helpers/envHelper.js";
@@ -30,6 +31,7 @@ export * from "./models/coerceType.js";
30
31
  export * from "./models/IDuration.js";
31
32
  export * from "./models/compressionType.js";
32
33
  export * from "./models/IComponent.js";
34
+ export * from "./models/IFacade.js";
33
35
  export * from "./models/IError.js";
34
36
  export * from "./models/II18nShared.js";
35
37
  export * from "./models/IKeyValue.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A facade wraps a component so a cross cutting concern can be applied to it without the
3
+ * component being modified. The wrapped component is returned in place of the original.
4
+ */
5
+ export interface IFacade<T = unknown> {
6
+ /**
7
+ * Wrap the target, returning a replacement.
8
+ * @param target The component to wrap.
9
+ * @returns The wrapped component.
10
+ */
11
+ wrap(target: T): T;
12
+ }
@@ -9,6 +9,8 @@
9
9
  * The timer only runs while there are entries; it stops automatically when the cache empties.
10
10
  *
11
11
  * `get` and `set` increment an entry's access frequency and reset its idle timer.
12
+ * `set` and `getOrSet` accept an optional hard expiry timestamp; the entry is removed once that
13
+ * time is reached however recently it was used, and the TTI still applies alongside it.
12
14
  * `has` and `keys` are pure peeks they evict idle entries but do not affect frequency or TTI.
13
15
  * Call `destroy` when the cache is no longer needed to stop the background timer.
14
16
  */
@@ -58,16 +60,20 @@ export declare class LfuCache<T> {
58
60
  * least-frequently-used entry is evicted (LRU among ties).
59
61
  * @param key The key to store.
60
62
  * @param value The value to cache.
63
+ * @param expires Hard expiry timestamp in milliseconds since the epoch. The entry is removed
64
+ * once this time is reached regardless of how recently it was used. Must be an integer.
61
65
  */
62
- set(key: string, value: T): void;
66
+ set(key: string, value: T, expires?: number): void;
63
67
  /**
64
68
  * Atomically get an existing value or create and store it once using an async factory.
65
69
  * Concurrent calls for the same key are serialized via a mutex.
66
70
  * @param key The key to get or create.
67
71
  * @param valueFactory Async callback used to build a value when the key is absent.
72
+ * @param expires Hard expiry timestamp in milliseconds since the epoch, applied to the entry
73
+ * when one is created. Must be an integer.
68
74
  * @returns The existing or newly created value.
69
75
  */
70
- getOrSet(key: string, valueFactory: () => Promise<T>): Promise<T>;
76
+ getOrSet(key: string, valueFactory: () => Promise<T>, expires?: number): Promise<T>;
71
77
  /**
72
78
  * Check whether a key exists in the cache and has not idled out.
73
79
  * Idle entries are evicted on peek, but a live entry's frequency and TTI are not updated.
@@ -7,6 +7,8 @@
7
7
  * The timer only runs while there are entries; it stops automatically when the cache empties.
8
8
  *
9
9
  * `get` and `set` both update an entry's LRU position and reset its idle timer.
10
+ * `set` and `getOrSet` accept an optional hard expiry timestamp; the entry is removed once that
11
+ * time is reached however recently it was used, and the TTI still applies alongside it.
10
12
  * `has` is a pure peek it evicts idle entries but does not refresh a live entry's TTI.
11
13
  * Call `destroy` when the cache is no longer needed to stop the background timer.
12
14
  */
@@ -56,16 +58,20 @@ export declare class LruCache<T = unknown> {
56
58
  * least-recently-used entry is evicted.
57
59
  * @param key The key to store.
58
60
  * @param value The value to cache.
61
+ * @param expires Hard expiry timestamp in milliseconds since the epoch. The entry is removed
62
+ * once this time is reached regardless of how recently it was used. Must be an integer.
59
63
  */
60
- set(key: string, value: T): void;
64
+ set(key: string, value: T, expires?: number): void;
61
65
  /**
62
66
  * Atomically get an existing value or create and store it once using an async factory.
63
67
  * Concurrent calls for the same key are serialized via a mutex.
64
68
  * @param key The key to get or create.
65
69
  * @param valueFactory Async callback used to build a value when the key is absent.
70
+ * @param expires Hard expiry timestamp in milliseconds since the epoch, applied to the entry
71
+ * when one is created. Must be an integer.
66
72
  * @returns The existing or newly created value.
67
73
  */
68
- getOrSet(key: string, valueFactory: () => Promise<T>): Promise<T>;
74
+ getOrSet(key: string, valueFactory: () => Promise<T>, expires?: number): Promise<T>;
69
75
  /**
70
76
  * Check whether a key exists in the cache and has not idled out.
71
77
  * Idle entries are evicted on peek, but a live entry's TTI is not reset.
package/docs/changelog.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.3-next.4](https://github.com/iotaledger/twin-framework/compare/core-v0.9.3-next.3...core-v0.9.3-next.4) (2026-09-04)
4
+
5
+
6
+ ### Features
7
+
8
+ * cache specific timeout ([#473](https://github.com/iotaledger/twin-framework/issues/473)) ([e4c6fac](https://github.com/iotaledger/twin-framework/commit/e4c6facdd874cf17f02db13932d256bedd55302f))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @twin.org/nameof bumped from 0.9.3-next.3 to 0.9.3-next.4
16
+ * devDependencies
17
+ * @twin.org/nameof-transformer bumped from 0.9.3-next.3 to 0.9.3-next.4
18
+ * @twin.org/nameof-vitest-plugin bumped from 0.9.3-next.3 to 0.9.3-next.4
19
+
20
+ ## [0.9.3-next.3](https://github.com/iotaledger/twin-framework/compare/core-v0.9.3-next.2...core-v0.9.3-next.3) (2026-09-02)
21
+
22
+
23
+ ### Features
24
+
25
+ * factory facades ([#470](https://github.com/iotaledger/twin-framework/issues/470)) ([004aade](https://github.com/iotaledger/twin-framework/commit/004aade8340ff640f24dbe2f05330e8f5920d3a9))
26
+
27
+
28
+ ### Dependencies
29
+
30
+ * The following workspace dependencies were updated
31
+ * dependencies
32
+ * @twin.org/nameof bumped from 0.9.3-next.2 to 0.9.3-next.3
33
+ * devDependencies
34
+ * @twin.org/nameof-transformer bumped from 0.9.3-next.2 to 0.9.3-next.3
35
+ * @twin.org/nameof-vitest-plugin bumped from 0.9.3-next.2 to 0.9.3-next.3
36
+
3
37
  ## [0.9.3-next.2](https://github.com/iotaledger/twin-framework/compare/core-v0.9.3-next.1...core-v0.9.3-next.2) (2026-08-28)
4
38
 
5
39