@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.
@@ -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`.
@@ -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: DEFAULT_PURGE_STALE_ON_GET_NO_LIMITS
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) instanceToSweep = null;
367
- instanceToSweep = _instancesCache[batchSweep - 1];
368
- } else {
369
- let threshold = Math.random() * totalSweepWeight;
370
- for (const inst of _instancesCache) {
371
- threshold -= inst._sweepWeight;
372
- if (threshold <= 0) {
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 instanceToSweep;
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 = DELETE_REASON.MANUAL) => {
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 !== DELETE_REASON.MANUAL) state.onExpire?.(key, entry[1], 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$1) {
370
+ let ENTRY_STATUS = /* @__PURE__ */ function(ENTRY_STATUS) {
413
371
  /** Valid and within TTL. */
414
- ENTRY_STATUS$1["FRESH"] = "fresh";
372
+ ENTRY_STATUS["FRESH"] = "fresh";
415
373
  /** Expired but within stale window; still served. */
416
- ENTRY_STATUS$1["STALE"] = "stale";
374
+ ENTRY_STATUS["STALE"] = "stale";
417
375
  /** Beyond stale window; not served. */
418
- ENTRY_STATUS$1["EXPIRED"] = "expired";
419
- return ENTRY_STATUS$1;
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 = ENTRY_STATUS.FRESH;
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 = ENTRY_STATUS.EXPIRED;
410
+ status = "expired";
453
411
  break;
454
412
  }
455
413
  if (tagStaleSinceAt >= entryCreatedAt) {
456
414
  if (tagStaleSinceAt < earliestTagStaleInvalidation) earliestTagStaleInvalidation = tagStaleSinceAt;
457
- status = ENTRY_STATUS.STALE;
415
+ status = "stale";
458
416
  }
459
417
  }
460
- return [status, status === ENTRY_STATUS.STALE ? earliestTagStaleInvalidation : 0];
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 === ENTRY_STATUS.EXPIRED) return ENTRY_STATUS.EXPIRED;
441
+ if (tagStatus === "expired") return "expired";
484
442
  const windowStale = staleExpiresAt - expiresAt;
485
- if (tagStatus === ENTRY_STATUS.STALE && staleExpiresAt > 0 && now < earliestTagStaleInvalidation + windowStale && now <= staleExpiresAt) return ENTRY_STATUS.STALE;
486
- if (now < expiresAt) return ENTRY_STATUS.FRESH;
487
- if (staleExpiresAt > 0 && now < staleExpiresAt) return ENTRY_STATUS.STALE;
488
- return ENTRY_STATUS.EXPIRED;
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 === ENTRY_STATUS.FRESH;
507
- return computeEntryStatus(state, entry, now) === ENTRY_STATUS.FRESH;
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 === ENTRY_STATUS.STALE;
526
- return computeEntryStatus(state, entry, now) === ENTRY_STATUS.STALE;
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 === ENTRY_STATUS.EXPIRED;
545
- return computeEntryStatus(state, entry, now) === ENTRY_STATUS.EXPIRED;
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" ? DEFAULT_PURGE_STALE_ON_SWEEP_NO_LIMITS : DEFAULT_PURGE_STALE_ON_GET_NO_LIMITS;
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, DELETE_REASON.EXPIRED);
607
+ deleteKey(state, key, "expired");
649
608
  expiredCount += 1;
650
609
  } else if (isStale(state, status, now)) {
651
610
  staleCount += 1;
652
- if (shouldPurge(state.purgeStaleOnSweep, state, "sweep")) deleteKey(state, key, DELETE_REASON.STALE);
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 + expiredStaleCount) / processed : 0
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. **Memoryconditioned sweep (control)**
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. **Roundrobin 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
- let expiredRatio = MINIMAL_EXPIRED_RATIO;
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 = OPTIMAL_SWEEP_TIME_BUDGET_IF_NOTE_METRICS_AVAILABLE;
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 = DEFAULT_STALE_WINDOW, purgeStaleOnGet, purgeStaleOnSweep, purgeResourceMetric, _autoStartSweep = true } = options;
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, DELETE_REASON.STALE);
790
+ if (shouldPurge(purgeMode ?? state.purgeStaleOnGet, state, "get")) deleteKey(state, key, "stale");
853
791
  return [status, entry];
854
792
  }
855
- deleteKey(state, key, DELETE_REASON.EXPIRED);
793
+ deleteKey(state, key, "expired");
856
794
  return [status, void 0];
857
795
  };
858
796
  /**