@neezco/cache 0.2.1 → 0.4.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.
@@ -163,6 +163,246 @@ const DEFAULT_CPU_WEIGHT = 8.5;
163
163
  * Complements CPU weight to assess overall processing capacity.
164
164
  */
165
165
  const DEFAULT_LOOP_WEIGHT = 6.5;
166
+ /**
167
+ * Fallback behavior for stale purging on GET
168
+ * when no resource limits are defined.
169
+ *
170
+ * In this scenario, threshold-based purging is disabled,
171
+ * so GET operations do NOT purge stale entries.
172
+ */
173
+ const DEFAULT_PURGE_STALE_ON_GET_NO_LIMITS = false;
174
+ /**
175
+ * Fallback behavior for stale purging on SWEEP
176
+ * when no resource limits are defined.
177
+ *
178
+ * In this scenario, threshold-based purging is disabled,
179
+ * so SWEEP operations DO purge stale entries to prevent buildup.
180
+ */
181
+ const DEFAULT_PURGE_STALE_ON_SWEEP_NO_LIMITS = true;
182
+ /**
183
+ * Default threshold for purging stale entries on get operations (backend with limits).
184
+ * Stale entries are purged when resource usage exceeds 80%.
185
+ *
186
+ * Note: This is used when limits are configured.
187
+ * When no limits are defined, purgeStaleOnGet defaults to false.
188
+ */
189
+ const DEFAULT_PURGE_STALE_ON_GET_THRESHOLD = .8;
190
+ /**
191
+ * Default threshold for purging stale entries during sweep operations (backend with limits).
192
+ * Stale entries are purged when resource usage exceeds 50%.
193
+ *
194
+ * Note: This is used when limits are configured.
195
+ * When no limits are defined, purgeStaleOnSweep defaults to true.
196
+ */
197
+ const DEFAULT_PURGE_STALE_ON_SWEEP_THRESHOLD = .5;
198
+
199
+ //#endregion
200
+ //#region src/resolve-purge-config/validators.ts
201
+ /**
202
+ * Validates if a numeric value is a valid positive limit.
203
+ * @internal
204
+ */
205
+ const isValidLimit = (value) => Number.isFinite(value) && value > 0;
206
+ /**
207
+ * Checks if the required limits are configured for the given metric.
208
+ * @internal
209
+ */
210
+ const checkRequiredLimits = (metric, limitStatus) => {
211
+ if (metric === "fixed") return false;
212
+ if (metric === "size") return limitStatus.hasSizeLimit;
213
+ if (metric === "memory") return limitStatus.hasMemoryLimit;
214
+ if (metric === "higher") return limitStatus.hasSizeLimit && limitStatus.hasMemoryLimit;
215
+ return false;
216
+ };
217
+
218
+ //#endregion
219
+ //#region src/resolve-purge-config/formatters.ts
220
+ /**
221
+ * Gets the requirement text for a metric when limits are missing.
222
+ * @internal
223
+ */
224
+ const getLimitRequirementText = (metric) => {
225
+ if (metric === "fixed") return "Numeric thresholds are not supported (metric is 'fixed')";
226
+ if (metric === "size") return "'maxSize' must be a valid positive number";
227
+ if (metric === "memory") return "'maxMemorySize' must be a valid positive number";
228
+ if (metric === "higher") return "both 'maxSize' and 'maxMemorySize' must be valid positive numbers";
229
+ return "required configuration";
230
+ };
231
+ /**
232
+ * Formats a purge mode value for display.
233
+ * @internal
234
+ */
235
+ const formatPurgeValue = (mode) => {
236
+ if (typeof mode === "number") return `threshold ${(mode * 100).toFixed(0)}%`;
237
+ return `${mode}`;
238
+ };
239
+
240
+ //#endregion
241
+ //#region src/resolve-purge-config/warnings.ts
242
+ /**
243
+ * Warns user about invalid purge configuration.
244
+ * Only called when user-provided threshold value is invalid.
245
+ *
246
+ * @internal
247
+ */
248
+ const warnInvalidPurgeMode = (config, invalidConditions) => {
249
+ if (invalidConditions.isOutOfRange) {
250
+ 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}'`);
251
+ return;
252
+ }
253
+ if (invalidConditions.isIncompatibleWithMetric) {
254
+ 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}'`);
255
+ return;
256
+ }
257
+ if (invalidConditions.isMissingLimits) {
258
+ const requirement = getLimitRequirementText(config.metric);
259
+ 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}'`);
260
+ }
261
+ };
262
+
263
+ //#endregion
264
+ //#region src/resolve-purge-config/core.ts
265
+ /**
266
+ * Generic purge mode resolver that handles both get and sweep operations.
267
+ *
268
+ * Resolves valid user values or returns appropriate defaults based on:
269
+ * - Available configuration limits (maxSize, maxMemorySize)
270
+ * - Purge resource metric support (size, memory, higher, fixed)
271
+ * - User-provided threshold validity (0 < value ≤ 1)
272
+ *
273
+ * Behavior:
274
+ * - Boolean values (true/false): always valid, returns as-is
275
+ * - Numeric thresholds (0-1): validated against 3 conditions:
276
+ * 1. Range validation: must be 0 < value ≤ 1
277
+ * 2. Metric compatibility: metric must support thresholds (not 'fixed')
278
+ * 3. Configuration requirement: metric's required limits must be set
279
+ * - Invalid numerics: logs warning and returns configuration default
280
+ *
281
+ * Defaults:
282
+ * - With required limits: threshold-based (0.80 for get, 0.5 for sweep)
283
+ * - Without required limits: boolean (false for get, true for sweep)
284
+ *
285
+ * @internal
286
+ */
287
+ const resolvePurgeMode = (limits, config, defaults, userValue) => {
288
+ const hasSizeLimit = isValidLimit(limits.maxSize);
289
+ const hasMemoryLimit = isValidLimit(limits.maxMemorySize);
290
+ const hasRequiredLimits = checkRequiredLimits(config.purgeResourceMetric, {
291
+ hasSizeLimit,
292
+ hasMemoryLimit
293
+ });
294
+ const fallback = hasRequiredLimits ? defaults.withLimits : defaults.withoutLimits;
295
+ if (userValue !== void 0) {
296
+ const isNumeric = typeof userValue === "number";
297
+ const isOutOfRange = isNumeric && (userValue <= 0 || userValue > 1);
298
+ const isIncompatibleWithMetric = isNumeric && config.purgeResourceMetric === "fixed";
299
+ const isMissingLimits = isNumeric && !hasRequiredLimits;
300
+ if (isOutOfRange || isIncompatibleWithMetric || isMissingLimits) {
301
+ warnInvalidPurgeMode({
302
+ mode: userValue,
303
+ metric: config.purgeResourceMetric,
304
+ operation: config.operation,
305
+ fallback
306
+ }, {
307
+ isOutOfRange,
308
+ isIncompatibleWithMetric,
309
+ isMissingLimits
310
+ });
311
+ return fallback;
312
+ }
313
+ return userValue;
314
+ }
315
+ return fallback;
316
+ };
317
+
318
+ //#endregion
319
+ //#region src/resolve-purge-config/get.ts
320
+ /**
321
+ * Resolves the purgeStaleOnGet mode based on available configuration.
322
+ *
323
+ * Returns:
324
+ * - User value if valid (boolean always valid; numeric must satisfy all conditions)
325
+ * - Configuration default if user value is invalid
326
+ *
327
+ * Validation for numeric user values (0-1 thresholds):
328
+ * - Must be in range: 0 < value ≤ 1
329
+ * - Metric must support thresholds: not 'fixed'
330
+ * - Metric must have required limits: 'size' needs maxSize, 'memory' needs maxMemorySize, 'higher' needs both
331
+ *
332
+ * Configuration defaults:
333
+ * - With limits matching metric: 0.80 (80% purge threshold)
334
+ * - Without matching limits: false (preserve stale entries)
335
+ *
336
+ * @param config - Configuration with limits, purgeResourceMetric, and optional userValue
337
+ * @returns Valid purgeStaleOnGet value (boolean or threshold 0-1)
338
+ *
339
+ * @internal
340
+ */
341
+ const resolvePurgeStaleOnGet = (config) => resolvePurgeMode(config.limits, {
342
+ purgeResourceMetric: config.purgeResourceMetric,
343
+ operation: "purgeStaleOnGet"
344
+ }, {
345
+ withLimits: DEFAULT_PURGE_STALE_ON_GET_THRESHOLD,
346
+ withoutLimits: DEFAULT_PURGE_STALE_ON_GET_NO_LIMITS
347
+ }, config.userValue);
348
+
349
+ //#endregion
350
+ //#region src/resolve-purge-config/metric.ts
351
+ /**
352
+ * Resolves the purge resource metric based on available limits and environment.
353
+ *
354
+ * - Browser: returns "size" if maxSize is valid, otherwise "fixed"
355
+ * - Backend with both maxSize and maxMemorySize: returns "higher"
356
+ * - Backend with only maxMemorySize: returns "memory"
357
+ * - Backend with only maxSize: returns "size"
358
+ * - Backend with no limits: returns "fixed"
359
+ *
360
+ * @param config - Configuration object with maxSize and maxMemorySize limits
361
+ * @returns The appropriate purge resource metric for this configuration
362
+ *
363
+ * @internal
364
+ */
365
+ const resolvePurgeResourceMetric = (config) => {
366
+ const limitStatus = {
367
+ hasSizeLimit: isValidLimit(config.maxSize),
368
+ hasMemoryLimit: isValidLimit(config.maxMemorySize)
369
+ };
370
+ if (limitStatus.hasSizeLimit && limitStatus.hasMemoryLimit) return "higher";
371
+ if (limitStatus.hasMemoryLimit) return "memory";
372
+ if (limitStatus.hasSizeLimit) return "size";
373
+ return "fixed";
374
+ };
375
+
376
+ //#endregion
377
+ //#region src/resolve-purge-config/sweep.ts
378
+ /**
379
+ * Resolves the purgeStaleOnSweep mode based on available configuration.
380
+ *
381
+ * Returns:
382
+ * - User value if valid (boolean always valid; numeric must satisfy all conditions)
383
+ * - Configuration default if user value is invalid
384
+ *
385
+ * Validation for numeric user values (0-1 thresholds):
386
+ * - Must be in range: 0 < value ≤ 1
387
+ * - Metric must support thresholds: not 'fixed'
388
+ * - Metric must have required limits: 'size' needs maxSize, 'memory' needs maxMemorySize, 'higher' needs both
389
+ *
390
+ * Configuration defaults:
391
+ * - With limits matching metric: 0.5 (50% purge threshold)
392
+ * - Without matching limits: true (always purge to prevent unbounded growth)
393
+ *
394
+ * @param config - Configuration with limits, purgeResourceMetric, and optional userValue
395
+ * @returns Valid purgeStaleOnSweep value (boolean or threshold 0-1)
396
+ *
397
+ * @internal
398
+ */
399
+ const resolvePurgeStaleOnSweep = (config) => resolvePurgeMode(config.limits, {
400
+ purgeResourceMetric: config.purgeResourceMetric,
401
+ operation: "purgeStaleOnSweep"
402
+ }, {
403
+ withLimits: DEFAULT_PURGE_STALE_ON_SWEEP_THRESHOLD,
404
+ withoutLimits: true
405
+ }, config.userValue);
166
406
 
