@neezco/cache 0.3.0 → 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.
- package/CHANGELOG.md +13 -0
- package/dist/browser/index.d.ts +213 -212
- package/dist/browser/index.js +366 -174
- package/dist/browser/index.js.map +1 -1
- package/dist/node/index.cjs +371 -173
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.d.cts +213 -212
- package/dist/node/index.d.mts +213 -212
- package/dist/node/index.mjs +371 -174
- package/dist/node/index.mjs.map +1 -1
- package/package.json +9 -3
package/dist/node/index.cjs
CHANGED
|
@@ -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
|
-
*
|
|
751
|
+
* Entry status: fresh, stale, or expired.
|
|
512
752
|
*/
|
|
513
753
|
let ENTRY_STATUS = /* @__PURE__ */ function(ENTRY_STATUS$1) {
|
|
514
|
-
/**
|
|
754
|
+
/** Valid and within TTL. */
|
|
515
755
|
ENTRY_STATUS$1["FRESH"] = "fresh";
|
|
516
|
-
/**
|
|
756
|
+
/** Expired but within stale window; still served. */
|
|
517
757
|
ENTRY_STATUS$1["STALE"] = "stale";
|
|
518
|
-
/**
|
|
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
|
-
|
|
820
|
-
if (typeof
|
|
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
|
|
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,12 +1202,7 @@ const createCache = (options = {}) => {
|
|
|
858
1202
|
_tags: /* @__PURE__ */ new Map()
|
|
859
1203
|
};
|
|
860
1204
|
state._instanceIndexState = _instancesCache.push(state) - 1;
|
|
861
|
-
|
|
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
|
|
|
@@ -880,13 +1219,13 @@ const createCache = (options = {}) => {
|
|
|
880
1219
|
*
|
|
881
1220
|
* @internal
|
|
882
1221
|
*/
|
|
883
|
-
const getWithStatus = (state, key, now = Date.now()) => {
|
|
1222
|
+
const getWithStatus = (state, key, purgeMode, now = Date.now()) => {
|
|
884
1223
|
const entry = state.store.get(key);
|
|
885
1224
|
if (!entry) return [null, void 0];
|
|
886
1225
|
const status = computeEntryStatus(state, entry, now);
|
|
887
1226
|
if (isFresh(state, status, now)) return [status, entry];
|
|
888
1227
|
if (isStale(state, status, now)) {
|
|
889
|
-
if (state.purgeStaleOnGet) deleteKey(state, key, DELETE_REASON.STALE);
|
|
1228
|
+
if (shouldPurge(purgeMode ?? state.purgeStaleOnGet, state, "get")) deleteKey(state, key, DELETE_REASON.STALE);
|
|
890
1229
|
return [status, entry];
|
|
891
1230
|
}
|
|
892
1231
|
deleteKey(state, key, DELETE_REASON.EXPIRED);
|
|
@@ -901,8 +1240,8 @@ const getWithStatus = (state, key, now = Date.now()) => {
|
|
|
901
1240
|
*
|
|
902
1241
|
* @internal
|
|
903
1242
|
*/
|
|
904
|
-
const get = (state, key, now = Date.now()) => {
|
|
905
|
-
const [, entry] = getWithStatus(state, key, now);
|
|
1243
|
+
const get = (state, key, purgeMode, now = Date.now()) => {
|
|
1244
|
+
const [, entry] = getWithStatus(state, key, purgeMode, now);
|
|
906
1245
|
return entry ? entry[1] : void 0;
|
|
907
1246
|
};
|
|
908
1247
|
|
|
@@ -1002,16 +1341,12 @@ const setOrUpdate = (state, input, now = Date.now()) => {
|
|
|
1002
1341
|
//#region src/index.ts
|
|
1003
1342
|
/**
|
|
1004
1343
|
* A TTL (Time-To-Live) cache implementation with support for expiration,
|
|
1005
|
-
* stale windows, tag-based invalidation, and automatic sweeping.
|
|
1006
|
-
*
|
|
1007
|
-
* Provides O(1) constant-time operations for all core methods.
|
|
1344
|
+
* stale windows, tag-based invalidation, and smart automatic sweeping.
|
|
1008
1345
|
*
|
|
1009
|
-
*
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
1013
|
-
* const user = cache.get("user:123"); // { name: "Alice" }
|
|
1014
|
-
* ```
|
|
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
|
|
1015
1350
|
*/
|
|
1016
1351
|
var LocalTtlCache = class {
|
|
1017
1352
|
state;
|
|
@@ -1020,41 +1355,16 @@ var LocalTtlCache = class {
|
|
|
1020
1355
|
*
|
|
1021
1356
|
* @param options - Configuration options for the cache (defaultTtl, defaultStaleWindow, maxSize, etc.)
|
|
1022
1357
|
*
|
|
1023
|
-
* @example
|
|
1024
|
-
* ```typescript
|
|
1025
|
-
* const cache = new LocalTtlCache({
|
|
1026
|
-
* defaultTtl: 30 * 60 * 1000, // 30 minutes
|
|
1027
|
-
* defaultStaleWindow: 5 * 60 * 1000, // 5 minutes
|
|
1028
|
-
* maxSize: 500_000, // Maximum 500_000 entries
|
|
1029
|
-
* onExpire: (key, value) => console.log(`Expired: ${key}`),
|
|
1030
|
-
* onDelete: (key, value, reason) => console.log(`Deleted: ${key}, reason: ${reason}`),
|
|
1031
|
-
* });
|
|
1032
|
-
* ```
|
|
1033
1358
|
*/
|
|
1034
1359
|
constructor(options) {
|
|
1035
1360
|
this.state = createCache(options);
|
|
1036
1361
|
}
|
|
1037
|
-
/**
|
|
1038
|
-
* Gets the current number of entries tracked by the cache.
|
|
1039
|
-
*
|
|
1040
|
-
* This value may include entries that are already expired but have not yet been
|
|
1041
|
-
* removed by the lazy cleanup system. Expired keys are cleaned only when it is
|
|
1042
|
-
* efficient to do so, so the count can temporarily be higher than the number of
|
|
1043
|
-
* actually valid (non‑expired) entries.
|
|
1044
|
-
*
|
|
1045
|
-
* @returns The number of entries currently stored (including entries pending cleanup)
|
|
1046
|
-
*
|
|
1047
|
-
* @example
|
|
1048
|
-
* ```typescript
|
|
1049
|
-
* console.log(cache.size); // e.g., 42
|
|
1050
|
-
* ```
|
|
1051
|
-
*/
|
|
1052
1362
|
get size() {
|
|
1053
1363
|
return this.state.size;
|
|
1054
1364
|
}
|
|
1055
1365
|
get(key, options) {
|
|
1056
|
-
if (options?.includeMetadata) {
|
|
1057
|
-
const [status, entry] = getWithStatus(this.state, key);
|
|
1366
|
+
if (options?.includeMetadata === true) {
|
|
1367
|
+
const [status, entry] = getWithStatus(this.state, key, options.purgeStale);
|
|
1058
1368
|
if (!entry) return void 0;
|
|
1059
1369
|
const [timestamps, value, tags] = entry;
|
|
1060
1370
|
const [, expiresAt, staleExpiresAt] = timestamps;
|
|
@@ -1066,44 +1376,8 @@ var LocalTtlCache = class {
|
|
|
1066
1376
|
tags
|
|
1067
1377
|
};
|
|
1068
1378
|
}
|
|
1069
|
-
return get(this.state, key);
|
|
1379
|
+
return get(this.state, key, options?.purgeStale);
|
|
1070
1380
|
}
|
|
1071
|
-
/**
|
|
1072
|
-
* Sets or updates a value in the cache.
|
|
1073
|
-
*
|
|
1074
|
-
* If the key already exists, it will be completely replaced.
|
|
1075
|
-
*
|
|
1076
|
-
* @param key - The key under which to store the value
|
|
1077
|
-
* @param value - The value to cache (any type)
|
|
1078
|
-
* @param options - Optional configuration for this specific entry
|
|
1079
|
-
* @param options.ttl - Time-To-Live in milliseconds. Defaults to `defaultTtl`
|
|
1080
|
-
* @param options.staleWindow - How long to serve stale data after expiration (milliseconds)
|
|
1081
|
-
* @param options.tags - One or more tags for group invalidation
|
|
1082
|
-
* @returns True if the entry was set or updated, false if rejected due to limits or invalid input
|
|
1083
|
-
*
|
|
1084
|
-
* @example
|
|
1085
|
-
* ```typescript
|
|
1086
|
-
* const success = cache.set("user:123", { name: "Alice" }, {
|
|
1087
|
-
* ttl: 5 * 60 * 1000,
|
|
1088
|
-
* staleWindow: 1 * 60 * 1000,
|
|
1089
|
-
* tags: "user:123",
|
|
1090
|
-
* });
|
|
1091
|
-
*
|
|
1092
|
-
* if (!success) {
|
|
1093
|
-
* console.log("Entry was rejected due to size or memory limits");
|
|
1094
|
-
* }
|
|
1095
|
-
* ```
|
|
1096
|
-
*
|
|
1097
|
-
* @edge-cases
|
|
1098
|
-
* - Overwriting an existing key replaces it completely
|
|
1099
|
-
* - If `ttl` is 0 or Infinite, the entry never expires
|
|
1100
|
-
* - If `staleWindow` is larger than `ttl`, the entry can be served as stale longer than it was fresh
|
|
1101
|
-
* - Tags are optional; only necessary for group invalidation via `invalidateTag()`
|
|
1102
|
-
* - Returns `false` if value is `undefined` (existing value remains untouched)
|
|
1103
|
-
* - Returns `false` if new key would exceed [`maxSize`](./docs/configuration.md#maxsize-number) limit
|
|
1104
|
-
* - Returns `false` if new key would exceed [`maxMemorySize`](./docs/configuration.md#maxmemorysize-number) limit
|
|
1105
|
-
* - Updating existing keys always succeeds, even at limit
|
|
1106
|
-
*/
|
|
1107
1381
|
set(key, value, options) {
|
|
1108
1382
|
return setOrUpdate(this.state, {
|
|
1109
1383
|
key,
|
|
@@ -1113,97 +1387,21 @@ var LocalTtlCache = class {
|
|
|
1113
1387
|
tags: options?.tags
|
|
1114
1388
|
});
|
|
1115
1389
|
}
|
|
1116
|
-
/**
|
|
1117
|
-
* Deletes a specific key from the cache.
|
|
1118
|
-
*
|
|
1119
|
-
* @param key - The key to delete
|
|
1120
|
-
* @returns True if the key was deleted, false if it didn't exist
|
|
1121
|
-
*
|
|
1122
|
-
* @example
|
|
1123
|
-
* ```typescript
|
|
1124
|
-
* const wasDeleted = cache.delete("user:123");
|
|
1125
|
-
* ```
|
|
1126
|
-
*
|
|
1127
|
-
* @edge-cases
|
|
1128
|
-
* - Triggers the `onDelete` callback with reason `'manual'`
|
|
1129
|
-
* - Does not trigger the `onExpire` callback
|
|
1130
|
-
* - Returns `false` if the key was already expired
|
|
1131
|
-
* - Deleting a non-existent key returns `false` without error
|
|
1132
|
-
*/
|
|
1133
1390
|
delete(key) {
|
|
1134
1391
|
return deleteKey(this.state, key);
|
|
1135
1392
|
}
|
|
1136
|
-
/**
|
|
1137
|
-
* Checks if a key exists in the cache and is not fully expired.
|
|
1138
|
-
*
|
|
1139
|
-
* Returns true if the key exists and is either fresh or within the stale window.
|
|
1140
|
-
* Use this when you only need to check existence without retrieving the value.
|
|
1141
|
-
*
|
|
1142
|
-
* @param key - The key to check
|
|
1143
|
-
* @returns True if the key exists and is valid, false otherwise
|
|
1144
|
-
*
|
|
1145
|
-
* @example
|
|
1146
|
-
* ```typescript
|
|
1147
|
-
* if (cache.has("user:123")) {
|
|
1148
|
-
* // Key exists (either fresh or stale)
|
|
1149
|
-
* }
|
|
1150
|
-
* ```
|
|
1151
|
-
*
|
|
1152
|
-
* @edge-cases
|
|
1153
|
-
* - Returns `false` if the key doesn't exist
|
|
1154
|
-
* - Returns `false` if the key has expired beyond the stale window
|
|
1155
|
-
* - Returns `true` if the key is in the stale window (still being served)
|
|
1156
|
-
* - Both `has()` and `get()` have O(1) complexity; prefer `get()` if you need the value
|
|
1157
|
-
*/
|
|
1158
1393
|
has(key) {
|
|
1159
1394
|
return has(this.state, key);
|
|
1160
1395
|
}
|
|
1161
|
-
/**
|
|
1162
|
-
* Removes all entries from the cache at once.
|
|
1163
|
-
*
|
|
1164
|
-
* This is useful for resetting the cache or freeing memory when needed.
|
|
1165
|
-
* The `onDelete` callback is NOT invoked during clear (intentional optimization).
|
|
1166
|
-
*
|
|
1167
|
-
* @example
|
|
1168
|
-
* ```typescript
|
|
1169
|
-
* cache.clear(); // cache.size is now 0
|
|
1170
|
-
* ```
|
|
1171
|
-
*
|
|
1172
|
-
* @edge-cases
|
|
1173
|
-
* - The `onDelete` callback is NOT triggered during clear
|
|
1174
|
-
* - Clears both expired and fresh entries
|
|
1175
|
-
* - Resets `cache.size` to 0
|
|
1176
|
-
*/
|
|
1177
1396
|
clear() {
|
|
1178
1397
|
clear(this.state);
|
|
1179
1398
|
}
|
|
1180
|
-
/**
|
|
1181
|
-
* Marks all entries with one or more tags as expired (or stale, if requested).
|
|
1182
|
-
*
|
|
1183
|
-
* If an entry has multiple tags, invalidating ANY of those tags will invalidate the entry.
|
|
1184
|
-
*
|
|
1185
|
-
* @param tags - A single tag (string) or array of tags to invalidate
|
|
1186
|
-
* @param asStale - If true, marks entries as stale instead of fully expired (still served from stale window)
|
|
1187
|
-
*
|
|
1188
|
-
* @example
|
|
1189
|
-
* ```typescript
|
|
1190
|
-
* // Invalidate a single tag
|
|
1191
|
-
* cache.invalidateTag("user:123");
|
|
1192
|
-
*
|
|
1193
|
-
* // Invalidate multiple tags
|
|
1194
|
-
* cache.invalidateTag(["user:123", "posts:456"]);
|
|
1195
|
-
* ```
|
|
1196
|
-
*
|
|
1197
|
-
* @edge-cases
|
|
1198
|
-
* - Does not throw errors if a tag has no associated entries
|
|
1199
|
-
* - Invalidating a tag doesn't prevent new entries from being tagged with it later
|
|
1200
|
-
* - The `onDelete` callback is triggered with reason `'expired'` (even if `asStale` is true)
|
|
1201
|
-
*/
|
|
1202
1399
|
invalidateTag(tags, options) {
|
|
1203
1400
|
invalidateTag(this.state, tags, options ?? {});
|
|
1204
1401
|
}
|
|
1205
1402
|
};
|
|
1206
1403
|
|
|
1207
1404
|
//#endregion
|
|
1405
|
+
exports.ENTRY_STATUS = ENTRY_STATUS;
|
|
1208
1406
|
exports.LocalTtlCache = LocalTtlCache;
|
|
1209
1407
|
//# sourceMappingURL=index.cjs.map
|