@neezco/cache 0.5.0 → 0.7.0
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/browser/index.d.ts +2 -2
- package/dist/browser/index.js +43 -105
- package/dist/browser/index.js.map +1 -1
- package/dist/node/index.cjs +58 -145
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.d.cts +2 -2
- package/dist/node/index.d.mts +2 -2
- package/dist/node/index.mjs +54 -142
- package/dist/node/index.mjs.map +1 -1
- package/package.json +16 -26
- package/CHANGELOG.md +0 -63
- package/docs/.gitkeep +0 -0
package/dist/browser/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
declare const enum DELETE_REASON {
|
|
3
3
|
MANUAL = "manual",
|
|
4
4
|
EXPIRED = "expired",
|
|
5
|
-
STALE = "stale"
|
|
5
|
+
STALE = "stale"
|
|
6
6
|
}
|
|
7
7
|
//#endregion
|
|
8
8
|
//#region src/types.d.ts
|
|
@@ -167,7 +167,7 @@ declare enum ENTRY_STATUS {
|
|
|
167
167
|
/** Expired but within stale window; still served. */
|
|
168
168
|
STALE = "stale",
|
|
169
169
|
/** Beyond stale window; not served. */
|
|
170
|
-
EXPIRED = "expired"
|
|
170
|
+
EXPIRED = "expired"
|
|
171
171
|
}
|
|
172
172
|
/**
|
|
173
173
|
* Metadata returned from `get()` with `includeMetadata: true`.
|
package/dist/browser/index.js
CHANGED
|
@@ -28,11 +28,6 @@ const ONE_MINUTE = 60 * ONE_SECOND;
|
|
|
28
28
|
*/
|
|
29
29
|
const DEFAULT_TTL = 30 * ONE_MINUTE;
|
|
30
30
|
/**
|
|
31
|
-
* Default stale window in milliseconds after expiration.
|
|
32
|
-
* Allows serving slightly outdated data while fetching fresh data.
|
|
33
|
-
*/
|
|
34
|
-
const DEFAULT_STALE_WINDOW = 0;
|
|
35
|
-
/**
|
|
36
31
|
* Maximum number of entries the cache can hold.
|
|
37
32
|
* Beyond this limit, new entries are ignored.
|
|
38
33
|
*/
|
|
@@ -55,11 +50,6 @@ const DEFAULT_MAX_MEMORY_SIZE = Infinity;
|
|
|
55
50
|
*/
|
|
56
51
|
const MAX_KEYS_PER_BATCH = 1e3;
|
|
57
52
|
/**
|
|
58
|
-
* Minimal expired ratio enforced during sweeps.
|
|
59
|
-
* Ensures control sweeps run above {@link EXPIRED_RATIO_MEMORY_THRESHOLD}.
|
|
60
|
-
*/
|
|
61
|
-
const MINIMAL_EXPIRED_RATIO = .05;
|
|
62
|
-
/**
|
|
63
53
|
* Maximum allowed expired ratio when memory usage is low.
|
|
64
54
|
* Upper bound for interpolation with MINIMAL_EXPIRED_RATIO.
|
|
65
55
|
* Recommended range: `0.3 – 0.5` .
|
|
@@ -77,27 +67,6 @@ const DEFAULT_MAX_EXPIRED_RATIO = .4;
|
|
|
77
67
|
*/
|
|
78
68
|
const OPTIMAL_SWEEP_INTERVAL = 2 * ONE_SECOND;
|
|
79
69
|
/**
|
|
80
|
-
* Optimal time budget in milliseconds for each sweep cycle.
|
|
81
|
-
* Used when performance metrics are not available or unreliable.
|
|
82
|
-
*/
|
|
83
|
-
const OPTIMAL_SWEEP_TIME_BUDGET_IF_NOTE_METRICS_AVAILABLE = 15;
|
|
84
|
-
/**
|
|
85
|
-
* Fallback behavior for stale purging on GET
|
|
86
|
-
* when no resource limits are defined.
|
|
87
|
-
*
|
|
88
|
-
* In this scenario, threshold-based purging is disabled,
|
|
89
|
-
* so GET operations do NOT purge stale entries.
|
|
90
|
-
*/
|
|
91
|
-
const DEFAULT_PURGE_STALE_ON_GET_NO_LIMITS = false;
|
|
92
|
-
/**
|
|
93
|
-
* Fallback behavior for stale purging on SWEEP
|
|
94
|
-
* when no resource limits are defined.
|
|
95
|
-
*
|
|
96
|
-
* In this scenario, threshold-based purging is disabled,
|
|
97
|
-
* so SWEEP operations DO purge stale entries to prevent buildup.
|
|
98
|
-
*/
|
|
99
|
-
const DEFAULT_PURGE_STALE_ON_SWEEP_NO_LIMITS = true;
|
|
100
|
-
/**
|
|
101
70
|
* Default threshold for purging stale entries on get operations (backend with limits).
|
|
102
71
|
* Stale entries are purged when resource usage exceeds 80%.
|
|
103
72
|
*
|
|
@@ -261,7 +230,7 @@ const resolvePurgeStaleOnGet = (config) => resolvePurgeMode(config.limits, {
|
|
|
261
230
|
operation: "purgeStaleOnGet"
|
|
262
231
|
}, {
|
|
263
232
|
withLimits: DEFAULT_PURGE_STALE_ON_GET_THRESHOLD,
|
|
264
|
-
withoutLimits:
|
|
233
|
+
withoutLimits: false
|
|
265
234
|
}, config.userValue);
|
|
266
235
|
|
|
267
236
|
//#endregion
|
|
@@ -361,38 +330,27 @@ function _batchUpdateExpiredRatio(currentExpiredRatios) {
|
|
|
361
330
|
* or `undefined` if the cache is empty.
|
|
362
331
|
*/
|
|
363
332
|
function _selectInstanceToSweep({ totalSweepWeight, batchSweep }) {
|
|
364
|
-
let instanceToSweep = _instancesCache[0];
|
|
365
333
|
if (totalSweepWeight <= 0) {
|
|
366
|
-
if (batchSweep > _instancesCache.length)
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
instanceToSweep = inst;
|
|
374
|
-
break;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
334
|
+
if (batchSweep > _instancesCache.length) return null;
|
|
335
|
+
return _instancesCache[batchSweep - 1];
|
|
336
|
+
}
|
|
337
|
+
let threshold = Math.random() * totalSweepWeight;
|
|
338
|
+
for (const inst of _instancesCache) {
|
|
339
|
+
threshold -= inst._sweepWeight;
|
|
340
|
+
if (threshold <= 0) return inst;
|
|
377
341
|
}
|
|
378
|
-
return
|
|
342
|
+
return _instancesCache[0];
|
|
379
343
|
}
|
|
380
344
|
|
|
381
345
|
//#endregion
|
|
382
346
|
//#region src/cache/delete.ts
|
|
383
|
-
let DELETE_REASON = /* @__PURE__ */ function(DELETE_REASON$1) {
|
|
384
|
-
DELETE_REASON$1["MANUAL"] = "manual";
|
|
385
|
-
DELETE_REASON$1["EXPIRED"] = "expired";
|
|
386
|
-
DELETE_REASON$1["STALE"] = "stale";
|
|
387
|
-
return DELETE_REASON$1;
|
|
388
|
-
}({});
|
|
389
347
|
/**
|
|
390
348
|
* Deletes a key from the cache.
|
|
391
349
|
* @param state - The cache state.
|
|
392
350
|
* @param key - The key.
|
|
393
351
|
* @returns A boolean indicating whether the key was successfully deleted.
|
|
394
352
|
*/
|
|
395
|
-
const deleteKey = (state, key, reason =
|
|
353
|
+
const deleteKey = (state, key, reason = "manual") => {
|
|
396
354
|
const onDelete = state.onDelete;
|
|
397
355
|
const onExpire = state.onExpire;
|
|
398
356
|
if (!onDelete && !onExpire) return state.store.delete(key);
|
|
@@ -400,7 +358,7 @@ const deleteKey = (state, key, reason = DELETE_REASON.MANUAL) => {
|
|
|
400
358
|
if (!entry) return false;
|
|
401
359
|
state.store.delete(key);
|
|
402
360
|
state.onDelete?.(key, entry[1], reason);
|
|
403
|
-
if (reason !==
|
|
361
|
+
if (reason !== "manual") state.onExpire?.(key, entry[1], reason);
|
|
404
362
|
return true;
|
|
405
363
|
};
|
|
406
364
|
|
|
@@ -409,14 +367,14 @@ const deleteKey = (state, key, reason = DELETE_REASON.MANUAL) => {
|
|
|
409
367
|
/**
|
|
410
368
|
* Entry status: fresh, stale, or expired.
|
|
411
369
|
*/
|
|
412
|
-
let ENTRY_STATUS = /* @__PURE__ */ function(ENTRY_STATUS
|
|
370
|
+
let ENTRY_STATUS = /* @__PURE__ */ function(ENTRY_STATUS) {
|
|
413
371
|
/** Valid and within TTL. */
|
|
414
|
-
ENTRY_STATUS
|
|
372
|
+
ENTRY_STATUS["FRESH"] = "fresh";
|
|
415
373
|
/** Expired but within stale window; still served. */
|
|
416
|
-
ENTRY_STATUS
|
|
374
|
+
ENTRY_STATUS["STALE"] = "stale";
|
|
417
375
|
/** Beyond stale window; not served. */
|
|
418
|
-
ENTRY_STATUS
|
|
419
|
-
return ENTRY_STATUS
|
|
376
|
+
ENTRY_STATUS["EXPIRED"] = "expired";
|
|
377
|
+
return ENTRY_STATUS;
|
|
420
378
|
}({});
|
|
421
379
|
|
|
422
380
|
//#endregion
|
|
@@ -442,22 +400,22 @@ let ENTRY_STATUS = /* @__PURE__ */ function(ENTRY_STATUS$1) {
|
|
|
442
400
|
function _statusFromTags(state, entry) {
|
|
443
401
|
const entryCreatedAt = entry[0][0];
|
|
444
402
|
let earliestTagStaleInvalidation = Infinity;
|
|
445
|
-
let status =
|
|
403
|
+
let status = "fresh";
|
|
446
404
|
const tags = entry[2];
|
|
447
405
|
if (tags) for (const tag of tags) {
|
|
448
406
|
const ts = state._tags.get(tag);
|
|
449
407
|
if (!ts) continue;
|
|
450
408
|
const [tagExpiredAt, tagStaleSinceAt] = ts;
|
|
451
409
|
if (tagExpiredAt >= entryCreatedAt) {
|
|
452
|
-
status =
|
|
410
|
+
status = "expired";
|
|
453
411
|
break;
|
|
454
412
|
}
|
|
455
413
|
if (tagStaleSinceAt >= entryCreatedAt) {
|
|
456
414
|
if (tagStaleSinceAt < earliestTagStaleInvalidation) earliestTagStaleInvalidation = tagStaleSinceAt;
|
|
457
|
-
status =
|
|
415
|
+
status = "stale";
|
|
458
416
|
}
|
|
459
417
|
}
|
|
460
|
-
return [status, status ===
|
|
418
|
+
return [status, status === "stale" ? earliestTagStaleInvalidation : 0];
|
|
461
419
|
}
|
|
462
420
|
|
|
463
421
|
//#endregion
|
|
@@ -480,12 +438,12 @@ function _statusFromTags(state, entry) {
|
|
|
480
438
|
function computeEntryStatus(state, entry, now) {
|
|
481
439
|
const [__createdAt, expiresAt, staleExpiresAt] = entry[0];
|
|
482
440
|
const [tagStatus, earliestTagStaleInvalidation] = _statusFromTags(state, entry);
|
|
483
|
-
if (tagStatus ===
|
|
441
|
+
if (tagStatus === "expired") return "expired";
|
|
484
442
|
const windowStale = staleExpiresAt - expiresAt;
|
|
485
|
-
if (tagStatus ===
|
|
486
|
-
if (now < expiresAt) return
|
|
487
|
-
if (staleExpiresAt > 0 && now < staleExpiresAt) return
|
|
488
|
-
return
|
|
443
|
+
if (tagStatus === "stale" && staleExpiresAt > 0 && now < earliestTagStaleInvalidation + windowStale && now <= staleExpiresAt) return "stale";
|
|
444
|
+
if (now < expiresAt) return "fresh";
|
|
445
|
+
if (staleExpiresAt > 0 && now < staleExpiresAt) return "stale";
|
|
446
|
+
return "expired";
|
|
489
447
|
}
|
|
490
448
|
/**
|
|
491
449
|
* Determines whether a cache entry is fresh.
|
|
@@ -503,8 +461,8 @@ function computeEntryStatus(state, entry, now) {
|
|
|
503
461
|
* @returns True if the entry is fresh.
|
|
504
462
|
*/
|
|
505
463
|
const isFresh = (state, entry, now) => {
|
|
506
|
-
if (typeof entry === "string") return entry ===
|
|
507
|
-
return computeEntryStatus(state, entry, now) ===
|
|
464
|
+
if (typeof entry === "string") return entry === "fresh";
|
|
465
|
+
return computeEntryStatus(state, entry, now) === "fresh";
|
|
508
466
|
};
|
|
509
467
|
/**
|
|
510
468
|
* Determines whether a cache entry is stale.
|
|
@@ -522,8 +480,8 @@ const isFresh = (state, entry, now) => {
|
|
|
522
480
|
* @returns True if the entry is stale.
|
|
523
481
|
*/
|
|
524
482
|
const isStale = (state, entry, now) => {
|
|
525
|
-
if (typeof entry === "string") return entry ===
|
|
526
|
-
return computeEntryStatus(state, entry, now) ===
|
|
483
|
+
if (typeof entry === "string") return entry === "stale";
|
|
484
|
+
return computeEntryStatus(state, entry, now) === "stale";
|
|
527
485
|
};
|
|
528
486
|
/**
|
|
529
487
|
* Determines whether a cache entry is expired.
|
|
@@ -541,8 +499,8 @@ const isStale = (state, entry, now) => {
|
|
|
541
499
|
* @returns True if the entry is expired.
|
|
542
500
|
*/
|
|
543
501
|
const isExpired = (state, entry, now) => {
|
|
544
|
-
if (typeof entry === "string") return entry ===
|
|
545
|
-
return computeEntryStatus(state, entry, now) ===
|
|
502
|
+
if (typeof entry === "string") return entry === "expired";
|
|
503
|
+
return computeEntryStatus(state, entry, now) === "expired";
|
|
546
504
|
};
|
|
547
505
|
|
|
548
506
|
//#endregion
|
|
@@ -613,7 +571,7 @@ const shouldPurge = (mode, state, purgeContext) => {
|
|
|
613
571
|
if (mode === false) return false;
|
|
614
572
|
if (mode === true) return true;
|
|
615
573
|
const userThreshold = Number(mode);
|
|
616
|
-
const defaultPurge = purgeContext === "sweep" ?
|
|
574
|
+
const defaultPurge = purgeContext === "sweep" ? true : false;
|
|
617
575
|
if (Number.isNaN(userThreshold)) return defaultPurge;
|
|
618
576
|
const usage = computeResourceUsage(state);
|
|
619
577
|
if (!usage) return defaultPurge;
|
|
@@ -631,9 +589,11 @@ const shouldPurge = (mode, state, purgeContext) => {
|
|
|
631
589
|
*/
|
|
632
590
|
function _sweepOnce(state, _maxKeysPerBatch = MAX_KEYS_PER_BATCH) {
|
|
633
591
|
if (!state._sweepIter) state._sweepIter = state.store.entries();
|
|
592
|
+
const now = Date.now();
|
|
634
593
|
let processed = 0;
|
|
635
594
|
let expiredCount = 0;
|
|
636
595
|
let staleCount = 0;
|
|
596
|
+
const shouldPurgeStale = shouldPurge(state.purgeStaleOnSweep, state, "sweep");
|
|
637
597
|
for (let i = 0; i < _maxKeysPerBatch; i++) {
|
|
638
598
|
const next = state._sweepIter.next();
|
|
639
599
|
if (next.done) {
|
|
@@ -642,22 +602,20 @@ function _sweepOnce(state, _maxKeysPerBatch = MAX_KEYS_PER_BATCH) {
|
|
|
642
602
|
}
|
|
643
603
|
processed += 1;
|
|
644
604
|
const [key, entry] = next.value;
|
|
645
|
-
const now = Date.now();
|
|
646
605
|
const status = computeEntryStatus(state, entry, now);
|
|
647
606
|
if (isExpired(state, status, now)) {
|
|
648
|
-
deleteKey(state, key,
|
|
607
|
+
deleteKey(state, key, "expired");
|
|
649
608
|
expiredCount += 1;
|
|
650
609
|
} else if (isStale(state, status, now)) {
|
|
651
610
|
staleCount += 1;
|
|
652
|
-
if (
|
|
611
|
+
if (shouldPurgeStale) deleteKey(state, key, "stale");
|
|
653
612
|
}
|
|
654
613
|
}
|
|
655
|
-
const expiredStaleCount = shouldPurge(state.purgeStaleOnSweep, state, "sweep") ? staleCount : 0;
|
|
656
614
|
return {
|
|
657
615
|
processed,
|
|
658
616
|
expiredCount,
|
|
659
617
|
staleCount,
|
|
660
|
-
ratio: processed > 0 ? (expiredCount +
|
|
618
|
+
ratio: processed > 0 ? (expiredCount + (shouldPurgeStale ? staleCount : 0)) / processed : 0
|
|
661
619
|
};
|
|
662
620
|
}
|
|
663
621
|
|
|
@@ -673,8 +631,6 @@ function _sweepOnce(state, _maxKeysPerBatch = MAX_KEYS_PER_BATCH) {
|
|
|
673
631
|
* This function complements (`_selectInstanceToSweep`), which is responsible
|
|
674
632
|
* for selecting the correct instance based on the weights assigned here.
|
|
675
633
|
*
|
|
676
|
-
* ---
|
|
677
|
-
*
|
|
678
634
|
* ### Sweep systems:
|
|
679
635
|
* 1. **Normal sweep**
|
|
680
636
|
* - Runs whenever the percentage of expired keys exceeds the allowed threshold
|
|
@@ -682,14 +638,7 @@ function _sweepOnce(state, _maxKeysPerBatch = MAX_KEYS_PER_BATCH) {
|
|
|
682
638
|
* - It is the main cleanup mechanism and is applied proportionally to the
|
|
683
639
|
* store size and the expired‑key ratio.
|
|
684
640
|
*
|
|
685
|
-
* 2. **
|
|
686
|
-
* - Works exactly like the normal sweep, except it may run even when it
|
|
687
|
-
* normally wouldn’t.
|
|
688
|
-
* - Only activates under **high memory pressure**.
|
|
689
|
-
* - Serves as an additional control mechanism to adjust weights, keep the
|
|
690
|
-
* system updated, and help prevent memory overflows.
|
|
691
|
-
*
|
|
692
|
-
* 3. **Round‑robin sweep (minimal control)**
|
|
641
|
+
* 2. **Round‑robin sweep (minimal control)**
|
|
693
642
|
* - Always runs, even if the expired ratio is low or memory usage does not
|
|
694
643
|
* require it.
|
|
695
644
|
* - Processes a very small number of keys per instance, much smaller than
|
|
@@ -697,16 +646,6 @@ function _sweepOnce(state, _maxKeysPerBatch = MAX_KEYS_PER_BATCH) {
|
|
|
697
646
|
* - Its main purpose is to ensure that all instances receive at least a
|
|
698
647
|
* periodic weight update and minimal expired‑key control.
|
|
699
648
|
*
|
|
700
|
-
* ---
|
|
701
|
-
* #### Important notes:
|
|
702
|
-
* - A minimum `MINIMAL_EXPIRED_RATIO` (e.g., 5%) is assumed to ensure that
|
|
703
|
-
* control sweeps can always run under high‑memory scenarios.
|
|
704
|
-
* - Even with a minimum ratio, the normal sweep and the memory‑conditioned sweep
|
|
705
|
-
* may **skip execution** if memory usage allows it and the expired ratio is
|
|
706
|
-
* below the optimal maximum.
|
|
707
|
-
* - The round‑robin sweep is never skipped: it always runs with a very small,
|
|
708
|
-
* almost imperceptible cost.
|
|
709
|
-
*
|
|
710
649
|
* @returns The total accumulated sweep weight across all cache instances.
|
|
711
650
|
*/
|
|
712
651
|
function _updateWeightSweep() {
|
|
@@ -716,8 +655,7 @@ function _updateWeightSweep() {
|
|
|
716
655
|
instCache._sweepWeight = 0;
|
|
717
656
|
continue;
|
|
718
657
|
}
|
|
719
|
-
|
|
720
|
-
if (instCache._expiredRatio > MINIMAL_EXPIRED_RATIO) expiredRatio = instCache._expiredRatio;
|
|
658
|
+
const expiredRatio = instCache._expiredRatio;
|
|
721
659
|
instCache._sweepWeight = instCache.store.size * expiredRatio;
|
|
722
660
|
totalSweepWeight += instCache._sweepWeight;
|
|
723
661
|
}
|
|
@@ -743,7 +681,7 @@ const sweep = async (state, utilities = {}) => {
|
|
|
743
681
|
const { schedule = defaultSchedule, yieldFn = defaultYieldFn, now = Date.now(), runOnlyOne = false } = utilities;
|
|
744
682
|
const startTime = now;
|
|
745
683
|
let sweepIntervalMs = OPTIMAL_SWEEP_INTERVAL;
|
|
746
|
-
let sweepTimeBudgetMs =
|
|
684
|
+
let sweepTimeBudgetMs = 15;
|
|
747
685
|
const totalSweepWeight = _updateWeightSweep();
|
|
748
686
|
const currentExpiredRatios = [];
|
|
749
687
|
const maxKeysPerBatch = totalSweepWeight <= 0 ? MAX_KEYS_PER_BATCH / _instancesCache.length : MAX_KEYS_PER_BATCH;
|
|
@@ -780,7 +718,7 @@ const _instancesCache = [];
|
|
|
780
718
|
* @returns The initial cache state.
|
|
781
719
|
*/
|
|
782
720
|
const createCache = (options = {}) => {
|
|
783
|
-
const { onExpire, onDelete, defaultTtl = DEFAULT_TTL, maxSize = DEFAULT_MAX_SIZE, maxMemorySize = DEFAULT_MAX_MEMORY_SIZE, _maxAllowExpiredRatio = DEFAULT_MAX_EXPIRED_RATIO, defaultStaleWindow =
|
|
721
|
+
const { onExpire, onDelete, defaultTtl = DEFAULT_TTL, maxSize = DEFAULT_MAX_SIZE, maxMemorySize = DEFAULT_MAX_MEMORY_SIZE, _maxAllowExpiredRatio = DEFAULT_MAX_EXPIRED_RATIO, defaultStaleWindow = 0, purgeStaleOnGet, purgeStaleOnSweep, purgeResourceMetric, _autoStartSweep = true } = options;
|
|
784
722
|
_instanceCount++;
|
|
785
723
|
if (_instanceCount > INSTANCE_WARNING_THRESHOLD) console.warn(`Too many instances detected (${_instanceCount}). This may indicate a configuration issue; consider minimizing instance creation or grouping keys by expected expiration ranges. See the documentation: https://github.com/neezco/cache/docs/getting-started.md`);
|
|
786
724
|
const resolvedPurgeResourceMetric = purgeResourceMetric ?? resolvePurgeResourceMetric({
|
|
@@ -849,10 +787,10 @@ const getWithStatus = (state, key, purgeMode, now = Date.now()) => {
|
|
|
849
787
|
const status = computeEntryStatus(state, entry, now);
|
|
850
788
|
if (isFresh(state, status, now)) return [status, entry];
|
|
851
789
|
if (isStale(state, status, now)) {
|
|
852
|
-
if (shouldPurge(purgeMode ?? state.purgeStaleOnGet, state, "get")) deleteKey(state, key,
|
|
790
|
+
if (shouldPurge(purgeMode ?? state.purgeStaleOnGet, state, "get")) deleteKey(state, key, "stale");
|
|
853
791
|
return [status, entry];
|
|
854
792
|
}
|
|
855
|
-
deleteKey(state, key,
|
|
793
|
+
deleteKey(state, key, "expired");
|
|
856
794
|
return [status, void 0];
|
|
857
795
|
};
|
|
858
796
|
/**
|