@neezco/cache 0.3.0 → 0.4.1

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.
@@ -81,6 +81,242 @@ const OPTIMAL_SWEEP_INTERVAL = 2 * ONE_SECOND;
81
81
  * Used when performance metrics are not available or unreliable.
82
82
  */
83
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
+ * Default threshold for purging stale entries on get operations (backend with limits).
102
+ * Stale entries are purged when resource usage exceeds 80%.
103
+ *
104
+ * Note: This is used when limits are configured.
105
+ * When no limits are defined, purgeStaleOnGet defaults to false.
106
+ */
107
+ const DEFAULT_PURGE_STALE_ON_GET_THRESHOLD = .8;
108
+ /**
109
+ * Default threshold for purging stale entries during sweep operations (backend with limits).
110
+ * Stale entries are purged when resource usage exceeds 50%.
111
+ *
112
+ * Note: This is used when limits are configured.
113
+ * When no limits are defined, purgeStaleOnSweep defaults to true.
114
+ */
115
+ const DEFAULT_PURGE_STALE_ON_SWEEP_THRESHOLD = .5;
116
+
117
+ //#endregion
118
+ //#region src/resolve-purge-config/validators.ts
119
+ /**
120
+ * Validates if a numeric value is a valid positive limit.
121
+ * @internal
122
+ */
123
+ const isValidLimit = (value) => Number.isFinite(value) && value > 0;
124
+ /**
125
+ * Checks if the required limits are configured for the given metric.
126
+ * @internal
127
+ */
128
+ const checkRequiredLimits = (metric, limitStatus) => {
129
+ if (metric === "fixed") return false;
130
+ if (metric === "size") return limitStatus.hasSizeLimit;
131
+ if (metric === "memory") return limitStatus.hasMemoryLimit;
132
+ if (metric === "higher") return limitStatus.hasSizeLimit && limitStatus.hasMemoryLimit;
133
+ return false;
134
+ };
135
+
136
+ //#endregion
137
+ //#region src/resolve-purge-config/formatters.ts
138
+ /**
139
+ * Gets the requirement text for a metric when limits are missing.
140
+ * @internal
141
+ */
142
+ const getLimitRequirementText = (metric) => {
143
+ if (metric === "fixed") return "Numeric thresholds are not supported (metric is 'fixed')";
144
+ if (metric === "size") return "'maxSize' must be a valid positive number";
145
+ if (metric === "memory") return "'maxMemorySize' must be a valid positive number";
146
+ if (metric === "higher") return "both 'maxSize' and 'maxMemorySize' must be valid positive numbers";
147
+ return "required configuration";
148
+ };
149
+ /**
150
+ * Formats a purge mode value for display.
151
+ * @internal
152
+ */
153
+ const formatPurgeValue = (mode) => {
154
+ if (typeof mode === "number") return `threshold ${(mode * 100).toFixed(0)}%`;
155
+ return `${mode}`;
156
+ };
157
+
158
+ //#endregion
159
+ //#region src/resolve-purge-config/warnings.ts
160
+ /**
161
+ * Warns user about invalid purge configuration.
162
+ * Only called when user-provided threshold value is invalid.
163
+ *
164
+ * @internal
165
+ */
166
+ const warnInvalidPurgeMode = (config, invalidConditions) => {
167
+ if (invalidConditions.isOutOfRange) {
168
+ console.warn(`[Cache] ${config.operation}: Set to ${formatPurgeValue(config.mode)} with purgeResourceMetric '${config.metric}'.\n ⚠ Invalid: Numeric threshold must be between 0 (exclusive) and 1 (inclusive).\n ✓ Fallback: ${config.operation} = ${formatPurgeValue(config.fallback)}, purgeResourceMetric = '${config.metric}'`);
169
+ return;
170
+ }
171
+ if (invalidConditions.isIncompatibleWithMetric) {
172
+ console.warn(`[Cache] ${config.operation}: Set to ${formatPurgeValue(config.mode)} with purgeResourceMetric '${config.metric}'.\n ⚠ Not supported: Numeric thresholds don't work with purgeResourceMetric 'fixed'.\n ✓ Fallback: ${config.operation} = ${formatPurgeValue(config.fallback)}, purgeResourceMetric = '${config.metric}'`);
173
+ return;
174
+ }
175
+ if (invalidConditions.isMissingLimits) {
176
+ const requirement = getLimitRequirementText(config.metric);
177
+ console.warn(`[Cache] ${config.operation}: Set to ${formatPurgeValue(config.mode)} with purgeResourceMetric '${config.metric}'.\n ⚠ Not supported: ${requirement}\n ✓ Fallback: ${config.operation} = ${formatPurgeValue(config.fallback)}, purgeResourceMetric = '${config.metric}'`);
178
+ }
179
+ };
180
+
181
+ //#endregion
182
+ //#region src/resolve-purge-config/core.ts
183
+ /**
184
+ * Generic purge mode resolver that handles both get and sweep operations.
185
+ *
186
+ * Resolves valid user values or returns appropriate defaults based on:
187
+ * - Available configuration limits (maxSize, maxMemorySize)
188
+ * - Purge resource metric support (size, memory, higher, fixed)
189
+ * - User-provided threshold validity (0 < value ≤ 1)
190
+ *
191
+ * Behavior:
192
+ * - Boolean values (true/false): always valid, returns as-is
193
+ * - Numeric thresholds (0-1): validated against 3 conditions:
194
+ * 1. Range validation: must be 0 < value ≤ 1
195
+ * 2. Metric compatibility: metric must support thresholds (not 'fixed')
196
+ * 3. Configuration requirement: metric's required limits must be set
197
+ * - Invalid numerics: logs warning and returns configuration default
198
+ *
199
+ * Defaults:
200
+ * - With required limits: threshold-based (0.80 for get, 0.5 for sweep)
201
+ * - Without required limits: boolean (false for get, true for sweep)
202
+ *
203
+ * @internal
204
+ */
205
+ const resolvePurgeMode = (limits, config, defaults, userValue) => {
206
+ const hasSizeLimit = isValidLimit(limits.maxSize);
207
+ const hasMemoryLimit = isValidLimit(limits.maxMemorySize);
208
+ const hasRequiredLimits = checkRequiredLimits(config.purgeResourceMetric, {
209
+ hasSizeLimit,
210
+ hasMemoryLimit
211
+ });
212
+ const fallback = hasRequiredLimits ? defaults.withLimits : defaults.withoutLimits;
213
+ if (userValue !== void 0) {
214
+ const isNumeric = typeof userValue === "number";
215
+ const isOutOfRange = isNumeric && (userValue <= 0 || userValue > 1);
216
+ const isIncompatibleWithMetric = isNumeric && config.purgeResourceMetric === "fixed";
217
+ const isMissingLimits = isNumeric && !hasRequiredLimits;
218
+ if (isOutOfRange || isIncompatibleWithMetric || isMissingLimits) {
219
+ warnInvalidPurgeMode({
220
+ mode: userValue,
221
+ metric: config.purgeResourceMetric,
222
+ operation: config.operation,
223
+ fallback
224
+ }, {
225
+ isOutOfRange,
226
+ isIncompatibleWithMetric,
227
+ isMissingLimits
228
+ });
229
+ return fallback;
230
+ }
231
+ return userValue;
232
+ }
233
+ return fallback;
234
+ };
235
+
236
+ //#endregion
237
+ //#region src/resolve-purge-config/get.ts
238
+ /**
239
+ * Resolves the purgeStaleOnGet mode based on available configuration.
240
+ *
241
+ * Returns:
242
+ * - User value if valid (boolean always valid; numeric must satisfy all conditions)
243
+ * - Configuration default if user value is invalid
244
+ *
245
+ * Validation for numeric user values (0-1 thresholds):
246
+ * - Must be in range: 0 < value ≤ 1
247
+ * - Metric must support thresholds: not 'fixed'
248
+ * - Metric must have required limits: 'size' needs maxSize, 'memory' needs maxMemorySize, 'higher' needs both
249
+ *
250
+ * Configuration defaults:
251
+ * - With limits matching metric: 0.80 (80% purge threshold)
252
+ * - Without matching limits: false (preserve stale entries)
253
+ *
254
+ * @param config - Configuration with limits, purgeResourceMetric, and optional userValue
255
+ * @returns Valid purgeStaleOnGet value (boolean or threshold 0-1)
256
+ *
257
+ * @internal
258
+ */
259
+ const resolvePurgeStaleOnGet = (config) => resolvePurgeMode(config.limits, {
260
+ purgeResourceMetric: config.purgeResourceMetric,
261
+ operation: "purgeStaleOnGet"
262
+ }, {
263
+ withLimits: DEFAULT_PURGE_STALE_ON_GET_THRESHOLD,
264
+ withoutLimits: DEFAULT_PURGE_STALE_ON_GET_NO_LIMITS
265
+ }, config.userValue);
266
+
267
+ //#endregion
268
+ //#region src/resolve-purge-config/metric.ts
269
+ /**
270
+ * Resolves the purge resource metric based on available limits and environment.
271
+ *
272
+ * - Browser: returns "size" if maxSize is valid, otherwise "fixed"
273
+ * - Backend with both maxSize and maxMemorySize: returns "higher"
274
+ * - Backend with only maxMemorySize: returns "memory"
275
+ * - Backend with only maxSize: returns "size"
276
+ * - Backend with no limits: returns "fixed"
277
+ *
278
+ * @param config - Configuration object with maxSize and maxMemorySize limits
279
+ * @returns The appropriate purge resource metric for this configuration
280
+ *
281
+ * @internal
282
+ */
283
+ const resolvePurgeResourceMetric = (config) => {
284
+ return {
285
+ hasSizeLimit: isValidLimit(config.maxSize),
286
+ hasMemoryLimit: isValidLimit(config.maxMemorySize)
287
+ }.hasSizeLimit ? "size" : "fixed";
288
+ };
289
+
290
+ //#endregion
291
+ //#region src/resolve-purge-config/sweep.ts
292
+ /**
293
+ * Resolves the purgeStaleOnSweep mode based on available configuration.
294
+ *
295
+ * Returns:
296
+ * - User value if valid (boolean always valid; numeric must satisfy all conditions)
297
+ * - Configuration default if user value is invalid
298
+ *
299
+ * Validation for numeric user values (0-1 thresholds):
300
+ * - Must be in range: 0 < value ≤ 1
301
+ * - Metric must support thresholds: not 'fixed'
302
+ * - Metric must have required limits: 'size' needs maxSize, 'memory' needs maxMemorySize, 'higher' needs both
303
+ *
304
+ * Configuration defaults:
305
+ * - With limits matching metric: 0.5 (50% purge threshold)
306
+ * - Without matching limits: true (always purge to prevent unbounded growth)
307
+ *
308
+ * @param config - Configuration with limits, purgeResourceMetric, and optional userValue
309
+ * @returns Valid purgeStaleOnSweep value (boolean or threshold 0-1)
310
+ *
311
+ * @internal
312
+ */
313
+ const resolvePurgeStaleOnSweep = (config) => resolvePurgeMode(config.limits, {
314
+ purgeResourceMetric: config.purgeResourceMetric,
315
+ operation: "purgeStaleOnSweep"
316
+ }, {
317
+ withLimits: DEFAULT_PURGE_STALE_ON_SWEEP_THRESHOLD,
318
+ withoutLimits: true
319
+ }, config.userValue);
84
320
 
