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