layercache 1.2.6 → 1.2.8
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/README.md +4 -4
- package/dist/cli.cjs +12 -1
- package/dist/cli.js +12 -1
- package/dist/{edge-DLstcDMn.d.cts → edge-DBs8Ko5W.d.cts} +22 -23
- package/dist/{edge-DLstcDMn.d.ts → edge-DBs8Ko5W.d.ts} +22 -23
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/index.cjs +1325 -1112
- package/dist/index.d.cts +4 -5
- package/dist/index.d.ts +4 -5
- package/dist/index.js +1178 -965
- package/package.json +1 -1
- package/packages/nestjs/dist/index.cjs +1304 -1021
- package/packages/nestjs/dist/index.d.cts +22 -23
- package/packages/nestjs/dist/index.d.ts +22 -23
- package/packages/nestjs/dist/index.js +1303 -1020
package/dist/index.js
CHANGED
|
@@ -22,6 +22,127 @@ import { EventEmitter } from "events";
|
|
|
22
22
|
|
|
23
23
|
// src/CacheNamespace.ts
|
|
24
24
|
import { Mutex } from "async-mutex";
|
|
25
|
+
|
|
26
|
+
// src/internal/CacheNamespaceMetrics.ts
|
|
27
|
+
function createEmptyNamespaceMetrics(resetAt = Date.now()) {
|
|
28
|
+
return {
|
|
29
|
+
hits: 0,
|
|
30
|
+
misses: 0,
|
|
31
|
+
fetches: 0,
|
|
32
|
+
sets: 0,
|
|
33
|
+
deletes: 0,
|
|
34
|
+
backfills: 0,
|
|
35
|
+
invalidations: 0,
|
|
36
|
+
staleHits: 0,
|
|
37
|
+
refreshes: 0,
|
|
38
|
+
refreshErrors: 0,
|
|
39
|
+
writeFailures: 0,
|
|
40
|
+
singleFlightWaits: 0,
|
|
41
|
+
negativeCacheHits: 0,
|
|
42
|
+
circuitBreakerTrips: 0,
|
|
43
|
+
degradedOperations: 0,
|
|
44
|
+
hitsByLayer: {},
|
|
45
|
+
missesByLayer: {},
|
|
46
|
+
latencyByLayer: {},
|
|
47
|
+
resetAt
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function cloneNamespaceMetrics(metrics) {
|
|
51
|
+
return {
|
|
52
|
+
...metrics,
|
|
53
|
+
hitsByLayer: { ...metrics.hitsByLayer },
|
|
54
|
+
missesByLayer: { ...metrics.missesByLayer },
|
|
55
|
+
latencyByLayer: Object.fromEntries(
|
|
56
|
+
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
57
|
+
)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function diffNamespaceMetrics(before, after) {
|
|
61
|
+
const latencyByLayer = Object.fromEntries(
|
|
62
|
+
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
63
|
+
layer,
|
|
64
|
+
{
|
|
65
|
+
avgMs: value.avgMs,
|
|
66
|
+
maxMs: value.maxMs,
|
|
67
|
+
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
68
|
+
}
|
|
69
|
+
])
|
|
70
|
+
);
|
|
71
|
+
return {
|
|
72
|
+
hits: after.hits - before.hits,
|
|
73
|
+
misses: after.misses - before.misses,
|
|
74
|
+
fetches: after.fetches - before.fetches,
|
|
75
|
+
sets: after.sets - before.sets,
|
|
76
|
+
deletes: after.deletes - before.deletes,
|
|
77
|
+
backfills: after.backfills - before.backfills,
|
|
78
|
+
invalidations: after.invalidations - before.invalidations,
|
|
79
|
+
staleHits: after.staleHits - before.staleHits,
|
|
80
|
+
refreshes: after.refreshes - before.refreshes,
|
|
81
|
+
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
82
|
+
writeFailures: after.writeFailures - before.writeFailures,
|
|
83
|
+
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
84
|
+
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
85
|
+
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
86
|
+
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
87
|
+
hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
|
|
88
|
+
missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
|
|
89
|
+
latencyByLayer,
|
|
90
|
+
resetAt: after.resetAt
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function addNamespaceMetrics(base, delta) {
|
|
94
|
+
return {
|
|
95
|
+
hits: base.hits + delta.hits,
|
|
96
|
+
misses: base.misses + delta.misses,
|
|
97
|
+
fetches: base.fetches + delta.fetches,
|
|
98
|
+
sets: base.sets + delta.sets,
|
|
99
|
+
deletes: base.deletes + delta.deletes,
|
|
100
|
+
backfills: base.backfills + delta.backfills,
|
|
101
|
+
invalidations: base.invalidations + delta.invalidations,
|
|
102
|
+
staleHits: base.staleHits + delta.staleHits,
|
|
103
|
+
refreshes: base.refreshes + delta.refreshes,
|
|
104
|
+
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
105
|
+
writeFailures: base.writeFailures + delta.writeFailures,
|
|
106
|
+
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
107
|
+
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
108
|
+
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
109
|
+
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
110
|
+
hitsByLayer: addMetricMap(base.hitsByLayer, delta.hitsByLayer),
|
|
111
|
+
missesByLayer: addMetricMap(base.missesByLayer, delta.missesByLayer),
|
|
112
|
+
latencyByLayer: cloneNamespaceMetrics(delta).latencyByLayer,
|
|
113
|
+
resetAt: base.resetAt
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function computeNamespaceHitRate(metrics) {
|
|
117
|
+
const total = metrics.hits + metrics.misses;
|
|
118
|
+
const overall = total === 0 ? 0 : metrics.hits / total;
|
|
119
|
+
const byLayer = {};
|
|
120
|
+
const layers = /* @__PURE__ */ new Set([...Object.keys(metrics.hitsByLayer), ...Object.keys(metrics.missesByLayer)]);
|
|
121
|
+
for (const layer of layers) {
|
|
122
|
+
const hits = metrics.hitsByLayer[layer] ?? 0;
|
|
123
|
+
const misses = metrics.missesByLayer[layer] ?? 0;
|
|
124
|
+
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
125
|
+
}
|
|
126
|
+
return { overall, byLayer };
|
|
127
|
+
}
|
|
128
|
+
function diffMetricMap(before, after) {
|
|
129
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
130
|
+
const result = {};
|
|
131
|
+
for (const key of keys) {
|
|
132
|
+
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
function addMetricMap(base, delta) {
|
|
137
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
138
|
+
const result = {};
|
|
139
|
+
for (const key of keys) {
|
|
140
|
+
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/CacheNamespace.ts
|
|
25
146
|
var CacheNamespace = class _CacheNamespace {
|
|
26
147
|
constructor(cache, prefix) {
|
|
27
148
|
this.cache = cache;
|
|
@@ -31,7 +152,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
31
152
|
cache;
|
|
32
153
|
prefix;
|
|
33
154
|
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
34
|
-
metrics =
|
|
155
|
+
metrics = createEmptyNamespaceMetrics();
|
|
35
156
|
async get(key, fetcher, options) {
|
|
36
157
|
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
37
158
|
}
|
|
@@ -128,19 +249,10 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
128
249
|
);
|
|
129
250
|
}
|
|
130
251
|
getMetrics() {
|
|
131
|
-
return
|
|
252
|
+
return cloneNamespaceMetrics(this.metrics);
|
|
132
253
|
}
|
|
133
254
|
getHitRate() {
|
|
134
|
-
|
|
135
|
-
const overall = total === 0 ? 0 : this.metrics.hits / total;
|
|
136
|
-
const byLayer = {};
|
|
137
|
-
const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
|
|
138
|
-
for (const layer of layers) {
|
|
139
|
-
const hits = this.metrics.hitsByLayer[layer] ?? 0;
|
|
140
|
-
const misses = this.metrics.missesByLayer[layer] ?? 0;
|
|
141
|
-
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
142
|
-
}
|
|
143
|
-
return { overall, byLayer };
|
|
255
|
+
return computeNamespaceHitRate(this.metrics);
|
|
144
256
|
}
|
|
145
257
|
/**
|
|
146
258
|
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
@@ -181,7 +293,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
181
293
|
const before = this.cache.getMetrics();
|
|
182
294
|
const result = await operation();
|
|
183
295
|
const after = this.cache.getMetrics();
|
|
184
|
-
this.metrics =
|
|
296
|
+
this.metrics = addNamespaceMetrics(this.metrics, diffNamespaceMetrics(before, after));
|
|
185
297
|
return result;
|
|
186
298
|
});
|
|
187
299
|
}
|
|
@@ -195,111 +307,6 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
195
307
|
return mutex;
|
|
196
308
|
}
|
|
197
309
|
};
|
|
198
|
-
function emptyMetrics() {
|
|
199
|
-
return {
|
|
200
|
-
hits: 0,
|
|
201
|
-
misses: 0,
|
|
202
|
-
fetches: 0,
|
|
203
|
-
sets: 0,
|
|
204
|
-
deletes: 0,
|
|
205
|
-
backfills: 0,
|
|
206
|
-
invalidations: 0,
|
|
207
|
-
staleHits: 0,
|
|
208
|
-
refreshes: 0,
|
|
209
|
-
refreshErrors: 0,
|
|
210
|
-
writeFailures: 0,
|
|
211
|
-
singleFlightWaits: 0,
|
|
212
|
-
negativeCacheHits: 0,
|
|
213
|
-
circuitBreakerTrips: 0,
|
|
214
|
-
degradedOperations: 0,
|
|
215
|
-
hitsByLayer: {},
|
|
216
|
-
missesByLayer: {},
|
|
217
|
-
latencyByLayer: {},
|
|
218
|
-
resetAt: Date.now()
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
function cloneMetrics(metrics) {
|
|
222
|
-
return {
|
|
223
|
-
...metrics,
|
|
224
|
-
hitsByLayer: { ...metrics.hitsByLayer },
|
|
225
|
-
missesByLayer: { ...metrics.missesByLayer },
|
|
226
|
-
latencyByLayer: Object.fromEntries(
|
|
227
|
-
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
228
|
-
)
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
function diffMetrics(before, after) {
|
|
232
|
-
const latencyByLayer = Object.fromEntries(
|
|
233
|
-
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
234
|
-
layer,
|
|
235
|
-
{
|
|
236
|
-
avgMs: value.avgMs,
|
|
237
|
-
maxMs: value.maxMs,
|
|
238
|
-
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
239
|
-
}
|
|
240
|
-
])
|
|
241
|
-
);
|
|
242
|
-
return {
|
|
243
|
-
hits: after.hits - before.hits,
|
|
244
|
-
misses: after.misses - before.misses,
|
|
245
|
-
fetches: after.fetches - before.fetches,
|
|
246
|
-
sets: after.sets - before.sets,
|
|
247
|
-
deletes: after.deletes - before.deletes,
|
|
248
|
-
backfills: after.backfills - before.backfills,
|
|
249
|
-
invalidations: after.invalidations - before.invalidations,
|
|
250
|
-
staleHits: after.staleHits - before.staleHits,
|
|
251
|
-
refreshes: after.refreshes - before.refreshes,
|
|
252
|
-
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
253
|
-
writeFailures: after.writeFailures - before.writeFailures,
|
|
254
|
-
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
255
|
-
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
256
|
-
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
257
|
-
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
258
|
-
hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
|
|
259
|
-
missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
|
|
260
|
-
latencyByLayer,
|
|
261
|
-
resetAt: after.resetAt
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
function addMetrics(base, delta) {
|
|
265
|
-
return {
|
|
266
|
-
hits: base.hits + delta.hits,
|
|
267
|
-
misses: base.misses + delta.misses,
|
|
268
|
-
fetches: base.fetches + delta.fetches,
|
|
269
|
-
sets: base.sets + delta.sets,
|
|
270
|
-
deletes: base.deletes + delta.deletes,
|
|
271
|
-
backfills: base.backfills + delta.backfills,
|
|
272
|
-
invalidations: base.invalidations + delta.invalidations,
|
|
273
|
-
staleHits: base.staleHits + delta.staleHits,
|
|
274
|
-
refreshes: base.refreshes + delta.refreshes,
|
|
275
|
-
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
276
|
-
writeFailures: base.writeFailures + delta.writeFailures,
|
|
277
|
-
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
278
|
-
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
279
|
-
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
280
|
-
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
281
|
-
hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
|
|
282
|
-
missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
|
|
283
|
-
latencyByLayer: cloneMetrics(delta).latencyByLayer,
|
|
284
|
-
resetAt: base.resetAt
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
function diffMap(before, after) {
|
|
288
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
289
|
-
const result = {};
|
|
290
|
-
for (const key of keys) {
|
|
291
|
-
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
292
|
-
}
|
|
293
|
-
return result;
|
|
294
|
-
}
|
|
295
|
-
function addMap(base, delta) {
|
|
296
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
297
|
-
const result = {};
|
|
298
|
-
for (const key of keys) {
|
|
299
|
-
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
300
|
-
}
|
|
301
|
-
return result;
|
|
302
|
-
}
|
|
303
310
|
function validateNamespaceKey(key) {
|
|
304
311
|
if (key.length === 0) {
|
|
305
312
|
throw new Error("Namespace prefix must not be empty.");
|
|
@@ -365,119 +372,503 @@ var CacheKeyDiscovery = class {
|
|
|
365
372
|
);
|
|
366
373
|
return [...matches];
|
|
367
374
|
}
|
|
368
|
-
async collectKeysMatchingPattern(pattern, maxMatches = false) {
|
|
369
|
-
const matches = /* @__PURE__ */ new Set();
|
|
370
|
-
if (this.options.tagIndex.forEachKeyMatchingPattern) {
|
|
371
|
-
await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
|
|
372
|
-
matches.add(key);
|
|
373
|
-
this.assertWithinMatchLimit(matches, maxMatches);
|
|
374
|
-
});
|
|
375
|
-
} else {
|
|
376
|
-
for (const key of await this.options.tagIndex.matchPattern(pattern)) {
|
|
377
|
-
matches.add(key);
|
|
378
|
-
this.assertWithinMatchLimit(matches, maxMatches);
|
|
379
|
-
}
|
|
375
|
+
async collectKeysMatchingPattern(pattern, maxMatches = false) {
|
|
376
|
+
const matches = /* @__PURE__ */ new Set();
|
|
377
|
+
if (this.options.tagIndex.forEachKeyMatchingPattern) {
|
|
378
|
+
await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
|
|
379
|
+
matches.add(key);
|
|
380
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
381
|
+
});
|
|
382
|
+
} else {
|
|
383
|
+
for (const key of await this.options.tagIndex.matchPattern(pattern)) {
|
|
384
|
+
matches.add(key);
|
|
385
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
await Promise.all(
|
|
389
|
+
this.options.layers.map(async (layer) => {
|
|
390
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
if (layer.forEachKey) {
|
|
395
|
+
await layer.forEachKey(async (key) => {
|
|
396
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
397
|
+
matches.add(key);
|
|
398
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const keys = await layer.keys?.();
|
|
404
|
+
for (const key of keys ?? []) {
|
|
405
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
406
|
+
matches.add(key);
|
|
407
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
} catch (error) {
|
|
411
|
+
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
);
|
|
415
|
+
return [...matches];
|
|
416
|
+
}
|
|
417
|
+
assertWithinMatchLimit(matches, maxMatches) {
|
|
418
|
+
if (maxMatches !== false && matches.size > maxMatches) {
|
|
419
|
+
throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// src/internal/CacheKeySerialization.ts
|
|
425
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
426
|
+
function normalizeForSerialization(value) {
|
|
427
|
+
if (Array.isArray(value)) {
|
|
428
|
+
return value.map((entry) => normalizeForSerialization(entry));
|
|
429
|
+
}
|
|
430
|
+
if (value && typeof value === "object") {
|
|
431
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
432
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
433
|
+
return normalized;
|
|
434
|
+
}
|
|
435
|
+
normalized[key] = normalizeForSerialization(value[key]);
|
|
436
|
+
return normalized;
|
|
437
|
+
}, {});
|
|
438
|
+
}
|
|
439
|
+
return value;
|
|
440
|
+
}
|
|
441
|
+
function serializeKeyPart(value) {
|
|
442
|
+
if (typeof value === "string") {
|
|
443
|
+
return `s:${value}`;
|
|
444
|
+
}
|
|
445
|
+
if (typeof value === "number") {
|
|
446
|
+
return `n:${value}`;
|
|
447
|
+
}
|
|
448
|
+
if (typeof value === "boolean") {
|
|
449
|
+
return `b:${value}`;
|
|
450
|
+
}
|
|
451
|
+
return `j:${JSON.stringify(normalizeForSerialization(value))}`;
|
|
452
|
+
}
|
|
453
|
+
function serializeOptions(options) {
|
|
454
|
+
return JSON.stringify(normalizeForSerialization(options) ?? null);
|
|
455
|
+
}
|
|
456
|
+
function createInstanceId() {
|
|
457
|
+
if (globalThis.crypto?.randomUUID) {
|
|
458
|
+
return globalThis.crypto.randomUUID();
|
|
459
|
+
}
|
|
460
|
+
const bytes = new Uint8Array(16);
|
|
461
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
462
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
463
|
+
} else {
|
|
464
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
465
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/internal/CacheStackGeneration.ts
|
|
472
|
+
var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
|
|
473
|
+
function generationPrefix(generation) {
|
|
474
|
+
return generation === void 0 ? "" : `v${generation}:`;
|
|
475
|
+
}
|
|
476
|
+
function qualifyGenerationKey(key, generation) {
|
|
477
|
+
const prefix = generationPrefix(generation);
|
|
478
|
+
return prefix ? `${prefix}${key}` : key;
|
|
479
|
+
}
|
|
480
|
+
function qualifyGenerationPattern(pattern, generation) {
|
|
481
|
+
return qualifyGenerationKey(pattern, generation);
|
|
482
|
+
}
|
|
483
|
+
function stripGenerationPrefix(key, generation) {
|
|
484
|
+
const prefix = generationPrefix(generation);
|
|
485
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
486
|
+
return key;
|
|
487
|
+
}
|
|
488
|
+
return key.slice(prefix.length);
|
|
489
|
+
}
|
|
490
|
+
function resolveGenerationCleanupTarget({
|
|
491
|
+
previousGeneration,
|
|
492
|
+
nextGeneration,
|
|
493
|
+
generationCleanup
|
|
494
|
+
}) {
|
|
495
|
+
if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
return previousGeneration;
|
|
499
|
+
}
|
|
500
|
+
function resolveGenerationCleanupBatchSize(generationCleanup) {
|
|
501
|
+
if (typeof generationCleanup !== "object" || generationCleanup === null) {
|
|
502
|
+
return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
503
|
+
}
|
|
504
|
+
return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
505
|
+
}
|
|
506
|
+
function planGenerationCleanupBatches(keys, generationCleanup) {
|
|
507
|
+
if (keys.length === 0) {
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
|
|
511
|
+
const batches = [];
|
|
512
|
+
for (let index = 0; index < keys.length; index += batchSize) {
|
|
513
|
+
batches.push(keys.slice(index, index + batchSize));
|
|
514
|
+
}
|
|
515
|
+
return batches;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/internal/CacheStackInvalidationSupport.ts
|
|
519
|
+
var CacheStackInvalidationSupport = class {
|
|
520
|
+
constructor(options) {
|
|
521
|
+
this.options = options;
|
|
522
|
+
}
|
|
523
|
+
options;
|
|
524
|
+
async collectKeysForTag(tag, maxKeys) {
|
|
525
|
+
const keys = /* @__PURE__ */ new Set();
|
|
526
|
+
if (this.options.tagIndex.forEachKeyForTag) {
|
|
527
|
+
await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
528
|
+
keys.add(key);
|
|
529
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
530
|
+
});
|
|
531
|
+
return [...keys];
|
|
532
|
+
}
|
|
533
|
+
for (const key of await this.options.tagIndex.keysForTag(tag)) {
|
|
534
|
+
keys.add(key);
|
|
535
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
536
|
+
}
|
|
537
|
+
return [...keys];
|
|
538
|
+
}
|
|
539
|
+
intersectKeys(groups) {
|
|
540
|
+
if (groups.length === 0) {
|
|
541
|
+
return [];
|
|
542
|
+
}
|
|
543
|
+
const [firstGroup, ...rest] = groups;
|
|
544
|
+
const restSets = rest.map((group) => new Set(group));
|
|
545
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
546
|
+
}
|
|
547
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
548
|
+
await Promise.all(
|
|
549
|
+
layers.map(async (layer) => {
|
|
550
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (layer.deleteMany) {
|
|
554
|
+
try {
|
|
555
|
+
await layer.deleteMany(keys);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
await Promise.all(
|
|
562
|
+
keys.map(async (key) => {
|
|
563
|
+
try {
|
|
564
|
+
await layer.delete(key);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
567
|
+
}
|
|
568
|
+
})
|
|
569
|
+
);
|
|
570
|
+
})
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
assertWithinInvalidationKeyLimit(size, maxKeys) {
|
|
574
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
575
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// src/internal/CacheStackLayerWriter.ts
|
|
581
|
+
var CacheStackLayerWriter = class {
|
|
582
|
+
constructor(options) {
|
|
583
|
+
this.options = options;
|
|
584
|
+
}
|
|
585
|
+
options;
|
|
586
|
+
async writeAcrossLayers(key, kind, value, writeOptions) {
|
|
587
|
+
const now = Date.now();
|
|
588
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
589
|
+
const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
|
|
590
|
+
const immediateOperations = [];
|
|
591
|
+
const deferredOperations = [];
|
|
592
|
+
for (const layer of this.options.layers) {
|
|
593
|
+
const operation = async () => {
|
|
594
|
+
if (this.options.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, writeOptions, now);
|
|
601
|
+
try {
|
|
602
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
603
|
+
} catch (error) {
|
|
604
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
608
|
+
deferredOperations.push(operation);
|
|
609
|
+
} else {
|
|
610
|
+
immediateOperations.push(operation);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
614
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
615
|
+
}
|
|
616
|
+
async writeBatch(entries) {
|
|
617
|
+
const now = Date.now();
|
|
618
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
619
|
+
const entryEpochs = new Map(
|
|
620
|
+
entries.map((entry) => [entry.key, this.options.maintenance.currentKeyEpoch(entry.key)])
|
|
621
|
+
);
|
|
622
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
623
|
+
const immediateOperations = [];
|
|
624
|
+
const deferredOperations = [];
|
|
625
|
+
for (const entry of entries) {
|
|
626
|
+
for (const layer of this.options.layers) {
|
|
627
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
631
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
632
|
+
bucket.push(layerEntry);
|
|
633
|
+
entriesByLayer.set(layer, bucket);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
637
|
+
const operation = async () => {
|
|
638
|
+
if (clearEpoch !== this.options.maintenance.currentClearEpoch()) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const activeEntries = layerEntries.filter(
|
|
642
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.options.maintenance.currentKeyEpoch(entry.key)
|
|
643
|
+
);
|
|
644
|
+
if (activeEntries.length === 0) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
if (layer.setMany) {
|
|
649
|
+
await layer.setMany(activeEntries);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
653
|
+
} catch (error) {
|
|
654
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
658
|
+
deferredOperations.push(operation);
|
|
659
|
+
} else {
|
|
660
|
+
immediateOperations.push(operation);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
664
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
665
|
+
return { clearEpoch, entryEpochs };
|
|
666
|
+
}
|
|
667
|
+
async executeLayerOperations(operations, context) {
|
|
668
|
+
if (this.options.writePolicy !== "best-effort") {
|
|
669
|
+
await Promise.all(operations.map((operation) => operation()));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
673
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
674
|
+
if (failures.length === 0) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
this.options.onWriteFailures(
|
|
678
|
+
context,
|
|
679
|
+
failures.map((failure) => failure.reason)
|
|
680
|
+
);
|
|
681
|
+
if (failures.length === operations.length) {
|
|
682
|
+
throw new AggregateError(
|
|
683
|
+
failures.map((failure) => failure.reason),
|
|
684
|
+
`${context.action} failed for every cache layer`
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
|
|
689
|
+
const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
|
|
690
|
+
const staleWhileRevalidate = this.options.resolveLayerSeconds(
|
|
691
|
+
layer.name,
|
|
692
|
+
writeOptions?.staleWhileRevalidate,
|
|
693
|
+
this.options.globalStaleWhileRevalidate
|
|
694
|
+
);
|
|
695
|
+
const staleIfError = this.options.resolveLayerSeconds(
|
|
696
|
+
layer.name,
|
|
697
|
+
writeOptions?.staleIfError,
|
|
698
|
+
this.options.globalStaleIfError
|
|
699
|
+
);
|
|
700
|
+
const payload = createStoredValueEnvelope({
|
|
701
|
+
kind,
|
|
702
|
+
value,
|
|
703
|
+
freshTtlSeconds: freshTtl,
|
|
704
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
705
|
+
staleIfErrorSeconds: staleIfError,
|
|
706
|
+
now
|
|
707
|
+
});
|
|
708
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
709
|
+
return {
|
|
710
|
+
key,
|
|
711
|
+
value: payload,
|
|
712
|
+
ttl
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
// src/internal/CacheStackMaintenance.ts
|
|
718
|
+
var CacheStackMaintenance = class {
|
|
719
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
720
|
+
writeBehindQueue = [];
|
|
721
|
+
writeBehindTimer;
|
|
722
|
+
writeBehindFlushPromise;
|
|
723
|
+
generationCleanupPromise;
|
|
724
|
+
clearEpoch = 0;
|
|
725
|
+
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
726
|
+
if (writeStrategy !== "write-behind") {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
730
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
this.disposeWriteBehindTimer();
|
|
734
|
+
this.writeBehindTimer = setInterval(() => {
|
|
735
|
+
void flush();
|
|
736
|
+
}, flushIntervalMs);
|
|
737
|
+
this.writeBehindTimer.unref?.();
|
|
738
|
+
}
|
|
739
|
+
disposeWriteBehindTimer() {
|
|
740
|
+
if (!this.writeBehindTimer) {
|
|
741
|
+
return;
|
|
380
742
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
try {
|
|
387
|
-
if (layer.forEachKey) {
|
|
388
|
-
await layer.forEachKey(async (key) => {
|
|
389
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
390
|
-
matches.add(key);
|
|
391
|
-
this.assertWithinMatchLimit(matches, maxMatches);
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
const keys = await layer.keys?.();
|
|
397
|
-
for (const key of keys ?? []) {
|
|
398
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
399
|
-
matches.add(key);
|
|
400
|
-
this.assertWithinMatchLimit(matches, maxMatches);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
} catch (error) {
|
|
404
|
-
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
405
|
-
}
|
|
406
|
-
})
|
|
407
|
-
);
|
|
408
|
-
return [...matches];
|
|
743
|
+
clearInterval(this.writeBehindTimer);
|
|
744
|
+
this.writeBehindTimer = void 0;
|
|
409
745
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
746
|
+
beginClearEpoch() {
|
|
747
|
+
this.clearEpoch += 1;
|
|
748
|
+
this.keyEpochs.clear();
|
|
749
|
+
this.writeBehindQueue.length = 0;
|
|
750
|
+
}
|
|
751
|
+
currentClearEpoch() {
|
|
752
|
+
return this.clearEpoch;
|
|
753
|
+
}
|
|
754
|
+
currentKeyEpoch(key) {
|
|
755
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
756
|
+
}
|
|
757
|
+
bumpKeyEpochs(keys) {
|
|
758
|
+
for (const key of keys) {
|
|
759
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
413
760
|
}
|
|
414
761
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
762
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
763
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
return false;
|
|
422
770
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
return
|
|
430
|
-
}
|
|
771
|
+
async enqueueWriteBehind(operation, options, flushBatch) {
|
|
772
|
+
this.writeBehindQueue.push(operation);
|
|
773
|
+
const batchSize = options?.batchSize ?? 100;
|
|
774
|
+
const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
|
|
775
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
776
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
780
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
781
|
+
}
|
|
431
782
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
783
|
+
async flushWriteBehindQueue(options, flushBatch) {
|
|
784
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
785
|
+
await this.writeBehindFlushPromise;
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
const batchSize = options?.batchSize ?? 100;
|
|
789
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
790
|
+
this.writeBehindFlushPromise = flushBatch(batch);
|
|
791
|
+
try {
|
|
792
|
+
await this.writeBehindFlushPromise;
|
|
793
|
+
} finally {
|
|
794
|
+
this.writeBehindFlushPromise = void 0;
|
|
795
|
+
}
|
|
796
|
+
if (this.writeBehindQueue.length > 0) {
|
|
797
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
798
|
+
}
|
|
437
799
|
}
|
|
438
|
-
|
|
439
|
-
|
|
800
|
+
scheduleGenerationCleanup(generation, task, onError) {
|
|
801
|
+
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
802
|
+
onError(generation, error);
|
|
803
|
+
});
|
|
804
|
+
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
805
|
+
if (this.generationCleanupPromise === scheduledTask) {
|
|
806
|
+
this.generationCleanupPromise = void 0;
|
|
807
|
+
}
|
|
808
|
+
});
|
|
440
809
|
}
|
|
441
|
-
|
|
442
|
-
|
|
810
|
+
async waitForGenerationCleanup() {
|
|
811
|
+
await this.generationCleanupPromise;
|
|
443
812
|
}
|
|
444
|
-
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// src/internal/CacheStackRuntimePolicy.ts
|
|
816
|
+
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
817
|
+
return degradedUntil !== void 0 && degradedUntil > now;
|
|
445
818
|
}
|
|
446
|
-
function
|
|
447
|
-
|
|
819
|
+
function shouldStartBackgroundRefresh({
|
|
820
|
+
isDisconnecting,
|
|
821
|
+
hasRefreshInFlight
|
|
822
|
+
}) {
|
|
823
|
+
return !isDisconnecting && !hasRefreshInFlight;
|
|
448
824
|
}
|
|
449
|
-
function
|
|
450
|
-
if (
|
|
451
|
-
return
|
|
452
|
-
}
|
|
453
|
-
const bytes = new Uint8Array(16);
|
|
454
|
-
if (globalThis.crypto?.getRandomValues) {
|
|
455
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
456
|
-
} else {
|
|
457
|
-
for (let i = 0; i < bytes.length; i += 1) {
|
|
458
|
-
bytes[i] = Math.floor(Math.random() * 256);
|
|
459
|
-
}
|
|
825
|
+
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
826
|
+
if (!gracefulDegradation) {
|
|
827
|
+
return { degrade: false };
|
|
460
828
|
}
|
|
461
|
-
|
|
829
|
+
const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
830
|
+
return {
|
|
831
|
+
degrade: true,
|
|
832
|
+
degradedUntil: now + retryAfterMs
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function planFreshReadPolicies({
|
|
836
|
+
stored,
|
|
837
|
+
hasFetcher,
|
|
838
|
+
slidingTtl,
|
|
839
|
+
refreshAheadSeconds
|
|
840
|
+
}) {
|
|
841
|
+
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
842
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
843
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
844
|
+
return {
|
|
845
|
+
refreshedStored,
|
|
846
|
+
refreshedStoredTtl,
|
|
847
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
848
|
+
};
|
|
462
849
|
}
|
|
463
850
|
|
|
851
|
+
// src/internal/CacheStackSnapshotManager.ts
|
|
852
|
+
import { constants, promises as fs } from "fs";
|
|
853
|
+
import path from "path";
|
|
854
|
+
|
|
464
855
|
// src/internal/CacheSnapshotFile.ts
|
|
465
|
-
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator,
|
|
466
|
-
const relative =
|
|
467
|
-
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) ||
|
|
856
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
|
|
857
|
+
const relative = path2.relative(realBaseDir, candidatePath);
|
|
858
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
|
|
468
859
|
}
|
|
469
|
-
async function findExistingAncestor(directory,
|
|
860
|
+
async function findExistingAncestor(directory, fs3, path2) {
|
|
470
861
|
let current = directory;
|
|
471
862
|
while (true) {
|
|
472
863
|
try {
|
|
473
|
-
await
|
|
864
|
+
await fs3.lstat(current);
|
|
474
865
|
return current;
|
|
475
866
|
} catch (error) {
|
|
476
867
|
if (error.code !== "ENOENT") {
|
|
477
868
|
throw error;
|
|
478
869
|
}
|
|
479
870
|
}
|
|
480
|
-
const parent =
|
|
871
|
+
const parent = path2.dirname(current);
|
|
481
872
|
if (parent === current) {
|
|
482
873
|
return current;
|
|
483
874
|
}
|
|
@@ -491,39 +882,39 @@ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = p
|
|
|
491
882
|
if (filePath.includes("\0")) {
|
|
492
883
|
throw new Error("filePath must not contain null bytes.");
|
|
493
884
|
}
|
|
494
|
-
const { promises:
|
|
495
|
-
const
|
|
496
|
-
const resolved =
|
|
497
|
-
const baseDir = snapshotBaseDir === false ? false :
|
|
885
|
+
const { promises: fs3 } = await import("fs");
|
|
886
|
+
const path2 = await import("path");
|
|
887
|
+
const resolved = path2.resolve(filePath);
|
|
888
|
+
const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
|
|
498
889
|
if (baseDir === false) {
|
|
499
890
|
return resolved;
|
|
500
891
|
}
|
|
501
|
-
await
|
|
502
|
-
const realBaseDir = await
|
|
503
|
-
if (!isWithinSnapshotBase(realBaseDir, resolved,
|
|
892
|
+
await fs3.mkdir(baseDir, { recursive: true });
|
|
893
|
+
const realBaseDir = await fs3.realpath(baseDir);
|
|
894
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
|
|
504
895
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
505
896
|
}
|
|
506
897
|
if (mode === "read") {
|
|
507
|
-
const realTarget = await
|
|
508
|
-
if (!isWithinSnapshotBase(realBaseDir, realTarget,
|
|
898
|
+
const realTarget = await fs3.realpath(resolved);
|
|
899
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
|
|
509
900
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
510
901
|
}
|
|
511
902
|
return realTarget;
|
|
512
903
|
}
|
|
513
|
-
const parentDir =
|
|
514
|
-
const existingAncestor = await findExistingAncestor(parentDir,
|
|
515
|
-
const realExistingAncestor = await
|
|
516
|
-
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor,
|
|
904
|
+
const parentDir = path2.dirname(resolved);
|
|
905
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs3, path2);
|
|
906
|
+
const realExistingAncestor = await fs3.realpath(existingAncestor);
|
|
907
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
|
|
517
908
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
518
909
|
}
|
|
519
|
-
await
|
|
520
|
-
const realParentDir = await
|
|
521
|
-
if (!isWithinSnapshotBase(realBaseDir, realParentDir,
|
|
910
|
+
await fs3.mkdir(parentDir, { recursive: true });
|
|
911
|
+
const realParentDir = await fs3.realpath(parentDir);
|
|
912
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
|
|
522
913
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
523
914
|
}
|
|
524
|
-
const targetPath =
|
|
915
|
+
const targetPath = path2.join(realParentDir, path2.basename(resolved));
|
|
525
916
|
try {
|
|
526
|
-
const existing = await
|
|
917
|
+
const existing = await fs3.lstat(targetPath);
|
|
527
918
|
if (existing.isSymbolicLink()) {
|
|
528
919
|
throw new Error("filePath must not point to a symbolic link.");
|
|
529
920
|
}
|
|
@@ -557,6 +948,144 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
|
557
948
|
return Buffer.concat(chunks).toString("utf8");
|
|
558
949
|
}
|
|
559
950
|
|
|
951
|
+
// src/internal/CacheStackSnapshotManager.ts
|
|
952
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
953
|
+
var CacheStackSnapshotManager = class {
|
|
954
|
+
constructor(options) {
|
|
955
|
+
this.options = options;
|
|
956
|
+
}
|
|
957
|
+
options;
|
|
958
|
+
async exportState(maxEntries) {
|
|
959
|
+
const entries = [];
|
|
960
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
961
|
+
entries.push(entry);
|
|
962
|
+
});
|
|
963
|
+
return entries;
|
|
964
|
+
}
|
|
965
|
+
async importState(entries) {
|
|
966
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
967
|
+
key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
|
|
968
|
+
value: entry.value,
|
|
969
|
+
ttl: entry.ttl
|
|
970
|
+
}));
|
|
971
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
972
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
973
|
+
await Promise.all(
|
|
974
|
+
batch.map(async (entry) => {
|
|
975
|
+
await Promise.all(this.options.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
976
|
+
await this.options.tagIndex.touch(entry.key);
|
|
977
|
+
})
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
async persistToFile(filePath, snapshotBaseDir, maxEntries) {
|
|
982
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
983
|
+
const tempPath = path.join(
|
|
984
|
+
path.dirname(targetPath),
|
|
985
|
+
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
986
|
+
);
|
|
987
|
+
let handle;
|
|
988
|
+
try {
|
|
989
|
+
handle = await fs.open(tempPath, "wx");
|
|
990
|
+
const openedHandle = handle;
|
|
991
|
+
await openedHandle.writeFile("[", "utf8");
|
|
992
|
+
let wroteAny = false;
|
|
993
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
994
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
995
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
996
|
+
wroteAny = true;
|
|
997
|
+
});
|
|
998
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
999
|
+
await openedHandle.close();
|
|
1000
|
+
handle = void 0;
|
|
1001
|
+
await fs.rename(tempPath, targetPath);
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
await handle?.close().catch(() => void 0);
|
|
1004
|
+
await fs.unlink(tempPath).catch(() => void 0);
|
|
1005
|
+
throw error;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
|
|
1009
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
|
|
1010
|
+
const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
1011
|
+
let raw;
|
|
1012
|
+
try {
|
|
1013
|
+
if (maxBytes !== false) {
|
|
1014
|
+
const stat = await handle.stat();
|
|
1015
|
+
if (stat.size > maxBytes) {
|
|
1016
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
raw = await readUtf8HandleWithLimit(handle, maxBytes);
|
|
1020
|
+
} finally {
|
|
1021
|
+
await handle.close();
|
|
1022
|
+
}
|
|
1023
|
+
let parsed;
|
|
1024
|
+
try {
|
|
1025
|
+
parsed = JSON.parse(raw);
|
|
1026
|
+
} catch (cause) {
|
|
1027
|
+
throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
|
|
1028
|
+
}
|
|
1029
|
+
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1030
|
+
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1031
|
+
}
|
|
1032
|
+
await this.importState(
|
|
1033
|
+
parsed.map((entry) => ({
|
|
1034
|
+
key: entry.key,
|
|
1035
|
+
value: this.sanitizeSnapshotValue(entry.value),
|
|
1036
|
+
ttl: entry.ttl
|
|
1037
|
+
}))
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
1041
|
+
const exported = /* @__PURE__ */ new Set();
|
|
1042
|
+
for (const layer of this.options.layers) {
|
|
1043
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
const visitKey = async (key) => {
|
|
1047
|
+
const exportedKey = this.options.stripQualifiedKey(key);
|
|
1048
|
+
if (exported.has(exportedKey)) {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const stored = await this.options.readLayerEntry(layer, key);
|
|
1052
|
+
if (stored === null) {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
exported.add(exportedKey);
|
|
1056
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
1057
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
1058
|
+
}
|
|
1059
|
+
await visitor({
|
|
1060
|
+
key: exportedKey,
|
|
1061
|
+
value: stored,
|
|
1062
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
1063
|
+
});
|
|
1064
|
+
};
|
|
1065
|
+
if (layer.forEachKey) {
|
|
1066
|
+
await layer.forEachKey(visitKey);
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const keys = await layer.keys?.();
|
|
1070
|
+
for (const key of keys ?? []) {
|
|
1071
|
+
await visitKey(key);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
isCacheSnapshotEntries(value) {
|
|
1076
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1077
|
+
if (!entry || typeof entry !== "object") {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
const candidate = entry;
|
|
1081
|
+
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
sanitizeSnapshotValue(value) {
|
|
1085
|
+
return this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
|
|
560
1089
|
// src/internal/CacheStackValidation.ts
|
|
561
1090
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
562
1091
|
var MAX_PATTERN_LENGTH = 1024;
|
|
@@ -710,7 +1239,6 @@ var CircuitBreakerManager = class {
|
|
|
710
1239
|
if (!options) {
|
|
711
1240
|
return;
|
|
712
1241
|
}
|
|
713
|
-
this.pruneIfNeeded();
|
|
714
1242
|
const failureThreshold = options.failureThreshold ?? 3;
|
|
715
1243
|
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
716
1244
|
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
|
|
@@ -719,6 +1247,7 @@ var CircuitBreakerManager = class {
|
|
|
719
1247
|
state.openUntil = Date.now() + cooldownMs;
|
|
720
1248
|
}
|
|
721
1249
|
this.breakers.set(key, state);
|
|
1250
|
+
this.pruneIfNeeded();
|
|
722
1251
|
}
|
|
723
1252
|
recordSuccess(key) {
|
|
724
1253
|
this.breakers.delete(key);
|
|
@@ -784,7 +1313,11 @@ var FetchRateLimiter = class {
|
|
|
784
1313
|
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
785
1314
|
nextFetcherBucketId = 0;
|
|
786
1315
|
drainTimer;
|
|
1316
|
+
isDisposed = false;
|
|
787
1317
|
async schedule(options, context, task) {
|
|
1318
|
+
if (this.isDisposed) {
|
|
1319
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1320
|
+
}
|
|
788
1321
|
if (!options) {
|
|
789
1322
|
return task();
|
|
790
1323
|
}
|
|
@@ -807,6 +1340,27 @@ var FetchRateLimiter = class {
|
|
|
807
1340
|
this.drain();
|
|
808
1341
|
});
|
|
809
1342
|
}
|
|
1343
|
+
dispose() {
|
|
1344
|
+
this.isDisposed = true;
|
|
1345
|
+
if (this.drainTimer) {
|
|
1346
|
+
clearTimeout(this.drainTimer);
|
|
1347
|
+
this.drainTimer = void 0;
|
|
1348
|
+
}
|
|
1349
|
+
for (const bucket of this.buckets.values()) {
|
|
1350
|
+
if (bucket.cleanupTimer) {
|
|
1351
|
+
clearTimeout(bucket.cleanupTimer);
|
|
1352
|
+
bucket.cleanupTimer = void 0;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
for (const queue of this.queuesByBucket.values()) {
|
|
1356
|
+
for (const item of queue) {
|
|
1357
|
+
item.reject(new Error("FetchRateLimiter has been disposed."));
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
this.queuesByBucket.clear();
|
|
1361
|
+
this.pendingBuckets.clear();
|
|
1362
|
+
this.buckets.clear();
|
|
1363
|
+
}
|
|
810
1364
|
normalize(options) {
|
|
811
1365
|
const maxConcurrent = options.maxConcurrent;
|
|
812
1366
|
const intervalMs = options.intervalMs;
|
|
@@ -842,6 +1396,9 @@ var FetchRateLimiter = class {
|
|
|
842
1396
|
return "global";
|
|
843
1397
|
}
|
|
844
1398
|
drain() {
|
|
1399
|
+
if (this.isDisposed) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
845
1402
|
if (this.drainTimer) {
|
|
846
1403
|
clearTimeout(this.drainTimer);
|
|
847
1404
|
this.drainTimer = void 0;
|
|
@@ -938,6 +1495,9 @@ var FetchRateLimiter = class {
|
|
|
938
1495
|
}
|
|
939
1496
|
}
|
|
940
1497
|
bucketState(bucketKey) {
|
|
1498
|
+
if (this.isDisposed) {
|
|
1499
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1500
|
+
}
|
|
941
1501
|
const existing = this.buckets.get(bucketKey);
|
|
942
1502
|
if (existing) {
|
|
943
1503
|
return existing;
|
|
@@ -1173,39 +1733,31 @@ var TtlResolver = class {
|
|
|
1173
1733
|
}
|
|
1174
1734
|
};
|
|
1175
1735
|
|
|
1176
|
-
// src/
|
|
1177
|
-
var
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
}
|
|
1183
|
-
deserialize(payload) {
|
|
1184
|
-
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1185
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1186
|
-
}
|
|
1187
|
-
};
|
|
1188
|
-
var MAX_SANITIZE_DEPTH = 200;
|
|
1189
|
-
function sanitizeJsonValue(value, depth, state) {
|
|
1736
|
+
// src/internal/StructuredDataSanitizer.ts
|
|
1737
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1738
|
+
function sanitizeStructuredData(value, options) {
|
|
1739
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
1740
|
+
}
|
|
1741
|
+
function sanitizeValue(value, depth, state, options) {
|
|
1190
1742
|
state.count += 1;
|
|
1191
|
-
if (state.count >
|
|
1192
|
-
throw new Error(
|
|
1743
|
+
if (state.count > options.maxNodes) {
|
|
1744
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
1193
1745
|
}
|
|
1194
|
-
if (depth >
|
|
1195
|
-
throw new Error(
|
|
1746
|
+
if (depth > options.maxDepth) {
|
|
1747
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
1196
1748
|
}
|
|
1197
1749
|
if (Array.isArray(value)) {
|
|
1198
|
-
return value.map((entry) =>
|
|
1750
|
+
return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
|
|
1199
1751
|
}
|
|
1200
1752
|
if (!isPlainObject(value)) {
|
|
1201
1753
|
return value;
|
|
1202
1754
|
}
|
|
1203
|
-
const sanitized = {};
|
|
1755
|
+
const sanitized = options.createObject?.() ?? {};
|
|
1204
1756
|
for (const [key, entry] of Object.entries(value)) {
|
|
1205
|
-
if (
|
|
1757
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
1206
1758
|
continue;
|
|
1207
1759
|
}
|
|
1208
|
-
sanitized[key] =
|
|
1760
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
1209
1761
|
}
|
|
1210
1762
|
return sanitized;
|
|
1211
1763
|
}
|
|
@@ -1213,6 +1765,21 @@ function isPlainObject(value) {
|
|
|
1213
1765
|
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1214
1766
|
}
|
|
1215
1767
|
|
|
1768
|
+
// src/serialization/JsonSerializer.ts
|
|
1769
|
+
var JsonSerializer = class {
|
|
1770
|
+
serialize(value) {
|
|
1771
|
+
return JSON.stringify(value);
|
|
1772
|
+
}
|
|
1773
|
+
deserialize(payload) {
|
|
1774
|
+
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1775
|
+
return sanitizeStructuredData(JSON.parse(normalized), {
|
|
1776
|
+
label: "JSON payload",
|
|
1777
|
+
maxDepth: 200,
|
|
1778
|
+
maxNodes: 1e4
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1216
1783
|
// src/stampede/StampedeGuard.ts
|
|
1217
1784
|
import { Mutex as Mutex2 } from "async-mutex";
|
|
1218
1785
|
var StampedeGuard = class {
|
|
@@ -1257,7 +1824,6 @@ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
|
1257
1824
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1258
1825
|
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1259
1826
|
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1260
|
-
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1261
1827
|
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
1262
1828
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1263
1829
|
var DebugLogger = class {
|
|
@@ -1314,6 +1880,35 @@ var CacheStack = class extends EventEmitter {
|
|
|
1314
1880
|
await this.handleLayerFailure(layer, operation, error);
|
|
1315
1881
|
}
|
|
1316
1882
|
});
|
|
1883
|
+
this.invalidation = new CacheStackInvalidationSupport({
|
|
1884
|
+
tagIndex: this.tagIndex,
|
|
1885
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1886
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
1887
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
this.layerWriter = new CacheStackLayerWriter({
|
|
1891
|
+
layers: this.layers,
|
|
1892
|
+
maintenance: this.maintenance,
|
|
1893
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1894
|
+
shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
|
|
1895
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
1896
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
1897
|
+
},
|
|
1898
|
+
enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
|
|
1899
|
+
resolveFreshTtl: this.resolveFreshTtl.bind(this),
|
|
1900
|
+
resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
|
|
1901
|
+
globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
|
|
1902
|
+
globalStaleIfError: this.options.staleIfError,
|
|
1903
|
+
writePolicy: this.options.writePolicy,
|
|
1904
|
+
onWriteFailures: (context, failures) => {
|
|
1905
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
1906
|
+
this.logger.debug?.("write-failure", {
|
|
1907
|
+
...context,
|
|
1908
|
+
failures: failures.map((failure) => this.formatError(failure))
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1317
1912
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
1318
1913
|
this.logger.warn?.(
|
|
1319
1914
|
"Using the default in-memory TagIndex with a shared cache layer only tracks keys seen by this process. Use RedisTagIndex for cross-instance tag invalidation."
|
|
@@ -1329,6 +1924,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
1329
1924
|
"broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
|
|
1330
1925
|
);
|
|
1331
1926
|
}
|
|
1927
|
+
this.snapshots = new CacheStackSnapshotManager({
|
|
1928
|
+
layers: this.layers,
|
|
1929
|
+
tagIndex: this.tagIndex,
|
|
1930
|
+
snapshotSerializer: this.snapshotSerializer,
|
|
1931
|
+
readLayerEntry: this.readLayerEntry.bind(this),
|
|
1932
|
+
qualifyKey: this.qualifyKey.bind(this),
|
|
1933
|
+
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
1934
|
+
validateCacheKey,
|
|
1935
|
+
formatError: this.formatError.bind(this)
|
|
1936
|
+
});
|
|
1332
1937
|
this.initializeWriteBehind(options.writeBehind);
|
|
1333
1938
|
this.startup = this.initialize();
|
|
1334
1939
|
}
|
|
@@ -1344,17 +1949,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
1344
1949
|
keyDiscovery;
|
|
1345
1950
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1346
1951
|
snapshotSerializer = new JsonSerializer();
|
|
1952
|
+
invalidation;
|
|
1953
|
+
layerWriter;
|
|
1954
|
+
snapshots;
|
|
1347
1955
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1348
1956
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1349
|
-
|
|
1957
|
+
maintenance = new CacheStackMaintenance();
|
|
1350
1958
|
ttlResolver;
|
|
1351
1959
|
circuitBreakerManager;
|
|
1960
|
+
nextOperationId = 0;
|
|
1352
1961
|
currentGeneration;
|
|
1353
|
-
writeBehindQueue = [];
|
|
1354
|
-
writeBehindTimer;
|
|
1355
|
-
writeBehindFlushPromise;
|
|
1356
|
-
generationCleanupPromise;
|
|
1357
|
-
clearEpoch = 0;
|
|
1358
1962
|
isDisconnecting = false;
|
|
1359
1963
|
disconnectPromise;
|
|
1360
1964
|
/**
|
|
@@ -1364,10 +1968,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
1364
1968
|
* and no `fetcher` is provided.
|
|
1365
1969
|
*/
|
|
1366
1970
|
async get(key, fetcher, options) {
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1971
|
+
return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
|
|
1972
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1973
|
+
this.validateWriteOptions(options);
|
|
1974
|
+
await this.awaitStartup("get");
|
|
1975
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
1976
|
+
});
|
|
1371
1977
|
}
|
|
1372
1978
|
async getPrepared(normalizedKey, fetcher, options) {
|
|
1373
1979
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
@@ -1489,28 +2095,32 @@ var CacheStack = class extends EventEmitter {
|
|
|
1489
2095
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1490
2096
|
*/
|
|
1491
2097
|
async set(key, value, options) {
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
2098
|
+
await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
|
|
2099
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2100
|
+
this.validateWriteOptions(options);
|
|
2101
|
+
await this.awaitStartup("set");
|
|
2102
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
2103
|
+
});
|
|
1496
2104
|
}
|
|
1497
2105
|
/**
|
|
1498
2106
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1499
2107
|
*/
|
|
1500
2108
|
async delete(key) {
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
2109
|
+
await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
|
|
2110
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2111
|
+
await this.awaitStartup("delete");
|
|
2112
|
+
await this.deleteKeys([normalizedKey]);
|
|
2113
|
+
await this.publishInvalidation({
|
|
2114
|
+
scope: "key",
|
|
2115
|
+
keys: [normalizedKey],
|
|
2116
|
+
sourceId: this.instanceId,
|
|
2117
|
+
operation: "delete"
|
|
2118
|
+
});
|
|
1509
2119
|
});
|
|
1510
2120
|
}
|
|
1511
2121
|
async clear() {
|
|
1512
2122
|
await this.awaitStartup("clear");
|
|
1513
|
-
this.beginClearEpoch();
|
|
2123
|
+
this.maintenance.beginClearEpoch();
|
|
1514
2124
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
1515
2125
|
await this.tagIndex.clear();
|
|
1516
2126
|
this.ttlResolver.clearProfiles();
|
|
@@ -1538,95 +2148,99 @@ var CacheStack = class extends EventEmitter {
|
|
|
1538
2148
|
});
|
|
1539
2149
|
}
|
|
1540
2150
|
async mget(entries) {
|
|
1541
|
-
this.
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
2151
|
+
return this.observeOperation("layercache.mget", void 0, async () => {
|
|
2152
|
+
this.assertActive("mget");
|
|
2153
|
+
if (entries.length === 0) {
|
|
2154
|
+
return [];
|
|
2155
|
+
}
|
|
2156
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2157
|
+
...entry,
|
|
2158
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2159
|
+
}));
|
|
2160
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2161
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
2162
|
+
if (!canFastPath) {
|
|
2163
|
+
await this.awaitStartup("mget");
|
|
2164
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
2165
|
+
return Promise.all(
|
|
2166
|
+
normalizedEntries.map((entry) => {
|
|
2167
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
2168
|
+
const existing = pendingReads.get(entry.key);
|
|
2169
|
+
if (!existing) {
|
|
2170
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2171
|
+
pendingReads.set(entry.key, {
|
|
2172
|
+
promise,
|
|
2173
|
+
fetch: entry.fetch,
|
|
2174
|
+
optionsSignature
|
|
2175
|
+
});
|
|
2176
|
+
return promise;
|
|
2177
|
+
}
|
|
2178
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
2179
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
2180
|
+
}
|
|
2181
|
+
return existing.promise;
|
|
2182
|
+
})
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
1552
2185
|
await this.awaitStartup("mget");
|
|
1553
|
-
const
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
});
|
|
1565
|
-
return promise;
|
|
1566
|
-
}
|
|
1567
|
-
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
1568
|
-
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
1569
|
-
}
|
|
1570
|
-
return existing.promise;
|
|
1571
|
-
})
|
|
1572
|
-
);
|
|
1573
|
-
}
|
|
1574
|
-
await this.awaitStartup("mget");
|
|
1575
|
-
const pending = /* @__PURE__ */ new Set();
|
|
1576
|
-
const indexesByKey = /* @__PURE__ */ new Map();
|
|
1577
|
-
const resultsByKey = /* @__PURE__ */ new Map();
|
|
1578
|
-
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
1579
|
-
const entry = normalizedEntries[index];
|
|
1580
|
-
if (!entry) continue;
|
|
1581
|
-
const key = entry.key;
|
|
1582
|
-
const indexes = indexesByKey.get(key) ?? [];
|
|
1583
|
-
indexes.push(index);
|
|
1584
|
-
indexesByKey.set(key, indexes);
|
|
1585
|
-
pending.add(key);
|
|
1586
|
-
}
|
|
1587
|
-
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
1588
|
-
const layer = this.layers[layerIndex];
|
|
1589
|
-
if (!layer) continue;
|
|
1590
|
-
const keys = [...pending];
|
|
1591
|
-
if (keys.length === 0) {
|
|
1592
|
-
break;
|
|
2186
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2187
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
2188
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
2189
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2190
|
+
const entry = normalizedEntries[index];
|
|
2191
|
+
if (!entry) continue;
|
|
2192
|
+
const key = entry.key;
|
|
2193
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
2194
|
+
indexes.push(index);
|
|
2195
|
+
indexesByKey.set(key, indexes);
|
|
2196
|
+
pending.add(key);
|
|
1593
2197
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
const
|
|
1598
|
-
if (
|
|
1599
|
-
|
|
2198
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2199
|
+
const layer = this.layers[layerIndex];
|
|
2200
|
+
if (!layer) continue;
|
|
2201
|
+
const keys = [...pending];
|
|
2202
|
+
if (keys.length === 0) {
|
|
2203
|
+
break;
|
|
1600
2204
|
}
|
|
1601
|
-
const
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
2205
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2206
|
+
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2207
|
+
const key = keys[offset];
|
|
2208
|
+
const stored = values[offset];
|
|
2209
|
+
if (!key || stored === null) {
|
|
2210
|
+
continue;
|
|
2211
|
+
}
|
|
2212
|
+
const resolved = resolveStoredValue(stored);
|
|
2213
|
+
if (resolved.state === "expired") {
|
|
2214
|
+
await layer.delete(key);
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
await this.tagIndex.touch(key);
|
|
2218
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
2219
|
+
resultsByKey.set(key, resolved.value);
|
|
2220
|
+
pending.delete(key);
|
|
2221
|
+
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
1605
2222
|
}
|
|
1606
|
-
await this.tagIndex.touch(key);
|
|
1607
|
-
await this.backfill(key, stored, layerIndex - 1);
|
|
1608
|
-
resultsByKey.set(key, resolved.value);
|
|
1609
|
-
pending.delete(key);
|
|
1610
|
-
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
1611
2223
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
2224
|
+
if (pending.size > 0) {
|
|
2225
|
+
for (const key of pending) {
|
|
2226
|
+
await this.tagIndex.remove(key);
|
|
2227
|
+
this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
|
|
2228
|
+
}
|
|
1617
2229
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
2230
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
2231
|
+
});
|
|
1620
2232
|
}
|
|
1621
2233
|
async mset(entries) {
|
|
1622
|
-
this.
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
2234
|
+
await this.observeOperation("layercache.mset", void 0, async () => {
|
|
2235
|
+
this.assertActive("mset");
|
|
2236
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2237
|
+
...entry,
|
|
2238
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2239
|
+
}));
|
|
2240
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2241
|
+
await this.awaitStartup("mset");
|
|
2242
|
+
await this.writeBatch(normalizedEntries);
|
|
2243
|
+
});
|
|
1630
2244
|
}
|
|
1631
2245
|
async warm(entries, options = {}) {
|
|
1632
2246
|
this.assertActive("warm");
|
|
@@ -1679,40 +2293,50 @@ var CacheStack = class extends EventEmitter {
|
|
|
1679
2293
|
return new CacheNamespace(this, prefix);
|
|
1680
2294
|
}
|
|
1681
2295
|
async invalidateByTag(tag) {
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
2296
|
+
await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
|
|
2297
|
+
validateTag(tag);
|
|
2298
|
+
await this.awaitStartup("invalidateByTag");
|
|
2299
|
+
const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
|
|
2300
|
+
await this.deleteKeys(keys);
|
|
2301
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2302
|
+
});
|
|
1687
2303
|
}
|
|
1688
2304
|
async invalidateByTags(tags, mode = "any") {
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
2305
|
+
await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
|
|
2306
|
+
if (tags.length === 0) {
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
validateTags(tags);
|
|
2310
|
+
await this.awaitStartup("invalidateByTags");
|
|
2311
|
+
const keysByTag = await Promise.all(
|
|
2312
|
+
tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
|
|
2313
|
+
);
|
|
2314
|
+
const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2315
|
+
this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
|
|
2316
|
+
await this.deleteKeys(keys);
|
|
2317
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2318
|
+
});
|
|
1699
2319
|
}
|
|
1700
2320
|
async invalidateByPattern(pattern) {
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
this.
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
2321
|
+
await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
|
|
2322
|
+
validatePattern(pattern);
|
|
2323
|
+
await this.awaitStartup("invalidateByPattern");
|
|
2324
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2325
|
+
this.qualifyPattern(pattern),
|
|
2326
|
+
this.invalidationMaxKeys()
|
|
2327
|
+
);
|
|
2328
|
+
await this.deleteKeys(keys);
|
|
2329
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2330
|
+
});
|
|
1709
2331
|
}
|
|
1710
2332
|
async invalidateByPrefix(prefix) {
|
|
1711
|
-
await this.
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
2333
|
+
await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
|
|
2334
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
2335
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2336
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
2337
|
+
await this.deleteKeys(keys);
|
|
2338
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2339
|
+
});
|
|
1716
2340
|
}
|
|
1717
2341
|
getMetrics() {
|
|
1718
2342
|
return this.metricsCollector.snapshot;
|
|
@@ -1768,9 +2392,15 @@ var CacheStack = class extends EventEmitter {
|
|
|
1768
2392
|
bumpGeneration(nextGeneration) {
|
|
1769
2393
|
const current = this.currentGeneration ?? 0;
|
|
1770
2394
|
const previousGeneration = this.currentGeneration;
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
2395
|
+
const updatedGeneration = nextGeneration ?? current + 1;
|
|
2396
|
+
const generationToCleanup = resolveGenerationCleanupTarget({
|
|
2397
|
+
previousGeneration,
|
|
2398
|
+
nextGeneration: updatedGeneration,
|
|
2399
|
+
generationCleanup: this.options.generationCleanup
|
|
2400
|
+
});
|
|
2401
|
+
this.currentGeneration = updatedGeneration;
|
|
2402
|
+
if (generationToCleanup !== null) {
|
|
2403
|
+
this.scheduleGenerationCleanup(generationToCleanup);
|
|
1774
2404
|
}
|
|
1775
2405
|
return this.currentGeneration;
|
|
1776
2406
|
}
|
|
@@ -1817,95 +2447,19 @@ var CacheStack = class extends EventEmitter {
|
|
|
1817
2447
|
}
|
|
1818
2448
|
async exportState() {
|
|
1819
2449
|
await this.awaitStartup("exportState");
|
|
1820
|
-
|
|
1821
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
1822
|
-
entries.push(entry);
|
|
1823
|
-
});
|
|
1824
|
-
return entries;
|
|
2450
|
+
return this.snapshots.exportState(this.snapshotMaxEntries());
|
|
1825
2451
|
}
|
|
1826
2452
|
async importState(entries) {
|
|
1827
2453
|
await this.awaitStartup("importState");
|
|
1828
|
-
|
|
1829
|
-
key: this.qualifyKey(validateCacheKey(entry.key)),
|
|
1830
|
-
value: entry.value,
|
|
1831
|
-
ttl: entry.ttl
|
|
1832
|
-
}));
|
|
1833
|
-
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
1834
|
-
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
1835
|
-
await Promise.all(
|
|
1836
|
-
batch.map(async (entry) => {
|
|
1837
|
-
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1838
|
-
await this.tagIndex.touch(entry.key);
|
|
1839
|
-
})
|
|
1840
|
-
);
|
|
1841
|
-
}
|
|
2454
|
+
await this.snapshots.importState(entries);
|
|
1842
2455
|
}
|
|
1843
2456
|
async persistToFile(filePath) {
|
|
1844
2457
|
this.assertActive("persistToFile");
|
|
1845
|
-
|
|
1846
|
-
const path = await import("path");
|
|
1847
|
-
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
1848
|
-
const tempPath = path.join(
|
|
1849
|
-
path.dirname(targetPath),
|
|
1850
|
-
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
1851
|
-
);
|
|
1852
|
-
let handle;
|
|
1853
|
-
try {
|
|
1854
|
-
handle = await fs2.open(tempPath, "wx");
|
|
1855
|
-
const openedHandle = handle;
|
|
1856
|
-
await openedHandle.writeFile("[", "utf8");
|
|
1857
|
-
let wroteAny = false;
|
|
1858
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
1859
|
-
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
1860
|
-
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
1861
|
-
wroteAny = true;
|
|
1862
|
-
});
|
|
1863
|
-
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
1864
|
-
await openedHandle.close();
|
|
1865
|
-
handle = void 0;
|
|
1866
|
-
await fs2.rename(tempPath, targetPath);
|
|
1867
|
-
} catch (error) {
|
|
1868
|
-
await handle?.close().catch(() => void 0);
|
|
1869
|
-
await fs2.unlink(tempPath).catch(() => void 0);
|
|
1870
|
-
throw error;
|
|
1871
|
-
}
|
|
2458
|
+
await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
|
|
1872
2459
|
}
|
|
1873
2460
|
async restoreFromFile(filePath) {
|
|
1874
2461
|
this.assertActive("restoreFromFile");
|
|
1875
|
-
|
|
1876
|
-
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
1877
|
-
const handle = await fs2.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
1878
|
-
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
1879
|
-
let raw;
|
|
1880
|
-
try {
|
|
1881
|
-
if (snapshotMaxBytes !== false) {
|
|
1882
|
-
const stat = await handle.stat();
|
|
1883
|
-
if (stat.size > snapshotMaxBytes) {
|
|
1884
|
-
throw new Error(
|
|
1885
|
-
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
1886
|
-
);
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
1890
|
-
} finally {
|
|
1891
|
-
await handle.close();
|
|
1892
|
-
}
|
|
1893
|
-
let parsed;
|
|
1894
|
-
try {
|
|
1895
|
-
parsed = JSON.parse(raw);
|
|
1896
|
-
} catch (cause) {
|
|
1897
|
-
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
1898
|
-
}
|
|
1899
|
-
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1900
|
-
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1901
|
-
}
|
|
1902
|
-
await this.importState(
|
|
1903
|
-
parsed.map((entry) => ({
|
|
1904
|
-
key: entry.key,
|
|
1905
|
-
value: this.sanitizeSnapshotValue(entry.value),
|
|
1906
|
-
ttl: entry.ttl
|
|
1907
|
-
}))
|
|
1908
|
-
);
|
|
2462
|
+
await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
|
|
1909
2463
|
}
|
|
1910
2464
|
async disconnect() {
|
|
1911
2465
|
if (!this.disconnectPromise) {
|
|
@@ -1914,12 +2468,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
1914
2468
|
await this.startup;
|
|
1915
2469
|
await this.unsubscribeInvalidation?.();
|
|
1916
2470
|
await this.flushWriteBehindQueue();
|
|
1917
|
-
await this.
|
|
2471
|
+
await this.maintenance.waitForGenerationCleanup();
|
|
1918
2472
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
this.writeBehindTimer = void 0;
|
|
1922
|
-
}
|
|
2473
|
+
this.maintenance.disposeWriteBehindTimer();
|
|
2474
|
+
this.fetchRateLimiter.dispose();
|
|
1923
2475
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
1924
2476
|
})();
|
|
1925
2477
|
}
|
|
@@ -1995,13 +2547,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
1995
2547
|
if (!this.shouldNegativeCache(options)) {
|
|
1996
2548
|
return null;
|
|
1997
2549
|
}
|
|
1998
|
-
if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2550
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
1999
2551
|
this.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
2000
2552
|
key,
|
|
2001
2553
|
expectedClearEpoch,
|
|
2002
|
-
clearEpoch: this.
|
|
2554
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2003
2555
|
expectedKeyEpoch,
|
|
2004
|
-
keyEpoch: this.currentKeyEpoch(key)
|
|
2556
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2005
2557
|
});
|
|
2006
2558
|
return null;
|
|
2007
2559
|
}
|
|
@@ -2017,13 +2569,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
2017
2569
|
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2018
2570
|
}
|
|
2019
2571
|
}
|
|
2020
|
-
if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2572
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2021
2573
|
this.logger.debug?.("skip-store-after-invalidation", {
|
|
2022
2574
|
key,
|
|
2023
2575
|
expectedClearEpoch,
|
|
2024
|
-
clearEpoch: this.
|
|
2576
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2025
2577
|
expectedKeyEpoch,
|
|
2026
|
-
keyEpoch: this.currentKeyEpoch(key)
|
|
2578
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2027
2579
|
});
|
|
2028
2580
|
return fetched;
|
|
2029
2581
|
}
|
|
@@ -2031,10 +2583,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
2031
2583
|
return fetched;
|
|
2032
2584
|
}
|
|
2033
2585
|
async storeEntry(key, kind, value, options) {
|
|
2034
|
-
const clearEpoch = this.
|
|
2035
|
-
const keyEpoch = this.currentKeyEpoch(key);
|
|
2036
|
-
await this.writeAcrossLayers(key, kind, value, options);
|
|
2037
|
-
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2586
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2587
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2588
|
+
await this.layerWriter.writeAcrossLayers(key, kind, value, options);
|
|
2589
|
+
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2038
2590
|
return;
|
|
2039
2591
|
}
|
|
2040
2592
|
if (options?.tags) {
|
|
@@ -2050,57 +2602,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
2050
2602
|
}
|
|
2051
2603
|
}
|
|
2052
2604
|
async writeBatch(entries) {
|
|
2053
|
-
const
|
|
2054
|
-
|
|
2055
|
-
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
|
|
2056
|
-
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2057
|
-
const immediateOperations = [];
|
|
2058
|
-
const deferredOperations = [];
|
|
2059
|
-
for (const entry of entries) {
|
|
2060
|
-
for (const layer of this.layers) {
|
|
2061
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2062
|
-
continue;
|
|
2063
|
-
}
|
|
2064
|
-
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
2065
|
-
const bucket = entriesByLayer.get(layer) ?? [];
|
|
2066
|
-
bucket.push(layerEntry);
|
|
2067
|
-
entriesByLayer.set(layer, bucket);
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2071
|
-
const operation = async () => {
|
|
2072
|
-
if (clearEpoch !== this.clearEpoch) {
|
|
2073
|
-
return;
|
|
2074
|
-
}
|
|
2075
|
-
const activeEntries = layerEntries.filter(
|
|
2076
|
-
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
|
|
2077
|
-
);
|
|
2078
|
-
if (activeEntries.length === 0) {
|
|
2079
|
-
return;
|
|
2080
|
-
}
|
|
2081
|
-
try {
|
|
2082
|
-
if (layer.setMany) {
|
|
2083
|
-
await layer.setMany(activeEntries);
|
|
2084
|
-
return;
|
|
2085
|
-
}
|
|
2086
|
-
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2087
|
-
} catch (error) {
|
|
2088
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
2089
|
-
}
|
|
2090
|
-
};
|
|
2091
|
-
if (this.shouldWriteBehind(layer)) {
|
|
2092
|
-
deferredOperations.push(operation);
|
|
2093
|
-
} else {
|
|
2094
|
-
immediateOperations.push(operation);
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
|
-
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2098
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2099
|
-
if (clearEpoch !== this.clearEpoch) {
|
|
2605
|
+
const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
|
|
2606
|
+
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2100
2607
|
return;
|
|
2101
2608
|
}
|
|
2102
2609
|
for (const entry of entries) {
|
|
2103
|
-
if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2610
|
+
if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2104
2611
|
continue;
|
|
2105
2612
|
}
|
|
2106
2613
|
if (entry.options?.tags) {
|
|
@@ -2181,77 +2688,25 @@ var CacheStack = class extends EventEmitter {
|
|
|
2181
2688
|
return this.handleLayerFailure(layer, "read", error);
|
|
2182
2689
|
}
|
|
2183
2690
|
}
|
|
2184
|
-
async backfill(key, stored, upToIndex, options) {
|
|
2185
|
-
if (upToIndex < 0) {
|
|
2186
|
-
return;
|
|
2187
|
-
}
|
|
2188
|
-
for (let index = 0; index <= upToIndex; index += 1) {
|
|
2189
|
-
const layer = this.layers[index];
|
|
2190
|
-
if (!layer || this.shouldSkipLayer(layer)) {
|
|
2191
|
-
continue;
|
|
2192
|
-
}
|
|
2193
|
-
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
2194
|
-
try {
|
|
2195
|
-
await layer.set(key, stored, ttl);
|
|
2196
|
-
} catch (error) {
|
|
2197
|
-
await this.handleLayerFailure(layer, "backfill", error);
|
|
2198
|
-
continue;
|
|
2199
|
-
}
|
|
2200
|
-
this.metricsCollector.increment("backfills");
|
|
2201
|
-
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
2202
|
-
this.emit("backfill", { key, layer: layer.name });
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
async writeAcrossLayers(key, kind, value, options) {
|
|
2206
|
-
const now = Date.now();
|
|
2207
|
-
const clearEpoch = this.clearEpoch;
|
|
2208
|
-
const keyEpoch = this.currentKeyEpoch(key);
|
|
2209
|
-
const immediateOperations = [];
|
|
2210
|
-
const deferredOperations = [];
|
|
2211
|
-
for (const layer of this.layers) {
|
|
2212
|
-
const operation = async () => {
|
|
2213
|
-
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2214
|
-
return;
|
|
2215
|
-
}
|
|
2216
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2217
|
-
return;
|
|
2218
|
-
}
|
|
2219
|
-
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
2220
|
-
try {
|
|
2221
|
-
await layer.set(entry.key, entry.value, entry.ttl);
|
|
2222
|
-
} catch (error) {
|
|
2223
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
2224
|
-
}
|
|
2225
|
-
};
|
|
2226
|
-
if (this.shouldWriteBehind(layer)) {
|
|
2227
|
-
deferredOperations.push(operation);
|
|
2228
|
-
} else {
|
|
2229
|
-
immediateOperations.push(operation);
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
2233
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2234
|
-
}
|
|
2235
|
-
async executeLayerOperations(operations, context) {
|
|
2236
|
-
if (this.options.writePolicy !== "best-effort") {
|
|
2237
|
-
await Promise.all(operations.map((operation) => operation()));
|
|
2238
|
-
return;
|
|
2239
|
-
}
|
|
2240
|
-
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
2241
|
-
const failures = results.filter((result) => result.status === "rejected");
|
|
2242
|
-
if (failures.length === 0) {
|
|
2691
|
+
async backfill(key, stored, upToIndex, options) {
|
|
2692
|
+
if (upToIndex < 0) {
|
|
2243
2693
|
return;
|
|
2244
2694
|
}
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2695
|
+
for (let index = 0; index <= upToIndex; index += 1) {
|
|
2696
|
+
const layer = this.layers[index];
|
|
2697
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
2698
|
+
continue;
|
|
2699
|
+
}
|
|
2700
|
+
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
2701
|
+
try {
|
|
2702
|
+
await layer.set(key, stored, ttl);
|
|
2703
|
+
} catch (error) {
|
|
2704
|
+
await this.handleLayerFailure(layer, "backfill", error);
|
|
2705
|
+
continue;
|
|
2706
|
+
}
|
|
2707
|
+
this.metricsCollector.increment("backfills");
|
|
2708
|
+
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
2709
|
+
this.emit("backfill", { key, layer: layer.name });
|
|
2255
2710
|
}
|
|
2256
2711
|
}
|
|
2257
2712
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
@@ -2273,11 +2728,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
2273
2728
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
2274
2729
|
}
|
|
2275
2730
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
2276
|
-
if (
|
|
2731
|
+
if (!shouldStartBackgroundRefresh({
|
|
2732
|
+
isDisconnecting: this.isDisconnecting,
|
|
2733
|
+
hasRefreshInFlight: this.backgroundRefreshes.has(key)
|
|
2734
|
+
})) {
|
|
2277
2735
|
return;
|
|
2278
2736
|
}
|
|
2279
|
-
const clearEpoch = this.
|
|
2280
|
-
const keyEpoch = this.currentKeyEpoch(key);
|
|
2737
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2738
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2281
2739
|
const refresh = (async () => {
|
|
2282
2740
|
this.metricsCollector.increment("refreshes");
|
|
2283
2741
|
try {
|
|
@@ -2315,8 +2773,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
2315
2773
|
if (keys.length === 0) {
|
|
2316
2774
|
return;
|
|
2317
2775
|
}
|
|
2318
|
-
this.bumpKeyEpochs(keys);
|
|
2319
|
-
await this.deleteKeysFromLayers(this.layers, keys);
|
|
2776
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
2777
|
+
await this.invalidation.deleteKeysFromLayers(this.layers, keys);
|
|
2320
2778
|
for (const key of keys) {
|
|
2321
2779
|
await this.tagIndex.remove(key);
|
|
2322
2780
|
this.ttlResolver.deleteProfile(key);
|
|
@@ -2339,7 +2797,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2339
2797
|
}
|
|
2340
2798
|
const localLayers = this.layers.filter((layer) => layer.isLocal);
|
|
2341
2799
|
if (message.scope === "clear") {
|
|
2342
|
-
this.beginClearEpoch();
|
|
2800
|
+
this.maintenance.beginClearEpoch();
|
|
2343
2801
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
2344
2802
|
await this.tagIndex.clear();
|
|
2345
2803
|
this.ttlResolver.clearProfiles();
|
|
@@ -2347,8 +2805,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
2347
2805
|
return;
|
|
2348
2806
|
}
|
|
2349
2807
|
const keys = message.keys ?? [];
|
|
2350
|
-
this.bumpKeyEpochs(keys);
|
|
2351
|
-
await this.deleteKeysFromLayers(localLayers, keys);
|
|
2808
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
2809
|
+
await this.invalidation.deleteKeysFromLayers(localLayers, keys);
|
|
2352
2810
|
if (message.operation !== "write") {
|
|
2353
2811
|
for (const key of keys) {
|
|
2354
2812
|
await this.tagIndex.remove(key);
|
|
@@ -2405,35 +2863,47 @@ var CacheStack = class extends EventEmitter {
|
|
|
2405
2863
|
shouldBroadcastL1Invalidation() {
|
|
2406
2864
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2407
2865
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2866
|
+
async observeOperation(name, attributes, execute) {
|
|
2867
|
+
const id = this.nextOperationId;
|
|
2868
|
+
this.nextOperationId += 1;
|
|
2869
|
+
this.emit("operation-start", { id, name, attributes });
|
|
2870
|
+
try {
|
|
2871
|
+
const result = await execute();
|
|
2872
|
+
this.emit("operation-end", {
|
|
2873
|
+
id,
|
|
2874
|
+
name,
|
|
2875
|
+
attributes,
|
|
2876
|
+
success: true,
|
|
2877
|
+
result: result === null ? "null" : void 0
|
|
2878
|
+
});
|
|
2879
|
+
return result;
|
|
2880
|
+
} catch (error) {
|
|
2881
|
+
this.emit("operation-end", {
|
|
2882
|
+
id,
|
|
2883
|
+
name,
|
|
2884
|
+
attributes,
|
|
2885
|
+
success: false,
|
|
2886
|
+
error
|
|
2887
|
+
});
|
|
2888
|
+
throw error;
|
|
2889
|
+
}
|
|
2414
2890
|
}
|
|
2415
2891
|
scheduleGenerationCleanup(generation) {
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
this.generationCleanupPromise = void 0;
|
|
2892
|
+
this.maintenance.scheduleGenerationCleanup(
|
|
2893
|
+
generation,
|
|
2894
|
+
async (generationToClean) => this.cleanupGeneration(generationToClean),
|
|
2895
|
+
(failedGeneration, error) => {
|
|
2896
|
+
this.logger.warn?.("generation-cleanup-error", {
|
|
2897
|
+
generation: failedGeneration,
|
|
2898
|
+
error: this.formatError(error)
|
|
2899
|
+
});
|
|
2425
2900
|
}
|
|
2426
|
-
|
|
2901
|
+
);
|
|
2427
2902
|
}
|
|
2428
2903
|
async cleanupGeneration(generation) {
|
|
2429
2904
|
const prefix = `v${generation}:`;
|
|
2430
2905
|
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
2431
|
-
|
|
2432
|
-
return;
|
|
2433
|
-
}
|
|
2434
|
-
const batchSize = this.generationCleanupBatchSize();
|
|
2435
|
-
for (let index = 0; index < keys.length; index += batchSize) {
|
|
2436
|
-
const batch = keys.slice(index, index + batchSize);
|
|
2906
|
+
for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
|
|
2437
2907
|
await this.deleteKeys(batch);
|
|
2438
2908
|
await this.publishInvalidation({
|
|
2439
2909
|
scope: "keys",
|
|
@@ -2444,161 +2914,43 @@ var CacheStack = class extends EventEmitter {
|
|
|
2444
2914
|
}
|
|
2445
2915
|
}
|
|
2446
2916
|
initializeWriteBehind(options) {
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
return;
|
|
2453
|
-
}
|
|
2454
|
-
this.writeBehindTimer = setInterval(() => {
|
|
2455
|
-
void this.flushWriteBehindQueue();
|
|
2456
|
-
}, flushIntervalMs);
|
|
2457
|
-
this.writeBehindTimer.unref?.();
|
|
2917
|
+
this.maintenance.initializeWriteBehindTimer(
|
|
2918
|
+
this.options.writeStrategy,
|
|
2919
|
+
options,
|
|
2920
|
+
this.flushWriteBehindQueue.bind(this)
|
|
2921
|
+
);
|
|
2458
2922
|
}
|
|
2459
2923
|
shouldWriteBehind(layer) {
|
|
2460
2924
|
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2461
2925
|
}
|
|
2462
|
-
beginClearEpoch() {
|
|
2463
|
-
this.clearEpoch += 1;
|
|
2464
|
-
this.keyEpochs.clear();
|
|
2465
|
-
this.writeBehindQueue.length = 0;
|
|
2466
|
-
}
|
|
2467
|
-
currentKeyEpoch(key) {
|
|
2468
|
-
return this.keyEpochs.get(key) ?? 0;
|
|
2469
|
-
}
|
|
2470
|
-
bumpKeyEpochs(keys) {
|
|
2471
|
-
for (const key of keys) {
|
|
2472
|
-
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
2473
|
-
}
|
|
2474
|
-
}
|
|
2475
|
-
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
2476
|
-
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
2477
|
-
return true;
|
|
2478
|
-
}
|
|
2479
|
-
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
2480
|
-
return true;
|
|
2481
|
-
}
|
|
2482
|
-
return false;
|
|
2483
|
-
}
|
|
2484
2926
|
async enqueueWriteBehind(operation) {
|
|
2485
|
-
this.
|
|
2486
|
-
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2487
|
-
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
2488
|
-
if (this.writeBehindQueue.length >= batchSize) {
|
|
2489
|
-
await this.flushWriteBehindQueue();
|
|
2490
|
-
return;
|
|
2491
|
-
}
|
|
2492
|
-
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
2493
|
-
await this.flushWriteBehindQueue();
|
|
2494
|
-
}
|
|
2927
|
+
await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
2495
2928
|
}
|
|
2496
2929
|
async flushWriteBehindQueue() {
|
|
2497
|
-
|
|
2498
|
-
|
|
2930
|
+
await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
2931
|
+
}
|
|
2932
|
+
async runWriteBehindBatch(batch) {
|
|
2933
|
+
const results = await Promise.allSettled(batch.map((operation) => operation()));
|
|
2934
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
2935
|
+
if (failures.length === 0) {
|
|
2499
2936
|
return;
|
|
2500
2937
|
}
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
if (failures.length > 0) {
|
|
2507
|
-
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2508
|
-
this.logger.error?.("write-behind-flush-failure", {
|
|
2509
|
-
failed: failures.length,
|
|
2510
|
-
total: batch.length,
|
|
2511
|
-
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
2512
|
-
});
|
|
2513
|
-
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2514
|
-
}
|
|
2515
|
-
})();
|
|
2516
|
-
await this.writeBehindFlushPromise;
|
|
2517
|
-
this.writeBehindFlushPromise = void 0;
|
|
2518
|
-
if (this.writeBehindQueue.length > 0) {
|
|
2519
|
-
await this.flushWriteBehindQueue();
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
|
-
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
2523
|
-
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
2524
|
-
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
2525
|
-
layer.name,
|
|
2526
|
-
options?.staleWhileRevalidate,
|
|
2527
|
-
this.options.staleWhileRevalidate
|
|
2528
|
-
);
|
|
2529
|
-
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
2530
|
-
const payload = createStoredValueEnvelope({
|
|
2531
|
-
kind,
|
|
2532
|
-
value,
|
|
2533
|
-
freshTtlSeconds: freshTtl,
|
|
2534
|
-
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
2535
|
-
staleIfErrorSeconds: staleIfError,
|
|
2536
|
-
now
|
|
2938
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2939
|
+
this.logger.error?.("write-behind-flush-failure", {
|
|
2940
|
+
failed: failures.length,
|
|
2941
|
+
total: batch.length,
|
|
2942
|
+
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
2537
2943
|
});
|
|
2538
|
-
|
|
2539
|
-
return {
|
|
2540
|
-
key,
|
|
2541
|
-
value: payload,
|
|
2542
|
-
ttl
|
|
2543
|
-
};
|
|
2544
|
-
}
|
|
2545
|
-
intersectKeys(groups) {
|
|
2546
|
-
if (groups.length === 0) {
|
|
2547
|
-
return [];
|
|
2548
|
-
}
|
|
2549
|
-
const [firstGroup, ...rest] = groups;
|
|
2550
|
-
if (!firstGroup) {
|
|
2551
|
-
return [];
|
|
2552
|
-
}
|
|
2553
|
-
const restSets = rest.map((group) => new Set(group));
|
|
2554
|
-
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
2944
|
+
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2555
2945
|
}
|
|
2556
2946
|
qualifyKey(key) {
|
|
2557
|
-
|
|
2558
|
-
return prefix ? `${prefix}${key}` : key;
|
|
2947
|
+
return qualifyGenerationKey(key, this.currentGeneration);
|
|
2559
2948
|
}
|
|
2560
2949
|
qualifyPattern(pattern) {
|
|
2561
|
-
|
|
2562
|
-
return prefix ? `${prefix}${pattern}` : pattern;
|
|
2950
|
+
return qualifyGenerationPattern(pattern, this.currentGeneration);
|
|
2563
2951
|
}
|
|
2564
2952
|
stripQualifiedKey(key) {
|
|
2565
|
-
|
|
2566
|
-
if (!prefix || !key.startsWith(prefix)) {
|
|
2567
|
-
return key;
|
|
2568
|
-
}
|
|
2569
|
-
return key.slice(prefix.length);
|
|
2570
|
-
}
|
|
2571
|
-
generationPrefix() {
|
|
2572
|
-
if (this.currentGeneration === void 0) {
|
|
2573
|
-
return "";
|
|
2574
|
-
}
|
|
2575
|
-
return `v${this.currentGeneration}:`;
|
|
2576
|
-
}
|
|
2577
|
-
async deleteKeysFromLayers(layers, keys) {
|
|
2578
|
-
await Promise.all(
|
|
2579
|
-
layers.map(async (layer) => {
|
|
2580
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2581
|
-
return;
|
|
2582
|
-
}
|
|
2583
|
-
if (layer.deleteMany) {
|
|
2584
|
-
try {
|
|
2585
|
-
await layer.deleteMany(keys);
|
|
2586
|
-
} catch (error) {
|
|
2587
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
2588
|
-
}
|
|
2589
|
-
return;
|
|
2590
|
-
}
|
|
2591
|
-
await Promise.all(
|
|
2592
|
-
keys.map(async (key) => {
|
|
2593
|
-
try {
|
|
2594
|
-
await layer.delete(key);
|
|
2595
|
-
} catch (error) {
|
|
2596
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
2597
|
-
}
|
|
2598
|
-
})
|
|
2599
|
-
);
|
|
2600
|
-
})
|
|
2601
|
-
);
|
|
2953
|
+
return stripGenerationPrefix(key, this.currentGeneration);
|
|
2602
2954
|
}
|
|
2603
2955
|
validateConfiguration() {
|
|
2604
2956
|
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
@@ -2663,37 +3015,38 @@ var CacheStack = class extends EventEmitter {
|
|
|
2663
3015
|
this.assertActive(operation);
|
|
2664
3016
|
}
|
|
2665
3017
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
2666
|
-
const
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
3018
|
+
const plan = planFreshReadPolicies({
|
|
3019
|
+
stored: hit.stored,
|
|
3020
|
+
hasFetcher: Boolean(fetcher),
|
|
3021
|
+
slidingTtl: options?.slidingTtl ?? false,
|
|
3022
|
+
refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
3023
|
+
});
|
|
3024
|
+
if (plan.refreshedStored) {
|
|
2671
3025
|
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
2672
3026
|
const layer = this.layers[index];
|
|
2673
3027
|
if (!layer || this.shouldSkipLayer(layer)) {
|
|
2674
3028
|
continue;
|
|
2675
3029
|
}
|
|
2676
3030
|
try {
|
|
2677
|
-
await layer.set(key,
|
|
3031
|
+
await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
|
|
2678
3032
|
} catch (error) {
|
|
2679
3033
|
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
2680
3034
|
}
|
|
2681
3035
|
}
|
|
2682
3036
|
}
|
|
2683
|
-
if (fetcher &&
|
|
3037
|
+
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
2684
3038
|
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
2685
3039
|
}
|
|
2686
3040
|
}
|
|
2687
3041
|
shouldSkipLayer(layer) {
|
|
2688
|
-
|
|
2689
|
-
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
3042
|
+
return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
|
|
2690
3043
|
}
|
|
2691
3044
|
async handleLayerFailure(layer, operation, error) {
|
|
2692
|
-
|
|
3045
|
+
const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
|
|
3046
|
+
if (!recovery.degrade) {
|
|
2693
3047
|
throw error;
|
|
2694
3048
|
}
|
|
2695
|
-
|
|
2696
|
-
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
3049
|
+
this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
|
|
2697
3050
|
this.metricsCollector.increment("degradedOperations");
|
|
2698
3051
|
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
2699
3052
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
@@ -2729,18 +3082,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2729
3082
|
this.emit("error", { operation, ...context });
|
|
2730
3083
|
}
|
|
2731
3084
|
}
|
|
2732
|
-
isCacheSnapshotEntries(value) {
|
|
2733
|
-
return Array.isArray(value) && value.every((entry) => {
|
|
2734
|
-
if (!entry || typeof entry !== "object") {
|
|
2735
|
-
return false;
|
|
2736
|
-
}
|
|
2737
|
-
const candidate = entry;
|
|
2738
|
-
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
2739
|
-
});
|
|
2740
|
-
}
|
|
2741
|
-
sanitizeSnapshotValue(value) {
|
|
2742
|
-
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2743
|
-
}
|
|
2744
3085
|
snapshotMaxBytes() {
|
|
2745
3086
|
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
2746
3087
|
}
|
|
@@ -2750,62 +3091,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2750
3091
|
invalidationMaxKeys() {
|
|
2751
3092
|
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
2752
3093
|
}
|
|
2753
|
-
async collectKeysForTag(tag) {
|
|
2754
|
-
const keys = /* @__PURE__ */ new Set();
|
|
2755
|
-
if (this.tagIndex.forEachKeyForTag) {
|
|
2756
|
-
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
2757
|
-
keys.add(key);
|
|
2758
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
2759
|
-
});
|
|
2760
|
-
return [...keys];
|
|
2761
|
-
}
|
|
2762
|
-
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
2763
|
-
keys.add(key);
|
|
2764
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
2765
|
-
}
|
|
2766
|
-
return [...keys];
|
|
2767
|
-
}
|
|
2768
|
-
assertWithinInvalidationKeyLimit(size) {
|
|
2769
|
-
const maxKeys = this.invalidationMaxKeys();
|
|
2770
|
-
if (maxKeys !== false && size > maxKeys) {
|
|
2771
|
-
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
2772
|
-
}
|
|
2773
|
-
}
|
|
2774
|
-
async visitExportEntries(maxEntries, visitor) {
|
|
2775
|
-
const exported = /* @__PURE__ */ new Set();
|
|
2776
|
-
for (const layer of this.layers) {
|
|
2777
|
-
if (!layer.keys && !layer.forEachKey) {
|
|
2778
|
-
continue;
|
|
2779
|
-
}
|
|
2780
|
-
const visitKey = async (key) => {
|
|
2781
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
2782
|
-
if (exported.has(exportedKey)) {
|
|
2783
|
-
return;
|
|
2784
|
-
}
|
|
2785
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
2786
|
-
if (stored === null) {
|
|
2787
|
-
return;
|
|
2788
|
-
}
|
|
2789
|
-
exported.add(exportedKey);
|
|
2790
|
-
if (maxEntries !== false && exported.size > maxEntries) {
|
|
2791
|
-
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
2792
|
-
}
|
|
2793
|
-
await visitor({
|
|
2794
|
-
key: exportedKey,
|
|
2795
|
-
value: stored,
|
|
2796
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
2797
|
-
});
|
|
2798
|
-
};
|
|
2799
|
-
if (layer.forEachKey) {
|
|
2800
|
-
await layer.forEachKey(visitKey);
|
|
2801
|
-
continue;
|
|
2802
|
-
}
|
|
2803
|
-
const keys = await layer.keys?.();
|
|
2804
|
-
for (const key of keys ?? []) {
|
|
2805
|
-
await visitKey(key);
|
|
2806
|
-
}
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2809
3094
|
};
|
|
2810
3095
|
|
|
2811
3096
|
// src/invalidation/RedisInvalidationBus.ts
|
|
@@ -2847,7 +3132,12 @@ var RedisInvalidationBus = class {
|
|
|
2847
3132
|
async dispatchToHandlers(payload) {
|
|
2848
3133
|
let message;
|
|
2849
3134
|
try {
|
|
2850
|
-
const parsed =
|
|
3135
|
+
const parsed = sanitizeStructuredData(JSON.parse(payload), {
|
|
3136
|
+
label: "Invalidation payload",
|
|
3137
|
+
maxDepth: 64,
|
|
3138
|
+
maxNodes: 1e4,
|
|
3139
|
+
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
3140
|
+
});
|
|
2851
3141
|
if (!this.isInvalidationMessage(parsed)) {
|
|
2852
3142
|
throw new Error("Invalid invalidation payload shape.");
|
|
2853
3143
|
}
|
|
@@ -2884,31 +3174,6 @@ var RedisInvalidationBus = class {
|
|
|
2884
3174
|
console.error(`[layercache] ${message}`, error);
|
|
2885
3175
|
}
|
|
2886
3176
|
};
|
|
2887
|
-
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2888
|
-
var MAX_SANITIZE_DEPTH2 = 64;
|
|
2889
|
-
var MAX_SANITIZE_NODES2 = 1e4;
|
|
2890
|
-
function sanitizeJsonValue2(value, depth = 0, state = { count: 0 }) {
|
|
2891
|
-
state.count += 1;
|
|
2892
|
-
if (state.count > MAX_SANITIZE_NODES2) {
|
|
2893
|
-
throw new Error(`Invalidation payload exceeds max node count of ${MAX_SANITIZE_NODES2}.`);
|
|
2894
|
-
}
|
|
2895
|
-
if (depth > MAX_SANITIZE_DEPTH2) {
|
|
2896
|
-
throw new Error(`Invalidation payload exceeds max depth of ${MAX_SANITIZE_DEPTH2}.`);
|
|
2897
|
-
}
|
|
2898
|
-
if (Array.isArray(value)) {
|
|
2899
|
-
return value.map((entry) => sanitizeJsonValue2(entry, depth + 1, state));
|
|
2900
|
-
}
|
|
2901
|
-
if (value && typeof value === "object") {
|
|
2902
|
-
const result = /* @__PURE__ */ Object.create(null);
|
|
2903
|
-
for (const key of Object.keys(value)) {
|
|
2904
|
-
if (!DANGEROUS_KEYS.has(key)) {
|
|
2905
|
-
result[key] = sanitizeJsonValue2(value[key], depth + 1, state);
|
|
2906
|
-
}
|
|
2907
|
-
}
|
|
2908
|
-
return result;
|
|
2909
|
-
}
|
|
2910
|
-
return value;
|
|
2911
|
-
}
|
|
2912
3177
|
|
|
2913
3178
|
// src/http/createCacheStatsHandler.ts
|
|
2914
3179
|
function createCacheStatsHandler(cache, options = {}) {
|
|
@@ -3053,64 +3318,37 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
|
3053
3318
|
|
|
3054
3319
|
// src/integrations/opentelemetry.ts
|
|
3055
3320
|
function createOpenTelemetryPlugin(cache, tracer) {
|
|
3056
|
-
const
|
|
3057
|
-
|
|
3058
|
-
set:
|
|
3059
|
-
delete: cache.delete.bind(cache),
|
|
3060
|
-
mget: cache.mget.bind(cache),
|
|
3061
|
-
mset: cache.mset.bind(cache),
|
|
3062
|
-
invalidateByTag: cache.invalidateByTag.bind(cache),
|
|
3063
|
-
invalidateByTags: cache.invalidateByTags.bind(cache),
|
|
3064
|
-
invalidateByPattern: cache.invalidateByPattern.bind(cache),
|
|
3065
|
-
invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
|
|
3321
|
+
const spans = /* @__PURE__ */ new Map();
|
|
3322
|
+
const onStart = (event) => {
|
|
3323
|
+
spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
|
|
3066
3324
|
};
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
"layercache.key": String(args[0] ?? "")
|
|
3072
|
-
}));
|
|
3073
|
-
cache.delete = instrument("layercache.delete", tracer, originals.delete, (args) => ({
|
|
3074
|
-
"layercache.key": String(args[0] ?? "")
|
|
3075
|
-
}));
|
|
3076
|
-
cache.mget = instrument("layercache.mget", tracer, originals.mget);
|
|
3077
|
-
cache.mset = instrument("layercache.mset", tracer, originals.mset);
|
|
3078
|
-
cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
|
|
3079
|
-
cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
|
|
3080
|
-
cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
|
|
3081
|
-
cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
|
|
3082
|
-
return {
|
|
3083
|
-
uninstall() {
|
|
3084
|
-
cache.get = originals.get;
|
|
3085
|
-
cache.set = originals.set;
|
|
3086
|
-
cache.delete = originals.delete;
|
|
3087
|
-
cache.mget = originals.mget;
|
|
3088
|
-
cache.mset = originals.mset;
|
|
3089
|
-
cache.invalidateByTag = originals.invalidateByTag;
|
|
3090
|
-
cache.invalidateByTags = originals.invalidateByTags;
|
|
3091
|
-
cache.invalidateByPattern = originals.invalidateByPattern;
|
|
3092
|
-
cache.invalidateByPrefix = originals.invalidateByPrefix;
|
|
3325
|
+
const onEnd = (event) => {
|
|
3326
|
+
const span = spans.get(event.id);
|
|
3327
|
+
if (!span) {
|
|
3328
|
+
return;
|
|
3093
3329
|
}
|
|
3330
|
+
spans.delete(event.id);
|
|
3331
|
+
span.setAttribute?.("layercache.success", event.success);
|
|
3332
|
+
if (event.result) {
|
|
3333
|
+
span.setAttribute?.("layercache.result", event.result);
|
|
3334
|
+
}
|
|
3335
|
+
if (event.error !== void 0) {
|
|
3336
|
+
span.recordException?.(event.error);
|
|
3337
|
+
}
|
|
3338
|
+
span.end();
|
|
3094
3339
|
};
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
return
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
span.
|
|
3102
|
-
|
|
3103
|
-
span.setAttribute?.("layercache.result", "null");
|
|
3340
|
+
cache.on("operation-start", onStart);
|
|
3341
|
+
cache.on("operation-end", onEnd);
|
|
3342
|
+
return {
|
|
3343
|
+
uninstall() {
|
|
3344
|
+
cache.off("operation-start", onStart);
|
|
3345
|
+
cache.off("operation-end", onEnd);
|
|
3346
|
+
for (const span of spans.values()) {
|
|
3347
|
+
span.end();
|
|
3104
3348
|
}
|
|
3105
|
-
|
|
3106
|
-
} catch (error) {
|
|
3107
|
-
span.setAttribute?.("layercache.success", false);
|
|
3108
|
-
span.recordException?.(error);
|
|
3109
|
-
throw error;
|
|
3110
|
-
} finally {
|
|
3111
|
-
span.end();
|
|
3349
|
+
spans.clear();
|
|
3112
3350
|
}
|
|
3113
|
-
}
|
|
3351
|
+
};
|
|
3114
3352
|
}
|
|
3115
3353
|
|
|
3116
3354
|
// src/integrations/trpc.ts
|
|
@@ -3474,7 +3712,7 @@ var RedisLayer = class {
|
|
|
3474
3712
|
|
|
3475
3713
|
// src/layers/DiskLayer.ts
|
|
3476
3714
|
import { createHash } from "crypto";
|
|
3477
|
-
import { promises as
|
|
3715
|
+
import { promises as fs2 } from "fs";
|
|
3478
3716
|
import { join, resolve } from "path";
|
|
3479
3717
|
var FILE_SCAN_CONCURRENCY = 32;
|
|
3480
3718
|
var DiskLayer = class {
|
|
@@ -3518,7 +3756,7 @@ var DiskLayer = class {
|
|
|
3518
3756
|
}
|
|
3519
3757
|
async set(key, value, ttl = this.defaultTtl) {
|
|
3520
3758
|
await this.enqueueWrite(async () => {
|
|
3521
|
-
await
|
|
3759
|
+
await fs2.mkdir(this.directory, { recursive: true });
|
|
3522
3760
|
const entry = {
|
|
3523
3761
|
key,
|
|
3524
3762
|
value,
|
|
@@ -3528,8 +3766,8 @@ var DiskLayer = class {
|
|
|
3528
3766
|
const targetPath = this.keyToPath(key);
|
|
3529
3767
|
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3530
3768
|
try {
|
|
3531
|
-
await
|
|
3532
|
-
await
|
|
3769
|
+
await fs2.writeFile(tempPath, payload);
|
|
3770
|
+
await fs2.rename(tempPath, targetPath);
|
|
3533
3771
|
} catch (error) {
|
|
3534
3772
|
await this.safeDelete(tempPath);
|
|
3535
3773
|
throw error;
|
|
@@ -3583,7 +3821,7 @@ var DiskLayer = class {
|
|
|
3583
3821
|
await this.enqueueWrite(async () => {
|
|
3584
3822
|
let entries;
|
|
3585
3823
|
try {
|
|
3586
|
-
entries = await
|
|
3824
|
+
entries = await fs2.readdir(this.directory);
|
|
3587
3825
|
} catch {
|
|
3588
3826
|
return;
|
|
3589
3827
|
}
|
|
@@ -3617,7 +3855,7 @@ var DiskLayer = class {
|
|
|
3617
3855
|
}
|
|
3618
3856
|
async ping() {
|
|
3619
3857
|
try {
|
|
3620
|
-
await
|
|
3858
|
+
await fs2.mkdir(this.directory, { recursive: true });
|
|
3621
3859
|
return true;
|
|
3622
3860
|
} catch {
|
|
3623
3861
|
return false;
|
|
@@ -3660,7 +3898,7 @@ var DiskLayer = class {
|
|
|
3660
3898
|
async readEntryFile(filePath) {
|
|
3661
3899
|
let handle;
|
|
3662
3900
|
try {
|
|
3663
|
-
handle = await
|
|
3901
|
+
handle = await fs2.open(filePath, "r");
|
|
3664
3902
|
return await this.readHandleWithLimit(handle);
|
|
3665
3903
|
} catch {
|
|
3666
3904
|
await this.safeDelete(filePath);
|
|
@@ -3700,7 +3938,7 @@ var DiskLayer = class {
|
|
|
3700
3938
|
async scanEntries(visitor) {
|
|
3701
3939
|
let entries;
|
|
3702
3940
|
try {
|
|
3703
|
-
entries = await
|
|
3941
|
+
entries = await fs2.readdir(this.directory);
|
|
3704
3942
|
} catch {
|
|
3705
3943
|
return;
|
|
3706
3944
|
}
|
|
@@ -3763,7 +4001,7 @@ var DiskLayer = class {
|
|
|
3763
4001
|
}
|
|
3764
4002
|
async safeDelete(filePath) {
|
|
3765
4003
|
try {
|
|
3766
|
-
await
|
|
4004
|
+
await fs2.unlink(filePath);
|
|
3767
4005
|
} catch {
|
|
3768
4006
|
}
|
|
3769
4007
|
}
|
|
@@ -3781,7 +4019,7 @@ var DiskLayer = class {
|
|
|
3781
4019
|
}
|
|
3782
4020
|
let entries;
|
|
3783
4021
|
try {
|
|
3784
|
-
entries = await
|
|
4022
|
+
entries = await fs2.readdir(this.directory);
|
|
3785
4023
|
} catch {
|
|
3786
4024
|
return;
|
|
3787
4025
|
}
|
|
@@ -3793,7 +4031,7 @@ var DiskLayer = class {
|
|
|
3793
4031
|
lcFiles.map(async (name) => {
|
|
3794
4032
|
const filePath = join(this.directory, name);
|
|
3795
4033
|
try {
|
|
3796
|
-
const stat = await
|
|
4034
|
+
const stat = await fs2.stat(filePath);
|
|
3797
4035
|
return { filePath, mtimeMs: stat.mtimeMs };
|
|
3798
4036
|
} catch {
|
|
3799
4037
|
return { filePath, mtimeMs: 0 };
|
|
@@ -3889,44 +4127,19 @@ var MemcachedLayer = class {
|
|
|
3889
4127
|
|
|
3890
4128
|
// src/serialization/MsgpackSerializer.ts
|
|
3891
4129
|
import { decode, encode } from "@msgpack/msgpack";
|
|
3892
|
-
var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
3893
|
-
var MAX_SANITIZE_DEPTH3 = 64;
|
|
3894
|
-
var MAX_SANITIZE_NODES3 = 1e4;
|
|
3895
4130
|
var MsgpackSerializer = class {
|
|
3896
4131
|
serialize(value) {
|
|
3897
4132
|
return Buffer.from(encode(value));
|
|
3898
4133
|
}
|
|
3899
4134
|
deserialize(payload) {
|
|
3900
|
-
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
3901
|
-
return
|
|
4135
|
+
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
|
|
4136
|
+
return sanitizeStructuredData(decode(normalized), {
|
|
4137
|
+
label: "MessagePack payload",
|
|
4138
|
+
maxDepth: 64,
|
|
4139
|
+
maxNodes: 1e4
|
|
4140
|
+
});
|
|
3902
4141
|
}
|
|
3903
4142
|
};
|
|
3904
|
-
function sanitizeMsgpackValue(value, depth, state) {
|
|
3905
|
-
state.count += 1;
|
|
3906
|
-
if (state.count > MAX_SANITIZE_NODES3) {
|
|
3907
|
-
throw new Error(`MessagePack payload exceeds max node count of ${MAX_SANITIZE_NODES3}.`);
|
|
3908
|
-
}
|
|
3909
|
-
if (depth > MAX_SANITIZE_DEPTH3) {
|
|
3910
|
-
throw new Error(`MessagePack payload exceeds max depth of ${MAX_SANITIZE_DEPTH3}.`);
|
|
3911
|
-
}
|
|
3912
|
-
if (Array.isArray(value)) {
|
|
3913
|
-
return value.map((entry) => sanitizeMsgpackValue(entry, depth + 1, state));
|
|
3914
|
-
}
|
|
3915
|
-
if (!isPlainObject2(value)) {
|
|
3916
|
-
return value;
|
|
3917
|
-
}
|
|
3918
|
-
const sanitized = {};
|
|
3919
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
3920
|
-
if (DANGEROUS_KEYS2.has(key)) {
|
|
3921
|
-
continue;
|
|
3922
|
-
}
|
|
3923
|
-
sanitized[key] = sanitizeMsgpackValue(entry, depth + 1, state);
|
|
3924
|
-
}
|
|
3925
|
-
return sanitized;
|
|
3926
|
-
}
|
|
3927
|
-
function isPlainObject2(value) {
|
|
3928
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
3929
|
-
}
|
|
3930
4143
|
|
|
3931
4144
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
3932
4145
|
import { randomUUID } from "crypto";
|