167
407
  //#endregion
168
408
  //#region src/utils/get-process-memory-limit.ts
@@ -508,14 +748,14 @@ const deleteKey = (state, key, reason = DELETE_REASON.MANUAL) => {
508
748
  //#endregion
509
749
  //#region src/types.ts
510
750
  /**
511
- * Status of a cache entry.
751
+ * Entry status: fresh, stale, or expired.
512
752
  */
513
753
  let ENTRY_STATUS = /* @__PURE__ */ function(ENTRY_STATUS$1) {
514
- /** The entry is fresh and valid. */
754
+ /** Valid and within TTL. */
515
755
  ENTRY_STATUS$1["FRESH"] = "fresh";
516
- /** The entry is stale but can still be served. */
756
+ /** Expired but within stale window; still served. */
517
757
  ENTRY_STATUS$1["STALE"] = "stale";
518
- /** The entry has expired and is no longer valid. */
758
+ /** Beyond stale window; not served. */
519
759
  ENTRY_STATUS$1["EXPIRED"] = "expired";
520
760
  return ENTRY_STATUS$1;
521
761
  }({});
@@ -583,7 +823,7 @@ function computeEntryStatus(state, entry, now) {
583
823
  const [tagStatus, earliestTagStaleInvalidation] = _statusFromTags(state, entry);
584
824
  if (tagStatus === ENTRY_STATUS.EXPIRED) return ENTRY_STATUS.EXPIRED;
585
825
  const windowStale = staleExpiresAt - expiresAt;
586
- if (tagStatus === ENTRY_STATUS.STALE && staleExpiresAt > 0 && now < earliestTagStaleInvalidation + windowStale) return ENTRY_STATUS.STALE;
826
+ if (tagStatus === ENTRY_STATUS.STALE && staleExpiresAt > 0 && now < earliestTagStaleInvalidation + windowStale && now <= staleExpiresAt) return ENTRY_STATUS.STALE;
587
827
  if (now < expiresAt) return ENTRY_STATUS.FRESH;
588
828
  if (staleExpiresAt > 0 && now < staleExpiresAt) return ENTRY_STATUS.STALE;
589
829
  return ENTRY_STATUS.EXPIRED;
@@ -646,6 +886,82 @@ const isExpired = (state, entry, now) => {
646
886
  return computeEntryStatus(state, entry, now) === ENTRY_STATUS.EXPIRED;
647
887
  };
648
888
 
889
+ //#endregion
890
+ //#region src/utils/purge-eval.ts
891
+ /**
892
+ * Computes memory utilization as a normalized 0–1 value.
893
+ *
894
+ * In backend environments where metrics are available, returns the actual
895
+ * memory utilization from the monitor. In browser environments or when
896
+ * metrics are unavailable, returns 0.
897
+ *
898
+ * @returns Memory utilization in range [0, 1]
899
+ *
900
+ * @internal
901
+ */
902
+ const getMemoryUtilization = () => {
903
+ if (!_metrics) return 0;
904
+ return _metrics.memory?.utilization ?? 0;
905
+ };
906
+ /**
907
+ * Computes size utilization as a normalized 0–1 value.
908
+ *
909
+ * If maxSize is finite, returns `currentSize / maxSize`. Otherwise returns 0.
910
+ *
911
+ * @param state - The cache state
912
+ * @returns Size utilization in range [0, 1]
913
+ *
914
+ * @internal
915
+ */
916
+ const getSizeUtilization = (state) => {
917
+ if (!Number.isFinite(state.maxSize) || state.maxSize <= 0 || state.size <= 0) return 0;
918
+ return Math.min(1, state.size / state.maxSize);
919
+ };
920
+ /**
921
+ * Computes a 0–1 resource usage metric based on the configured purge metric.
922
+ *
923
+ * - `"size"`: Returns size utilization only.
924
+ * - `"memory"`: Returns memory utilization (backend only; returns 0 in browser).
925
+ * - `"higher"`: Returns the maximum of memory and size utilization.
926
+ *
927
+ * The result is always clamped to [0, 1].
928
+ *
929
+ * @param state - The cache state
930
+ * @returns Resource usage in range [0, 1]
931
+ *
932
+ * @internal
933
+ */
934
+ const computeResourceUsage = (state) => {
935
+ const metric = state.purgeResourceMetric;
936
+ if (!metric || metric === "fixed") return null;
937
+ if (metric === "size") return getSizeUtilization(state);
938
+ if (metric === "memory") return getMemoryUtilization();
939
+ if (metric === "higher") return Math.min(1, Math.max(getMemoryUtilization(), getSizeUtilization(state)));
940
+ return null;
941
+ };
942
+ /**
943
+ * Determines whether stale entries should be purged based on the purge mode and current resource usage.
944
+ *
945
+ * @param mode - The purge mode setting
946
+ * - `false` → never purge
947
+ * - `true` → always purge
948
+ * - `number (0–1)` → purge when `resourceUsage >= threshold`
949
+ * @param state - The cache state
950
+ * @returns True if stale entries should be purged, false otherwise
951
+ *
952
+ * @internal
953
+ */
954
+ const shouldPurge = (mode, state, purgeContext) => {
955
+ if (mode === false) return false;
956
+ if (mode === true) return true;
957
+ const userThreshold = Number(mode);
958
+ const defaultPurge = purgeContext === "sweep" ? DEFAULT_PURGE_STALE_ON_SWEEP_NO_LIMITS : DEFAULT_PURGE_STALE_ON_GET_NO_LIMITS;
959
+ if (Number.isNaN(userThreshold)) return defaultPurge;
960
+ const usage = computeResourceUsage(state);
961
+ if (!usage) return defaultPurge;
962
+ return usage >= Math.max(0, Math.min(1, userThreshold));
963
+ };
964
+
649
965
  //#endregion
650
966
  //#region src/sweep/sweep-once.ts
651
967
  /**
@@ -675,10 +991,10 @@ function _sweepOnce(state, _maxKeysPerBatch = MAX_KEYS_PER_BATCH) {
675
991
  expiredCount += 1;
676
992
  } else if (isStale(state, status, now)) {
677
993
  staleCount += 1;
678
- if (state.purgeStaleOnSweep) deleteKey(state, key, DELETE_REASON.STALE);
994
+ if (shouldPurge(state.purgeStaleOnSweep, state, "sweep")) deleteKey(state, key, DELETE_REASON.STALE);
679
995
  }
680
996
  }
681
- const expiredStaleCount = state.purgeStaleOnSweep ? staleCount : 0;
997
+ const expiredStaleCount = shouldPurge(state.purgeStaleOnSweep, state, "sweep") ? staleCount : 0;
682
998
  return {
683
999
  processed,
684
1000
  expiredCount,
@@ -785,6 +1101,14 @@ function _updateWeightSweep() {
785
1101
 
786
1102
  //#endregion
787
1103
  //#region src/sweep/sweep.ts
1104
+ let _isSweepActive = false;
1105
+ let _pendingSweepTimeout = null;
1106
+ function startSweep(state) {
1107
+ if (_isSweepActive) return;
1108
+ _isSweepActive = true;
1109
+ startMonitor();
1110
+ sweep(state);
1111
+ }
788
1112
  /**
789
1113
  * Performs a sweep operation on the cache to remove expired and optionally stale entries.
790
1114
  * Uses a linear scan with a saved pointer to resume from the last processed key.
@@ -816,8 +1140,8 @@ const sweep = async (state, utilities = {}) => {
816
1140
  if (!runOnlyOne) schedule(() => void sweep(state, utilities), sweepIntervalMs);
817
1141
  };
818
1142
  const defaultSchedule = (fn, ms) => {
819
- const t = setTimeout(fn, ms);
820
- if (typeof t.unref === "function") t.unref();
1143
+ _pendingSweepTimeout = setTimeout(fn, ms);
1144
+ if (typeof _pendingSweepTimeout.unref === "function") _pendingSweepTimeout.unref();
821
1145
  };
822
1146
  const defaultYieldFn = () => new Promise((resolve) => setImmediate(resolve));
823
1147
 
@@ -826,16 +1150,35 @@ const defaultYieldFn = () => new Promise((resolve) => setImmediate(resolve));
826
1150
  let _instanceCount = 0;
827
1151
  const INSTANCE_WARNING_THRESHOLD = 99;
828
1152
  const _instancesCache = [];
829
- let _initSweepScheduled = false;
830
1153
  /**
831
1154
  * Creates the initial state for the TTL cache.
832
1155
  * @param options - Configuration options for the cache.
833
1156
  * @returns The initial cache state.
834
1157
  */
835
1158
  const createCache = (options = {}) => {
836
- 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;
1159
+ 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;
837
1160
  _instanceCount++;
838
1161
  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`);
1162
+ const resolvedPurgeResourceMetric = purgeResourceMetric ?? resolvePurgeResourceMetric({
1163
+ maxSize,
1164
+ maxMemorySize
1165
+ });
1166
+ const resolvedPurgeStaleOnGet = resolvePurgeStaleOnGet({
1167
+ limits: {
1168
+ maxSize,
1169
+ maxMemorySize
1170
+ },
1171
+ purgeResourceMetric: resolvedPurgeResourceMetric,
1172
+ userValue: purgeStaleOnGet
1173
+ });
1174
+ const resolvedPurgeStaleOnSweep = resolvePurgeStaleOnSweep({
1175
+ limits: {
1176
+ maxSize,
1177
+ maxMemorySize
1178
+ },
1179
+ purgeResourceMetric: resolvedPurgeResourceMetric,
1180
+ userValue: purgeStaleOnSweep
1181
+ });
839
1182
  const state = {
840
1183
  store: /* @__PURE__ */ new Map(),
841
1184
  _sweepIter: null,
@@ -848,8 +1191,9 @@ const createCache = (options = {}) => {
848
1191
  maxMemorySize,
849
1192
  defaultTtl,
850
1193
  defaultStaleWindow,
851
- purgeStaleOnGet,
852
- purgeStaleOnSweep,
1194
+ purgeStaleOnGet: resolvedPurgeStaleOnGet,
1195
+ purgeStaleOnSweep: resolvedPurgeStaleOnSweep,
1196
+ purgeResourceMetric: resolvedPurgeResourceMetric,
853
1197
  _maxAllowExpiredRatio,
854
1198
  _autoStartSweep,
855
1199
  _instanceIndexState: -1,
@@ -858,34 +1202,47 @@ const createCache = (options = {}) => {
858
1202
  _tags: /* @__PURE__ */ new Map()
859
1203
  };
860
1204
  state._instanceIndexState = _instancesCache.push(state) - 1;
861
- if (_autoStartSweep) {
862
- if (_initSweepScheduled) return state;
863
- _initSweepScheduled = true;
864
- sweep(state);
865
- }
866
- startMonitor();
1205
+ startSweep(state);
867
1206
  return state;
868
1207
  };
869
1208
 
870
1209
  //#endregion
871
1210
  //#region src/cache/get.ts
872
1211
  /**
873
- * Retrieves a value from the cache if the entry is valid.
1212
+ * Internal function that retrieves a value from the cache with its status information.
1213
+ * Returns a tuple containing the entry status and the complete cache entry.
1214
+ *
874
1215
  * @param state - The cache state.
875
1216
  * @param key - The key to retrieve.
876
1217
  * @param now - Optional timestamp override (defaults to Date.now()).
877
- * @returns The cached value if valid, null otherwise.
1218
+ * @returns A tuple of [status, entry] if the entry is valid, or [null, undefined] if not found or expired.
1219
+ *
1220
+ * @internal
878
1221
  */
879
- const get = (state, key, now = Date.now()) => {
1222
+ const getWithStatus = (state, key, purgeMode, now = Date.now()) => {
880
1223
  const entry = state.store.get(key);
881
- if (!entry) return void 0;
1224
+ if (!entry) return [null, void 0];
882
1225
  const status = computeEntryStatus(state, entry, now);
883
- if (isFresh(state, status, now)) return entry[1];
1226
+ if (isFresh(state, status, now)) return [status, entry];
884
1227
  if (isStale(state, status, now)) {
885
- if (state.purgeStaleOnGet) deleteKey(state, key, DELETE_REASON.STALE);
886
- return entry[1];
1228
+ if (shouldPurge(purgeMode ?? state.purgeStaleOnGet, state, "get")) deleteKey(state, key, DELETE_REASON.STALE);
1229
+ return [status, entry];
887
1230
  }
888
1231
  deleteKey(state, key, DELETE_REASON.EXPIRED);
1232
+ return [status, void 0];
1233
+ };
1234
+ /**
1235
+ * Retrieves a value from the cache if the entry is valid.
1236
+ * @param state - The cache state.
1237
+ * @param key - The key to retrieve.
1238
+ * @param now - Optional timestamp override (defaults to Date.now()).
1239
+ * @returns The cached value if valid, undefined otherwise.
1240
+ *
1241
+ * @internal
1242
+ */
1243
+ const get = (state, key, purgeMode, now = Date.now()) => {
1244
+ const [, entry] = getWithStatus(state, key, purgeMode, now);
1245
+ return entry ? entry[1] : void 0;
889
1246
  };
890
1247
 
891
1248
  //#endregion
@@ -984,16 +1341,12 @@ const setOrUpdate = (state, input, now = Date.now()) => {
984
1341
  //#region src/index.ts
985
1342
  /**
986
1343
  * A TTL (Time-To-Live) cache implementation with support for expiration,
987
- * stale windows, tag-based invalidation, and automatic sweeping.
988
- *
989
- * Provides O(1) constant-time operations for all core methods.
1344
+ * stale windows, tag-based invalidation, and smart automatic sweeping.
990
1345
  *
991
- * @example
992
- * ```typescript
993
- * const cache = new LocalTtlCache();
994
- * cache.set("user:123", { name: "Alice" }, { ttl: 5 * 60 * 1000 });
995
- * const user = cache.get("user:123"); // { name: "Alice" }
996
- * ```
1346
+ * Provides O(1) constant-time operations for all core methods with support for:
1347
+ * - Expiration and stale windows
1348
+ * - Tag-based invalidation
1349
+ * - Automatic sweeping
997
1350
  */
998
1351
  var LocalTtlCache = class {
999
1352
  state;
@@ -1002,98 +1355,29 @@ var LocalTtlCache = class {
1002
1355
  *
1003
1356
  * @param options - Configuration options for the cache (defaultTtl, defaultStaleWindow, maxSize, etc.)
1004
1357
  *
1005
- * @example
1006
- * ```typescript
1007
- * const cache = new LocalTtlCache({
1008
- * defaultTtl: 30 * 60 * 1000, // 30 minutes
1009
- * defaultStaleWindow: 5 * 60 * 1000, // 5 minutes
1010
- * maxSize: 500_000, // Maximum 500_000 entries
1011
- * onExpire: (key, value) => console.log(`Expired: ${key}`),
1012
- * onDelete: (key, value, reason) => console.log(`Deleted: ${key}, reason: ${reason}`),
1013
- * });
1014
- * ```
1015
1358
  */
1016
1359
  constructor(options) {
1017
1360
  this.state = createCache(options);
1018
1361
  }
1019
- /**
1020
- * Gets the current number of entries tracked by the cache.
1021
- *
1022
- * This value may include entries that are already expired but have not yet been
1023
- * removed by the lazy cleanup system. Expired keys are cleaned only when it is
1024
- * efficient to do so, so the count can temporarily be higher than the number of
1025
- * actually valid (non‑expired) entries.
1026
- *
1027
- * @returns The number of entries currently stored (including entries pending cleanup)
1028
- *
1029
- * @example
1030
- * ```typescript
1031
- * console.log(cache.size); // e.g., 42
1032
- * ```
1033
- */
1034
1362
  get size() {
1035
1363
  return this.state.size;
1036
1364
  }
1037
- /**
1038
- * Retrieves a value from the cache.
1039
- *
1040
- * Returns the value if it exists and is not fully expired. If an entry is in the
1041
- * stale window (expired but still within staleWindow), the stale value is returned.
1042
- *
1043
-
1044
- * @param key - The key to retrieve
1045
- * @returns The cached value if valid, undefined otherwise
1046
- *
1047
- * @example
1048
- * ```typescript
1049
- * const user = cache.get<{ name: string }>("user:123");
1050
- * ```
1051
- *
1052
- * @edge-cases
1053
- * - Returns `undefined` if the key doesn't exist
1054
- * - Returns `undefined` if the key has expired beyond the stale window
1055
- * - Returns the stale value if within the stale window
1056
- * - If `purgeStaleOnGet` is enabled, stale entries are deleted after being returned
1057
- */
1058
- get(key) {
1059
- return get(this.state, key);
1365
+ get(key, options) {
1366
+ if (options?.includeMetadata === true) {
1367
+ const [status, entry] = getWithStatus(this.state, key, options.purgeStale);
1368
+ if (!entry) return void 0;
1369
+ const [timestamps, value, tags] = entry;
1370
+ const [, expiresAt, staleExpiresAt] = timestamps;
1371
+ return {
1372
+ data: value,
1373
+ expirationTime: expiresAt,
1374
+ staleWindowExpiration: staleExpiresAt,
1375
+ status,
1376
+ tags
1377
+ };
1378
+ }
1379
+ return get(this.state, key, options?.purgeStale);
1060
1380
  }
1061
- /**
1062
- * Sets or updates a value in the cache.
1063
- *
1064
- * If the key already exists, it will be completely replaced.
1065
- *
1066
- * @param key - The key under which to store the value
1067
- * @param value - The value to cache (any type)
1068
- * @param options - Optional configuration for this specific entry
1069
- * @param options.ttl - Time-To-Live in milliseconds. Defaults to `defaultTtl`
1070
- * @param options.staleWindow - How long to serve stale data after expiration (milliseconds)
1071
- * @param options.tags - One or more tags for group invalidation
1072
- * @returns True if the entry was set or updated, false if rejected due to limits or invalid input
1073
- *
1074
- * @example
1075
- * ```typescript
1076
- * const success = cache.set("user:123", { name: "Alice" }, {
1077
- * ttl: 5 * 60 * 1000,
1078
- * staleWindow: 1 * 60 * 1000,
1079
- * tags: "user:123",
1080
- * });
1081
- *
1082
- * if (!success) {
1083
- * console.log("Entry was rejected due to size or memory limits");
1084
- * }
1085
- * ```
1086
- *
1087
- * @edge-cases
1088
- * - Overwriting an existing key replaces it completely
1089
- * - If `ttl` is 0 or Infinite, the entry never expires
1090
- * - If `staleWindow` is larger than `ttl`, the entry can be served as stale longer than it was fresh
1091
- * - Tags are optional; only necessary for group invalidation via `invalidateTag()`
1092
- * - Returns `false` if value is `undefined` (existing value remains untouched)
1093
- * - Returns `false` if new key would exceed [`maxSize`](./docs/configuration.md#maxsize-number) limit
1094
- * - Returns `false` if new key would exceed [`maxMemorySize`](./docs/configuration.md#maxmemorysize-number) limit
1095
- * - Updating existing keys always succeeds, even at limit
1096
- */
1097
1381
  set(key, value, options) {
1098
1382
  return setOrUpdate(this.state, {
1099
1383
  key,
@@ -1103,97 +1387,21 @@ var LocalTtlCache = class {
1103
1387
  tags: options?.tags
1104
1388
  });
1105
1389
  }
1106
- /**
1107
- * Deletes a specific key from the cache.
1108
- *
1109
- * @param key - The key to delete
1110
- * @returns True if the key was deleted, false if it didn't exist
1111
- *
1112
- * @example
1113
- * ```typescript
1114
- * const wasDeleted = cache.delete("user:123");
1115
- * ```
1116
- *
1117
- * @edge-cases
1118
- * - Triggers the `onDelete` callback with reason `'manual'`
1119
- * - Does not trigger the `onExpire` callback
1120
- * - Returns `false` if the key was already expired
1121
- * - Deleting a non-existent key returns `false` without error
1122
- */
1123
1390
  delete(key) {
1124
1391
  return deleteKey(this.state, key);
1125
1392
  }
1126
- /**
1127
- * Checks if a key exists in the cache and is not fully expired.
1128
- *
1129
- * Returns true if the key exists and is either fresh or within the stale window.
1130
- * Use this when you only need to check existence without retrieving the value.
1131
- *
1132
- * @param key - The key to check
1133
- * @returns True if the key exists and is valid, false otherwise
1134
- *
1135
- * @example
1136
- * ```typescript
1137
- * if (cache.has("user:123")) {
1138
- * // Key exists (either fresh or stale)
1139
- * }
1140
- * ```
1141
- *
1142
- * @edge-cases
1143
- * - Returns `false` if the key doesn't exist
1144
- * - Returns `false` if the key has expired beyond the stale window
1145
- * - Returns `true` if the key is in the stale window (still being served)
1146
- * - Both `has()` and `get()` have O(1) complexity; prefer `get()` if you need the value
1147
- */
1148
1393
  has(key) {
1149
1394
  return has(this.state, key);
1150
1395
  }
1151
- /**
1152
- * Removes all entries from the cache at once.
1153
- *
1154
- * This is useful for resetting the cache or freeing memory when needed.
1155
- * The `onDelete` callback is NOT invoked during clear (intentional optimization).
1156
- *
1157
- * @example
1158
- * ```typescript
1159
- * cache.clear(); // cache.size is now 0
1160
- * ```
1161
- *
1162
- * @edge-cases
1163
- * - The `onDelete` callback is NOT triggered during clear
1164
- * - Clears both expired and fresh entries
1165
- * - Resets `cache.size` to 0
1166
- */
1167
1396
  clear() {
1168
1397
  clear(this.state);
1169
1398
  }
1170
- /**
1171
- * Marks all entries with one or more tags as expired (or stale, if requested).
1172
- *
1173
- * If an entry has multiple tags, invalidating ANY of those tags will invalidate the entry.
1174
- *
1175
- * @param tags - A single tag (string) or array of tags to invalidate
1176
- * @param asStale - If true, marks entries as stale instead of fully expired (still served from stale window)
1177
- *
1178
- * @example
1179
- * ```typescript
1180
- * // Invalidate a single tag
1181
- * cache.invalidateTag("user:123");
1182
- *
1183
- * // Invalidate multiple tags
1184
- * cache.invalidateTag(["user:123", "posts:456"]);
1185
- * ```
1186
- *
1187
- * @edge-cases
1188
- * - Does not throw errors if a tag has no associated entries
1189
- * - Invalidating a tag doesn't prevent new entries from being tagged with it later
1190
- * - The `onDelete` callback is triggered with reason `'expired'` (even if `asStale` is true)
1191
- */
1192
1399
  invalidateTag(tags, options) {
1193
1400
  invalidateTag(this.state, tags, options ?? {});
1194
1401
  }
1195
1402
  };
1196
1403
 
1197
1404
  //#endregion
1405
+ exports.ENTRY_STATUS = ENTRY_STATUS;
1198
1406
  exports.LocalTtlCache = LocalTtlCache;
1199
1407
  //# sourceMappingURL=index.cjs.map