@neezco/cache 0.4.1 → 0.6.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/docs/api-reference.md +4 -0
- package/package.json +12 -6
- package/CHANGELOG.md +0 -56
- package/dist/browser/index.d.ts +0 -338
- package/dist/browser/index.js +0 -1029
- package/dist/browser/index.js.map +0 -1
- package/dist/node/index.cjs +0 -1407
- package/dist/node/index.cjs.map +0 -1
- package/dist/node/index.d.cts +0 -338
- package/dist/node/index.d.mts +0 -338
- package/dist/node/index.mjs +0 -1377
- package/dist/node/index.mjs.map +0 -1
- package/docs/.gitkeep +0 -0
package/dist/node/index.cjs
DELETED
|
@@ -1,1407 +0,0 @@
|
|
|
1
|
-
//#region rolldown:runtime
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __copyProps = (to, from, except, desc) => {
|
|
9
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
-
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
-
key = keys[i];
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
13
|
-
__defProp(to, key, {
|
|
14
|
-
get: ((k) => from[k]).bind(null, key),
|
|
15
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return to;
|
|
21
|
-
};
|
|
22
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
-
value: mod,
|
|
24
|
-
enumerable: true
|
|
25
|
-
}) : target, mod));
|
|
26
|
-
|
|
27
|
-
//#endregion
|
|
28
|
-
let fs = require("fs");
|
|
29
|
-
fs = __toESM(fs);
|
|
30
|
-
let v8 = require("v8");
|
|
31
|
-
v8 = __toESM(v8);
|
|
32
|
-
let perf_hooks = require("perf_hooks");
|
|
33
|
-
|
|
34
|
-
//#region src/cache/clear.ts
|
|
35
|
-
/**
|
|
36
|
-
* Clears all entries from the cache without invoking callbacks.
|
|
37
|
-
*
|
|
38
|
-
* @note The `onDelete` callback is NOT invoked during a clear operation.
|
|
39
|
-
* This is intentional to avoid unnecessary overhead when bulk-removing entries.
|
|
40
|
-
*
|
|
41
|
-
* @param state - The cache state.
|
|
42
|
-
* @returns void
|
|
43
|
-
*/
|
|
44
|
-
const clear = (state) => {
|
|
45
|
-
state.store.clear();
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
//#endregion
|
|
49
|
-
//#region src/defaults.ts
|
|
50
|
-
const ONE_SECOND = 1e3;
|
|
51
|
-
const ONE_MINUTE = 60 * ONE_SECOND;
|
|
52
|
-
/**
|
|
53
|
-
* ===================================================================
|
|
54
|
-
* Cache Entry Lifecycle
|
|
55
|
-
* Default TTL and stale window settings for short-lived cache entries.
|
|
56
|
-
* ===================================================================
|
|
57
|
-
*/
|
|
58
|
-
/**
|
|
59
|
-
* Default Time-To-Live in milliseconds for cache entries.
|
|
60
|
-
* @default 1_800_000 (30 minutes)
|
|
61
|
-
*/
|
|
62
|
-
const DEFAULT_TTL = 30 * ONE_MINUTE;
|
|
63
|
-
/**
|
|
64
|
-
* Default stale window in milliseconds after expiration.
|
|
65
|
-
* Allows serving slightly outdated data while fetching fresh data.
|
|
66
|
-
*/
|
|
67
|
-
const DEFAULT_STALE_WINDOW = 0;
|
|
68
|
-
/**
|
|
69
|
-
* Maximum number of entries the cache can hold.
|
|
70
|
-
* Beyond this limit, new entries are ignored.
|
|
71
|
-
*/
|
|
72
|
-
const DEFAULT_MAX_SIZE = Infinity;
|
|
73
|
-
/**
|
|
74
|
-
* Default maximum memory size in MB the cache can use.
|
|
75
|
-
* Beyond this limit, new entries are ignored.
|
|
76
|
-
* @default Infinite (unlimited)
|
|
77
|
-
*/
|
|
78
|
-
const DEFAULT_MAX_MEMORY_SIZE = Infinity;
|
|
79
|
-
/**
|
|
80
|
-
* ===================================================================
|
|
81
|
-
* Sweep & Cleanup Operations
|
|
82
|
-
* Parameters controlling how and when expired entries are removed.
|
|
83
|
-
* ===================================================================
|
|
84
|
-
*/
|
|
85
|
-
/**
|
|
86
|
-
* Maximum number of keys to process in a single sweep batch.
|
|
87
|
-
* Higher values = more aggressive cleanup, lower latency overhead.
|
|
88
|
-
*/
|
|
89
|
-
const MAX_KEYS_PER_BATCH = 1e3;
|
|
90
|
-
/**
|
|
91
|
-
* Minimal expired ratio enforced during sweeps.
|
|
92
|
-
* Ensures control sweeps run above {@link EXPIRED_RATIO_MEMORY_THRESHOLD}.
|
|
93
|
-
*/
|
|
94
|
-
const MINIMAL_EXPIRED_RATIO = .05;
|
|
95
|
-
/**
|
|
96
|
-
* Memory usage threshold (normalized 0–1) triggering control sweeps.
|
|
97
|
-
* At or above this level, sweeping becomes more aggressive.
|
|
98
|
-
*/
|
|
99
|
-
const EXPIRED_RATIO_MEMORY_THRESHOLD = .8;
|
|
100
|
-
/**
|
|
101
|
-
* Maximum allowed expired ratio when memory usage is low.
|
|
102
|
-
* Upper bound for interpolation with MINIMAL_EXPIRED_RATIO.
|
|
103
|
-
* Recommended range: `0.3 – 0.5` .
|
|
104
|
-
*/
|
|
105
|
-
const DEFAULT_MAX_EXPIRED_RATIO = .4;
|
|
106
|
-
/**
|
|
107
|
-
* ===================================================================
|
|
108
|
-
* Sweep Intervals & Timing
|
|
109
|
-
* Frequency and time budgets for cleanup operations.
|
|
110
|
-
* ===================================================================
|
|
111
|
-
*/
|
|
112
|
-
/**
|
|
113
|
-
* Optimal interval in milliseconds between sweeps.
|
|
114
|
-
* Used when system load is minimal and metrics are available.
|
|
115
|
-
*/
|
|
116
|
-
const OPTIMAL_SWEEP_INTERVAL = 2 * ONE_SECOND;
|
|
117
|
-
/**
|
|
118
|
-
* Worst-case interval in milliseconds between sweeps.
|
|
119
|
-
* Used when system load is high or metrics unavailable.
|
|
120
|
-
*/
|
|
121
|
-
const WORST_SWEEP_INTERVAL = 200;
|
|
122
|
-
/**
|
|
123
|
-
* Maximum time budget in milliseconds for sweep operations.
|
|
124
|
-
* Prevents sweeping from consuming excessive CPU during high load.
|
|
125
|
-
*/
|
|
126
|
-
const WORST_SWEEP_TIME_BUDGET = 40;
|
|
127
|
-
/**
|
|
128
|
-
* Optimal time budget in milliseconds for each sweep cycle.
|
|
129
|
-
* Used when performance metrics are not available or unreliable.
|
|
130
|
-
*/
|
|
131
|
-
const OPTIMAL_SWEEP_TIME_BUDGET_IF_NOTE_METRICS_AVAILABLE = 15;
|
|
132
|
-
/**
|
|
133
|
-
* ===================================================================
|
|
134
|
-
* Memory Management
|
|
135
|
-
* Process limits and memory-safe thresholds.
|
|
136
|
-
* ===================================================================
|
|
137
|
-
*/
|
|
138
|
-
/**
|
|
139
|
-
* Default maximum process memory limit in megabytes.
|
|
140
|
-
* Acts as fallback when environment detection is unavailable.
|
|
141
|
-
* NOTE: Overridable via environment detection at runtime.
|
|
142
|
-
*/
|
|
143
|
-
const DEFAULT_MAX_PROCESS_MEMORY_MB = 1024;
|
|
144
|
-
/**
|
|
145
|
-
* ===================================================================
|
|
146
|
-
* System Utilization Weights
|
|
147
|
-
* Balance how memory, CPU, and event-loop pressure influence sweep behavior.
|
|
148
|
-
* Sum of all weights: 10 + 8.5 + 6.5 = 25
|
|
149
|
-
* ===================================================================
|
|
150
|
-
*/
|
|
151
|
-
/**
|
|
152
|
-
* Weight applied to memory utilization in sweep calculations.
|
|
153
|
-
* Higher weight = memory pressure has more influence on sweep aggressiveness.
|
|
154
|
-
*/
|
|
155
|
-
const DEFAULT_MEMORY_WEIGHT = 10;
|
|
156
|
-
/**
|
|
157
|
-
* Weight applied to CPU utilization in sweep calculations.
|
|
158
|
-
* Combined with event-loop weight to balance CPU-related pressure.
|
|
159
|
-
*/
|
|
160
|
-
const DEFAULT_CPU_WEIGHT = 8.5;
|
|
161
|
-
/**
|
|
162
|
-
* Weight applied to event-loop utilization in sweep calculations.
|
|
163
|
-
* Complements CPU weight to assess overall processing capacity.
|
|
164
|
-
*/
|
|
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);
|
|
406
|
-
|
|
407
|
-
//#endregion
|
|
408
|
-
//#region src/utils/get-process-memory-limit.ts
|
|
409
|
-
/**
|
|
410
|
-
* Reads a number from a file.
|
|
411
|
-
* @param path File path to read the number from.
|
|
412
|
-
* @returns The number read from the file, or null if reading fails.
|
|
413
|
-
*/
|
|
414
|
-
function readNumber(path) {
|
|
415
|
-
try {
|
|
416
|
-
const raw = fs.default.readFileSync(path, "utf8").trim();
|
|
417
|
-
const n = Number(raw);
|
|
418
|
-
return Number.isFinite(n) ? n : null;
|
|
419
|
-
} catch {
|
|
420
|
-
return null;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* Gets the memory limit imposed by cgroups, if any.
|
|
425
|
-
* @return The memory limit in bytes, or null if no limit is found.
|
|
426
|
-
*/
|
|
427
|
-
function getCgroupLimit() {
|
|
428
|
-
const v2 = readNumber("/sys/fs/cgroup/memory.max");
|
|
429
|
-
if (v2 !== null) return v2;
|
|
430
|
-
const v1 = readNumber("/sys/fs/cgroup/memory/memory.limit_in_bytes");
|
|
431
|
-
if (v1 !== null) return v1;
|
|
432
|
-
return null;
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Gets the effective memory limit for the current process, considering both V8 heap limits and cgroup limits.
|
|
436
|
-
* @returns The effective memory limit in bytes.
|
|
437
|
-
*/
|
|
438
|
-
function getProcessMemoryLimit() {
|
|
439
|
-
const heapLimit = v8.default.getHeapStatistics().heap_size_limit;
|
|
440
|
-
const cgroupLimit = getCgroupLimit();
|
|
441
|
-
if (cgroupLimit && cgroupLimit > 0 && cgroupLimit < Infinity) return Math.min(heapLimit, cgroupLimit);
|
|
442
|
-
return heapLimit;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
//#endregion
|
|
446
|
-
//#region src/utils/process-monitor.ts
|
|
447
|
-
/**
|
|
448
|
-
* Creates a performance monitor that periodically samples memory usage,
|
|
449
|
-
* CPU usage, and event loop utilization for the current Node.js process.
|
|
450
|
-
*
|
|
451
|
-
* The monitor runs on a configurable interval and optionally invokes a
|
|
452
|
-
* callback with the collected metrics on each cycle. It also exposes
|
|
453
|
-
* methods to start and stop monitoring, retrieve the latest metrics,
|
|
454
|
-
* and update configuration dynamically.
|
|
455
|
-
*
|
|
456
|
-
* @param options Configuration options for the monitor, including sampling
|
|
457
|
-
* interval, maximum thresholds for normalization, and an optional callback.
|
|
458
|
-
* @returns An API object that allows controlling the monitor lifecycle.
|
|
459
|
-
*/
|
|
460
|
-
function createMonitorObserver(options) {
|
|
461
|
-
let intervalId = null;
|
|
462
|
-
let lastMetrics = null;
|
|
463
|
-
let prevHrtime = process.hrtime.bigint();
|
|
464
|
-
let prevMem = process.memoryUsage();
|
|
465
|
-
let prevCpu = process.cpuUsage();
|
|
466
|
-
let prevLoop = perf_hooks.performance.eventLoopUtilization();
|
|
467
|
-
let lastCollectedAt = Date.now();
|
|
468
|
-
const config = {
|
|
469
|
-
interval: options?.interval ?? 500,
|
|
470
|
-
maxMemory: (options?.maxMemory ?? 512) * 1024 * 1024
|
|
471
|
-
};
|
|
472
|
-
function start() {
|
|
473
|
-
if (intervalId) return;
|
|
474
|
-
intervalId = setInterval(() => {
|
|
475
|
-
try {
|
|
476
|
-
const now = Date.now();
|
|
477
|
-
const metrics = collectMetrics({
|
|
478
|
-
prevCpu,
|
|
479
|
-
prevHrtime,
|
|
480
|
-
prevMem,
|
|
481
|
-
prevLoop,
|
|
482
|
-
maxMemory: config.maxMemory,
|
|
483
|
-
collectedAtMs: now,
|
|
484
|
-
previousCollectedAtMs: lastCollectedAt,
|
|
485
|
-
interval: config.interval
|
|
486
|
-
});
|
|
487
|
-
lastMetrics = metrics;
|
|
488
|
-
options?.callback?.(metrics);
|
|
489
|
-
prevCpu = metrics.cpu.total;
|
|
490
|
-
prevLoop = metrics.loop.total;
|
|
491
|
-
prevMem = metrics.memory.total;
|
|
492
|
-
prevHrtime = process.hrtime.bigint();
|
|
493
|
-
lastCollectedAt = now;
|
|
494
|
-
} catch (e) {
|
|
495
|
-
stop();
|
|
496
|
-
throw new Error("MonitorObserver: Not available", { cause: e });
|
|
497
|
-
}
|
|
498
|
-
}, config.interval);
|
|
499
|
-
if (typeof intervalId.unref === "function") intervalId.unref();
|
|
500
|
-
}
|
|
501
|
-
function stop() {
|
|
502
|
-
if (intervalId) {
|
|
503
|
-
clearInterval(intervalId);
|
|
504
|
-
intervalId = null;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
function getMetrics() {
|
|
508
|
-
if (lastMetrics) return lastMetrics;
|
|
509
|
-
return null;
|
|
510
|
-
}
|
|
511
|
-
function updateConfig(newConfig) {
|
|
512
|
-
if (newConfig.maxMemory !== void 0) config.maxMemory = newConfig.maxMemory * 1024 * 1024;
|
|
513
|
-
if (newConfig.interval !== void 0) {
|
|
514
|
-
config.interval = newConfig.interval;
|
|
515
|
-
if (intervalId) {
|
|
516
|
-
stop();
|
|
517
|
-
start();
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
return {
|
|
522
|
-
start,
|
|
523
|
-
stop,
|
|
524
|
-
getMetrics,
|
|
525
|
-
updateConfig
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* Collects and normalizes performance metrics for the current process,
|
|
530
|
-
* including memory usage, CPU usage, and event loop utilization.
|
|
531
|
-
*
|
|
532
|
-
* CPU and event loop metrics are computed as deltas relative to previously
|
|
533
|
-
* recorded values. All metrics are normalized into a utilization between 0 and 1
|
|
534
|
-
* based on the configured maximum thresholds.
|
|
535
|
-
*
|
|
536
|
-
* @param props Previous metric snapshots and normalization limits.
|
|
537
|
-
* @returns A structured object containing normalized performance metrics.
|
|
538
|
-
*/
|
|
539
|
-
function collectMetrics(props) {
|
|
540
|
-
const nowHrtime = process.hrtime.bigint();
|
|
541
|
-
const elapsedMs = Number(nowHrtime - props.prevHrtime) / 1e6;
|
|
542
|
-
const actualElapsed = props.collectedAtMs - props.previousCollectedAtMs;
|
|
543
|
-
const mem = process.memoryUsage();
|
|
544
|
-
const deltaMem = {
|
|
545
|
-
rss: mem.rss - props.prevMem.rss,
|
|
546
|
-
heapTotal: mem.heapTotal - props.prevMem.heapTotal,
|
|
547
|
-
heapUsed: mem.heapUsed - props.prevMem.heapUsed,
|
|
548
|
-
external: mem.external - props.prevMem.external,
|
|
549
|
-
arrayBuffers: mem.arrayBuffers - props.prevMem.arrayBuffers
|
|
550
|
-
};
|
|
551
|
-
const memRatio = Math.min(1, mem.rss / props.maxMemory);
|
|
552
|
-
const cpuDelta = process.cpuUsage(props.prevCpu);
|
|
553
|
-
const cpuRatio = (cpuDelta.system + cpuDelta.user) / 1e3 / elapsedMs;
|
|
554
|
-
const loop = perf_hooks.performance.eventLoopUtilization(props.prevLoop);
|
|
555
|
-
return {
|
|
556
|
-
cpu: {
|
|
557
|
-
utilization: cpuRatio,
|
|
558
|
-
delta: cpuDelta,
|
|
559
|
-
total: process.cpuUsage()
|
|
560
|
-
},
|
|
561
|
-
loop: {
|
|
562
|
-
utilization: loop.utilization,
|
|
563
|
-
delta: loop,
|
|
564
|
-
total: perf_hooks.performance.eventLoopUtilization()
|
|
565
|
-
},
|
|
566
|
-
memory: {
|
|
567
|
-
utilization: memRatio,
|
|
568
|
-
delta: deltaMem,
|
|
569
|
-
total: mem
|
|
570
|
-
},
|
|
571
|
-
collectedAt: props.collectedAtMs,
|
|
572
|
-
previousCollectedAt: props.previousCollectedAtMs,
|
|
573
|
-
interval: props.interval,
|
|
574
|
-
actualElapsed
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
//#endregion
|
|
579
|
-
//#region src/utils/start-monitor.ts
|
|
580
|
-
let _monitorInstance = null;
|
|
581
|
-
/** Latest collected metrics from the monitor */
|
|
582
|
-
let _metrics;
|
|
583
|
-
/** Maximum memory limit for the monitor (in MB) */
|
|
584
|
-
let maxMemoryLimit = DEFAULT_MAX_PROCESS_MEMORY_MB;
|
|
585
|
-
/** Use 90% of the effective limit */
|
|
586
|
-
const SAFE_MEMORY_LIMIT_RATIO = .9;
|
|
587
|
-
function startMonitor() {
|
|
588
|
-
if (!_monitorInstance) {
|
|
589
|
-
try {
|
|
590
|
-
const processMemoryLimit = getProcessMemoryLimit();
|
|
591
|
-
if (processMemoryLimit && processMemoryLimit > 0) maxMemoryLimit = processMemoryLimit / 1024 / 1024 * SAFE_MEMORY_LIMIT_RATIO;
|
|
592
|
-
} catch {}
|
|
593
|
-
_monitorInstance = createMonitorObserver({
|
|
594
|
-
callback(metrics) {
|
|
595
|
-
_metrics = metrics;
|
|
596
|
-
},
|
|
597
|
-
interval: WORST_SWEEP_INTERVAL,
|
|
598
|
-
maxMemory: maxMemoryLimit
|
|
599
|
-
});
|
|
600
|
-
_monitorInstance.start();
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
//#endregion
|
|
605
|
-
//#region src/sweep/batchUpdateExpiredRatio.ts
|
|
606
|
-
/**
|
|
607
|
-
* Updates the expired ratio for each cache instance based on the collected ratios.
|
|
608
|
-
* @param currentExpiredRatios - An array of arrays containing expired ratios for each cache instance.
|
|
609
|
-
* @internal
|
|
610
|
-
*/
|
|
611
|
-
function _batchUpdateExpiredRatio(currentExpiredRatios) {
|
|
612
|
-
for (const inst of _instancesCache) {
|
|
613
|
-
const ratios = currentExpiredRatios[inst._instanceIndexState];
|
|
614
|
-
if (ratios && ratios.length > 0) {
|
|
615
|
-
const avgRatio = ratios.reduce((sum, val) => sum + val, 0) / ratios.length;
|
|
616
|
-
const alpha = .6;
|
|
617
|
-
inst._expiredRatio = inst._expiredRatio * (1 - alpha) + avgRatio * alpha;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
//#endregion
|
|
623
|
-
//#region src/utils/interpolate.ts
|
|
624
|
-
/**
|
|
625
|
-
* Interpolates a value between two numeric ranges.
|
|
626
|
-
*
|
|
627
|
-
* Maps `value` from [fromStart, fromEnd] to [toStart, toEnd].
|
|
628
|
-
* Works with inverted ranges, negative values, and any numeric input.
|
|
629
|
-
*/
|
|
630
|
-
function interpolate({ value, fromStart, fromEnd, toStart, toEnd }) {
|
|
631
|
-
if (fromStart === fromEnd) return toStart;
|
|
632
|
-
return toStart + (value - fromStart) / (fromEnd - fromStart) * (toEnd - toStart);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
//#endregion
|
|
636
|
-
//#region src/sweep/calculate-optimal-sweep-params.ts
|
|
637
|
-
/**
|
|
638
|
-
* Calculates adaptive sweep parameters based on real-time system utilization.
|
|
639
|
-
*
|
|
640
|
-
* Memory utilization is used as-is: higher memory usage → more conservative sweeps.
|
|
641
|
-
* CPU and event loop utilization are inverted: lower usage → more conservative sweeps.
|
|
642
|
-
*
|
|
643
|
-
* This inversion ensures:
|
|
644
|
-
* - When CPU and loop are *free*, sweeping becomes more aggressive (worst-case behavior).
|
|
645
|
-
* - When CPU and loop are *busy*, sweeping becomes more conservative (optimal behavior).
|
|
646
|
-
*
|
|
647
|
-
* The final ratio is a weighted average of the three metrics, clamped to [0, 1].
|
|
648
|
-
* This ratio is then used to interpolate between optimal and worst-case sweep settings.
|
|
649
|
-
*
|
|
650
|
-
* @param options - Optional configuration for weights and sweep bounds.
|
|
651
|
-
* @returns Interpolated sweep interval, time budget, and the ratio used.
|
|
652
|
-
*/
|
|
653
|
-
const calculateOptimalSweepParams = (options) => {
|
|
654
|
-
const { metrics, weights = {}, optimalSweepIntervalMs = OPTIMAL_SWEEP_INTERVAL, worstSweepIntervalMs = WORST_SWEEP_INTERVAL, worstSweepTimeBudgetMs = WORST_SWEEP_TIME_BUDGET } = options;
|
|
655
|
-
const memoryWeight = weights.memory ?? DEFAULT_MEMORY_WEIGHT;
|
|
656
|
-
const cpuWeight = weights.cpu ?? DEFAULT_CPU_WEIGHT;
|
|
657
|
-
const loopWeight = weights.loop ?? DEFAULT_LOOP_WEIGHT;
|
|
658
|
-
const memoryUtilization = metrics?.memory.utilization ?? 0;
|
|
659
|
-
const cpuUtilizationRaw = metrics?.cpu.utilization ?? 0;
|
|
660
|
-
const loopUtilizationRaw = metrics?.loop.utilization ?? 0;
|
|
661
|
-
const cpuUtilization = 1 - cpuUtilizationRaw;
|
|
662
|
-
const loopUtilization = 1 - loopUtilizationRaw;
|
|
663
|
-
const weightedSum = memoryUtilization * memoryWeight + cpuUtilization * cpuWeight + loopUtilization * loopWeight;
|
|
664
|
-
const totalWeight = memoryWeight + cpuWeight + loopWeight;
|
|
665
|
-
const ratio = Math.min(1, Math.max(0, weightedSum / totalWeight));
|
|
666
|
-
return {
|
|
667
|
-
sweepIntervalMs: interpolate({
|
|
668
|
-
value: ratio,
|
|
669
|
-
fromStart: 0,
|
|
670
|
-
fromEnd: 1,
|
|
671
|
-
toStart: optimalSweepIntervalMs,
|
|
672
|
-
toEnd: worstSweepIntervalMs
|
|
673
|
-
}),
|
|
674
|
-
sweepTimeBudgetMs: interpolate({
|
|
675
|
-
value: ratio,
|
|
676
|
-
fromStart: 0,
|
|
677
|
-
fromEnd: 1,
|
|
678
|
-
toStart: 0,
|
|
679
|
-
toEnd: worstSweepTimeBudgetMs
|
|
680
|
-
})
|
|
681
|
-
};
|
|
682
|
-
};
|
|
683
|
-
|
|
684
|
-
//#endregion
|
|
685
|
-
//#region src/sweep/select-instance-to-sweep.ts
|
|
686
|
-
/**
|
|
687
|
-
* Selects a cache instance to sweep based on sweep weights or round‑robin order.
|
|
688
|
-
*
|
|
689
|
-
* Two selection modes are supported:
|
|
690
|
-
* - **Round‑robin mode**: If `totalSweepWeight` ≤ 0, instances are selected
|
|
691
|
-
* deterministically in sequence using `batchSweep`. Once all instances
|
|
692
|
-
* have been processed, returns `null`.
|
|
693
|
-
* - **Weighted mode**: If sweep weights are available, performs a probabilistic
|
|
694
|
-
* selection. Each instance’s `_sweepWeight` contributes proportionally to its
|
|
695
|
-
* chance of being chosen.
|
|
696
|
-
*
|
|
697
|
-
* This function depends on `_updateWeightSweep` to maintain accurate sweep weights.
|
|
698
|
-
*
|
|
699
|
-
* @param totalSweepWeight - Sum of all sweep weights across instances.
|
|
700
|
-
* @param batchSweep - Current batch index used for round‑robin selection.
|
|
701
|
-
* @returns The selected `CacheState` instance, `null` if no instance remains,
|
|
702
|
-
* or `undefined` if the cache is empty.
|
|
703
|
-
*/
|
|
704
|
-
function _selectInstanceToSweep({ totalSweepWeight, batchSweep }) {
|
|
705
|
-
let instanceToSweep = _instancesCache[0];
|
|
706
|
-
if (totalSweepWeight <= 0) {
|
|
707
|
-
if (batchSweep > _instancesCache.length) instanceToSweep = null;
|
|
708
|
-
instanceToSweep = _instancesCache[batchSweep - 1];
|
|
709
|
-
} else {
|
|
710
|
-
let threshold = Math.random() * totalSweepWeight;
|
|
711
|
-
for (const inst of _instancesCache) {
|
|
712
|
-
threshold -= inst._sweepWeight;
|
|
713
|
-
if (threshold <= 0) {
|
|
714
|
-
instanceToSweep = inst;
|
|
715
|
-
break;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
return instanceToSweep;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
//#endregion
|
|
723
|
-
//#region src/cache/delete.ts
|
|
724
|
-
let DELETE_REASON = /* @__PURE__ */ function(DELETE_REASON$1) {
|
|
725
|
-
DELETE_REASON$1["MANUAL"] = "manual";
|
|
726
|
-
DELETE_REASON$1["EXPIRED"] = "expired";
|
|
727
|
-
DELETE_REASON$1["STALE"] = "stale";
|
|
728
|
-
return DELETE_REASON$1;
|
|
729
|
-
}({});
|
|
730
|
-
/**
|
|
731
|
-
* Deletes a key from the cache.
|
|
732
|
-
* @param state - The cache state.
|
|
733
|
-
* @param key - The key.
|
|
734
|
-
* @returns A boolean indicating whether the key was successfully deleted.
|
|
735
|
-
*/
|
|
736
|
-
const deleteKey = (state, key, reason = DELETE_REASON.MANUAL) => {
|
|
737
|
-
const onDelete = state.onDelete;
|
|
738
|
-
const onExpire = state.onExpire;
|
|
739
|
-
if (!onDelete && !onExpire) return state.store.delete(key);
|
|
740
|
-
const entry = state.store.get(key);
|
|
741
|
-
if (!entry) return false;
|
|
742
|
-
state.store.delete(key);
|
|
743
|
-
state.onDelete?.(key, entry[1], reason);
|
|
744
|
-
if (reason !== DELETE_REASON.MANUAL) state.onExpire?.(key, entry[1], reason);
|
|
745
|
-
return true;
|
|
746
|
-
};
|
|
747
|
-
|
|
748
|
-
//#endregion
|
|
749
|
-
//#region src/types.ts
|
|
750
|
-
/**
|
|
751
|
-
* Entry status: fresh, stale, or expired.
|
|
752
|
-
*/
|
|
753
|
-
let ENTRY_STATUS = /* @__PURE__ */ function(ENTRY_STATUS$1) {
|
|
754
|
-
/** Valid and within TTL. */
|
|
755
|
-
ENTRY_STATUS$1["FRESH"] = "fresh";
|
|
756
|
-
/** Expired but within stale window; still served. */
|
|
757
|
-
ENTRY_STATUS$1["STALE"] = "stale";
|
|
758
|
-
/** Beyond stale window; not served. */
|
|
759
|
-
ENTRY_STATUS$1["EXPIRED"] = "expired";
|
|
760
|
-
return ENTRY_STATUS$1;
|
|
761
|
-
}({});
|
|
762
|
-
|
|
763
|
-
//#endregion
|
|
764
|
-
//#region src/utils/status-from-tags.ts
|
|
765
|
-
/**
|
|
766
|
-
* Computes the derived status of a cache entry based on its associated tags.
|
|
767
|
-
*
|
|
768
|
-
* Tags may impose stricter expiration or stale rules on the entry. Only tags
|
|
769
|
-
* created at or after the entry's creation timestamp are considered relevant.
|
|
770
|
-
*
|
|
771
|
-
* Resolution rules:
|
|
772
|
-
* - If any applicable tag marks the entry as expired, the status becomes `EXPIRED`.
|
|
773
|
-
* - Otherwise, if any applicable tag marks it as stale, the status becomes `STALE`.
|
|
774
|
-
* - If no tag imposes stricter rules, the entry remains `FRESH`.
|
|
775
|
-
*
|
|
776
|
-
* @param state - The cache state containing tag metadata.
|
|
777
|
-
* @param entry - The cache entry whose status is being evaluated.
|
|
778
|
-
* @returns A tuple containing:
|
|
779
|
-
* - The final {@link ENTRY_STATUS} imposed by tags.
|
|
780
|
-
* - The earliest timestamp at which a tag marked the entry as stale
|
|
781
|
-
* (or 0 if no tag imposed a stale rule).
|
|
782
|
-
*/
|
|
783
|
-
function _statusFromTags(state, entry) {
|
|
784
|
-
const entryCreatedAt = entry[0][0];
|
|
785
|
-
let earliestTagStaleInvalidation = Infinity;
|
|
786
|
-
let status = ENTRY_STATUS.FRESH;
|
|
787
|
-
const tags = entry[2];
|
|
788
|
-
if (tags) for (const tag of tags) {
|
|
789
|
-
const ts = state._tags.get(tag);
|
|
790
|
-
if (!ts) continue;
|
|
791
|
-
const [tagExpiredAt, tagStaleSinceAt] = ts;
|
|
792
|
-
if (tagExpiredAt >= entryCreatedAt) {
|
|
793
|
-
status = ENTRY_STATUS.EXPIRED;
|
|
794
|
-
break;
|
|
795
|
-
}
|
|
796
|
-
if (tagStaleSinceAt >= entryCreatedAt) {
|
|
797
|
-
if (tagStaleSinceAt < earliestTagStaleInvalidation) earliestTagStaleInvalidation = tagStaleSinceAt;
|
|
798
|
-
status = ENTRY_STATUS.STALE;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
return [status, status === ENTRY_STATUS.STALE ? earliestTagStaleInvalidation : 0];
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
//#endregion
|
|
805
|
-
//#region src/cache/validators.ts
|
|
806
|
-
/**
|
|
807
|
-
* Computes the final derived status of a cache entry by combining:
|
|
808
|
-
*
|
|
809
|
-
* - The entry's own expiration timestamps (TTL and stale TTL).
|
|
810
|
-
* - Any stricter expiration or stale rules imposed by its associated tags.
|
|
811
|
-
*
|
|
812
|
-
* Precedence rules:
|
|
813
|
-
* - `EXPIRED` overrides everything.
|
|
814
|
-
* - `STALE` overrides `FRESH`.
|
|
815
|
-
* - If neither the entry nor its tags impose stricter rules, the entry is `FRESH`.
|
|
816
|
-
*
|
|
817
|
-
* @param state - The cache state containing tag metadata.
|
|
818
|
-
* @param entry - The cache entry being evaluated.
|
|
819
|
-
* @returns The final {@link ENTRY_STATUS} for the entry.
|
|
820
|
-
*/
|
|
821
|
-
function computeEntryStatus(state, entry, now) {
|
|
822
|
-
const [__createdAt, expiresAt, staleExpiresAt] = entry[0];
|
|
823
|
-
const [tagStatus, earliestTagStaleInvalidation] = _statusFromTags(state, entry);
|
|
824
|
-
if (tagStatus === ENTRY_STATUS.EXPIRED) return ENTRY_STATUS.EXPIRED;
|
|
825
|
-
const windowStale = staleExpiresAt - expiresAt;
|
|
826
|
-
if (tagStatus === ENTRY_STATUS.STALE && staleExpiresAt > 0 && now < earliestTagStaleInvalidation + windowStale && now <= staleExpiresAt) return ENTRY_STATUS.STALE;
|
|
827
|
-
if (now < expiresAt) return ENTRY_STATUS.FRESH;
|
|
828
|
-
if (staleExpiresAt > 0 && now < staleExpiresAt) return ENTRY_STATUS.STALE;
|
|
829
|
-
return ENTRY_STATUS.EXPIRED;
|
|
830
|
-
}
|
|
831
|
-
/**
|
|
832
|
-
* Determines whether a cache entry is fresh.
|
|
833
|
-
*
|
|
834
|
-
* A fresh entry is one whose final derived status is `FRESH`, meaning:
|
|
835
|
-
* - It has not expired according to its own timestamps, and
|
|
836
|
-
* - No associated tag imposes a stricter stale or expired rule.
|
|
837
|
-
*
|
|
838
|
-
* `entry` can be either a {@link CacheEntry} or a pre-computed {@link ENTRY_STATUS}.
|
|
839
|
-
* Passing a pre-computed status avoids recalculating the entry status.
|
|
840
|
-
*
|
|
841
|
-
* @param state - The cache state containing tag metadata.
|
|
842
|
-
* @param entry - The cache entry or pre-computed status being evaluated.
|
|
843
|
-
* @param now - The current timestamp.
|
|
844
|
-
* @returns True if the entry is fresh.
|
|
845
|
-
*/
|
|
846
|
-
const isFresh = (state, entry, now) => {
|
|
847
|
-
if (typeof entry === "string") return entry === ENTRY_STATUS.FRESH;
|
|
848
|
-
return computeEntryStatus(state, entry, now) === ENTRY_STATUS.FRESH;
|
|
849
|
-
};
|
|
850
|
-
/**
|
|
851
|
-
* Determines whether a cache entry is stale.
|
|
852
|
-
*
|
|
853
|
-
* A stale entry is one whose final derived status is `STALE`, meaning:
|
|
854
|
-
* - It has passed its TTL but is still within its stale window, or
|
|
855
|
-
* - A tag imposes a stale rule that applies to this entry.
|
|
856
|
-
*
|
|
857
|
-
* `entry` can be either a {@link CacheEntry} or a pre-computed {@link ENTRY_STATUS}.
|
|
858
|
-
* Passing a pre-computed status avoids recalculating the entry status.
|
|
859
|
-
*
|
|
860
|
-
* @param state - The cache state containing tag metadata.
|
|
861
|
-
* @param entry - The cache entry or pre-computed status being evaluated.
|
|
862
|
-
* @param now - The current timestamp.
|
|
863
|
-
* @returns True if the entry is stale.
|
|
864
|
-
*/
|
|
865
|
-
const isStale = (state, entry, now) => {
|
|
866
|
-
if (typeof entry === "string") return entry === ENTRY_STATUS.STALE;
|
|
867
|
-
return computeEntryStatus(state, entry, now) === ENTRY_STATUS.STALE;
|
|
868
|
-
};
|
|
869
|
-
/**
|
|
870
|
-
* Determines whether a cache entry is expired.
|
|
871
|
-
*
|
|
872
|
-
* An expired entry is one whose final derived status is `EXPIRED`, meaning:
|
|
873
|
-
* - It has exceeded both its TTL and stale TTL, or
|
|
874
|
-
* - A tag imposes an expiration rule that applies to this entry.
|
|
875
|
-
*
|
|
876
|
-
* `entry` can be either a {@link CacheEntry} or a pre-computed {@link ENTRY_STATUS}.
|
|
877
|
-
* Passing a pre-computed status avoids recalculating the entry status.
|
|
878
|
-
*
|
|
879
|
-
* @param state - The cache state containing tag metadata.
|
|
880
|
-
* @param entry - The cache entry or pre-computed status being evaluated.
|
|
881
|
-
* @param now - The current timestamp.
|
|
882
|
-
* @returns True if the entry is expired.
|
|
883
|
-
*/
|
|
884
|
-
const isExpired = (state, entry, now) => {
|
|
885
|
-
if (typeof entry === "string") return entry === ENTRY_STATUS.EXPIRED;
|
|
886
|
-
return computeEntryStatus(state, entry, now) === ENTRY_STATUS.EXPIRED;
|
|
887
|
-
};
|
|
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
|
-
|
|
965
|
-
//#endregion
|
|
966
|
-
//#region src/sweep/sweep-once.ts
|
|
967
|
-
/**
|
|
968
|
-
* Performs a single sweep operation on the cache to remove expired and optionally stale entries.
|
|
969
|
-
* Uses a linear scan with a saved pointer to resume from the last processed key.
|
|
970
|
-
* @param state - The cache state.
|
|
971
|
-
* @param _maxKeysPerBatch - Maximum number of keys to process in this sweep.
|
|
972
|
-
* @returns An object containing statistics about the sweep operation.
|
|
973
|
-
*/
|
|
974
|
-
function _sweepOnce(state, _maxKeysPerBatch = MAX_KEYS_PER_BATCH) {
|
|
975
|
-
if (!state._sweepIter) state._sweepIter = state.store.entries();
|
|
976
|
-
let processed = 0;
|
|
977
|
-
let expiredCount = 0;
|
|
978
|
-
let staleCount = 0;
|
|
979
|
-
for (let i = 0; i < _maxKeysPerBatch; i++) {
|
|
980
|
-
const next = state._sweepIter.next();
|
|
981
|
-
if (next.done) {
|
|
982
|
-
state._sweepIter = state.store.entries();
|
|
983
|
-
break;
|
|
984
|
-
}
|
|
985
|
-
processed += 1;
|
|
986
|
-
const [key, entry] = next.value;
|
|
987
|
-
const now = Date.now();
|
|
988
|
-
const status = computeEntryStatus(state, entry, now);
|
|
989
|
-
if (isExpired(state, status, now)) {
|
|
990
|
-
deleteKey(state, key, DELETE_REASON.EXPIRED);
|
|
991
|
-
expiredCount += 1;
|
|
992
|
-
} else if (isStale(state, status, now)) {
|
|
993
|
-
staleCount += 1;
|
|
994
|
-
if (shouldPurge(state.purgeStaleOnSweep, state, "sweep")) deleteKey(state, key, DELETE_REASON.STALE);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
const expiredStaleCount = shouldPurge(state.purgeStaleOnSweep, state, "sweep") ? staleCount : 0;
|
|
998
|
-
return {
|
|
999
|
-
processed,
|
|
1000
|
-
expiredCount,
|
|
1001
|
-
staleCount,
|
|
1002
|
-
ratio: processed > 0 ? (expiredCount + expiredStaleCount) / processed : 0
|
|
1003
|
-
};
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
//#endregion
|
|
1007
|
-
//#region src/sweep/calculate-optimal-max-expired-ratio.ts
|
|
1008
|
-
/**
|
|
1009
|
-
* Calculates the optimal maximum expired ratio based on current memory utilization.
|
|
1010
|
-
*
|
|
1011
|
-
* This function interpolates between `maxAllowExpiredRatio` and `MINIMAL_EXPIRED_RATIO`
|
|
1012
|
-
* depending on the memory usage reported by `_metrics`. At low memory usage (0%),
|
|
1013
|
-
* the optimal ratio equals `maxAllowExpiredRatio`. As memory usage approaches or exceeds
|
|
1014
|
-
* 80% of the memory limit, the optimal ratio decreases toward `MINIMAL_EXPIRED_RATIO`.
|
|
1015
|
-
*
|
|
1016
|
-
* @param maxAllowExpiredRatio - The maximum allowed expired ratio at minimal memory usage.
|
|
1017
|
-
* Defaults to `DEFAULT_MAX_EXPIRED_RATIO`.
|
|
1018
|
-
* @returns A normalized value between 0 and 1 representing the optimal expired ratio.
|
|
1019
|
-
*/
|
|
1020
|
-
function calculateOptimalMaxExpiredRatio(maxAllowExpiredRatio = DEFAULT_MAX_EXPIRED_RATIO) {
|
|
1021
|
-
const EFFECTIVE_MEMORY_THRESHOLD = EXPIRED_RATIO_MEMORY_THRESHOLD / SAFE_MEMORY_LIMIT_RATIO;
|
|
1022
|
-
const optimalExpiredRatio = interpolate({
|
|
1023
|
-
value: _metrics?.memory.utilization ?? 0,
|
|
1024
|
-
fromStart: 0,
|
|
1025
|
-
fromEnd: EFFECTIVE_MEMORY_THRESHOLD,
|
|
1026
|
-
toStart: maxAllowExpiredRatio,
|
|
1027
|
-
toEnd: MINIMAL_EXPIRED_RATIO
|
|
1028
|
-
});
|
|
1029
|
-
return Math.min(1, Math.max(0, optimalExpiredRatio));
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
//#endregion
|
|
1033
|
-
//#region src/sweep/update-weight.ts
|
|
1034
|
-
/**
|
|
1035
|
-
* Updates the sweep weight (`_sweepWeight`) for each cache instance.
|
|
1036
|
-
*
|
|
1037
|
-
* The sweep weight determines the probability that an instance will be selected
|
|
1038
|
-
* for a cleanup (sweep) process. It is calculated based on the store size and
|
|
1039
|
-
* the ratio of expired keys.
|
|
1040
|
-
*
|
|
1041
|
-
* This function complements (`_selectInstanceToSweep`), which is responsible
|
|
1042
|
-
* for selecting the correct instance based on the weights assigned here.
|
|
1043
|
-
*
|
|
1044
|
-
* ---
|
|
1045
|
-
*
|
|
1046
|
-
* ### Sweep systems:
|
|
1047
|
-
* 1. **Normal sweep**
|
|
1048
|
-
* - Runs whenever the percentage of expired keys exceeds the allowed threshold
|
|
1049
|
-
* calculated by `calculateOptimalMaxExpiredRatio`.
|
|
1050
|
-
* - It is the main cleanup mechanism and is applied proportionally to the
|
|
1051
|
-
* store size and the expired‑key ratio.
|
|
1052
|
-
*
|
|
1053
|
-
* 2. **Memory‑conditioned sweep (control)**
|
|
1054
|
-
* - Works exactly like the normal sweep, except it may run even when it
|
|
1055
|
-
* normally wouldn’t.
|
|
1056
|
-
* - Only activates under **high memory pressure**.
|
|
1057
|
-
* - Serves as an additional control mechanism to adjust weights, keep the
|
|
1058
|
-
* system updated, and help prevent memory overflows.
|
|
1059
|
-
*
|
|
1060
|
-
* 3. **Round‑robin sweep (minimal control)**
|
|
1061
|
-
* - Always runs, even if the expired ratio is low or memory usage does not
|
|
1062
|
-
* require it.
|
|
1063
|
-
* - Processes a very small number of keys per instance, much smaller than
|
|
1064
|
-
* the normal sweep.
|
|
1065
|
-
* - Its main purpose is to ensure that all instances receive at least a
|
|
1066
|
-
* periodic weight update and minimal expired‑key control.
|
|
1067
|
-
*
|
|
1068
|
-
* ---
|
|
1069
|
-
* #### Important notes:
|
|
1070
|
-
* - A minimum `MINIMAL_EXPIRED_RATIO` (e.g., 5%) is assumed to ensure that
|
|
1071
|
-
* control sweeps can always run under high‑memory scenarios.
|
|
1072
|
-
* - Even with a minimum ratio, the normal sweep and the memory‑conditioned sweep
|
|
1073
|
-
* may **skip execution** if memory usage allows it and the expired ratio is
|
|
1074
|
-
* below the optimal maximum.
|
|
1075
|
-
* - The round‑robin sweep is never skipped: it always runs with a very small,
|
|
1076
|
-
* almost imperceptible cost.
|
|
1077
|
-
*
|
|
1078
|
-
* @returns The total accumulated sweep weight across all cache instances.
|
|
1079
|
-
*/
|
|
1080
|
-
function _updateWeightSweep() {
|
|
1081
|
-
let totalSweepWeight = 0;
|
|
1082
|
-
for (const instCache of _instancesCache) {
|
|
1083
|
-
if (instCache.store.size <= 0) {
|
|
1084
|
-
instCache._sweepWeight = 0;
|
|
1085
|
-
continue;
|
|
1086
|
-
}
|
|
1087
|
-
let expiredRatio = MINIMAL_EXPIRED_RATIO;
|
|
1088
|
-
if (instCache._expiredRatio > MINIMAL_EXPIRED_RATIO) expiredRatio = instCache._expiredRatio;
|
|
1089
|
-
{
|
|
1090
|
-
const optimalMaxExpiredRatio = calculateOptimalMaxExpiredRatio(instCache._maxAllowExpiredRatio);
|
|
1091
|
-
if (expiredRatio <= optimalMaxExpiredRatio) {
|
|
1092
|
-
instCache._sweepWeight = 0;
|
|
1093
|
-
continue;
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
instCache._sweepWeight = instCache.store.size * expiredRatio;
|
|
1097
|
-
totalSweepWeight += instCache._sweepWeight;
|
|
1098
|
-
}
|
|
1099
|
-
return totalSweepWeight;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
//#endregion
|
|
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
|
-
}
|
|
1112
|
-
/**
|
|
1113
|
-
* Performs a sweep operation on the cache to remove expired and optionally stale entries.
|
|
1114
|
-
* Uses a linear scan with a saved pointer to resume from the last processed key.
|
|
1115
|
-
* @param state - The cache state.
|
|
1116
|
-
*/
|
|
1117
|
-
const sweep = async (state, utilities = {}) => {
|
|
1118
|
-
const { schedule = defaultSchedule, yieldFn = defaultYieldFn, now = Date.now(), runOnlyOne = false } = utilities;
|
|
1119
|
-
const startTime = now;
|
|
1120
|
-
let sweepIntervalMs = OPTIMAL_SWEEP_INTERVAL;
|
|
1121
|
-
let sweepTimeBudgetMs = OPTIMAL_SWEEP_TIME_BUDGET_IF_NOTE_METRICS_AVAILABLE;
|
|
1122
|
-
if (_metrics) ({sweepIntervalMs, sweepTimeBudgetMs} = calculateOptimalSweepParams({ metrics: _metrics }));
|
|
1123
|
-
const totalSweepWeight = _updateWeightSweep();
|
|
1124
|
-
const currentExpiredRatios = [];
|
|
1125
|
-
const maxKeysPerBatch = totalSweepWeight <= 0 ? MAX_KEYS_PER_BATCH / _instancesCache.length : MAX_KEYS_PER_BATCH;
|
|
1126
|
-
let batchSweep = 0;
|
|
1127
|
-
while (true) {
|
|
1128
|
-
batchSweep += 1;
|
|
1129
|
-
const instanceToSweep = _selectInstanceToSweep({
|
|
1130
|
-
batchSweep,
|
|
1131
|
-
totalSweepWeight
|
|
1132
|
-
});
|
|
1133
|
-
if (!instanceToSweep) break;
|
|
1134
|
-
const { ratio } = _sweepOnce(instanceToSweep, maxKeysPerBatch);
|
|
1135
|
-
(currentExpiredRatios[instanceToSweep._instanceIndexState] ??= []).push(ratio);
|
|
1136
|
-
if (Date.now() - startTime > sweepTimeBudgetMs) break;
|
|
1137
|
-
await yieldFn();
|
|
1138
|
-
}
|
|
1139
|
-
_batchUpdateExpiredRatio(currentExpiredRatios);
|
|
1140
|
-
if (!runOnlyOne) schedule(() => void sweep(state, utilities), sweepIntervalMs);
|
|
1141
|
-
};
|
|
1142
|
-
const defaultSchedule = (fn, ms) => {
|
|
1143
|
-
_pendingSweepTimeout = setTimeout(fn, ms);
|
|
1144
|
-
if (typeof _pendingSweepTimeout.unref === "function") _pendingSweepTimeout.unref();
|
|
1145
|
-
};
|
|
1146
|
-
const defaultYieldFn = () => new Promise((resolve) => setImmediate(resolve));
|
|
1147
|
-
|
|
1148
|
-
//#endregion
|
|
1149
|
-
//#region src/cache/create-cache.ts
|
|
1150
|
-
let _instanceCount = 0;
|
|
1151
|
-
const INSTANCE_WARNING_THRESHOLD = 99;
|
|
1152
|
-
const _instancesCache = [];
|
|
1153
|
-
/**
|
|
1154
|
-
* Creates the initial state for the TTL cache.
|
|
1155
|
-
* @param options - Configuration options for the cache.
|
|
1156
|
-
* @returns The initial cache state.
|
|
1157
|
-
*/
|
|
1158
|
-
const createCache = (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;
|
|
1160
|
-
_instanceCount++;
|
|
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
|
-
});
|
|
1182
|
-
const state = {
|
|
1183
|
-
store: /* @__PURE__ */ new Map(),
|
|
1184
|
-
_sweepIter: null,
|
|
1185
|
-
get size() {
|
|
1186
|
-
return state.store.size;
|
|
1187
|
-
},
|
|
1188
|
-
onExpire,
|
|
1189
|
-
onDelete,
|
|
1190
|
-
maxSize,
|
|
1191
|
-
maxMemorySize,
|
|
1192
|
-
defaultTtl,
|
|
1193
|
-
defaultStaleWindow,
|
|
1194
|
-
purgeStaleOnGet: resolvedPurgeStaleOnGet,
|
|
1195
|
-
purgeStaleOnSweep: resolvedPurgeStaleOnSweep,
|
|
1196
|
-
purgeResourceMetric: resolvedPurgeResourceMetric,
|
|
1197
|
-
_maxAllowExpiredRatio,
|
|
1198
|
-
_autoStartSweep,
|
|
1199
|
-
_instanceIndexState: -1,
|
|
1200
|
-
_expiredRatio: 0,
|
|
1201
|
-
_sweepWeight: 0,
|
|
1202
|
-
_tags: /* @__PURE__ */ new Map()
|
|
1203
|
-
};
|
|
1204
|
-
state._instanceIndexState = _instancesCache.push(state) - 1;
|
|
1205
|
-
startSweep(state);
|
|
1206
|
-
return state;
|
|
1207
|
-
};
|
|
1208
|
-
|
|
1209
|
-
//#endregion
|
|
1210
|
-
//#region src/cache/get.ts
|
|
1211
|
-
/**
|
|
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
|
-
*
|
|
1215
|
-
* @param state - The cache state.
|
|
1216
|
-
* @param key - The key to retrieve.
|
|
1217
|
-
* @param now - Optional timestamp override (defaults to Date.now()).
|
|
1218
|
-
* @returns A tuple of [status, entry] if the entry is valid, or [null, undefined] if not found or expired.
|
|
1219
|
-
*
|
|
1220
|
-
* @internal
|
|
1221
|
-
*/
|
|
1222
|
-
const getWithStatus = (state, key, purgeMode, now = Date.now()) => {
|
|
1223
|
-
const entry = state.store.get(key);
|
|
1224
|
-
if (!entry) return [null, void 0];
|
|
1225
|
-
const status = computeEntryStatus(state, entry, now);
|
|
1226
|
-
if (isFresh(state, status, now)) return [status, entry];
|
|
1227
|
-
if (isStale(state, status, now)) {
|
|
1228
|
-
if (shouldPurge(purgeMode ?? state.purgeStaleOnGet, state, "get")) deleteKey(state, key, DELETE_REASON.STALE);
|
|
1229
|
-
return [status, entry];
|
|
1230
|
-
}
|
|
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;
|
|
1246
|
-
};
|
|
1247
|
-
|
|
1248
|
-
//#endregion
|
|
1249
|
-
//#region src/cache/has.ts
|
|
1250
|
-
/**
|
|
1251
|
-
* Checks if a key exists in the cache and is not expired.
|
|
1252
|
-
* @param state - The cache state.
|
|
1253
|
-
* @param key - The key to check.
|
|
1254
|
-
* @param now - Optional timestamp override (defaults to Date.now()).
|
|
1255
|
-
* @returns True if the key exists and is valid, false otherwise.
|
|
1256
|
-
*/
|
|
1257
|
-
const has = (state, key, now = Date.now()) => {
|
|
1258
|
-
return get(state, key, now) !== void 0;
|
|
1259
|
-
};
|
|
1260
|
-
|
|
1261
|
-
//#endregion
|
|
1262
|
-
//#region src/cache/invalidate-tag.ts
|
|
1263
|
-
/**
|
|
1264
|
-
* Invalidates one or more tags so that entries associated with them
|
|
1265
|
-
* become expired or stale from this moment onward.
|
|
1266
|
-
*
|
|
1267
|
-
* Semantics:
|
|
1268
|
-
* - Each tag maintains two timestamps in `state._tags`:
|
|
1269
|
-
* [expiredAt, staleSinceAt].
|
|
1270
|
-
* - Calling this function updates one of those timestamps to `_now`,
|
|
1271
|
-
* depending on whether the tag should force expiration or staleness.
|
|
1272
|
-
*
|
|
1273
|
-
* Rules:
|
|
1274
|
-
* - If `asStale` is false (default), the tag forces expiration:
|
|
1275
|
-
* entries created before `_now` will be considered expired.
|
|
1276
|
-
* - If `asStale` is true, the tag forces staleness:
|
|
1277
|
-
* entries created before `_now` will be considered stale,
|
|
1278
|
-
* but only if they support a stale window.
|
|
1279
|
-
*
|
|
1280
|
-
* Behavior:
|
|
1281
|
-
* - Each call replaces any previous invalidation timestamp for the tag.
|
|
1282
|
-
* - Entries created after `_now` are unaffected.
|
|
1283
|
-
*
|
|
1284
|
-
* @param state - The cache state containing tag metadata.
|
|
1285
|
-
* @param tags - A tag or list of tags to invalidate.
|
|
1286
|
-
* @param options.asStale - Whether the tag should mark entries as stale.
|
|
1287
|
-
*/
|
|
1288
|
-
function invalidateTag(state, tags, options = {}, _now = Date.now()) {
|
|
1289
|
-
const tagList = Array.isArray(tags) ? tags : [tags];
|
|
1290
|
-
const asStale = options.asStale ?? false;
|
|
1291
|
-
for (const tag of tagList) {
|
|
1292
|
-
const currentTag = state._tags.get(tag);
|
|
1293
|
-
if (currentTag) if (asStale) currentTag[1] = _now;
|
|
1294
|
-
else currentTag[0] = _now;
|
|
1295
|
-
else state._tags.set(tag, [asStale ? 0 : _now, asStale ? _now : 0]);
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
//#endregion
|
|
1300
|
-
//#region src/cache/set.ts
|
|
1301
|
-
/**
|
|
1302
|
-
* Sets or updates a value in the cache with TTL and an optional stale window.
|
|
1303
|
-
*
|
|
1304
|
-
* @param state - The cache state.
|
|
1305
|
-
* @param input - Cache entry definition (key, value, ttl, staleWindow, tags).
|
|
1306
|
-
* @param now - Optional timestamp override used as the base time (defaults to Date.now()).
|
|
1307
|
-
* @returns True if the entry was created or updated, false if rejected due to limits or invalid input.
|
|
1308
|
-
*
|
|
1309
|
-
* @remarks
|
|
1310
|
-
* - `ttl` defines when the entry becomes expired.
|
|
1311
|
-
* - `staleWindow` defines how long the entry may still be served as stale
|
|
1312
|
-
* after the expiration moment (`now + ttl`).
|
|
1313
|
-
* - Returns false if value is `undefined` (entry ignored, existing value untouched).
|
|
1314
|
-
* - Returns false if new entry would exceed `maxSize` limit (existing keys always allowed).
|
|
1315
|
-
* - Returns false if new entry would exceed `maxMemorySize` limit (existing keys always allowed).
|
|
1316
|
-
* - Returns true if entry was set or updated (or if existing key was updated at limit).
|
|
1317
|
-
*/
|
|
1318
|
-
const setOrUpdate = (state, input, now = Date.now()) => {
|
|
1319
|
-
const { key, value, ttl: ttlInput, staleWindow: staleWindowInput, tags } = input;
|
|
1320
|
-
if (value === void 0) return false;
|
|
1321
|
-
if (key == null) throw new Error("Missing key.");
|
|
1322
|
-
if (state.size >= state.maxSize && !state.store.has(key)) return false;
|
|
1323
|
-
if (_metrics?.memory.total.rss && _metrics?.memory.total.rss >= state.maxMemorySize * 1024 * 1024 && !state.store.has(key)) return false;
|
|
1324
|
-
const ttl = ttlInput ?? state.defaultTtl;
|
|
1325
|
-
const staleWindow = staleWindowInput ?? state.defaultStaleWindow;
|
|
1326
|
-
const expiresAt = ttl > 0 ? now + ttl : Infinity;
|
|
1327
|
-
const entry = [
|
|
1328
|
-
[
|
|
1329
|
-
now,
|
|
1330
|
-
expiresAt,
|
|
1331
|
-
staleWindow > 0 ? expiresAt + staleWindow : 0
|
|
1332
|
-
],
|
|
1333
|
-
value,
|
|
1334
|
-
typeof tags === "string" ? [tags] : Array.isArray(tags) ? tags : null
|
|
1335
|
-
];
|
|
1336
|
-
state.store.set(key, entry);
|
|
1337
|
-
return true;
|
|
1338
|
-
};
|
|
1339
|
-
|
|
1340
|
-
//#endregion
|
|
1341
|
-
//#region src/index.ts
|
|
1342
|
-
/**
|
|
1343
|
-
* A TTL (Time-To-Live) cache implementation with support for expiration,
|
|
1344
|
-
* stale windows, tag-based invalidation, and smart automatic sweeping.
|
|
1345
|
-
*
|
|
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
|
|
1350
|
-
*/
|
|
1351
|
-
var LocalTtlCache = class {
|
|
1352
|
-
state;
|
|
1353
|
-
/**
|
|
1354
|
-
* Creates a new cache instance.
|
|
1355
|
-
*
|
|
1356
|
-
* @param options - Configuration options for the cache (defaultTtl, defaultStaleWindow, maxSize, etc.)
|
|
1357
|
-
*
|
|
1358
|
-
*/
|
|
1359
|
-
constructor(options) {
|
|
1360
|
-
this.state = createCache(options);
|
|
1361
|
-
}
|
|
1362
|
-
get size() {
|
|
1363
|
-
return this.state.size;
|
|
1364
|
-
}
|
|
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);
|
|
1380
|
-
}
|
|
1381
|
-
set(key, value, options) {
|
|
1382
|
-
return setOrUpdate(this.state, {
|
|
1383
|
-
key,
|
|
1384
|
-
value,
|
|
1385
|
-
ttl: options?.ttl,
|
|
1386
|
-
staleWindow: options?.staleWindow,
|
|
1387
|
-
tags: options?.tags
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1390
|
-
delete(key) {
|
|
1391
|
-
return deleteKey(this.state, key);
|
|
1392
|
-
}
|
|
1393
|
-
has(key) {
|
|
1394
|
-
return has(this.state, key);
|
|
1395
|
-
}
|
|
1396
|
-
clear() {
|
|
1397
|
-
clear(this.state);
|
|
1398
|
-
}
|
|
1399
|
-
invalidateTag(tags, options) {
|
|
1400
|
-
invalidateTag(this.state, tags, options ?? {});
|
|
1401
|
-
}
|
|
1402
|
-
};
|
|
1403
|
-
|
|
1404
|
-
//#endregion
|
|
1405
|
-
exports.ENTRY_STATUS = ENTRY_STATUS;
|
|
1406
|
-
exports.LocalTtlCache = LocalTtlCache;
|
|
1407
|
-
//# sourceMappingURL=index.cjs.map
|