85
321
  //#endregion
86
322
  //#region src/utils/start-monitor.ts
@@ -171,14 +407,14 @@ const deleteKey = (state, key, reason = DELETE_REASON.MANUAL) => {
171
407
  //#endregion
172
408
  //#region src/types.ts
173
409
  /**
174
- * Status of a cache entry.
410
+ * Entry status: fresh, stale, or expired.
175
411
  */
176
412
  let ENTRY_STATUS = /* @__PURE__ */ function(ENTRY_STATUS$1) {
177
- /** The entry is fresh and valid. */
413
+ /** Valid and within TTL. */
178
414
  ENTRY_STATUS$1["FRESH"] = "fresh";
179
- /** The entry is stale but can still be served. */
415
+ /** Expired but within stale window; still served. */
180
416
  ENTRY_STATUS$1["STALE"] = "stale";
181
- /** The entry has expired and is no longer valid. */
417
+ /** Beyond stale window; not served. */
182
418
  ENTRY_STATUS$1["EXPIRED"] = "expired";
183
419
  return ENTRY_STATUS$1;
184
420
  }({});
@@ -246,7 +482,7 @@ function computeEntryStatus(state, entry, now) {
246
482
  const [tagStatus, earliestTagStaleInvalidation] = _statusFromTags(state, entry);
247
483
  if (tagStatus === ENTRY_STATUS.EXPIRED) return ENTRY_STATUS.EXPIRED;
248
484
  const windowStale = staleExpiresAt - expiresAt;
249
- if (tagStatus === ENTRY_STATUS.STALE && staleExpiresAt > 0 && now < earliestTagStaleInvalidation + windowStale) return ENTRY_STATUS.STALE;
485
+ if (tagStatus === ENTRY_STATUS.STALE && staleExpiresAt > 0 && now < earliestTagStaleInvalidation + windowStale && now <= staleExpiresAt) return ENTRY_STATUS.STALE;
250
486
  if (now < expiresAt) return ENTRY_STATUS.FRESH;
251
487
  if (staleExpiresAt > 0 && now < staleExpiresAt) return ENTRY_STATUS.STALE;
252
488
  return ENTRY_STATUS.EXPIRED;
@@ -309,6 +545,81 @@ const isExpired = (state, entry, now) => {
309
545
  return computeEntryStatus(state, entry, now) === ENTRY_STATUS.EXPIRED;
310
546
  };
311
547
 
548
+ //#endregion
549
+ //#region src/utils/purge-eval.ts
550
+ /**
551
+ * Computes memory utilization as a normalized 0–1 value.
552
+ *
553
+ * In backend environments where metrics are available, returns the actual
554
+ * memory utilization from the monitor. In browser environments or when
555
+ * metrics are unavailable, returns 0.
556
+ *
557
+ * @returns Memory utilization in range [0, 1]
558
+ *
559
+ * @internal
560
+ */
561
+ const getMemoryUtilization = () => {
562
+ return 0;
563
+ };
564
+ /**
565
+ * Computes size utilization as a normalized 0–1 value.
566
+ *
567
+ * If maxSize is finite, returns `currentSize / maxSize`. Otherwise returns 0.
568
+ *
569
+ * @param state - The cache state
570
+ * @returns Size utilization in range [0, 1]
571
+ *
572
+ * @internal
573
+ */
574
+ const getSizeUtilization = (state) => {
575
+ if (!Number.isFinite(state.maxSize) || state.maxSize <= 0 || state.size <= 0) return 0;
576
+ return Math.min(1, state.size / state.maxSize);
577
+ };
578
+ /**
579
+ * Computes a 0–1 resource usage metric based on the configured purge metric.
580
+ *
581
+ * - `"size"`: Returns size utilization only.
582
+ * - `"memory"`: Returns memory utilization (backend only; returns 0 in browser).
583
+ * - `"higher"`: Returns the maximum of memory and size utilization.
584
+ *
585
+ * The result is always clamped to [0, 1].
586
+ *
587
+ * @param state - The cache state
588
+ * @returns Resource usage in range [0, 1]
589
+ *
590
+ * @internal
591
+ */
592
+ const computeResourceUsage = (state) => {
593
+ const metric = state.purgeResourceMetric;
594
+ if (!metric || metric === "fixed") return null;
595
+ if (metric === "size") return getSizeUtilization(state);
596
+ if (metric === "memory") return getMemoryUtilization();
597
+ if (metric === "higher") return Math.min(1, Math.max(getMemoryUtilization(), getSizeUtilization(state)));
598
+ return null;
599
+ };
600
+ /**
601
+ * Determines whether stale entries should be purged based on the purge mode and current resource usage.
602
+ *
603
+ * @param mode - The purge mode setting
604
+ * - `false` → never purge
605
+ * - `true` → always purge
606
+ * - `number (0–1)` → purge when `resourceUsage >= threshold`
607
+ * @param state - The cache state
608
+ * @returns True if stale entries should be purged, false otherwise
609
+ *
610
+ * @internal
611
+ */
612
+ const shouldPurge = (mode, state, purgeContext) => {
613
+ if (mode === false) return false;
614
+ if (mode === true) return true;
615
+ const userThreshold = Number(mode);
616
+ const defaultPurge = purgeContext === "sweep" ? DEFAULT_PURGE_STALE_ON_SWEEP_NO_LIMITS : DEFAULT_PURGE_STALE_ON_GET_NO_LIMITS;
617
+ if (Number.isNaN(userThreshold)) return defaultPurge;
618
+ const usage = computeResourceUsage(state);
619
+ if (!usage) return defaultPurge;
620
+ return usage >= Math.max(0, Math.min(1, userThreshold));
621
+ };
622
+
312
623
  //#endregion
313
624
  //#region src/sweep/sweep-once.ts
314
625
  /**
@@ -338,10 +649,10 @@ function _sweepOnce(state, _maxKeysPerBatch = MAX_KEYS_PER_BATCH) {
338
649
  expiredCount += 1;
339
650
  } else if (isStale(state, status, now)) {
340
651
  staleCount += 1;
341
- if (state.purgeStaleOnSweep) deleteKey(state, key, DELETE_REASON.STALE);
652
+ if (shouldPurge(state.purgeStaleOnSweep, state, "sweep")) deleteKey(state, key, DELETE_REASON.STALE);
342
653
  }
343
654
  }
344
- const expiredStaleCount = state.purgeStaleOnSweep ? staleCount : 0;
655
+ const expiredStaleCount = shouldPurge(state.purgeStaleOnSweep, state, "sweep") ? staleCount : 0;
345
656
  return {
346
657
  processed,
347
658
  expiredCount,
@@ -415,6 +726,14 @@ function _updateWeightSweep() {
415
726
 
416
727
  //#endregion
417
728
  //#region src/sweep/sweep.ts
729
+ let _isSweepActive = false;
730
+ let _pendingSweepTimeout = null;
731
+ function startSweep(state) {
732
+ if (_isSweepActive) return;
733
+ _isSweepActive = true;
734
+ /* @__PURE__ */ startMonitor();
735
+ sweep(state);
736
+ }
418
737
  /**
419
738
  * Performs a sweep operation on the cache to remove expired and optionally stale entries.
420
739
  * Uses a linear scan with a saved pointer to resume from the last processed key.
@@ -445,8 +764,8 @@ const sweep = async (state, utilities = {}) => {
445
764
  if (!runOnlyOne) schedule(() => void sweep(state, utilities), sweepIntervalMs);
446
765
  };
447
766
  const defaultSchedule = (fn, ms) => {
448
- const t = setTimeout(fn, ms);
449
- if (typeof t.unref === "function") t.unref();
767
+ _pendingSweepTimeout = setTimeout(fn, ms);
768
+ if (typeof _pendingSweepTimeout.unref === "function") _pendingSweepTimeout.unref();
450
769
  };
451
770
  const defaultYieldFn = () => new Promise((resolve) => setImmediate(resolve));
452
771
 
@@ -455,16 +774,35 @@ const defaultYieldFn = () => new Promise((resolve) => setImmediate(resolve));
455
774
  let _instanceCount = 0;
456
775
  const INSTANCE_WARNING_THRESHOLD = 99;
457
776
  const _instancesCache = [];
458
- let _initSweepScheduled = false;
459
777
  /**
460
778
  * Creates the initial state for the TTL cache.
461
779
  * @param options - Configuration options for the cache.
462
780
  * @returns The initial cache state.
463
781
  */
464
782
  const createCache = (options = {}) => {
465
- 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 = false, purgeStaleOnSweep = false, _autoStartSweep = true } = 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;
466
784
  _instanceCount++;
467
785
  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
+ const resolvedPurgeResourceMetric = purgeResourceMetric ?? resolvePurgeResourceMetric({
787
+ maxSize,
788
+ maxMemorySize
789
+ });
790
+ const resolvedPurgeStaleOnGet = resolvePurgeStaleOnGet({
791
+ limits: {
792
+ maxSize,
793
+ maxMemorySize
794
+ },
795
+ purgeResourceMetric: resolvedPurgeResourceMetric,
796
+ userValue: purgeStaleOnGet
797
+ });
798
+ const resolvedPurgeStaleOnSweep = resolvePurgeStaleOnSweep({
799
+ limits: {
800
+ maxSize,
801
+ maxMemorySize
802
+ },
803
+ purgeResourceMetric: resolvedPurgeResourceMetric,
804
+ userValue: purgeStaleOnSweep
805
+ });
468
806
  const state = {
469
807
  store: /* @__PURE__ */ new Map(),
470
808
  _sweepIter: null,
@@ -477,8 +815,9 @@ const createCache = (options = {}) => {
477
815
  maxMemorySize,
478
816
  defaultTtl,
479
817
  defaultStaleWindow,
480
- purgeStaleOnGet,
481
- purgeStaleOnSweep,
818
+ purgeStaleOnGet: resolvedPurgeStaleOnGet,
819
+ purgeStaleOnSweep: resolvedPurgeStaleOnSweep,
820
+ purgeResourceMetric: resolvedPurgeResourceMetric,
482
821
  _maxAllowExpiredRatio,
483
822
  _autoStartSweep,
484
823
  _instanceIndexState: -1,
@@ -487,12 +826,7 @@ const createCache = (options = {}) => {
487
826
  _tags: /* @__PURE__ */ new Map()
488
827
  };
489
828
  state._instanceIndexState = _instancesCache.push(state) - 1;
490
- if (_autoStartSweep) {
491
- if (_initSweepScheduled) return state;
492
- _initSweepScheduled = true;
493
- sweep(state);
494
- }
495
- /* @__PURE__ */ startMonitor();
829
+ startSweep(state);
496
830
  return state;
497
831
  };
498
832
 
@@ -509,13 +843,13 @@ const createCache = (options = {}) => {
509
843
  *
510
844
  * @internal
511
845
  */
512
- const getWithStatus = (state, key, now = Date.now()) => {
846
+ const getWithStatus = (state, key, purgeMode, now = Date.now()) => {
513
847
  const entry = state.store.get(key);
514
848
  if (!entry) return [null, void 0];
515
849
  const status = computeEntryStatus(state, entry, now);
516
850
  if (isFresh(state, status, now)) return [status, entry];
517
851
  if (isStale(state, status, now)) {
518
- if (state.purgeStaleOnGet) deleteKey(state, key, DELETE_REASON.STALE);
852
+ if (shouldPurge(purgeMode ?? state.purgeStaleOnGet, state, "get")) deleteKey(state, key, DELETE_REASON.STALE);
519
853
  return [status, entry];
520
854
  }
521
855
  deleteKey(state, key, DELETE_REASON.EXPIRED);
@@ -530,8 +864,8 @@ const getWithStatus = (state, key, now = Date.now()) => {
530
864
  *
531
865
  * @internal
532
866
  */
533
- const get = (state, key, now = Date.now()) => {
534
- const [, entry] = getWithStatus(state, key, now);
867
+ const get = (state, key, purgeMode, now = Date.now()) => {
868
+ const [, entry] = getWithStatus(state, key, purgeMode, now);
535
869
  return entry ? entry[1] : void 0;
536
870
  };
537
871
 
@@ -630,16 +964,12 @@ const setOrUpdate = (state, input, now = Date.now()) => {
630
964
  //#region src/index.ts
631
965
  /**
632
966
  * A TTL (Time-To-Live) cache implementation with support for expiration,
633
- * stale windows, tag-based invalidation, and automatic sweeping.
967
+ * stale windows, tag-based invalidation, and smart automatic sweeping.
634
968
  *
635
- * Provides O(1) constant-time operations for all core methods.
636
- *
637
- * @example
638
- * ```typescript
639
- * const cache = new LocalTtlCache();
640
- * cache.set("user:123", { name: "Alice" }, { ttl: 5 * 60 * 1000 });
641
- * const user = cache.get("user:123"); // { name: "Alice" }
642
- * ```
969
+ * Provides O(1) constant-time operations for all core methods with support for:
970
+ * - Expiration and stale windows
971
+ * - Tag-based invalidation
972
+ * - Automatic sweeping
643
973
  */
644
974
  var LocalTtlCache = class {
645
975
  state;
@@ -648,41 +978,16 @@ var LocalTtlCache = class {
648
978
  *
649
979
  * @param options - Configuration options for the cache (defaultTtl, defaultStaleWindow, maxSize, etc.)
650
980
  *
651
- * @example
652
- * ```typescript
653
- * const cache = new LocalTtlCache({
654
- * defaultTtl: 30 * 60 * 1000, // 30 minutes
655
- * defaultStaleWindow: 5 * 60 * 1000, // 5 minutes
656
- * maxSize: 500_000, // Maximum 500_000 entries
657
- * onExpire: (key, value) => console.log(`Expired: ${key}`),
658
- * onDelete: (key, value, reason) => console.log(`Deleted: ${key}, reason: ${reason}`),
659
- * });
660
- * ```
661
981
  */
662
982
  constructor(options) {
663
983
  this.state = createCache(options);
664
984
  }
665
- /**
666
- * Gets the current number of entries tracked by the cache.
667
- *
668
- * This value may include entries that are already expired but have not yet been
669
- * removed by the lazy cleanup system. Expired keys are cleaned only when it is
670
- * efficient to do so, so the count can temporarily be higher than the number of
671
- * actually valid (non‑expired) entries.
672
- *
673
- * @returns The number of entries currently stored (including entries pending cleanup)
674
- *
675
- * @example
676
- * ```typescript
677
- * console.log(cache.size); // e.g., 42
678
- * ```
679
- */
680
985
  get size() {
681
986
  return this.state.size;
682
987
  }
683
988
  get(key, options) {
684
- if (options?.includeMetadata) {
685
- const [status, entry] = getWithStatus(this.state, key);
989
+ if (options?.includeMetadata === true) {
990
+ const [status, entry] = getWithStatus(this.state, key, options.purgeStale);
686
991
  if (!entry) return void 0;
687
992
  const [timestamps, value, tags] = entry;
688
993
  const [, expiresAt, staleExpiresAt] = timestamps;
@@ -694,44 +999,8 @@ var LocalTtlCache = class {
694
999
  tags
695
1000
  };
696
1001
  }
697
- return get(this.state, key);
1002
+ return get(this.state, key, options?.purgeStale);
698
1003
  }
699
- /**
700
- * Sets or updates a value in the cache.
701
- *
702
- * If the key already exists, it will be completely replaced.
703
- *
704
- * @param key - The key under which to store the value
705
- * @param value - The value to cache (any type)
706
- * @param options - Optional configuration for this specific entry
707
- * @param options.ttl - Time-To-Live in milliseconds. Defaults to `defaultTtl`
708
- * @param options.staleWindow - How long to serve stale data after expiration (milliseconds)
709
- * @param options.tags - One or more tags for group invalidation
710
- * @returns True if the entry was set or updated, false if rejected due to limits or invalid input
711
- *
712
- * @example
713
- * ```typescript
714
- * const success = cache.set("user:123", { name: "Alice" }, {
715
- * ttl: 5 * 60 * 1000,
716
- * staleWindow: 1 * 60 * 1000,
717
- * tags: "user:123",
718
- * });
719
- *
720
- * if (!success) {
721
- * console.log("Entry was rejected due to size or memory limits");
722
- * }
723
- * ```
724
- *
725
- * @edge-cases
726
- * - Overwriting an existing key replaces it completely
727
- * - If `ttl` is 0 or Infinite, the entry never expires
728
- * - If `staleWindow` is larger than `ttl`, the entry can be served as stale longer than it was fresh
729
- * - Tags are optional; only necessary for group invalidation via `invalidateTag()`
730
- * - Returns `false` if value is `undefined` (existing value remains untouched)
731
- * - Returns `false` if new key would exceed [`maxSize`](./docs/configuration.md#maxsize-number) limit
732
- * - Returns `false` if new key would exceed [`maxMemorySize`](./docs/configuration.md#maxmemorysize-number) limit
733
- * - Updating existing keys always succeeds, even at limit
734
- */
735
1004
  set(key, value, options) {
736
1005
  return setOrUpdate(this.state, {
737
1006
  key,
@@ -741,97 +1010,20 @@ var LocalTtlCache = class {
741
1010
  tags: options?.tags
742
1011
  });
743
1012
  }
744
- /**
745
- * Deletes a specific key from the cache.
746
- *
747
- * @param key - The key to delete
748
- * @returns True if the key was deleted, false if it didn't exist
749
- *
750
- * @example
751
- * ```typescript
752
- * const wasDeleted = cache.delete("user:123");
753
- * ```
754
- *
755
- * @edge-cases
756
- * - Triggers the `onDelete` callback with reason `'manual'`
757
- * - Does not trigger the `onExpire` callback
758
- * - Returns `false` if the key was already expired
759
- * - Deleting a non-existent key returns `false` without error
760
- */
761
1013
  delete(key) {
762
1014
  return deleteKey(this.state, key);
763
1015
  }
764
- /**
765
- * Checks if a key exists in the cache and is not fully expired.
766
- *
767
- * Returns true if the key exists and is either fresh or within the stale window.
768
- * Use this when you only need to check existence without retrieving the value.
769
- *
770
- * @param key - The key to check
771
- * @returns True if the key exists and is valid, false otherwise
772
- *
773
- * @example
774
- * ```typescript
775
- * if (cache.has("user:123")) {
776
- * // Key exists (either fresh or stale)
777
- * }
778
- * ```
779
- *
780
- * @edge-cases
781
- * - Returns `false` if the key doesn't exist
782
- * - Returns `false` if the key has expired beyond the stale window
783
- * - Returns `true` if the key is in the stale window (still being served)
784
- * - Both `has()` and `get()` have O(1) complexity; prefer `get()` if you need the value
785
- */
786
1016
  has(key) {
787
1017
  return has(this.state, key);
788
1018
  }
789
- /**
790
- * Removes all entries from the cache at once.
791
- *
792
- * This is useful for resetting the cache or freeing memory when needed.
793
- * The `onDelete` callback is NOT invoked during clear (intentional optimization).
794
- *
795
- * @example
796
- * ```typescript
797
- * cache.clear(); // cache.size is now 0
798
- * ```
799
- *
800
- * @edge-cases
801
- * - The `onDelete` callback is NOT triggered during clear
802
- * - Clears both expired and fresh entries
803
- * - Resets `cache.size` to 0
804
- */
805
1019
  clear() {
806
1020
  clear(this.state);
807
1021
  }
808
- /**
809
- * Marks all entries with one or more tags as expired (or stale, if requested).
810
- *
811
- * If an entry has multiple tags, invalidating ANY of those tags will invalidate the entry.
812
- *
813
- * @param tags - A single tag (string) or array of tags to invalidate
814
- * @param asStale - If true, marks entries as stale instead of fully expired (still served from stale window)
815
- *
816
- * @example
817
- * ```typescript
818
- * // Invalidate a single tag
819
- * cache.invalidateTag("user:123");
820
- *
821
- * // Invalidate multiple tags
822
- * cache.invalidateTag(["user:123", "posts:456"]);
823
- * ```
824
- *
825
- * @edge-cases
826
- * - Does not throw errors if a tag has no associated entries
827
- * - Invalidating a tag doesn't prevent new entries from being tagged with it later
828
- * - The `onDelete` callback is triggered with reason `'expired'` (even if `asStale` is true)
829
- */
830
1022
  invalidateTag(tags, options) {
831
1023
  invalidateTag(this.state, tags, options ?? {});
832
1024
  }
833
1025
  };
834
1026
 
835
1027
  //#endregion
836
- export { LocalTtlCache };
1028
+ export { ENTRY_STATUS, LocalTtlCache };
837
1029
  //# sourceMappingURL=index.js.map