@twin.org/core 0.9.2-next.8 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/helpers/jsonHelper.js +1 -2
- package/dist/es/helpers/jsonHelper.js.map +1 -1
- package/dist/es/index.js +2 -0
- package/dist/es/index.js.map +1 -1
- package/dist/es/models/coerceType.js +5 -1
- package/dist/es/models/coerceType.js.map +1 -1
- package/dist/es/utils/coerce.js +30 -3
- package/dist/es/utils/coerce.js.map +1 -1
- package/dist/es/utils/is.js +24 -1
- package/dist/es/utils/is.js.map +1 -1
- package/dist/es/utils/lfuCache.js +380 -0
- package/dist/es/utils/lfuCache.js.map +1 -0
- package/dist/es/utils/lruCache.js +271 -0
- package/dist/es/utils/lruCache.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/models/coerceType.d.ts +4 -0
- package/dist/types/utils/coerce.d.ts +6 -0
- package/dist/types/utils/lfuCache.d.ts +100 -0
- package/dist/types/utils/lruCache.d.ts +97 -0
- package/docs/changelog.md +74 -0
- package/docs/reference/classes/Coerce.md +28 -0
- package/docs/reference/classes/LfuCache.md +265 -0
- package/docs/reference/classes/LruCache.md +262 -0
- package/docs/reference/index.md +2 -0
- package/docs/reference/variables/CoerceType.md +6 -0
- package/package.json +2 -2
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { Guards } from "./guards.js";
|
|
2
|
+
import { Is } from "./is.js";
|
|
3
|
+
import { Mutex } from "./mutex.js";
|
|
4
|
+
import { Validation } from "./validation.js";
|
|
5
|
+
import { RandomHelper } from "../helpers/randomHelper.js";
|
|
6
|
+
/**
|
|
7
|
+
* A fixed-capacity LFU cache with time-to-idle eviction.
|
|
8
|
+
*
|
|
9
|
+
* Entries are removed in two ways:
|
|
10
|
+
* - Capacity eviction: when the cache is full the least-frequently-used entry is removed first.
|
|
11
|
+
* Ties in frequency are broken by recency the least-recently-used entry among those with the
|
|
12
|
+
* minimum frequency is evicted.
|
|
13
|
+
* - TTI eviction: a background timer sweeps idle entries every ttiMs milliseconds.
|
|
14
|
+
* The timer only runs while there are entries; it stops automatically when the cache empties.
|
|
15
|
+
*
|
|
16
|
+
* `get` and `set` increment an entry's access frequency and reset its idle timer.
|
|
17
|
+
* `has` and `keys` are pure peeks they evict idle entries but do not affect frequency or TTI.
|
|
18
|
+
* Call `destroy` when the cache is no longer needed to stop the background timer.
|
|
19
|
+
*/
|
|
20
|
+
export class LfuCache {
|
|
21
|
+
/**
|
|
22
|
+
* Runtime name for the class.
|
|
23
|
+
*/
|
|
24
|
+
static CLASS_NAME = "LfuCache";
|
|
25
|
+
/**
|
|
26
|
+
* Default capacity.
|
|
27
|
+
*/
|
|
28
|
+
static DEFAULT_CAPACITY = 1000;
|
|
29
|
+
/**
|
|
30
|
+
* Default time-to-idle in milliseconds.
|
|
31
|
+
*/
|
|
32
|
+
static DEFAULT_TTI_MS = 10000;
|
|
33
|
+
/**
|
|
34
|
+
* The maximum number of entries the cache will hold.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
_capacity;
|
|
38
|
+
/**
|
|
39
|
+
* The idle duration in milliseconds after which an untouched entry is evicted.
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
_ttiMs;
|
|
43
|
+
/**
|
|
44
|
+
* Optional timeout in milliseconds for mutex acquisition.
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
_mutexTimeoutMs;
|
|
48
|
+
/**
|
|
49
|
+
* Per-instance namespace prefix for mutex keys.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
_mutexScope;
|
|
53
|
+
/**
|
|
54
|
+
* Maps each key to its cached value, access frequency, and last-accessed timestamp.
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
_keyMap;
|
|
58
|
+
/**
|
|
59
|
+
* Maps each frequency to the ordered set of keys at that frequency (insertion order = LRU).
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
_freqMap;
|
|
63
|
+
/**
|
|
64
|
+
* The lowest frequency among all live entries. Maintained to make eviction O(1).
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
_minFreq;
|
|
68
|
+
/**
|
|
69
|
+
* Handle for the pending idle-sweep timeout, or undefined if no timer is scheduled.
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
_sweepTimer;
|
|
73
|
+
/**
|
|
74
|
+
* Create a new instance of LfuCache.
|
|
75
|
+
* @param options The cache options.
|
|
76
|
+
* @param options.capacity Maximum number of entries. Defaults to 1000. Must be a positive integer.
|
|
77
|
+
* @param options.ttiMs Time-to-idle in milliseconds. Defaults to 10000. Must be a positive integer.
|
|
78
|
+
* @param options.mutexTimeoutMs Maximum time in milliseconds to wait for getOrSet mutex acquisition.
|
|
79
|
+
* @throws ValidationError if capacity or ttiMs is not a positive integer.
|
|
80
|
+
*/
|
|
81
|
+
constructor(options) {
|
|
82
|
+
const capacity = options?.capacity ?? LfuCache.DEFAULT_CAPACITY;
|
|
83
|
+
const ttiMs = options?.ttiMs ?? LfuCache.DEFAULT_TTI_MS;
|
|
84
|
+
const mutexTimeoutMs = options?.mutexTimeoutMs;
|
|
85
|
+
Guards.integer(LfuCache.CLASS_NAME, "capacity", capacity);
|
|
86
|
+
Guards.integer(LfuCache.CLASS_NAME, "ttiMs", ttiMs);
|
|
87
|
+
if (Is.notEmpty(mutexTimeoutMs)) {
|
|
88
|
+
Guards.integer(LfuCache.CLASS_NAME, "mutexTimeoutMs", mutexTimeoutMs);
|
|
89
|
+
}
|
|
90
|
+
const failures = [];
|
|
91
|
+
Validation.integer("capacity", capacity, failures, undefined, { minValue: 1 });
|
|
92
|
+
Validation.integer("ttiMs", ttiMs, failures, undefined, { minValue: 1 });
|
|
93
|
+
if (Is.notEmpty(mutexTimeoutMs)) {
|
|
94
|
+
Validation.integer("mutexTimeoutMs", mutexTimeoutMs, failures, undefined, {
|
|
95
|
+
minValue: 0
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
Validation.asValidationError(LfuCache.CLASS_NAME, "LfuCache", failures);
|
|
99
|
+
this._capacity = capacity;
|
|
100
|
+
this._ttiMs = ttiMs;
|
|
101
|
+
this._mutexTimeoutMs = mutexTimeoutMs;
|
|
102
|
+
this._mutexScope = `${LfuCache.CLASS_NAME}:${RandomHelper.generateUuidV7()}`;
|
|
103
|
+
this._keyMap = new Map();
|
|
104
|
+
this._freqMap = new Map();
|
|
105
|
+
this._minFreq = 0;
|
|
106
|
+
this._sweepTimer = undefined;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The number of entries currently held in the cache.
|
|
110
|
+
* @returns The number of entries in the cache.
|
|
111
|
+
*/
|
|
112
|
+
count() {
|
|
113
|
+
return this._keyMap.size;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get a value from the cache.
|
|
117
|
+
* Returns undefined if the key is absent or the entry has idled out.
|
|
118
|
+
* A successful hit increments the entry's frequency and resets its idle timer.
|
|
119
|
+
* @param key The key to retrieve.
|
|
120
|
+
* @returns The cached value, or undefined on a miss or idle eviction.
|
|
121
|
+
*/
|
|
122
|
+
get(key) {
|
|
123
|
+
const entry = this._keyMap.get(key);
|
|
124
|
+
if (entry === undefined) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
if (Date.now() - entry.lastAccessed >= this._ttiMs) {
|
|
128
|
+
this.removeEntry(key);
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
this.promote(key, entry);
|
|
132
|
+
return entry.value;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Store a value in the cache.
|
|
136
|
+
* If the key already exists its value and frequency are updated.
|
|
137
|
+
* When the cache is at capacity, idle entries are swept first; if it is still full the
|
|
138
|
+
* least-frequently-used entry is evicted (LRU among ties).
|
|
139
|
+
* @param key The key to store.
|
|
140
|
+
* @param value The value to cache.
|
|
141
|
+
*/
|
|
142
|
+
set(key, value) {
|
|
143
|
+
const existing = this._keyMap.get(key);
|
|
144
|
+
if (existing !== undefined) {
|
|
145
|
+
if (Date.now() - existing.lastAccessed >= this._ttiMs) {
|
|
146
|
+
// Idle: evict and fall through to add as a fresh entry
|
|
147
|
+
this.removeEntry(key);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
existing.value = value;
|
|
151
|
+
this.promote(key, existing);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (this._keyMap.size >= this._capacity) {
|
|
156
|
+
this.sweepIdle();
|
|
157
|
+
}
|
|
158
|
+
if (this._keyMap.size >= this._capacity) {
|
|
159
|
+
this.evictLfu();
|
|
160
|
+
}
|
|
161
|
+
const entry = { value, freq: 1, lastAccessed: Date.now() };
|
|
162
|
+
this._keyMap.set(key, entry);
|
|
163
|
+
let bucket = this._freqMap.get(1);
|
|
164
|
+
if (bucket === undefined) {
|
|
165
|
+
bucket = new Set();
|
|
166
|
+
this._freqMap.set(1, bucket);
|
|
167
|
+
}
|
|
168
|
+
bucket.add(key);
|
|
169
|
+
this._minFreq = 1;
|
|
170
|
+
this.startTimer();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Atomically get an existing value or create and store it once using an async factory.
|
|
174
|
+
* Concurrent calls for the same key are serialized via a mutex.
|
|
175
|
+
* @param key The key to get or create.
|
|
176
|
+
* @param valueFactory Async callback used to build a value when the key is absent.
|
|
177
|
+
* @returns The existing or newly created value.
|
|
178
|
+
*/
|
|
179
|
+
async getOrSet(key, valueFactory) {
|
|
180
|
+
Guards.stringValue(LfuCache.CLASS_NAME, "key", key);
|
|
181
|
+
Guards.function(LfuCache.CLASS_NAME, "valueFactory", valueFactory);
|
|
182
|
+
const mutexKey = `${this._mutexScope}:${key}`;
|
|
183
|
+
await Mutex.lock(mutexKey, {
|
|
184
|
+
timeoutMs: this._mutexTimeoutMs,
|
|
185
|
+
throwOnTimeout: true
|
|
186
|
+
});
|
|
187
|
+
try {
|
|
188
|
+
if (this.has(key)) {
|
|
189
|
+
return this.get(key);
|
|
190
|
+
}
|
|
191
|
+
const value = await valueFactory();
|
|
192
|
+
this.set(key, value);
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
Mutex.unlock(mutexKey);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Check whether a key exists in the cache and has not idled out.
|
|
201
|
+
* Idle entries are evicted on peek, but a live entry's frequency and TTI are not updated.
|
|
202
|
+
* @param key The key to test.
|
|
203
|
+
* @returns True if the key is present and not idle.
|
|
204
|
+
*/
|
|
205
|
+
has(key) {
|
|
206
|
+
const entry = this._keyMap.get(key);
|
|
207
|
+
if (entry === undefined) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (Date.now() - entry.lastAccessed >= this._ttiMs) {
|
|
211
|
+
this.removeEntry(key);
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Return all keys for entries that have not idled out.
|
|
218
|
+
* Idle entries encountered during iteration are evicted.
|
|
219
|
+
* Keys are returned in ascending frequency order; within the same frequency, LRU first.
|
|
220
|
+
* @returns An array of live keys ordered from least-frequently-used to most-frequently-used.
|
|
221
|
+
*/
|
|
222
|
+
keys() {
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
for (const [k, entry] of this._keyMap) {
|
|
225
|
+
if (now - entry.lastAccessed >= this._ttiMs) {
|
|
226
|
+
this.removeEntry(k);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const result = [];
|
|
230
|
+
const sortedFreqs = [...this._freqMap.keys()].sort((a, b) => a - b);
|
|
231
|
+
for (const freq of sortedFreqs) {
|
|
232
|
+
const bucket = this._freqMap.get(freq);
|
|
233
|
+
if (bucket !== undefined) {
|
|
234
|
+
for (const k of bucket) {
|
|
235
|
+
result.push(k);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Remove an entry from the cache.
|
|
243
|
+
* Cancels the background timer if the cache becomes empty.
|
|
244
|
+
* @param key The key to remove.
|
|
245
|
+
*/
|
|
246
|
+
delete(key) {
|
|
247
|
+
this.removeEntry(key);
|
|
248
|
+
if (this._keyMap.size === 0) {
|
|
249
|
+
this.cancelTimer();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Remove all entries from the cache and cancel the background timer.
|
|
254
|
+
*/
|
|
255
|
+
clear() {
|
|
256
|
+
this.cancelTimer();
|
|
257
|
+
this._keyMap.clear();
|
|
258
|
+
this._freqMap.clear();
|
|
259
|
+
this._minFreq = 0;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Stop the background idle-sweep timer and release all entries.
|
|
263
|
+
* The cache must not be used after this call.
|
|
264
|
+
*/
|
|
265
|
+
destroy() {
|
|
266
|
+
this.cancelTimer();
|
|
267
|
+
this._keyMap.clear();
|
|
268
|
+
this._freqMap.clear();
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Increment the frequency of an entry and move it to the correct frequency bucket.
|
|
272
|
+
* Updates lastAccessed to now.
|
|
273
|
+
* @param key The key to promote.
|
|
274
|
+
* @param entry The entry object to update in place.
|
|
275
|
+
* @param entry.value The cached value.
|
|
276
|
+
* @param entry.freq The current access frequency.
|
|
277
|
+
* @param entry.lastAccessed The last-accessed timestamp in milliseconds.
|
|
278
|
+
* @internal
|
|
279
|
+
*/
|
|
280
|
+
promote(key, entry) {
|
|
281
|
+
const oldFreq = entry.freq;
|
|
282
|
+
const oldBucket = this._freqMap.get(oldFreq);
|
|
283
|
+
if (oldBucket !== undefined) {
|
|
284
|
+
oldBucket.delete(key);
|
|
285
|
+
if (oldBucket.size === 0) {
|
|
286
|
+
this._freqMap.delete(oldFreq);
|
|
287
|
+
if (oldFreq === this._minFreq) {
|
|
288
|
+
this._minFreq = oldFreq + 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
entry.freq += 1;
|
|
293
|
+
entry.lastAccessed = Date.now();
|
|
294
|
+
let newBucket = this._freqMap.get(entry.freq);
|
|
295
|
+
if (newBucket === undefined) {
|
|
296
|
+
newBucket = new Set();
|
|
297
|
+
this._freqMap.set(entry.freq, newBucket);
|
|
298
|
+
}
|
|
299
|
+
newBucket.add(key);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Evict the least-frequently-used entry, breaking ties by recency.
|
|
303
|
+
* @internal
|
|
304
|
+
*/
|
|
305
|
+
evictLfu() {
|
|
306
|
+
const bucket = this._freqMap.get(this._minFreq);
|
|
307
|
+
if (bucket === undefined) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const evictKey = bucket.values().next().value;
|
|
311
|
+
bucket.delete(evictKey);
|
|
312
|
+
if (bucket.size === 0) {
|
|
313
|
+
this._freqMap.delete(this._minFreq);
|
|
314
|
+
}
|
|
315
|
+
this._keyMap.delete(evictKey);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Remove an entry from both the key map and its frequency bucket.
|
|
319
|
+
* Recalculates _minFreq if the removed entry was the last one at _minFreq.
|
|
320
|
+
* @param key The key to remove.
|
|
321
|
+
* @internal
|
|
322
|
+
*/
|
|
323
|
+
removeEntry(key) {
|
|
324
|
+
const entry = this._keyMap.get(key);
|
|
325
|
+
if (entry === undefined) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
this._keyMap.delete(key);
|
|
329
|
+
const bucket = this._freqMap.get(entry.freq);
|
|
330
|
+
if (bucket !== undefined) {
|
|
331
|
+
bucket.delete(key);
|
|
332
|
+
if (bucket.size === 0) {
|
|
333
|
+
this._freqMap.delete(entry.freq);
|
|
334
|
+
if (entry.freq === this._minFreq) {
|
|
335
|
+
let newMin = Number.MAX_SAFE_INTEGER;
|
|
336
|
+
for (const f of this._freqMap.keys()) {
|
|
337
|
+
if (f < newMin) {
|
|
338
|
+
newMin = f;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
this._minFreq = newMin === Number.MAX_SAFE_INTEGER ? 1 : newMin;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Cancel the pending timer, sweep idle entries, then restart the timer if entries remain.
|
|
348
|
+
* @internal
|
|
349
|
+
*/
|
|
350
|
+
sweepIdle() {
|
|
351
|
+
this.cancelTimer();
|
|
352
|
+
const now = Date.now();
|
|
353
|
+
for (const [k, entry] of this._keyMap) {
|
|
354
|
+
if (now - entry.lastAccessed >= this._ttiMs) {
|
|
355
|
+
this.removeEntry(k);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (this._keyMap.size > 0) {
|
|
359
|
+
this.startTimer();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Schedule the next idle sweep if no timer is already pending.
|
|
364
|
+
* @internal
|
|
365
|
+
*/
|
|
366
|
+
startTimer() {
|
|
367
|
+
this._sweepTimer ??= setTimeout(() => this.sweepIdle(), this._ttiMs);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Cancel the pending idle-sweep timer.
|
|
371
|
+
* @internal
|
|
372
|
+
*/
|
|
373
|
+
cancelTimer() {
|
|
374
|
+
if (Is.notEmpty(this._sweepTimer)) {
|
|
375
|
+
clearTimeout(this._sweepTimer);
|
|
376
|
+
this._sweepTimer = undefined;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
//# sourceMappingURL=lfuCache.js.map
|
|
@@ -0,0 +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"]}
|