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.cjs
CHANGED
|
@@ -62,6 +62,127 @@ var import_node_events = require("events");
|
|
|
62
62
|
|
|
63
63
|
// src/CacheNamespace.ts
|
|
64
64
|
var import_async_mutex = require("async-mutex");
|
|
65
|
+
|
|
66
|
+
// src/internal/CacheNamespaceMetrics.ts
|
|
67
|
+
function createEmptyNamespaceMetrics(resetAt = Date.now()) {
|
|
68
|
+
return {
|
|
69
|
+
hits: 0,
|
|
70
|
+
misses: 0,
|
|
71
|
+
fetches: 0,
|
|
72
|
+
sets: 0,
|
|
73
|
+
deletes: 0,
|
|
74
|
+
backfills: 0,
|
|
75
|
+
invalidations: 0,
|
|
76
|
+
staleHits: 0,
|
|
77
|
+
refreshes: 0,
|
|
78
|
+
refreshErrors: 0,
|
|
79
|
+
writeFailures: 0,
|
|
80
|
+
singleFlightWaits: 0,
|
|
81
|
+
negativeCacheHits: 0,
|
|
82
|
+
circuitBreakerTrips: 0,
|
|
83
|
+
degradedOperations: 0,
|
|
84
|
+
hitsByLayer: {},
|
|
85
|
+
missesByLayer: {},
|
|
86
|
+
latencyByLayer: {},
|
|
87
|
+
resetAt
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function cloneNamespaceMetrics(metrics) {
|
|
91
|
+
return {
|
|
92
|
+
...metrics,
|
|
93
|
+
hitsByLayer: { ...metrics.hitsByLayer },
|
|
94
|
+
missesByLayer: { ...metrics.missesByLayer },
|
|
95
|
+
latencyByLayer: Object.fromEntries(
|
|
96
|
+
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
97
|
+
)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function diffNamespaceMetrics(before, after) {
|
|
101
|
+
const latencyByLayer = Object.fromEntries(
|
|
102
|
+
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
103
|
+
layer,
|
|
104
|
+
{
|
|
105
|
+
avgMs: value.avgMs,
|
|
106
|
+
maxMs: value.maxMs,
|
|
107
|
+
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
108
|
+
}
|
|
109
|
+
])
|
|
110
|
+
);
|
|
111
|
+
return {
|
|
112
|
+
hits: after.hits - before.hits,
|
|
113
|
+
misses: after.misses - before.misses,
|
|
114
|
+
fetches: after.fetches - before.fetches,
|
|
115
|
+
sets: after.sets - before.sets,
|
|
116
|
+
deletes: after.deletes - before.deletes,
|
|
117
|
+
backfills: after.backfills - before.backfills,
|
|
118
|
+
invalidations: after.invalidations - before.invalidations,
|
|
119
|
+
staleHits: after.staleHits - before.staleHits,
|
|
120
|
+
refreshes: after.refreshes - before.refreshes,
|
|
121
|
+
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
122
|
+
writeFailures: after.writeFailures - before.writeFailures,
|
|
123
|
+
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
124
|
+
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
125
|
+
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
126
|
+
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
127
|
+
hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
|
|
128
|
+
missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
|
|
129
|
+
latencyByLayer,
|
|
130
|
+
resetAt: after.resetAt
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function addNamespaceMetrics(base, delta) {
|
|
134
|
+
return {
|
|
135
|
+
hits: base.hits + delta.hits,
|
|
136
|
+
misses: base.misses + delta.misses,
|
|
137
|
+
fetches: base.fetches + delta.fetches,
|
|
138
|
+
sets: base.sets + delta.sets,
|
|
139
|
+
deletes: base.deletes + delta.deletes,
|
|
140
|
+
backfills: base.backfills + delta.backfills,
|
|
141
|
+
invalidations: base.invalidations + delta.invalidations,
|
|
142
|
+
staleHits: base.staleHits + delta.staleHits,
|
|
143
|
+
refreshes: base.refreshes + delta.refreshes,
|
|
144
|
+
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
145
|
+
writeFailures: base.writeFailures + delta.writeFailures,
|
|
146
|
+
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
147
|
+
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
148
|
+
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
149
|
+
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
150
|
+
hitsByLayer: addMetricMap(base.hitsByLayer, delta.hitsByLayer),
|
|
151
|
+
missesByLayer: addMetricMap(base.missesByLayer, delta.missesByLayer),
|
|
152
|
+
latencyByLayer: cloneNamespaceMetrics(delta).latencyByLayer,
|
|
153
|
+
resetAt: base.resetAt
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function computeNamespaceHitRate(metrics) {
|
|
157
|
+
const total = metrics.hits + metrics.misses;
|
|
158
|
+
const overall = total === 0 ? 0 : metrics.hits / total;
|
|
159
|
+
const byLayer = {};
|
|
160
|
+
const layers = /* @__PURE__ */ new Set([...Object.keys(metrics.hitsByLayer), ...Object.keys(metrics.missesByLayer)]);
|
|
161
|
+
for (const layer of layers) {
|
|
162
|
+
const hits = metrics.hitsByLayer[layer] ?? 0;
|
|
163
|
+
const misses = metrics.missesByLayer[layer] ?? 0;
|
|
164
|
+
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
165
|
+
}
|
|
166
|
+
return { overall, byLayer };
|
|
167
|
+
}
|
|
168
|
+
function diffMetricMap(before, after) {
|
|
169
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
170
|
+
const result = {};
|
|
171
|
+
for (const key of keys) {
|
|
172
|
+
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
function addMetricMap(base, delta) {
|
|
177
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
178
|
+
const result = {};
|
|
179
|
+
for (const key of keys) {
|
|
180
|
+
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/CacheNamespace.ts
|
|
65
186
|
var CacheNamespace = class _CacheNamespace {
|
|
66
187
|
constructor(cache, prefix) {
|
|
67
188
|
this.cache = cache;
|
|
@@ -71,7 +192,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
71
192
|
cache;
|
|
72
193
|
prefix;
|
|
73
194
|
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
74
|
-
metrics =
|
|
195
|
+
metrics = createEmptyNamespaceMetrics();
|
|
75
196
|
async get(key, fetcher, options) {
|
|
76
197
|
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
77
198
|
}
|
|
@@ -168,19 +289,10 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
168
289
|
);
|
|
169
290
|
}
|
|
170
291
|
getMetrics() {
|
|
171
|
-
return
|
|
292
|
+
return cloneNamespaceMetrics(this.metrics);
|
|
172
293
|
}
|
|
173
294
|
getHitRate() {
|
|
174
|
-
|
|
175
|
-
const overall = total === 0 ? 0 : this.metrics.hits / total;
|
|
176
|
-
const byLayer = {};
|
|
177
|
-
const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
|
|
178
|
-
for (const layer of layers) {
|
|
179
|
-
const hits = this.metrics.hitsByLayer[layer] ?? 0;
|
|
180
|
-
const misses = this.metrics.missesByLayer[layer] ?? 0;
|
|
181
|
-
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
182
|
-
}
|
|
183
|
-
return { overall, byLayer };
|
|
295
|
+
return computeNamespaceHitRate(this.metrics);
|
|
184
296
|
}
|
|
185
297
|
/**
|
|
186
298
|
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
@@ -221,7 +333,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
221
333
|
const before = this.cache.getMetrics();
|
|
222
334
|
const result = await operation();
|
|
223
335
|
const after = this.cache.getMetrics();
|
|
224
|
-
this.metrics =
|
|
336
|
+
this.metrics = addNamespaceMetrics(this.metrics, diffNamespaceMetrics(before, after));
|
|
225
337
|
return result;
|
|
226
338
|
});
|
|
227
339
|
}
|
|
@@ -235,111 +347,6 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
235
347
|
return mutex;
|
|
236
348
|
}
|
|
237
349
|
};
|
|
238
|
-
function emptyMetrics() {
|
|
239
|
-
return {
|
|
240
|
-
hits: 0,
|
|
241
|
-
misses: 0,
|
|
242
|
-
fetches: 0,
|
|
243
|
-
sets: 0,
|
|
244
|
-
deletes: 0,
|
|
245
|
-
backfills: 0,
|
|
246
|
-
invalidations: 0,
|
|
247
|
-
staleHits: 0,
|
|
248
|
-
refreshes: 0,
|
|
249
|
-
refreshErrors: 0,
|
|
250
|
-
writeFailures: 0,
|
|
251
|
-
singleFlightWaits: 0,
|
|
252
|
-
negativeCacheHits: 0,
|
|
253
|
-
circuitBreakerTrips: 0,
|
|
254
|
-
degradedOperations: 0,
|
|
255
|
-
hitsByLayer: {},
|
|
256
|
-
missesByLayer: {},
|
|
257
|
-
latencyByLayer: {},
|
|
258
|
-
resetAt: Date.now()
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
function cloneMetrics(metrics) {
|
|
262
|
-
return {
|
|
263
|
-
...metrics,
|
|
264
|
-
hitsByLayer: { ...metrics.hitsByLayer },
|
|
265
|
-
missesByLayer: { ...metrics.missesByLayer },
|
|
266
|
-
latencyByLayer: Object.fromEntries(
|
|
267
|
-
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
268
|
-
)
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
function diffMetrics(before, after) {
|
|
272
|
-
const latencyByLayer = Object.fromEntries(
|
|
273
|
-
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
274
|
-
layer,
|
|
275
|
-
{
|
|
276
|
-
avgMs: value.avgMs,
|
|
277
|
-
maxMs: value.maxMs,
|
|
278
|
-
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
279
|
-
}
|
|
280
|
-
])
|
|
281
|
-
);
|
|
282
|
-
return {
|
|
283
|
-
hits: after.hits - before.hits,
|
|
284
|
-
misses: after.misses - before.misses,
|
|
285
|
-
fetches: after.fetches - before.fetches,
|
|
286
|
-
sets: after.sets - before.sets,
|
|
287
|
-
deletes: after.deletes - before.deletes,
|
|
288
|
-
backfills: after.backfills - before.backfills,
|
|
289
|
-
invalidations: after.invalidations - before.invalidations,
|
|
290
|
-
staleHits: after.staleHits - before.staleHits,
|
|
291
|
-
refreshes: after.refreshes - before.refreshes,
|
|
292
|
-
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
293
|
-
writeFailures: after.writeFailures - before.writeFailures,
|
|
294
|
-
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
295
|
-
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
296
|
-
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
297
|
-
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
298
|
-
hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
|
|
299
|
-
missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
|
|
300
|
-
latencyByLayer,
|
|
301
|
-
resetAt: after.resetAt
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
function addMetrics(base, delta) {
|
|
305
|
-
return {
|
|
306
|
-
hits: base.hits + delta.hits,
|
|
307
|
-
misses: base.misses + delta.misses,
|
|
308
|
-
fetches: base.fetches + delta.fetches,
|
|
309
|
-
sets: base.sets + delta.sets,
|
|
310
|
-
deletes: base.deletes + delta.deletes,
|
|
311
|
-
backfills: base.backfills + delta.backfills,
|
|
312
|
-
invalidations: base.invalidations + delta.invalidations,
|
|
313
|
-
staleHits: base.staleHits + delta.staleHits,
|
|
314
|
-
refreshes: base.refreshes + delta.refreshes,
|
|
315
|
-
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
316
|
-
writeFailures: base.writeFailures + delta.writeFailures,
|
|
317
|
-
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
318
|
-
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
319
|
-
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
320
|
-
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
321
|
-
hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
|
|
322
|
-
missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
|
|
323
|
-
latencyByLayer: cloneMetrics(delta).latencyByLayer,
|
|
324
|
-
resetAt: base.resetAt
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
function diffMap(before, after) {
|
|
328
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
329
|
-
const result = {};
|
|
330
|
-
for (const key of keys) {
|
|
331
|
-
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
332
|
-
}
|
|
333
|
-
return result;
|
|
334
|
-
}
|
|
335
|
-
function addMap(base, delta) {
|
|
336
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
337
|
-
const result = {};
|
|
338
|
-
for (const key of keys) {
|
|
339
|
-
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
340
|
-
}
|
|
341
|
-
return result;
|
|
342
|
-
}
|
|
343
350
|
function validateNamespaceKey(key) {
|
|
344
351
|
if (key.length === 0) {
|
|
345
352
|
throw new Error("Namespace prefix must not be empty.");
|
|
@@ -549,101 +556,781 @@ function createInstanceId() {
|
|
|
549
556
|
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
550
557
|
}
|
|
551
558
|
|
|
552
|
-
// src/internal/
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
return
|
|
559
|
+
// src/internal/CacheStackGeneration.ts
|
|
560
|
+
var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
|
|
561
|
+
function generationPrefix(generation) {
|
|
562
|
+
return generation === void 0 ? "" : `v${generation}:`;
|
|
556
563
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const parent = path.dirname(current);
|
|
569
|
-
if (parent === current) {
|
|
570
|
-
return current;
|
|
571
|
-
}
|
|
572
|
-
current = parent;
|
|
564
|
+
function qualifyGenerationKey(key, generation) {
|
|
565
|
+
const prefix = generationPrefix(generation);
|
|
566
|
+
return prefix ? `${prefix}${key}` : key;
|
|
567
|
+
}
|
|
568
|
+
function qualifyGenerationPattern(pattern, generation) {
|
|
569
|
+
return qualifyGenerationKey(pattern, generation);
|
|
570
|
+
}
|
|
571
|
+
function stripGenerationPrefix(key, generation) {
|
|
572
|
+
const prefix = generationPrefix(generation);
|
|
573
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
574
|
+
return key;
|
|
573
575
|
}
|
|
576
|
+
return key.slice(prefix.length);
|
|
574
577
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
+
function resolveGenerationCleanupTarget({
|
|
579
|
+
previousGeneration,
|
|
580
|
+
nextGeneration,
|
|
581
|
+
generationCleanup
|
|
582
|
+
}) {
|
|
583
|
+
if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
|
|
584
|
+
return null;
|
|
578
585
|
}
|
|
579
|
-
|
|
580
|
-
|
|
586
|
+
return previousGeneration;
|
|
587
|
+
}
|
|
588
|
+
function resolveGenerationCleanupBatchSize(generationCleanup) {
|
|
589
|
+
if (typeof generationCleanup !== "object" || generationCleanup === null) {
|
|
590
|
+
return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
581
591
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
return resolved;
|
|
592
|
+
return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
593
|
+
}
|
|
594
|
+
function planGenerationCleanupBatches(keys, generationCleanup) {
|
|
595
|
+
if (keys.length === 0) {
|
|
596
|
+
return [];
|
|
588
597
|
}
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
598
|
+
const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
|
|
599
|
+
const batches = [];
|
|
600
|
+
for (let index = 0; index < keys.length; index += batchSize) {
|
|
601
|
+
batches.push(keys.slice(index, index + batchSize));
|
|
593
602
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
603
|
+
return batches;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/internal/CacheStackInvalidationSupport.ts
|
|
607
|
+
var CacheStackInvalidationSupport = class {
|
|
608
|
+
constructor(options) {
|
|
609
|
+
this.options = options;
|
|
600
610
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
611
|
+
options;
|
|
612
|
+
async collectKeysForTag(tag, maxKeys) {
|
|
613
|
+
const keys = /* @__PURE__ */ new Set();
|
|
614
|
+
if (this.options.tagIndex.forEachKeyForTag) {
|
|
615
|
+
await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
616
|
+
keys.add(key);
|
|
617
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
618
|
+
});
|
|
619
|
+
return [...keys];
|
|
620
|
+
}
|
|
621
|
+
for (const key of await this.options.tagIndex.keysForTag(tag)) {
|
|
622
|
+
keys.add(key);
|
|
623
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
624
|
+
}
|
|
625
|
+
return [...keys];
|
|
606
626
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
627
|
+
intersectKeys(groups) {
|
|
628
|
+
if (groups.length === 0) {
|
|
629
|
+
return [];
|
|
630
|
+
}
|
|
631
|
+
const [firstGroup, ...rest] = groups;
|
|
632
|
+
const restSets = rest.map((group) => new Set(group));
|
|
633
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
611
634
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
635
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
636
|
+
await Promise.all(
|
|
637
|
+
layers.map(async (layer) => {
|
|
638
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (layer.deleteMany) {
|
|
642
|
+
try {
|
|
643
|
+
await layer.deleteMany(keys);
|
|
644
|
+
} catch (error) {
|
|
645
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
646
|
+
}
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
await Promise.all(
|
|
650
|
+
keys.map(async (key) => {
|
|
651
|
+
try {
|
|
652
|
+
await layer.delete(key);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
655
|
+
}
|
|
656
|
+
})
|
|
657
|
+
);
|
|
658
|
+
})
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
assertWithinInvalidationKeyLimit(size, maxKeys) {
|
|
662
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
663
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
// src/internal/StoredValue.ts
|
|
669
|
+
function isStoredValueEnvelope(value) {
|
|
670
|
+
if (typeof value !== "object" || value === null) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
const v = value;
|
|
674
|
+
if (v.__layercache !== 1) {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
if (v.kind !== "value" && v.kind !== "empty") {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
690
|
+
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
|
|
709
|
+
if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
function createStoredValueEnvelope(options) {
|
|
724
|
+
const now = options.now ?? Date.now();
|
|
725
|
+
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
726
|
+
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
727
|
+
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
728
|
+
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
729
|
+
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
730
|
+
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
731
|
+
return {
|
|
732
|
+
__layercache: 1,
|
|
733
|
+
kind: options.kind,
|
|
734
|
+
value: options.value,
|
|
735
|
+
freshUntil,
|
|
736
|
+
staleUntil,
|
|
737
|
+
errorUntil,
|
|
738
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
739
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
740
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function resolveStoredValue(stored, now = Date.now()) {
|
|
744
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
745
|
+
return { state: "fresh", value: stored, stored };
|
|
746
|
+
}
|
|
747
|
+
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
748
|
+
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
749
|
+
}
|
|
750
|
+
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
751
|
+
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
752
|
+
}
|
|
753
|
+
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
754
|
+
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
755
|
+
}
|
|
756
|
+
return { state: "expired", value: null, stored, envelope: stored };
|
|
757
|
+
}
|
|
758
|
+
function unwrapStoredValue(stored) {
|
|
759
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
760
|
+
return stored;
|
|
761
|
+
}
|
|
762
|
+
if (stored.kind === "empty") {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
return stored.value ?? null;
|
|
766
|
+
}
|
|
767
|
+
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
768
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
769
|
+
return void 0;
|
|
770
|
+
}
|
|
771
|
+
const expiry = maxExpiry(stored);
|
|
772
|
+
if (expiry === null) {
|
|
773
|
+
return void 0;
|
|
774
|
+
}
|
|
775
|
+
const remainingMs = expiry - now;
|
|
776
|
+
if (remainingMs <= 0) {
|
|
777
|
+
return 1;
|
|
778
|
+
}
|
|
779
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
780
|
+
}
|
|
781
|
+
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
782
|
+
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
783
|
+
return void 0;
|
|
784
|
+
}
|
|
785
|
+
const remainingMs = stored.freshUntil - now;
|
|
786
|
+
if (remainingMs <= 0) {
|
|
787
|
+
return 0;
|
|
788
|
+
}
|
|
789
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
790
|
+
}
|
|
791
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
792
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
793
|
+
return stored;
|
|
794
|
+
}
|
|
795
|
+
return createStoredValueEnvelope({
|
|
796
|
+
kind: stored.kind,
|
|
797
|
+
value: stored.value,
|
|
798
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
799
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
800
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
801
|
+
now
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
function maxExpiry(stored) {
|
|
805
|
+
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
806
|
+
(value) => value !== null
|
|
807
|
+
);
|
|
808
|
+
if (values.length === 0) {
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
return Math.max(...values);
|
|
812
|
+
}
|
|
813
|
+
function normalizePositiveSeconds(value) {
|
|
814
|
+
if (!value || value <= 0) {
|
|
815
|
+
return void 0;
|
|
816
|
+
}
|
|
817
|
+
return value;
|
|
818
|
+
}
|
|
819
|
+
function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
|
|
820
|
+
if (value == null) {
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/internal/CacheStackLayerWriter.ts
|
|
827
|
+
var CacheStackLayerWriter = class {
|
|
828
|
+
constructor(options) {
|
|
829
|
+
this.options = options;
|
|
830
|
+
}
|
|
831
|
+
options;
|
|
832
|
+
async writeAcrossLayers(key, kind, value, writeOptions) {
|
|
833
|
+
const now = Date.now();
|
|
834
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
835
|
+
const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
|
|
836
|
+
const immediateOperations = [];
|
|
837
|
+
const deferredOperations = [];
|
|
838
|
+
for (const layer of this.options.layers) {
|
|
839
|
+
const operation = async () => {
|
|
840
|
+
if (this.options.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, writeOptions, now);
|
|
847
|
+
try {
|
|
848
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
854
|
+
deferredOperations.push(operation);
|
|
855
|
+
} else {
|
|
856
|
+
immediateOperations.push(operation);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
860
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
861
|
+
}
|
|
862
|
+
async writeBatch(entries) {
|
|
863
|
+
const now = Date.now();
|
|
864
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
865
|
+
const entryEpochs = new Map(
|
|
866
|
+
entries.map((entry) => [entry.key, this.options.maintenance.currentKeyEpoch(entry.key)])
|
|
867
|
+
);
|
|
868
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
869
|
+
const immediateOperations = [];
|
|
870
|
+
const deferredOperations = [];
|
|
871
|
+
for (const entry of entries) {
|
|
872
|
+
for (const layer of this.options.layers) {
|
|
873
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
877
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
878
|
+
bucket.push(layerEntry);
|
|
879
|
+
entriesByLayer.set(layer, bucket);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
883
|
+
const operation = async () => {
|
|
884
|
+
if (clearEpoch !== this.options.maintenance.currentClearEpoch()) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const activeEntries = layerEntries.filter(
|
|
888
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.options.maintenance.currentKeyEpoch(entry.key)
|
|
889
|
+
);
|
|
890
|
+
if (activeEntries.length === 0) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
try {
|
|
894
|
+
if (layer.setMany) {
|
|
895
|
+
await layer.setMany(activeEntries);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
899
|
+
} catch (error) {
|
|
900
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
904
|
+
deferredOperations.push(operation);
|
|
905
|
+
} else {
|
|
906
|
+
immediateOperations.push(operation);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
910
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
911
|
+
return { clearEpoch, entryEpochs };
|
|
912
|
+
}
|
|
913
|
+
async executeLayerOperations(operations, context) {
|
|
914
|
+
if (this.options.writePolicy !== "best-effort") {
|
|
915
|
+
await Promise.all(operations.map((operation) => operation()));
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
919
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
920
|
+
if (failures.length === 0) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
this.options.onWriteFailures(
|
|
924
|
+
context,
|
|
925
|
+
failures.map((failure) => failure.reason)
|
|
926
|
+
);
|
|
927
|
+
if (failures.length === operations.length) {
|
|
928
|
+
throw new AggregateError(
|
|
929
|
+
failures.map((failure) => failure.reason),
|
|
930
|
+
`${context.action} failed for every cache layer`
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
|
|
935
|
+
const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
|
|
936
|
+
const staleWhileRevalidate = this.options.resolveLayerSeconds(
|
|
937
|
+
layer.name,
|
|
938
|
+
writeOptions?.staleWhileRevalidate,
|
|
939
|
+
this.options.globalStaleWhileRevalidate
|
|
940
|
+
);
|
|
941
|
+
const staleIfError = this.options.resolveLayerSeconds(
|
|
942
|
+
layer.name,
|
|
943
|
+
writeOptions?.staleIfError,
|
|
944
|
+
this.options.globalStaleIfError
|
|
945
|
+
);
|
|
946
|
+
const payload = createStoredValueEnvelope({
|
|
947
|
+
kind,
|
|
948
|
+
value,
|
|
949
|
+
freshTtlSeconds: freshTtl,
|
|
950
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
951
|
+
staleIfErrorSeconds: staleIfError,
|
|
952
|
+
now
|
|
953
|
+
});
|
|
954
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
955
|
+
return {
|
|
956
|
+
key,
|
|
957
|
+
value: payload,
|
|
958
|
+
ttl
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
// src/internal/CacheStackMaintenance.ts
|
|
964
|
+
var CacheStackMaintenance = class {
|
|
965
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
966
|
+
writeBehindQueue = [];
|
|
967
|
+
writeBehindTimer;
|
|
968
|
+
writeBehindFlushPromise;
|
|
969
|
+
generationCleanupPromise;
|
|
970
|
+
clearEpoch = 0;
|
|
971
|
+
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
972
|
+
if (writeStrategy !== "write-behind") {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
976
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
this.disposeWriteBehindTimer();
|
|
980
|
+
this.writeBehindTimer = setInterval(() => {
|
|
981
|
+
void flush();
|
|
982
|
+
}, flushIntervalMs);
|
|
983
|
+
this.writeBehindTimer.unref?.();
|
|
984
|
+
}
|
|
985
|
+
disposeWriteBehindTimer() {
|
|
986
|
+
if (!this.writeBehindTimer) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
clearInterval(this.writeBehindTimer);
|
|
990
|
+
this.writeBehindTimer = void 0;
|
|
991
|
+
}
|
|
992
|
+
beginClearEpoch() {
|
|
993
|
+
this.clearEpoch += 1;
|
|
994
|
+
this.keyEpochs.clear();
|
|
995
|
+
this.writeBehindQueue.length = 0;
|
|
996
|
+
}
|
|
997
|
+
currentClearEpoch() {
|
|
998
|
+
return this.clearEpoch;
|
|
999
|
+
}
|
|
1000
|
+
currentKeyEpoch(key) {
|
|
1001
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
1002
|
+
}
|
|
1003
|
+
bumpKeyEpochs(keys) {
|
|
1004
|
+
for (const key of keys) {
|
|
1005
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
1009
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
1010
|
+
return true;
|
|
1011
|
+
}
|
|
1012
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
async enqueueWriteBehind(operation, options, flushBatch) {
|
|
1018
|
+
this.writeBehindQueue.push(operation);
|
|
1019
|
+
const batchSize = options?.batchSize ?? 100;
|
|
1020
|
+
const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
|
|
1021
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
1022
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
1026
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
async flushWriteBehindQueue(options, flushBatch) {
|
|
1030
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
1031
|
+
await this.writeBehindFlushPromise;
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const batchSize = options?.batchSize ?? 100;
|
|
1035
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
1036
|
+
this.writeBehindFlushPromise = flushBatch(batch);
|
|
1037
|
+
try {
|
|
1038
|
+
await this.writeBehindFlushPromise;
|
|
1039
|
+
} finally {
|
|
1040
|
+
this.writeBehindFlushPromise = void 0;
|
|
1041
|
+
}
|
|
1042
|
+
if (this.writeBehindQueue.length > 0) {
|
|
1043
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
scheduleGenerationCleanup(generation, task, onError) {
|
|
1047
|
+
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
1048
|
+
onError(generation, error);
|
|
1049
|
+
});
|
|
1050
|
+
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
1051
|
+
if (this.generationCleanupPromise === scheduledTask) {
|
|
1052
|
+
this.generationCleanupPromise = void 0;
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
async waitForGenerationCleanup() {
|
|
1057
|
+
await this.generationCleanupPromise;
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
// src/internal/CacheStackRuntimePolicy.ts
|
|
1062
|
+
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
1063
|
+
return degradedUntil !== void 0 && degradedUntil > now;
|
|
1064
|
+
}
|
|
1065
|
+
function shouldStartBackgroundRefresh({
|
|
1066
|
+
isDisconnecting,
|
|
1067
|
+
hasRefreshInFlight
|
|
1068
|
+
}) {
|
|
1069
|
+
return !isDisconnecting && !hasRefreshInFlight;
|
|
1070
|
+
}
|
|
1071
|
+
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
1072
|
+
if (!gracefulDegradation) {
|
|
1073
|
+
return { degrade: false };
|
|
1074
|
+
}
|
|
1075
|
+
const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1076
|
+
return {
|
|
1077
|
+
degrade: true,
|
|
1078
|
+
degradedUntil: now + retryAfterMs
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
function planFreshReadPolicies({
|
|
1082
|
+
stored,
|
|
1083
|
+
hasFetcher,
|
|
1084
|
+
slidingTtl,
|
|
1085
|
+
refreshAheadSeconds
|
|
1086
|
+
}) {
|
|
1087
|
+
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
1088
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
1089
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
1090
|
+
return {
|
|
1091
|
+
refreshedStored,
|
|
1092
|
+
refreshedStoredTtl,
|
|
1093
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// src/internal/CacheStackSnapshotManager.ts
|
|
1098
|
+
var import_node_fs = require("fs");
|
|
1099
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
1100
|
+
|
|
1101
|
+
// src/internal/CacheSnapshotFile.ts
|
|
1102
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
|
|
1103
|
+
const relative = path2.relative(realBaseDir, candidatePath);
|
|
1104
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
|
|
1105
|
+
}
|
|
1106
|
+
async function findExistingAncestor(directory, fs3, path2) {
|
|
1107
|
+
let current = directory;
|
|
1108
|
+
while (true) {
|
|
1109
|
+
try {
|
|
1110
|
+
await fs3.lstat(current);
|
|
1111
|
+
return current;
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
if (error.code !== "ENOENT") {
|
|
1114
|
+
throw error;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
const parent = path2.dirname(current);
|
|
1118
|
+
if (parent === current) {
|
|
1119
|
+
return current;
|
|
1120
|
+
}
|
|
1121
|
+
current = parent;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
1125
|
+
if (filePath.length === 0) {
|
|
1126
|
+
throw new Error("filePath must not be empty.");
|
|
1127
|
+
}
|
|
1128
|
+
if (filePath.includes("\0")) {
|
|
1129
|
+
throw new Error("filePath must not contain null bytes.");
|
|
1130
|
+
}
|
|
1131
|
+
const { promises: fs3 } = await import("fs");
|
|
1132
|
+
const path2 = await import("path");
|
|
1133
|
+
const resolved = path2.resolve(filePath);
|
|
1134
|
+
const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
|
|
1135
|
+
if (baseDir === false) {
|
|
1136
|
+
return resolved;
|
|
1137
|
+
}
|
|
1138
|
+
await fs3.mkdir(baseDir, { recursive: true });
|
|
1139
|
+
const realBaseDir = await fs3.realpath(baseDir);
|
|
1140
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
|
|
1141
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1142
|
+
}
|
|
1143
|
+
if (mode === "read") {
|
|
1144
|
+
const realTarget = await fs3.realpath(resolved);
|
|
1145
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
|
|
1146
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1147
|
+
}
|
|
1148
|
+
return realTarget;
|
|
1149
|
+
}
|
|
1150
|
+
const parentDir = path2.dirname(resolved);
|
|
1151
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs3, path2);
|
|
1152
|
+
const realExistingAncestor = await fs3.realpath(existingAncestor);
|
|
1153
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
|
|
1154
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1155
|
+
}
|
|
1156
|
+
await fs3.mkdir(parentDir, { recursive: true });
|
|
1157
|
+
const realParentDir = await fs3.realpath(parentDir);
|
|
1158
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
|
|
1159
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1160
|
+
}
|
|
1161
|
+
const targetPath = path2.join(realParentDir, path2.basename(resolved));
|
|
1162
|
+
try {
|
|
1163
|
+
const existing = await fs3.lstat(targetPath);
|
|
1164
|
+
if (existing.isSymbolicLink()) {
|
|
1165
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
1166
|
+
}
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
if (error.code !== "ENOENT") {
|
|
1169
|
+
throw error;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return targetPath;
|
|
1173
|
+
}
|
|
1174
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
1175
|
+
if (byteLimit === false) {
|
|
1176
|
+
return handle.readFile({ encoding: "utf8" });
|
|
1177
|
+
}
|
|
1178
|
+
const chunks = [];
|
|
1179
|
+
let totalBytes = 0;
|
|
1180
|
+
let position = 0;
|
|
1181
|
+
while (true) {
|
|
1182
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
1183
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
1184
|
+
if (bytesRead === 0) {
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
totalBytes += bytesRead;
|
|
1188
|
+
if (totalBytes > byteLimit) {
|
|
1189
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
1190
|
+
}
|
|
1191
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
1192
|
+
position += bytesRead;
|
|
1193
|
+
}
|
|
1194
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// src/internal/CacheStackSnapshotManager.ts
|
|
1198
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1199
|
+
var CacheStackSnapshotManager = class {
|
|
1200
|
+
constructor(options) {
|
|
1201
|
+
this.options = options;
|
|
1202
|
+
}
|
|
1203
|
+
options;
|
|
1204
|
+
async exportState(maxEntries) {
|
|
1205
|
+
const entries = [];
|
|
1206
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
1207
|
+
entries.push(entry);
|
|
1208
|
+
});
|
|
1209
|
+
return entries;
|
|
1210
|
+
}
|
|
1211
|
+
async importState(entries) {
|
|
1212
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
1213
|
+
key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
|
|
1214
|
+
value: entry.value,
|
|
1215
|
+
ttl: entry.ttl
|
|
1216
|
+
}));
|
|
1217
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
1218
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
1219
|
+
await Promise.all(
|
|
1220
|
+
batch.map(async (entry) => {
|
|
1221
|
+
await Promise.all(this.options.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1222
|
+
await this.options.tagIndex.touch(entry.key);
|
|
1223
|
+
})
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
async persistToFile(filePath, snapshotBaseDir, maxEntries) {
|
|
1228
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1229
|
+
const tempPath = import_node_path.default.join(
|
|
1230
|
+
import_node_path.default.dirname(targetPath),
|
|
1231
|
+
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
1232
|
+
);
|
|
1233
|
+
let handle;
|
|
1234
|
+
try {
|
|
1235
|
+
handle = await import_node_fs.promises.open(tempPath, "wx");
|
|
1236
|
+
const openedHandle = handle;
|
|
1237
|
+
await openedHandle.writeFile("[", "utf8");
|
|
1238
|
+
let wroteAny = false;
|
|
1239
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
1240
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
1241
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
1242
|
+
wroteAny = true;
|
|
1243
|
+
});
|
|
1244
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
1245
|
+
await openedHandle.close();
|
|
1246
|
+
handle = void 0;
|
|
1247
|
+
await import_node_fs.promises.rename(tempPath, targetPath);
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
await handle?.close().catch(() => void 0);
|
|
1250
|
+
await import_node_fs.promises.unlink(tempPath).catch(() => void 0);
|
|
1251
|
+
throw error;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
|
|
1255
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
|
|
1256
|
+
const handle = await import_node_fs.promises.open(validatedPath, import_node_fs.constants.O_RDONLY | (import_node_fs.constants.O_NOFOLLOW ?? 0));
|
|
1257
|
+
let raw;
|
|
1258
|
+
try {
|
|
1259
|
+
if (maxBytes !== false) {
|
|
1260
|
+
const stat = await handle.stat();
|
|
1261
|
+
if (stat.size > maxBytes) {
|
|
1262
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
raw = await readUtf8HandleWithLimit(handle, maxBytes);
|
|
1266
|
+
} finally {
|
|
1267
|
+
await handle.close();
|
|
1268
|
+
}
|
|
1269
|
+
let parsed;
|
|
1270
|
+
try {
|
|
1271
|
+
parsed = JSON.parse(raw);
|
|
1272
|
+
} catch (cause) {
|
|
1273
|
+
throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
|
|
617
1274
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
throw error;
|
|
1275
|
+
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1276
|
+
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
621
1277
|
}
|
|
1278
|
+
await this.importState(
|
|
1279
|
+
parsed.map((entry) => ({
|
|
1280
|
+
key: entry.key,
|
|
1281
|
+
value: this.sanitizeSnapshotValue(entry.value),
|
|
1282
|
+
ttl: entry.ttl
|
|
1283
|
+
}))
|
|
1284
|
+
);
|
|
622
1285
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1286
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
1287
|
+
const exported = /* @__PURE__ */ new Set();
|
|
1288
|
+
for (const layer of this.options.layers) {
|
|
1289
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
const visitKey = async (key) => {
|
|
1293
|
+
const exportedKey = this.options.stripQualifiedKey(key);
|
|
1294
|
+
if (exported.has(exportedKey)) {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
const stored = await this.options.readLayerEntry(layer, key);
|
|
1298
|
+
if (stored === null) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
exported.add(exportedKey);
|
|
1302
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
1303
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
1304
|
+
}
|
|
1305
|
+
await visitor({
|
|
1306
|
+
key: exportedKey,
|
|
1307
|
+
value: stored,
|
|
1308
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
1309
|
+
});
|
|
1310
|
+
};
|
|
1311
|
+
if (layer.forEachKey) {
|
|
1312
|
+
await layer.forEachKey(visitKey);
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const keys = await layer.keys?.();
|
|
1316
|
+
for (const key of keys ?? []) {
|
|
1317
|
+
await visitKey(key);
|
|
1318
|
+
}
|
|
641
1319
|
}
|
|
642
|
-
chunks.push(buffer.subarray(0, bytesRead));
|
|
643
|
-
position += bytesRead;
|
|
644
1320
|
}
|
|
645
|
-
|
|
646
|
-
|
|
1321
|
+
isCacheSnapshotEntries(value) {
|
|
1322
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1323
|
+
if (!entry || typeof entry !== "object") {
|
|
1324
|
+
return false;
|
|
1325
|
+
}
|
|
1326
|
+
const candidate = entry;
|
|
1327
|
+
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
sanitizeSnapshotValue(value) {
|
|
1331
|
+
return this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
647
1334
|
|
|
648
1335
|
// src/internal/CacheStackValidation.ts
|
|
649
1336
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
@@ -798,7 +1485,6 @@ var CircuitBreakerManager = class {
|
|
|
798
1485
|
if (!options) {
|
|
799
1486
|
return;
|
|
800
1487
|
}
|
|
801
|
-
this.pruneIfNeeded();
|
|
802
1488
|
const failureThreshold = options.failureThreshold ?? 3;
|
|
803
1489
|
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
804
1490
|
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
|
|
@@ -807,6 +1493,7 @@ var CircuitBreakerManager = class {
|
|
|
807
1493
|
state.openUntil = Date.now() + cooldownMs;
|
|
808
1494
|
}
|
|
809
1495
|
this.breakers.set(key, state);
|
|
1496
|
+
this.pruneIfNeeded();
|
|
810
1497
|
}
|
|
811
1498
|
recordSuccess(key) {
|
|
812
1499
|
this.breakers.delete(key);
|
|
@@ -872,7 +1559,11 @@ var FetchRateLimiter = class {
|
|
|
872
1559
|
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
873
1560
|
nextFetcherBucketId = 0;
|
|
874
1561
|
drainTimer;
|
|
1562
|
+
isDisposed = false;
|
|
875
1563
|
async schedule(options, context, task) {
|
|
1564
|
+
if (this.isDisposed) {
|
|
1565
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1566
|
+
}
|
|
876
1567
|
if (!options) {
|
|
877
1568
|
return task();
|
|
878
1569
|
}
|
|
@@ -895,6 +1586,27 @@ var FetchRateLimiter = class {
|
|
|
895
1586
|
this.drain();
|
|
896
1587
|
});
|
|
897
1588
|
}
|
|
1589
|
+
dispose() {
|
|
1590
|
+
this.isDisposed = true;
|
|
1591
|
+
if (this.drainTimer) {
|
|
1592
|
+
clearTimeout(this.drainTimer);
|
|
1593
|
+
this.drainTimer = void 0;
|
|
1594
|
+
}
|
|
1595
|
+
for (const bucket of this.buckets.values()) {
|
|
1596
|
+
if (bucket.cleanupTimer) {
|
|
1597
|
+
clearTimeout(bucket.cleanupTimer);
|
|
1598
|
+
bucket.cleanupTimer = void 0;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
for (const queue of this.queuesByBucket.values()) {
|
|
1602
|
+
for (const item of queue) {
|
|
1603
|
+
item.reject(new Error("FetchRateLimiter has been disposed."));
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
this.queuesByBucket.clear();
|
|
1607
|
+
this.pendingBuckets.clear();
|
|
1608
|
+
this.buckets.clear();
|
|
1609
|
+
}
|
|
898
1610
|
normalize(options) {
|
|
899
1611
|
const maxConcurrent = options.maxConcurrent;
|
|
900
1612
|
const intervalMs = options.intervalMs;
|
|
@@ -930,6 +1642,9 @@ var FetchRateLimiter = class {
|
|
|
930
1642
|
return "global";
|
|
931
1643
|
}
|
|
932
1644
|
drain() {
|
|
1645
|
+
if (this.isDisposed) {
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
933
1648
|
if (this.drainTimer) {
|
|
934
1649
|
clearTimeout(this.drainTimer);
|
|
935
1650
|
this.drainTimer = void 0;
|
|
@@ -1026,6 +1741,9 @@ var FetchRateLimiter = class {
|
|
|
1026
1741
|
}
|
|
1027
1742
|
}
|
|
1028
1743
|
bucketState(bucketKey) {
|
|
1744
|
+
if (this.isDisposed) {
|
|
1745
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1746
|
+
}
|
|
1029
1747
|
const existing = this.buckets.get(bucketKey);
|
|
1030
1748
|
if (existing) {
|
|
1031
1749
|
return existing;
|
|
@@ -1150,164 +1868,6 @@ var MetricsCollector = class {
|
|
|
1150
1868
|
}
|
|
1151
1869
|
};
|
|
1152
1870
|
|
|
1153
|
-
// src/internal/StoredValue.ts
|
|
1154
|
-
function isStoredValueEnvelope(value) {
|
|
1155
|
-
if (typeof value !== "object" || value === null) {
|
|
1156
|
-
return false;
|
|
1157
|
-
}
|
|
1158
|
-
const v = value;
|
|
1159
|
-
if (v.__layercache !== 1) {
|
|
1160
|
-
return false;
|
|
1161
|
-
}
|
|
1162
|
-
if (v.kind !== "value" && v.kind !== "empty") {
|
|
1163
|
-
return false;
|
|
1164
|
-
}
|
|
1165
|
-
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
1166
|
-
return false;
|
|
1167
|
-
}
|
|
1168
|
-
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
1169
|
-
return false;
|
|
1170
|
-
}
|
|
1171
|
-
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
1172
|
-
return false;
|
|
1173
|
-
}
|
|
1174
|
-
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
1175
|
-
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
1176
|
-
return false;
|
|
1177
|
-
}
|
|
1178
|
-
if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
|
|
1179
|
-
return false;
|
|
1180
|
-
}
|
|
1181
|
-
if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
|
|
1182
|
-
return false;
|
|
1183
|
-
}
|
|
1184
|
-
if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
|
|
1185
|
-
return false;
|
|
1186
|
-
}
|
|
1187
|
-
if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
|
|
1188
|
-
return false;
|
|
1189
|
-
}
|
|
1190
|
-
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
1191
|
-
return false;
|
|
1192
|
-
}
|
|
1193
|
-
const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
|
|
1194
|
-
if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
|
|
1195
|
-
return false;
|
|
1196
|
-
}
|
|
1197
|
-
if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
|
|
1198
|
-
return false;
|
|
1199
|
-
}
|
|
1200
|
-
if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
|
|
1201
|
-
return false;
|
|
1202
|
-
}
|
|
1203
|
-
if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
|
|
1204
|
-
return false;
|
|
1205
|
-
}
|
|
1206
|
-
return true;
|
|
1207
|
-
}
|
|
1208
|
-
function createStoredValueEnvelope(options) {
|
|
1209
|
-
const now = options.now ?? Date.now();
|
|
1210
|
-
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
1211
|
-
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
1212
|
-
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
1213
|
-
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
1214
|
-
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
1215
|
-
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
1216
|
-
return {
|
|
1217
|
-
__layercache: 1,
|
|
1218
|
-
kind: options.kind,
|
|
1219
|
-
value: options.value,
|
|
1220
|
-
freshUntil,
|
|
1221
|
-
staleUntil,
|
|
1222
|
-
errorUntil,
|
|
1223
|
-
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
1224
|
-
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
1225
|
-
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
|
-
function resolveStoredValue(stored, now = Date.now()) {
|
|
1229
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1230
|
-
return { state: "fresh", value: stored, stored };
|
|
1231
|
-
}
|
|
1232
|
-
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
1233
|
-
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1234
|
-
}
|
|
1235
|
-
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
1236
|
-
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1237
|
-
}
|
|
1238
|
-
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
1239
|
-
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1240
|
-
}
|
|
1241
|
-
return { state: "expired", value: null, stored, envelope: stored };
|
|
1242
|
-
}
|
|
1243
|
-
function unwrapStoredValue(stored) {
|
|
1244
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1245
|
-
return stored;
|
|
1246
|
-
}
|
|
1247
|
-
if (stored.kind === "empty") {
|
|
1248
|
-
return null;
|
|
1249
|
-
}
|
|
1250
|
-
return stored.value ?? null;
|
|
1251
|
-
}
|
|
1252
|
-
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
1253
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1254
|
-
return void 0;
|
|
1255
|
-
}
|
|
1256
|
-
const expiry = maxExpiry(stored);
|
|
1257
|
-
if (expiry === null) {
|
|
1258
|
-
return void 0;
|
|
1259
|
-
}
|
|
1260
|
-
const remainingMs = expiry - now;
|
|
1261
|
-
if (remainingMs <= 0) {
|
|
1262
|
-
return 1;
|
|
1263
|
-
}
|
|
1264
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
1265
|
-
}
|
|
1266
|
-
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
1267
|
-
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
1268
|
-
return void 0;
|
|
1269
|
-
}
|
|
1270
|
-
const remainingMs = stored.freshUntil - now;
|
|
1271
|
-
if (remainingMs <= 0) {
|
|
1272
|
-
return 0;
|
|
1273
|
-
}
|
|
1274
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
1275
|
-
}
|
|
1276
|
-
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
1277
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1278
|
-
return stored;
|
|
1279
|
-
}
|
|
1280
|
-
return createStoredValueEnvelope({
|
|
1281
|
-
kind: stored.kind,
|
|
1282
|
-
value: stored.value,
|
|
1283
|
-
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
1284
|
-
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
1285
|
-
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
1286
|
-
now
|
|
1287
|
-
});
|
|
1288
|
-
}
|
|
1289
|
-
function maxExpiry(stored) {
|
|
1290
|
-
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
1291
|
-
(value) => value !== null
|
|
1292
|
-
);
|
|
1293
|
-
if (values.length === 0) {
|
|
1294
|
-
return null;
|
|
1295
|
-
}
|
|
1296
|
-
return Math.max(...values);
|
|
1297
|
-
}
|
|
1298
|
-
function normalizePositiveSeconds(value) {
|
|
1299
|
-
if (!value || value <= 0) {
|
|
1300
|
-
return void 0;
|
|
1301
|
-
}
|
|
1302
|
-
return value;
|
|
1303
|
-
}
|
|
1304
|
-
function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
|
|
1305
|
-
if (value == null) {
|
|
1306
|
-
return true;
|
|
1307
|
-
}
|
|
1308
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
1871
|
// src/internal/TtlResolver.ts
|
|
1312
1872
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
1313
1873
|
var TtlResolver = class {
|
|
@@ -1642,19 +2202,19 @@ var TagIndex = class {
|
|
|
1642
2202
|
if (!this.knownKeys.delete(key)) {
|
|
1643
2203
|
return;
|
|
1644
2204
|
}
|
|
1645
|
-
const
|
|
2205
|
+
const path2 = [];
|
|
1646
2206
|
let node = this.root;
|
|
1647
2207
|
for (const character of key) {
|
|
1648
2208
|
const child = node.children.get(character);
|
|
1649
2209
|
if (!child) {
|
|
1650
2210
|
return;
|
|
1651
2211
|
}
|
|
1652
|
-
|
|
2212
|
+
path2.push([node, character]);
|
|
1653
2213
|
node = child;
|
|
1654
2214
|
}
|
|
1655
2215
|
node.terminal = false;
|
|
1656
|
-
for (let index =
|
|
1657
|
-
const entry =
|
|
2216
|
+
for (let index = path2.length - 1; index >= 0; index -= 1) {
|
|
2217
|
+
const entry = path2[index];
|
|
1658
2218
|
if (!entry) {
|
|
1659
2219
|
continue;
|
|
1660
2220
|
}
|
|
@@ -1668,39 +2228,31 @@ var TagIndex = class {
|
|
|
1668
2228
|
}
|
|
1669
2229
|
};
|
|
1670
2230
|
|
|
1671
|
-
// src/
|
|
1672
|
-
var
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
}
|
|
1678
|
-
deserialize(payload) {
|
|
1679
|
-
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1680
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1681
|
-
}
|
|
1682
|
-
};
|
|
1683
|
-
var MAX_SANITIZE_DEPTH = 200;
|
|
1684
|
-
function sanitizeJsonValue(value, depth, state) {
|
|
2231
|
+
// src/internal/StructuredDataSanitizer.ts
|
|
2232
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2233
|
+
function sanitizeStructuredData(value, options) {
|
|
2234
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
2235
|
+
}
|
|
2236
|
+
function sanitizeValue(value, depth, state, options) {
|
|
1685
2237
|
state.count += 1;
|
|
1686
|
-
if (state.count >
|
|
1687
|
-
throw new Error(
|
|
2238
|
+
if (state.count > options.maxNodes) {
|
|
2239
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
1688
2240
|
}
|
|
1689
|
-
if (depth >
|
|
1690
|
-
throw new Error(
|
|
2241
|
+
if (depth > options.maxDepth) {
|
|
2242
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
1691
2243
|
}
|
|
1692
2244
|
if (Array.isArray(value)) {
|
|
1693
|
-
return value.map((entry) =>
|
|
2245
|
+
return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
|
|
1694
2246
|
}
|
|
1695
2247
|
if (!isPlainObject(value)) {
|
|
1696
2248
|
return value;
|
|
1697
2249
|
}
|
|
1698
|
-
const sanitized = {};
|
|
2250
|
+
const sanitized = options.createObject?.() ?? {};
|
|
1699
2251
|
for (const [key, entry] of Object.entries(value)) {
|
|
1700
|
-
if (
|
|
2252
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
1701
2253
|
continue;
|
|
1702
2254
|
}
|
|
1703
|
-
sanitized[key] =
|
|
2255
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
1704
2256
|
}
|
|
1705
2257
|
return sanitized;
|
|
1706
2258
|
}
|
|
@@ -1708,6 +2260,21 @@ function isPlainObject(value) {
|
|
|
1708
2260
|
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1709
2261
|
}
|
|
1710
2262
|
|
|
2263
|
+
// src/serialization/JsonSerializer.ts
|
|
2264
|
+
var JsonSerializer = class {
|
|
2265
|
+
serialize(value) {
|
|
2266
|
+
return JSON.stringify(value);
|
|
2267
|
+
}
|
|
2268
|
+
deserialize(payload) {
|
|
2269
|
+
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
2270
|
+
return sanitizeStructuredData(JSON.parse(normalized), {
|
|
2271
|
+
label: "JSON payload",
|
|
2272
|
+
maxDepth: 200,
|
|
2273
|
+
maxNodes: 1e4
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
|
|
1711
2278
|
// src/stampede/StampedeGuard.ts
|
|
1712
2279
|
var import_async_mutex2 = require("async-mutex");
|
|
1713
2280
|
var StampedeGuard = class {
|
|
@@ -1752,7 +2319,6 @@ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
|
1752
2319
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1753
2320
|
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1754
2321
|
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1755
|
-
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1756
2322
|
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
1757
2323
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1758
2324
|
var DebugLogger = class {
|
|
@@ -1809,6 +2375,35 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1809
2375
|
await this.handleLayerFailure(layer, operation, error);
|
|
1810
2376
|
}
|
|
1811
2377
|
});
|
|
2378
|
+
this.invalidation = new CacheStackInvalidationSupport({
|
|
2379
|
+
tagIndex: this.tagIndex,
|
|
2380
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2381
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
2382
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2383
|
+
}
|
|
2384
|
+
});
|
|
2385
|
+
this.layerWriter = new CacheStackLayerWriter({
|
|
2386
|
+
layers: this.layers,
|
|
2387
|
+
maintenance: this.maintenance,
|
|
2388
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2389
|
+
shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
|
|
2390
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
2391
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2392
|
+
},
|
|
2393
|
+
enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
|
|
2394
|
+
resolveFreshTtl: this.resolveFreshTtl.bind(this),
|
|
2395
|
+
resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
|
|
2396
|
+
globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
|
|
2397
|
+
globalStaleIfError: this.options.staleIfError,
|
|
2398
|
+
writePolicy: this.options.writePolicy,
|
|
2399
|
+
onWriteFailures: (context, failures) => {
|
|
2400
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2401
|
+
this.logger.debug?.("write-failure", {
|
|
2402
|
+
...context,
|
|
2403
|
+
failures: failures.map((failure) => this.formatError(failure))
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
});
|
|
1812
2407
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
1813
2408
|
this.logger.warn?.(
|
|
1814
2409
|
"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."
|
|
@@ -1824,6 +2419,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1824
2419
|
"broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
|
|
1825
2420
|
);
|
|
1826
2421
|
}
|
|
2422
|
+
this.snapshots = new CacheStackSnapshotManager({
|
|
2423
|
+
layers: this.layers,
|
|
2424
|
+
tagIndex: this.tagIndex,
|
|
2425
|
+
snapshotSerializer: this.snapshotSerializer,
|
|
2426
|
+
readLayerEntry: this.readLayerEntry.bind(this),
|
|
2427
|
+
qualifyKey: this.qualifyKey.bind(this),
|
|
2428
|
+
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
2429
|
+
validateCacheKey,
|
|
2430
|
+
formatError: this.formatError.bind(this)
|
|
2431
|
+
});
|
|
1827
2432
|
this.initializeWriteBehind(options.writeBehind);
|
|
1828
2433
|
this.startup = this.initialize();
|
|
1829
2434
|
}
|
|
@@ -1839,17 +2444,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1839
2444
|
keyDiscovery;
|
|
1840
2445
|
fetchRateLimiter = new FetchRateLimiter();
|
|
1841
2446
|
snapshotSerializer = new JsonSerializer();
|
|
2447
|
+
invalidation;
|
|
2448
|
+
layerWriter;
|
|
2449
|
+
snapshots;
|
|
1842
2450
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1843
2451
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1844
|
-
|
|
2452
|
+
maintenance = new CacheStackMaintenance();
|
|
1845
2453
|
ttlResolver;
|
|
1846
2454
|
circuitBreakerManager;
|
|
2455
|
+
nextOperationId = 0;
|
|
1847
2456
|
currentGeneration;
|
|
1848
|
-
writeBehindQueue = [];
|
|
1849
|
-
writeBehindTimer;
|
|
1850
|
-
writeBehindFlushPromise;
|
|
1851
|
-
generationCleanupPromise;
|
|
1852
|
-
clearEpoch = 0;
|
|
1853
2457
|
isDisconnecting = false;
|
|
1854
2458
|
disconnectPromise;
|
|
1855
2459
|
/**
|
|
@@ -1859,10 +2463,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1859
2463
|
* and no `fetcher` is provided.
|
|
1860
2464
|
*/
|
|
1861
2465
|
async get(key, fetcher, options) {
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2466
|
+
return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
|
|
2467
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2468
|
+
this.validateWriteOptions(options);
|
|
2469
|
+
await this.awaitStartup("get");
|
|
2470
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
2471
|
+
});
|
|
1866
2472
|
}
|
|
1867
2473
|
async getPrepared(normalizedKey, fetcher, options) {
|
|
1868
2474
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
@@ -1984,28 +2590,32 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1984
2590
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1985
2591
|
*/
|
|
1986
2592
|
async set(key, value, options) {
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
2593
|
+
await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
|
|
2594
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2595
|
+
this.validateWriteOptions(options);
|
|
2596
|
+
await this.awaitStartup("set");
|
|
2597
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
2598
|
+
});
|
|
1991
2599
|
}
|
|
1992
2600
|
/**
|
|
1993
2601
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1994
2602
|
*/
|
|
1995
2603
|
async delete(key) {
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2604
|
+
await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
|
|
2605
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2606
|
+
await this.awaitStartup("delete");
|
|
2607
|
+
await this.deleteKeys([normalizedKey]);
|
|
2608
|
+
await this.publishInvalidation({
|
|
2609
|
+
scope: "key",
|
|
2610
|
+
keys: [normalizedKey],
|
|
2611
|
+
sourceId: this.instanceId,
|
|
2612
|
+
operation: "delete"
|
|
2613
|
+
});
|
|
2004
2614
|
});
|
|
2005
2615
|
}
|
|
2006
2616
|
async clear() {
|
|
2007
2617
|
await this.awaitStartup("clear");
|
|
2008
|
-
this.beginClearEpoch();
|
|
2618
|
+
this.maintenance.beginClearEpoch();
|
|
2009
2619
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
2010
2620
|
await this.tagIndex.clear();
|
|
2011
2621
|
this.ttlResolver.clearProfiles();
|
|
@@ -2033,95 +2643,99 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2033
2643
|
});
|
|
2034
2644
|
}
|
|
2035
2645
|
async mget(entries) {
|
|
2036
|
-
this.
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2646
|
+
return this.observeOperation("layercache.mget", void 0, async () => {
|
|
2647
|
+
this.assertActive("mget");
|
|
2648
|
+
if (entries.length === 0) {
|
|
2649
|
+
return [];
|
|
2650
|
+
}
|
|
2651
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2652
|
+
...entry,
|
|
2653
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2654
|
+
}));
|
|
2655
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2656
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
2657
|
+
if (!canFastPath) {
|
|
2658
|
+
await this.awaitStartup("mget");
|
|
2659
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
2660
|
+
return Promise.all(
|
|
2661
|
+
normalizedEntries.map((entry) => {
|
|
2662
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
2663
|
+
const existing = pendingReads.get(entry.key);
|
|
2664
|
+
if (!existing) {
|
|
2665
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2666
|
+
pendingReads.set(entry.key, {
|
|
2667
|
+
promise,
|
|
2668
|
+
fetch: entry.fetch,
|
|
2669
|
+
optionsSignature
|
|
2670
|
+
});
|
|
2671
|
+
return promise;
|
|
2672
|
+
}
|
|
2673
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
2674
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
2675
|
+
}
|
|
2676
|
+
return existing.promise;
|
|
2677
|
+
})
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2047
2680
|
await this.awaitStartup("mget");
|
|
2048
|
-
const
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
});
|
|
2060
|
-
return promise;
|
|
2061
|
-
}
|
|
2062
|
-
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
2063
|
-
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
2064
|
-
}
|
|
2065
|
-
return existing.promise;
|
|
2066
|
-
})
|
|
2067
|
-
);
|
|
2068
|
-
}
|
|
2069
|
-
await this.awaitStartup("mget");
|
|
2070
|
-
const pending = /* @__PURE__ */ new Set();
|
|
2071
|
-
const indexesByKey = /* @__PURE__ */ new Map();
|
|
2072
|
-
const resultsByKey = /* @__PURE__ */ new Map();
|
|
2073
|
-
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2074
|
-
const entry = normalizedEntries[index];
|
|
2075
|
-
if (!entry) continue;
|
|
2076
|
-
const key = entry.key;
|
|
2077
|
-
const indexes = indexesByKey.get(key) ?? [];
|
|
2078
|
-
indexes.push(index);
|
|
2079
|
-
indexesByKey.set(key, indexes);
|
|
2080
|
-
pending.add(key);
|
|
2081
|
-
}
|
|
2082
|
-
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2083
|
-
const layer = this.layers[layerIndex];
|
|
2084
|
-
if (!layer) continue;
|
|
2085
|
-
const keys = [...pending];
|
|
2086
|
-
if (keys.length === 0) {
|
|
2087
|
-
break;
|
|
2681
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2682
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
2683
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
2684
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2685
|
+
const entry = normalizedEntries[index];
|
|
2686
|
+
if (!entry) continue;
|
|
2687
|
+
const key = entry.key;
|
|
2688
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
2689
|
+
indexes.push(index);
|
|
2690
|
+
indexesByKey.set(key, indexes);
|
|
2691
|
+
pending.add(key);
|
|
2088
2692
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
const
|
|
2093
|
-
if (
|
|
2094
|
-
|
|
2693
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2694
|
+
const layer = this.layers[layerIndex];
|
|
2695
|
+
if (!layer) continue;
|
|
2696
|
+
const keys = [...pending];
|
|
2697
|
+
if (keys.length === 0) {
|
|
2698
|
+
break;
|
|
2095
2699
|
}
|
|
2096
|
-
const
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2700
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2701
|
+
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2702
|
+
const key = keys[offset];
|
|
2703
|
+
const stored = values[offset];
|
|
2704
|
+
if (!key || stored === null) {
|
|
2705
|
+
continue;
|
|
2706
|
+
}
|
|
2707
|
+
const resolved = resolveStoredValue(stored);
|
|
2708
|
+
if (resolved.state === "expired") {
|
|
2709
|
+
await layer.delete(key);
|
|
2710
|
+
continue;
|
|
2711
|
+
}
|
|
2712
|
+
await this.tagIndex.touch(key);
|
|
2713
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
2714
|
+
resultsByKey.set(key, resolved.value);
|
|
2715
|
+
pending.delete(key);
|
|
2716
|
+
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
2100
2717
|
}
|
|
2101
|
-
await this.tagIndex.touch(key);
|
|
2102
|
-
await this.backfill(key, stored, layerIndex - 1);
|
|
2103
|
-
resultsByKey.set(key, resolved.value);
|
|
2104
|
-
pending.delete(key);
|
|
2105
|
-
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
2106
2718
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2719
|
+
if (pending.size > 0) {
|
|
2720
|
+
for (const key of pending) {
|
|
2721
|
+
await this.tagIndex.remove(key);
|
|
2722
|
+
this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
|
|
2723
|
+
}
|
|
2112
2724
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2725
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
2726
|
+
});
|
|
2115
2727
|
}
|
|
2116
2728
|
async mset(entries) {
|
|
2117
|
-
this.
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2729
|
+
await this.observeOperation("layercache.mset", void 0, async () => {
|
|
2730
|
+
this.assertActive("mset");
|
|
2731
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2732
|
+
...entry,
|
|
2733
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2734
|
+
}));
|
|
2735
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2736
|
+
await this.awaitStartup("mset");
|
|
2737
|
+
await this.writeBatch(normalizedEntries);
|
|
2738
|
+
});
|
|
2125
2739
|
}
|
|
2126
2740
|
async warm(entries, options = {}) {
|
|
2127
2741
|
this.assertActive("warm");
|
|
@@ -2174,40 +2788,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2174
2788
|
return new CacheNamespace(this, prefix);
|
|
2175
2789
|
}
|
|
2176
2790
|
async invalidateByTag(tag) {
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2791
|
+
await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
|
|
2792
|
+
validateTag(tag);
|
|
2793
|
+
await this.awaitStartup("invalidateByTag");
|
|
2794
|
+
const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
|
|
2795
|
+
await this.deleteKeys(keys);
|
|
2796
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2797
|
+
});
|
|
2182
2798
|
}
|
|
2183
2799
|
async invalidateByTags(tags, mode = "any") {
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2800
|
+
await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
|
|
2801
|
+
if (tags.length === 0) {
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
validateTags(tags);
|
|
2805
|
+
await this.awaitStartup("invalidateByTags");
|
|
2806
|
+
const keysByTag = await Promise.all(
|
|
2807
|
+
tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
|
|
2808
|
+
);
|
|
2809
|
+
const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2810
|
+
this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
|
|
2811
|
+
await this.deleteKeys(keys);
|
|
2812
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2813
|
+
});
|
|
2194
2814
|
}
|
|
2195
2815
|
async invalidateByPattern(pattern) {
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
this.
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2816
|
+
await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
|
|
2817
|
+
validatePattern(pattern);
|
|
2818
|
+
await this.awaitStartup("invalidateByPattern");
|
|
2819
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2820
|
+
this.qualifyPattern(pattern),
|
|
2821
|
+
this.invalidationMaxKeys()
|
|
2822
|
+
);
|
|
2823
|
+
await this.deleteKeys(keys);
|
|
2824
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2825
|
+
});
|
|
2204
2826
|
}
|
|
2205
2827
|
async invalidateByPrefix(prefix) {
|
|
2206
|
-
await this.
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2828
|
+
await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
|
|
2829
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
2830
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2831
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
2832
|
+
await this.deleteKeys(keys);
|
|
2833
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2834
|
+
});
|
|
2211
2835
|
}
|
|
2212
2836
|
getMetrics() {
|
|
2213
2837
|
return this.metricsCollector.snapshot;
|
|
@@ -2263,9 +2887,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2263
2887
|
bumpGeneration(nextGeneration) {
|
|
2264
2888
|
const current = this.currentGeneration ?? 0;
|
|
2265
2889
|
const previousGeneration = this.currentGeneration;
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2890
|
+
const updatedGeneration = nextGeneration ?? current + 1;
|
|
2891
|
+
const generationToCleanup = resolveGenerationCleanupTarget({
|
|
2892
|
+
previousGeneration,
|
|
2893
|
+
nextGeneration: updatedGeneration,
|
|
2894
|
+
generationCleanup: this.options.generationCleanup
|
|
2895
|
+
});
|
|
2896
|
+
this.currentGeneration = updatedGeneration;
|
|
2897
|
+
if (generationToCleanup !== null) {
|
|
2898
|
+
this.scheduleGenerationCleanup(generationToCleanup);
|
|
2269
2899
|
}
|
|
2270
2900
|
return this.currentGeneration;
|
|
2271
2901
|
}
|
|
@@ -2312,95 +2942,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2312
2942
|
}
|
|
2313
2943
|
async exportState() {
|
|
2314
2944
|
await this.awaitStartup("exportState");
|
|
2315
|
-
|
|
2316
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2317
|
-
entries.push(entry);
|
|
2318
|
-
});
|
|
2319
|
-
return entries;
|
|
2945
|
+
return this.snapshots.exportState(this.snapshotMaxEntries());
|
|
2320
2946
|
}
|
|
2321
2947
|
async importState(entries) {
|
|
2322
2948
|
await this.awaitStartup("importState");
|
|
2323
|
-
|
|
2324
|
-
key: this.qualifyKey(validateCacheKey(entry.key)),
|
|
2325
|
-
value: entry.value,
|
|
2326
|
-
ttl: entry.ttl
|
|
2327
|
-
}));
|
|
2328
|
-
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
2329
|
-
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
2330
|
-
await Promise.all(
|
|
2331
|
-
batch.map(async (entry) => {
|
|
2332
|
-
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2333
|
-
await this.tagIndex.touch(entry.key);
|
|
2334
|
-
})
|
|
2335
|
-
);
|
|
2336
|
-
}
|
|
2949
|
+
await this.snapshots.importState(entries);
|
|
2337
2950
|
}
|
|
2338
2951
|
async persistToFile(filePath) {
|
|
2339
2952
|
this.assertActive("persistToFile");
|
|
2340
|
-
|
|
2341
|
-
const path = await import("path");
|
|
2342
|
-
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
2343
|
-
const tempPath = path.join(
|
|
2344
|
-
path.dirname(targetPath),
|
|
2345
|
-
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
2346
|
-
);
|
|
2347
|
-
let handle;
|
|
2348
|
-
try {
|
|
2349
|
-
handle = await fs2.open(tempPath, "wx");
|
|
2350
|
-
const openedHandle = handle;
|
|
2351
|
-
await openedHandle.writeFile("[", "utf8");
|
|
2352
|
-
let wroteAny = false;
|
|
2353
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2354
|
-
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
2355
|
-
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
2356
|
-
wroteAny = true;
|
|
2357
|
-
});
|
|
2358
|
-
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
2359
|
-
await openedHandle.close();
|
|
2360
|
-
handle = void 0;
|
|
2361
|
-
await fs2.rename(tempPath, targetPath);
|
|
2362
|
-
} catch (error) {
|
|
2363
|
-
await handle?.close().catch(() => void 0);
|
|
2364
|
-
await fs2.unlink(tempPath).catch(() => void 0);
|
|
2365
|
-
throw error;
|
|
2366
|
-
}
|
|
2953
|
+
await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
|
|
2367
2954
|
}
|
|
2368
2955
|
async restoreFromFile(filePath) {
|
|
2369
2956
|
this.assertActive("restoreFromFile");
|
|
2370
|
-
|
|
2371
|
-
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
2372
|
-
const handle = await fs2.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2373
|
-
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
2374
|
-
let raw;
|
|
2375
|
-
try {
|
|
2376
|
-
if (snapshotMaxBytes !== false) {
|
|
2377
|
-
const stat = await handle.stat();
|
|
2378
|
-
if (stat.size > snapshotMaxBytes) {
|
|
2379
|
-
throw new Error(
|
|
2380
|
-
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
2381
|
-
);
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2384
|
-
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
2385
|
-
} finally {
|
|
2386
|
-
await handle.close();
|
|
2387
|
-
}
|
|
2388
|
-
let parsed;
|
|
2389
|
-
try {
|
|
2390
|
-
parsed = JSON.parse(raw);
|
|
2391
|
-
} catch (cause) {
|
|
2392
|
-
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
2393
|
-
}
|
|
2394
|
-
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
2395
|
-
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
2396
|
-
}
|
|
2397
|
-
await this.importState(
|
|
2398
|
-
parsed.map((entry) => ({
|
|
2399
|
-
key: entry.key,
|
|
2400
|
-
value: this.sanitizeSnapshotValue(entry.value),
|
|
2401
|
-
ttl: entry.ttl
|
|
2402
|
-
}))
|
|
2403
|
-
);
|
|
2957
|
+
await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
|
|
2404
2958
|
}
|
|
2405
2959
|
async disconnect() {
|
|
2406
2960
|
if (!this.disconnectPromise) {
|
|
@@ -2409,12 +2963,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2409
2963
|
await this.startup;
|
|
2410
2964
|
await this.unsubscribeInvalidation?.();
|
|
2411
2965
|
await this.flushWriteBehindQueue();
|
|
2412
|
-
await this.
|
|
2966
|
+
await this.maintenance.waitForGenerationCleanup();
|
|
2413
2967
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
this.writeBehindTimer = void 0;
|
|
2417
|
-
}
|
|
2968
|
+
this.maintenance.disposeWriteBehindTimer();
|
|
2969
|
+
this.fetchRateLimiter.dispose();
|
|
2418
2970
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
2419
2971
|
})();
|
|
2420
2972
|
}
|
|
@@ -2490,13 +3042,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2490
3042
|
if (!this.shouldNegativeCache(options)) {
|
|
2491
3043
|
return null;
|
|
2492
3044
|
}
|
|
2493
|
-
if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
3045
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2494
3046
|
this.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
2495
3047
|
key,
|
|
2496
3048
|
expectedClearEpoch,
|
|
2497
|
-
clearEpoch: this.
|
|
3049
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2498
3050
|
expectedKeyEpoch,
|
|
2499
|
-
keyEpoch: this.currentKeyEpoch(key)
|
|
3051
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2500
3052
|
});
|
|
2501
3053
|
return null;
|
|
2502
3054
|
}
|
|
@@ -2512,13 +3064,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2512
3064
|
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2513
3065
|
}
|
|
2514
3066
|
}
|
|
2515
|
-
if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
3067
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2516
3068
|
this.logger.debug?.("skip-store-after-invalidation", {
|
|
2517
3069
|
key,
|
|
2518
3070
|
expectedClearEpoch,
|
|
2519
|
-
clearEpoch: this.
|
|
3071
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2520
3072
|
expectedKeyEpoch,
|
|
2521
|
-
keyEpoch: this.currentKeyEpoch(key)
|
|
3073
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2522
3074
|
});
|
|
2523
3075
|
return fetched;
|
|
2524
3076
|
}
|
|
@@ -2526,10 +3078,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2526
3078
|
return fetched;
|
|
2527
3079
|
}
|
|
2528
3080
|
async storeEntry(key, kind, value, options) {
|
|
2529
|
-
const clearEpoch = this.
|
|
2530
|
-
const keyEpoch = this.currentKeyEpoch(key);
|
|
2531
|
-
await this.writeAcrossLayers(key, kind, value, options);
|
|
2532
|
-
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
3081
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3082
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3083
|
+
await this.layerWriter.writeAcrossLayers(key, kind, value, options);
|
|
3084
|
+
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2533
3085
|
return;
|
|
2534
3086
|
}
|
|
2535
3087
|
if (options?.tags) {
|
|
@@ -2545,57 +3097,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2545
3097
|
}
|
|
2546
3098
|
}
|
|
2547
3099
|
async writeBatch(entries) {
|
|
2548
|
-
const
|
|
2549
|
-
|
|
2550
|
-
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
|
|
2551
|
-
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2552
|
-
const immediateOperations = [];
|
|
2553
|
-
const deferredOperations = [];
|
|
2554
|
-
for (const entry of entries) {
|
|
2555
|
-
for (const layer of this.layers) {
|
|
2556
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2557
|
-
continue;
|
|
2558
|
-
}
|
|
2559
|
-
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
2560
|
-
const bucket = entriesByLayer.get(layer) ?? [];
|
|
2561
|
-
bucket.push(layerEntry);
|
|
2562
|
-
entriesByLayer.set(layer, bucket);
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2565
|
-
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2566
|
-
const operation = async () => {
|
|
2567
|
-
if (clearEpoch !== this.clearEpoch) {
|
|
2568
|
-
return;
|
|
2569
|
-
}
|
|
2570
|
-
const activeEntries = layerEntries.filter(
|
|
2571
|
-
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
|
|
2572
|
-
);
|
|
2573
|
-
if (activeEntries.length === 0) {
|
|
2574
|
-
return;
|
|
2575
|
-
}
|
|
2576
|
-
try {
|
|
2577
|
-
if (layer.setMany) {
|
|
2578
|
-
await layer.setMany(activeEntries);
|
|
2579
|
-
return;
|
|
2580
|
-
}
|
|
2581
|
-
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2582
|
-
} catch (error) {
|
|
2583
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
2584
|
-
}
|
|
2585
|
-
};
|
|
2586
|
-
if (this.shouldWriteBehind(layer)) {
|
|
2587
|
-
deferredOperations.push(operation);
|
|
2588
|
-
} else {
|
|
2589
|
-
immediateOperations.push(operation);
|
|
2590
|
-
}
|
|
2591
|
-
}
|
|
2592
|
-
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2593
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2594
|
-
if (clearEpoch !== this.clearEpoch) {
|
|
3100
|
+
const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
|
|
3101
|
+
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2595
3102
|
return;
|
|
2596
3103
|
}
|
|
2597
3104
|
for (const entry of entries) {
|
|
2598
|
-
if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
3105
|
+
if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2599
3106
|
continue;
|
|
2600
3107
|
}
|
|
2601
3108
|
if (entry.options?.tags) {
|
|
@@ -2670,83 +3177,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2670
3177
|
return this.handleLayerFailure(layer, "read", error);
|
|
2671
3178
|
}
|
|
2672
3179
|
}
|
|
2673
|
-
try {
|
|
2674
|
-
return await layer.get(key);
|
|
2675
|
-
} catch (error) {
|
|
2676
|
-
return this.handleLayerFailure(layer, "read", error);
|
|
2677
|
-
}
|
|
2678
|
-
}
|
|
2679
|
-
async backfill(key, stored, upToIndex, options) {
|
|
2680
|
-
if (upToIndex < 0) {
|
|
2681
|
-
return;
|
|
2682
|
-
}
|
|
2683
|
-
for (let index = 0; index <= upToIndex; index += 1) {
|
|
2684
|
-
const layer = this.layers[index];
|
|
2685
|
-
if (!layer || this.shouldSkipLayer(layer)) {
|
|
2686
|
-
continue;
|
|
2687
|
-
}
|
|
2688
|
-
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
2689
|
-
try {
|
|
2690
|
-
await layer.set(key, stored, ttl);
|
|
2691
|
-
} catch (error) {
|
|
2692
|
-
await this.handleLayerFailure(layer, "backfill", error);
|
|
2693
|
-
continue;
|
|
2694
|
-
}
|
|
2695
|
-
this.metricsCollector.increment("backfills");
|
|
2696
|
-
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
2697
|
-
this.emit("backfill", { key, layer: layer.name });
|
|
2698
|
-
}
|
|
2699
|
-
}
|
|
2700
|
-
async writeAcrossLayers(key, kind, value, options) {
|
|
2701
|
-
const now = Date.now();
|
|
2702
|
-
const clearEpoch = this.clearEpoch;
|
|
2703
|
-
const keyEpoch = this.currentKeyEpoch(key);
|
|
2704
|
-
const immediateOperations = [];
|
|
2705
|
-
const deferredOperations = [];
|
|
2706
|
-
for (const layer of this.layers) {
|
|
2707
|
-
const operation = async () => {
|
|
2708
|
-
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2709
|
-
return;
|
|
2710
|
-
}
|
|
2711
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2712
|
-
return;
|
|
2713
|
-
}
|
|
2714
|
-
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
2715
|
-
try {
|
|
2716
|
-
await layer.set(entry.key, entry.value, entry.ttl);
|
|
2717
|
-
} catch (error) {
|
|
2718
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
2719
|
-
}
|
|
2720
|
-
};
|
|
2721
|
-
if (this.shouldWriteBehind(layer)) {
|
|
2722
|
-
deferredOperations.push(operation);
|
|
2723
|
-
} else {
|
|
2724
|
-
immediateOperations.push(operation);
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
2728
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2729
|
-
}
|
|
2730
|
-
async executeLayerOperations(operations, context) {
|
|
2731
|
-
if (this.options.writePolicy !== "best-effort") {
|
|
2732
|
-
await Promise.all(operations.map((operation) => operation()));
|
|
2733
|
-
return;
|
|
3180
|
+
try {
|
|
3181
|
+
return await layer.get(key);
|
|
3182
|
+
} catch (error) {
|
|
3183
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
2734
3184
|
}
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
if (
|
|
3185
|
+
}
|
|
3186
|
+
async backfill(key, stored, upToIndex, options) {
|
|
3187
|
+
if (upToIndex < 0) {
|
|
2738
3188
|
return;
|
|
2739
3189
|
}
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
3190
|
+
for (let index = 0; index <= upToIndex; index += 1) {
|
|
3191
|
+
const layer = this.layers[index];
|
|
3192
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
3193
|
+
continue;
|
|
3194
|
+
}
|
|
3195
|
+
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
3196
|
+
try {
|
|
3197
|
+
await layer.set(key, stored, ttl);
|
|
3198
|
+
} catch (error) {
|
|
3199
|
+
await this.handleLayerFailure(layer, "backfill", error);
|
|
3200
|
+
continue;
|
|
3201
|
+
}
|
|
3202
|
+
this.metricsCollector.increment("backfills");
|
|
3203
|
+
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
3204
|
+
this.emit("backfill", { key, layer: layer.name });
|
|
2750
3205
|
}
|
|
2751
3206
|
}
|
|
2752
3207
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
@@ -2768,11 +3223,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2768
3223
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
2769
3224
|
}
|
|
2770
3225
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
2771
|
-
if (
|
|
3226
|
+
if (!shouldStartBackgroundRefresh({
|
|
3227
|
+
isDisconnecting: this.isDisconnecting,
|
|
3228
|
+
hasRefreshInFlight: this.backgroundRefreshes.has(key)
|
|
3229
|
+
})) {
|
|
2772
3230
|
return;
|
|
2773
3231
|
}
|
|
2774
|
-
const clearEpoch = this.
|
|
2775
|
-
const keyEpoch = this.currentKeyEpoch(key);
|
|
3232
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3233
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2776
3234
|
const refresh = (async () => {
|
|
2777
3235
|
this.metricsCollector.increment("refreshes");
|
|
2778
3236
|
try {
|
|
@@ -2810,8 +3268,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2810
3268
|
if (keys.length === 0) {
|
|
2811
3269
|
return;
|
|
2812
3270
|
}
|
|
2813
|
-
this.bumpKeyEpochs(keys);
|
|
2814
|
-
await this.deleteKeysFromLayers(this.layers, keys);
|
|
3271
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
3272
|
+
await this.invalidation.deleteKeysFromLayers(this.layers, keys);
|
|
2815
3273
|
for (const key of keys) {
|
|
2816
3274
|
await this.tagIndex.remove(key);
|
|
2817
3275
|
this.ttlResolver.deleteProfile(key);
|
|
@@ -2834,7 +3292,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2834
3292
|
}
|
|
2835
3293
|
const localLayers = this.layers.filter((layer) => layer.isLocal);
|
|
2836
3294
|
if (message.scope === "clear") {
|
|
2837
|
-
this.beginClearEpoch();
|
|
3295
|
+
this.maintenance.beginClearEpoch();
|
|
2838
3296
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
2839
3297
|
await this.tagIndex.clear();
|
|
2840
3298
|
this.ttlResolver.clearProfiles();
|
|
@@ -2842,8 +3300,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2842
3300
|
return;
|
|
2843
3301
|
}
|
|
2844
3302
|
const keys = message.keys ?? [];
|
|
2845
|
-
this.bumpKeyEpochs(keys);
|
|
2846
|
-
await this.deleteKeysFromLayers(localLayers, keys);
|
|
3303
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
3304
|
+
await this.invalidation.deleteKeysFromLayers(localLayers, keys);
|
|
2847
3305
|
if (message.operation !== "write") {
|
|
2848
3306
|
for (const key of keys) {
|
|
2849
3307
|
await this.tagIndex.remove(key);
|
|
@@ -2900,35 +3358,47 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2900
3358
|
shouldBroadcastL1Invalidation() {
|
|
2901
3359
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2902
3360
|
}
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
3361
|
+
async observeOperation(name, attributes, execute) {
|
|
3362
|
+
const id = this.nextOperationId;
|
|
3363
|
+
this.nextOperationId += 1;
|
|
3364
|
+
this.emit("operation-start", { id, name, attributes });
|
|
3365
|
+
try {
|
|
3366
|
+
const result = await execute();
|
|
3367
|
+
this.emit("operation-end", {
|
|
3368
|
+
id,
|
|
3369
|
+
name,
|
|
3370
|
+
attributes,
|
|
3371
|
+
success: true,
|
|
3372
|
+
result: result === null ? "null" : void 0
|
|
3373
|
+
});
|
|
3374
|
+
return result;
|
|
3375
|
+
} catch (error) {
|
|
3376
|
+
this.emit("operation-end", {
|
|
3377
|
+
id,
|
|
3378
|
+
name,
|
|
3379
|
+
attributes,
|
|
3380
|
+
success: false,
|
|
3381
|
+
error
|
|
3382
|
+
});
|
|
3383
|
+
throw error;
|
|
3384
|
+
}
|
|
2909
3385
|
}
|
|
2910
3386
|
scheduleGenerationCleanup(generation) {
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
this.generationCleanupPromise = void 0;
|
|
3387
|
+
this.maintenance.scheduleGenerationCleanup(
|
|
3388
|
+
generation,
|
|
3389
|
+
async (generationToClean) => this.cleanupGeneration(generationToClean),
|
|
3390
|
+
(failedGeneration, error) => {
|
|
3391
|
+
this.logger.warn?.("generation-cleanup-error", {
|
|
3392
|
+
generation: failedGeneration,
|
|
3393
|
+
error: this.formatError(error)
|
|
3394
|
+
});
|
|
2920
3395
|
}
|
|
2921
|
-
|
|
3396
|
+
);
|
|
2922
3397
|
}
|
|
2923
3398
|
async cleanupGeneration(generation) {
|
|
2924
3399
|
const prefix = `v${generation}:`;
|
|
2925
3400
|
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
2926
|
-
|
|
2927
|
-
return;
|
|
2928
|
-
}
|
|
2929
|
-
const batchSize = this.generationCleanupBatchSize();
|
|
2930
|
-
for (let index = 0; index < keys.length; index += batchSize) {
|
|
2931
|
-
const batch = keys.slice(index, index + batchSize);
|
|
3401
|
+
for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
|
|
2932
3402
|
await this.deleteKeys(batch);
|
|
2933
3403
|
await this.publishInvalidation({
|
|
2934
3404
|
scope: "keys",
|
|
@@ -2939,161 +3409,43 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2939
3409
|
}
|
|
2940
3410
|
}
|
|
2941
3411
|
initializeWriteBehind(options) {
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
return;
|
|
2948
|
-
}
|
|
2949
|
-
this.writeBehindTimer = setInterval(() => {
|
|
2950
|
-
void this.flushWriteBehindQueue();
|
|
2951
|
-
}, flushIntervalMs);
|
|
2952
|
-
this.writeBehindTimer.unref?.();
|
|
3412
|
+
this.maintenance.initializeWriteBehindTimer(
|
|
3413
|
+
this.options.writeStrategy,
|
|
3414
|
+
options,
|
|
3415
|
+
this.flushWriteBehindQueue.bind(this)
|
|
3416
|
+
);
|
|
2953
3417
|
}
|
|
2954
3418
|
shouldWriteBehind(layer) {
|
|
2955
3419
|
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2956
3420
|
}
|
|
2957
|
-
beginClearEpoch() {
|
|
2958
|
-
this.clearEpoch += 1;
|
|
2959
|
-
this.keyEpochs.clear();
|
|
2960
|
-
this.writeBehindQueue.length = 0;
|
|
2961
|
-
}
|
|
2962
|
-
currentKeyEpoch(key) {
|
|
2963
|
-
return this.keyEpochs.get(key) ?? 0;
|
|
2964
|
-
}
|
|
2965
|
-
bumpKeyEpochs(keys) {
|
|
2966
|
-
for (const key of keys) {
|
|
2967
|
-
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
2968
|
-
}
|
|
2969
|
-
}
|
|
2970
|
-
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
2971
|
-
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
2972
|
-
return true;
|
|
2973
|
-
}
|
|
2974
|
-
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
2975
|
-
return true;
|
|
2976
|
-
}
|
|
2977
|
-
return false;
|
|
2978
|
-
}
|
|
2979
3421
|
async enqueueWriteBehind(operation) {
|
|
2980
|
-
this.
|
|
2981
|
-
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2982
|
-
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
2983
|
-
if (this.writeBehindQueue.length >= batchSize) {
|
|
2984
|
-
await this.flushWriteBehindQueue();
|
|
2985
|
-
return;
|
|
2986
|
-
}
|
|
2987
|
-
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
2988
|
-
await this.flushWriteBehindQueue();
|
|
2989
|
-
}
|
|
3422
|
+
await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
2990
3423
|
}
|
|
2991
3424
|
async flushWriteBehindQueue() {
|
|
2992
|
-
|
|
2993
|
-
|
|
3425
|
+
await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
3426
|
+
}
|
|
3427
|
+
async runWriteBehindBatch(batch) {
|
|
3428
|
+
const results = await Promise.allSettled(batch.map((operation) => operation()));
|
|
3429
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
3430
|
+
if (failures.length === 0) {
|
|
2994
3431
|
return;
|
|
2995
3432
|
}
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
if (failures.length > 0) {
|
|
3002
|
-
this.metricsCollector.increment("writeFailures", failures.length);
|
|
3003
|
-
this.logger.error?.("write-behind-flush-failure", {
|
|
3004
|
-
failed: failures.length,
|
|
3005
|
-
total: batch.length,
|
|
3006
|
-
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
3007
|
-
});
|
|
3008
|
-
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
3009
|
-
}
|
|
3010
|
-
})();
|
|
3011
|
-
await this.writeBehindFlushPromise;
|
|
3012
|
-
this.writeBehindFlushPromise = void 0;
|
|
3013
|
-
if (this.writeBehindQueue.length > 0) {
|
|
3014
|
-
await this.flushWriteBehindQueue();
|
|
3015
|
-
}
|
|
3016
|
-
}
|
|
3017
|
-
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
3018
|
-
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
3019
|
-
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
3020
|
-
layer.name,
|
|
3021
|
-
options?.staleWhileRevalidate,
|
|
3022
|
-
this.options.staleWhileRevalidate
|
|
3023
|
-
);
|
|
3024
|
-
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
3025
|
-
const payload = createStoredValueEnvelope({
|
|
3026
|
-
kind,
|
|
3027
|
-
value,
|
|
3028
|
-
freshTtlSeconds: freshTtl,
|
|
3029
|
-
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
3030
|
-
staleIfErrorSeconds: staleIfError,
|
|
3031
|
-
now
|
|
3433
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
3434
|
+
this.logger.error?.("write-behind-flush-failure", {
|
|
3435
|
+
failed: failures.length,
|
|
3436
|
+
total: batch.length,
|
|
3437
|
+
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
3032
3438
|
});
|
|
3033
|
-
|
|
3034
|
-
return {
|
|
3035
|
-
key,
|
|
3036
|
-
value: payload,
|
|
3037
|
-
ttl
|
|
3038
|
-
};
|
|
3039
|
-
}
|
|
3040
|
-
intersectKeys(groups) {
|
|
3041
|
-
if (groups.length === 0) {
|
|
3042
|
-
return [];
|
|
3043
|
-
}
|
|
3044
|
-
const [firstGroup, ...rest] = groups;
|
|
3045
|
-
if (!firstGroup) {
|
|
3046
|
-
return [];
|
|
3047
|
-
}
|
|
3048
|
-
const restSets = rest.map((group) => new Set(group));
|
|
3049
|
-
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
3439
|
+
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
3050
3440
|
}
|
|
3051
3441
|
qualifyKey(key) {
|
|
3052
|
-
|
|
3053
|
-
return prefix ? `${prefix}${key}` : key;
|
|
3442
|
+
return qualifyGenerationKey(key, this.currentGeneration);
|
|
3054
3443
|
}
|
|
3055
3444
|
qualifyPattern(pattern) {
|
|
3056
|
-
|
|
3057
|
-
return prefix ? `${prefix}${pattern}` : pattern;
|
|
3445
|
+
return qualifyGenerationPattern(pattern, this.currentGeneration);
|
|
3058
3446
|
}
|
|
3059
3447
|
stripQualifiedKey(key) {
|
|
3060
|
-
|
|
3061
|
-
if (!prefix || !key.startsWith(prefix)) {
|
|
3062
|
-
return key;
|
|
3063
|
-
}
|
|
3064
|
-
return key.slice(prefix.length);
|
|
3065
|
-
}
|
|
3066
|
-
generationPrefix() {
|
|
3067
|
-
if (this.currentGeneration === void 0) {
|
|
3068
|
-
return "";
|
|
3069
|
-
}
|
|
3070
|
-
return `v${this.currentGeneration}:`;
|
|
3071
|
-
}
|
|
3072
|
-
async deleteKeysFromLayers(layers, keys) {
|
|
3073
|
-
await Promise.all(
|
|
3074
|
-
layers.map(async (layer) => {
|
|
3075
|
-
if (this.shouldSkipLayer(layer)) {
|
|
3076
|
-
return;
|
|
3077
|
-
}
|
|
3078
|
-
if (layer.deleteMany) {
|
|
3079
|
-
try {
|
|
3080
|
-
await layer.deleteMany(keys);
|
|
3081
|
-
} catch (error) {
|
|
3082
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
3083
|
-
}
|
|
3084
|
-
return;
|
|
3085
|
-
}
|
|
3086
|
-
await Promise.all(
|
|
3087
|
-
keys.map(async (key) => {
|
|
3088
|
-
try {
|
|
3089
|
-
await layer.delete(key);
|
|
3090
|
-
} catch (error) {
|
|
3091
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
3092
|
-
}
|
|
3093
|
-
})
|
|
3094
|
-
);
|
|
3095
|
-
})
|
|
3096
|
-
);
|
|
3448
|
+
return stripGenerationPrefix(key, this.currentGeneration);
|
|
3097
3449
|
}
|
|
3098
3450
|
validateConfiguration() {
|
|
3099
3451
|
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
@@ -3158,37 +3510,38 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3158
3510
|
this.assertActive(operation);
|
|
3159
3511
|
}
|
|
3160
3512
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
3161
|
-
const
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3513
|
+
const plan = planFreshReadPolicies({
|
|
3514
|
+
stored: hit.stored,
|
|
3515
|
+
hasFetcher: Boolean(fetcher),
|
|
3516
|
+
slidingTtl: options?.slidingTtl ?? false,
|
|
3517
|
+
refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
3518
|
+
});
|
|
3519
|
+
if (plan.refreshedStored) {
|
|
3166
3520
|
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
3167
3521
|
const layer = this.layers[index];
|
|
3168
3522
|
if (!layer || this.shouldSkipLayer(layer)) {
|
|
3169
3523
|
continue;
|
|
3170
3524
|
}
|
|
3171
3525
|
try {
|
|
3172
|
-
await layer.set(key,
|
|
3526
|
+
await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
|
|
3173
3527
|
} catch (error) {
|
|
3174
3528
|
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
3175
3529
|
}
|
|
3176
3530
|
}
|
|
3177
3531
|
}
|
|
3178
|
-
if (fetcher &&
|
|
3532
|
+
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
3179
3533
|
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
3180
3534
|
}
|
|
3181
3535
|
}
|
|
3182
3536
|
shouldSkipLayer(layer) {
|
|
3183
|
-
|
|
3184
|
-
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
3537
|
+
return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
|
|
3185
3538
|
}
|
|
3186
3539
|
async handleLayerFailure(layer, operation, error) {
|
|
3187
|
-
|
|
3540
|
+
const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
|
|
3541
|
+
if (!recovery.degrade) {
|
|
3188
3542
|
throw error;
|
|
3189
3543
|
}
|
|
3190
|
-
|
|
3191
|
-
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
3544
|
+
this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
|
|
3192
3545
|
this.metricsCollector.increment("degradedOperations");
|
|
3193
3546
|
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
3194
3547
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
@@ -3224,18 +3577,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3224
3577
|
this.emit("error", { operation, ...context });
|
|
3225
3578
|
}
|
|
3226
3579
|
}
|
|
3227
|
-
isCacheSnapshotEntries(value) {
|
|
3228
|
-
return Array.isArray(value) && value.every((entry) => {
|
|
3229
|
-
if (!entry || typeof entry !== "object") {
|
|
3230
|
-
return false;
|
|
3231
|
-
}
|
|
3232
|
-
const candidate = entry;
|
|
3233
|
-
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
3234
|
-
});
|
|
3235
|
-
}
|
|
3236
|
-
sanitizeSnapshotValue(value) {
|
|
3237
|
-
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
3238
|
-
}
|
|
3239
3580
|
snapshotMaxBytes() {
|
|
3240
3581
|
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
3241
3582
|
}
|
|
@@ -3245,62 +3586,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3245
3586
|
invalidationMaxKeys() {
|
|
3246
3587
|
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
3247
3588
|
}
|
|
3248
|
-
async collectKeysForTag(tag) {
|
|
3249
|
-
const keys = /* @__PURE__ */ new Set();
|
|
3250
|
-
if (this.tagIndex.forEachKeyForTag) {
|
|
3251
|
-
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
3252
|
-
keys.add(key);
|
|
3253
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3254
|
-
});
|
|
3255
|
-
return [...keys];
|
|
3256
|
-
}
|
|
3257
|
-
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3258
|
-
keys.add(key);
|
|
3259
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3260
|
-
}
|
|
3261
|
-
return [...keys];
|
|
3262
|
-
}
|
|
3263
|
-
assertWithinInvalidationKeyLimit(size) {
|
|
3264
|
-
const maxKeys = this.invalidationMaxKeys();
|
|
3265
|
-
if (maxKeys !== false && size > maxKeys) {
|
|
3266
|
-
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
3267
|
-
}
|
|
3268
|
-
}
|
|
3269
|
-
async visitExportEntries(maxEntries, visitor) {
|
|
3270
|
-
const exported = /* @__PURE__ */ new Set();
|
|
3271
|
-
for (const layer of this.layers) {
|
|
3272
|
-
if (!layer.keys && !layer.forEachKey) {
|
|
3273
|
-
continue;
|
|
3274
|
-
}
|
|
3275
|
-
const visitKey = async (key) => {
|
|
3276
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
3277
|
-
if (exported.has(exportedKey)) {
|
|
3278
|
-
return;
|
|
3279
|
-
}
|
|
3280
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
3281
|
-
if (stored === null) {
|
|
3282
|
-
return;
|
|
3283
|
-
}
|
|
3284
|
-
exported.add(exportedKey);
|
|
3285
|
-
if (maxEntries !== false && exported.size > maxEntries) {
|
|
3286
|
-
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
3287
|
-
}
|
|
3288
|
-
await visitor({
|
|
3289
|
-
key: exportedKey,
|
|
3290
|
-
value: stored,
|
|
3291
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
3292
|
-
});
|
|
3293
|
-
};
|
|
3294
|
-
if (layer.forEachKey) {
|
|
3295
|
-
await layer.forEachKey(visitKey);
|
|
3296
|
-
continue;
|
|
3297
|
-
}
|
|
3298
|
-
const keys = await layer.keys?.();
|
|
3299
|
-
for (const key of keys ?? []) {
|
|
3300
|
-
await visitKey(key);
|
|
3301
|
-
}
|
|
3302
|
-
}
|
|
3303
|
-
}
|
|
3304
3589
|
};
|
|
3305
3590
|
|
|
3306
3591
|
// src/invalidation/RedisInvalidationBus.ts
|
|
@@ -3342,7 +3627,12 @@ var RedisInvalidationBus = class {
|
|
|
3342
3627
|
async dispatchToHandlers(payload) {
|
|
3343
3628
|
let message;
|
|
3344
3629
|
try {
|
|
3345
|
-
const parsed =
|
|
3630
|
+
const parsed = sanitizeStructuredData(JSON.parse(payload), {
|
|
3631
|
+
label: "Invalidation payload",
|
|
3632
|
+
maxDepth: 64,
|
|
3633
|
+
maxNodes: 1e4,
|
|
3634
|
+
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
3635
|
+
});
|
|
3346
3636
|
if (!this.isInvalidationMessage(parsed)) {
|
|
3347
3637
|
throw new Error("Invalid invalidation payload shape.");
|
|
3348
3638
|
}
|
|
@@ -3379,31 +3669,6 @@ var RedisInvalidationBus = class {
|
|
|
3379
3669
|
console.error(`[layercache] ${message}`, error);
|
|
3380
3670
|
}
|
|
3381
3671
|
};
|
|
3382
|
-
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
3383
|
-
var MAX_SANITIZE_DEPTH2 = 64;
|
|
3384
|
-
var MAX_SANITIZE_NODES2 = 1e4;
|
|
3385
|
-
function sanitizeJsonValue2(value, depth = 0, state = { count: 0 }) {
|
|
3386
|
-
state.count += 1;
|
|
3387
|
-
if (state.count > MAX_SANITIZE_NODES2) {
|
|
3388
|
-
throw new Error(`Invalidation payload exceeds max node count of ${MAX_SANITIZE_NODES2}.`);
|
|
3389
|
-
}
|
|
3390
|
-
if (depth > MAX_SANITIZE_DEPTH2) {
|
|
3391
|
-
throw new Error(`Invalidation payload exceeds max depth of ${MAX_SANITIZE_DEPTH2}.`);
|
|
3392
|
-
}
|
|
3393
|
-
if (Array.isArray(value)) {
|
|
3394
|
-
return value.map((entry) => sanitizeJsonValue2(entry, depth + 1, state));
|
|
3395
|
-
}
|
|
3396
|
-
if (value && typeof value === "object") {
|
|
3397
|
-
const result = /* @__PURE__ */ Object.create(null);
|
|
3398
|
-
for (const key of Object.keys(value)) {
|
|
3399
|
-
if (!DANGEROUS_KEYS.has(key)) {
|
|
3400
|
-
result[key] = sanitizeJsonValue2(value[key], depth + 1, state);
|
|
3401
|
-
}
|
|
3402
|
-
}
|
|
3403
|
-
return result;
|
|
3404
|
-
}
|
|
3405
|
-
return value;
|
|
3406
|
-
}
|
|
3407
3672
|
|
|
3408
3673
|
// src/invalidation/RedisTagIndex.ts
|
|
3409
3674
|
var RedisTagIndex = class {
|
|
@@ -3773,64 +4038,37 @@ function normalizeUrl2(url) {
|
|
|
3773
4038
|
|
|
3774
4039
|
// src/integrations/opentelemetry.ts
|
|
3775
4040
|
function createOpenTelemetryPlugin(cache, tracer) {
|
|
3776
|
-
const
|
|
3777
|
-
|
|
3778
|
-
set:
|
|
3779
|
-
delete: cache.delete.bind(cache),
|
|
3780
|
-
mget: cache.mget.bind(cache),
|
|
3781
|
-
mset: cache.mset.bind(cache),
|
|
3782
|
-
invalidateByTag: cache.invalidateByTag.bind(cache),
|
|
3783
|
-
invalidateByTags: cache.invalidateByTags.bind(cache),
|
|
3784
|
-
invalidateByPattern: cache.invalidateByPattern.bind(cache),
|
|
3785
|
-
invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
|
|
4041
|
+
const spans = /* @__PURE__ */ new Map();
|
|
4042
|
+
const onStart = (event) => {
|
|
4043
|
+
spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
|
|
3786
4044
|
};
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
cache.mget = instrument("layercache.mget", tracer, originals.mget);
|
|
3797
|
-
cache.mset = instrument("layercache.mset", tracer, originals.mset);
|
|
3798
|
-
cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
|
|
3799
|
-
cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
|
|
3800
|
-
cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
|
|
3801
|
-
cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
|
|
3802
|
-
return {
|
|
3803
|
-
uninstall() {
|
|
3804
|
-
cache.get = originals.get;
|
|
3805
|
-
cache.set = originals.set;
|
|
3806
|
-
cache.delete = originals.delete;
|
|
3807
|
-
cache.mget = originals.mget;
|
|
3808
|
-
cache.mset = originals.mset;
|
|
3809
|
-
cache.invalidateByTag = originals.invalidateByTag;
|
|
3810
|
-
cache.invalidateByTags = originals.invalidateByTags;
|
|
3811
|
-
cache.invalidateByPattern = originals.invalidateByPattern;
|
|
3812
|
-
cache.invalidateByPrefix = originals.invalidateByPrefix;
|
|
4045
|
+
const onEnd = (event) => {
|
|
4046
|
+
const span = spans.get(event.id);
|
|
4047
|
+
if (!span) {
|
|
4048
|
+
return;
|
|
4049
|
+
}
|
|
4050
|
+
spans.delete(event.id);
|
|
4051
|
+
span.setAttribute?.("layercache.success", event.success);
|
|
4052
|
+
if (event.result) {
|
|
4053
|
+
span.setAttribute?.("layercache.result", event.result);
|
|
3813
4054
|
}
|
|
4055
|
+
if (event.error !== void 0) {
|
|
4056
|
+
span.recordException?.(event.error);
|
|
4057
|
+
}
|
|
4058
|
+
span.end();
|
|
3814
4059
|
};
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
return
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
span.
|
|
3822
|
-
|
|
3823
|
-
span.setAttribute?.("layercache.result", "null");
|
|
4060
|
+
cache.on("operation-start", onStart);
|
|
4061
|
+
cache.on("operation-end", onEnd);
|
|
4062
|
+
return {
|
|
4063
|
+
uninstall() {
|
|
4064
|
+
cache.off("operation-start", onStart);
|
|
4065
|
+
cache.off("operation-end", onEnd);
|
|
4066
|
+
for (const span of spans.values()) {
|
|
4067
|
+
span.end();
|
|
3824
4068
|
}
|
|
3825
|
-
|
|
3826
|
-
} catch (error) {
|
|
3827
|
-
span.setAttribute?.("layercache.success", false);
|
|
3828
|
-
span.recordException?.(error);
|
|
3829
|
-
throw error;
|
|
3830
|
-
} finally {
|
|
3831
|
-
span.end();
|
|
4069
|
+
spans.clear();
|
|
3832
4070
|
}
|
|
3833
|
-
}
|
|
4071
|
+
};
|
|
3834
4072
|
}
|
|
3835
4073
|
|
|
3836
4074
|
// src/integrations/trpc.ts
|
|
@@ -4382,8 +4620,8 @@ var RedisLayer = class {
|
|
|
4382
4620
|
|
|
4383
4621
|
// src/layers/DiskLayer.ts
|
|
4384
4622
|
var import_node_crypto = require("crypto");
|
|
4385
|
-
var
|
|
4386
|
-
var
|
|
4623
|
+
var import_node_fs2 = require("fs");
|
|
4624
|
+
var import_node_path2 = require("path");
|
|
4387
4625
|
var FILE_SCAN_CONCURRENCY = 32;
|
|
4388
4626
|
var DiskLayer = class {
|
|
4389
4627
|
name;
|
|
@@ -4426,7 +4664,7 @@ var DiskLayer = class {
|
|
|
4426
4664
|
}
|
|
4427
4665
|
async set(key, value, ttl = this.defaultTtl) {
|
|
4428
4666
|
await this.enqueueWrite(async () => {
|
|
4429
|
-
await
|
|
4667
|
+
await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
|
|
4430
4668
|
const entry = {
|
|
4431
4669
|
key,
|
|
4432
4670
|
value,
|
|
@@ -4436,8 +4674,8 @@ var DiskLayer = class {
|
|
|
4436
4674
|
const targetPath = this.keyToPath(key);
|
|
4437
4675
|
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
4438
4676
|
try {
|
|
4439
|
-
await
|
|
4440
|
-
await
|
|
4677
|
+
await import_node_fs2.promises.writeFile(tempPath, payload);
|
|
4678
|
+
await import_node_fs2.promises.rename(tempPath, targetPath);
|
|
4441
4679
|
} catch (error) {
|
|
4442
4680
|
await this.safeDelete(tempPath);
|
|
4443
4681
|
throw error;
|
|
@@ -4491,12 +4729,12 @@ var DiskLayer = class {
|
|
|
4491
4729
|
await this.enqueueWrite(async () => {
|
|
4492
4730
|
let entries;
|
|
4493
4731
|
try {
|
|
4494
|
-
entries = await
|
|
4732
|
+
entries = await import_node_fs2.promises.readdir(this.directory);
|
|
4495
4733
|
} catch {
|
|
4496
4734
|
return;
|
|
4497
4735
|
}
|
|
4498
4736
|
await this.deletePathsWithConcurrency(
|
|
4499
|
-
entries.filter((name) => name.endsWith(".lc")).map((name) => (0,
|
|
4737
|
+
entries.filter((name) => name.endsWith(".lc")).map((name) => (0, import_node_path2.join)(this.directory, name))
|
|
4500
4738
|
);
|
|
4501
4739
|
});
|
|
4502
4740
|
}
|
|
@@ -4525,7 +4763,7 @@ var DiskLayer = class {
|
|
|
4525
4763
|
}
|
|
4526
4764
|
async ping() {
|
|
4527
4765
|
try {
|
|
4528
|
-
await
|
|
4766
|
+
await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
|
|
4529
4767
|
return true;
|
|
4530
4768
|
} catch {
|
|
4531
4769
|
return false;
|
|
@@ -4535,7 +4773,7 @@ var DiskLayer = class {
|
|
|
4535
4773
|
}
|
|
4536
4774
|
keyToPath(key) {
|
|
4537
4775
|
const hash = (0, import_node_crypto.createHash)("sha256").update(key).digest("hex");
|
|
4538
|
-
return (0,
|
|
4776
|
+
return (0, import_node_path2.join)(this.directory, `${hash}.lc`);
|
|
4539
4777
|
}
|
|
4540
4778
|
resolveDirectory(directory) {
|
|
4541
4779
|
if (typeof directory !== "string" || directory.trim().length === 0) {
|
|
@@ -4544,7 +4782,7 @@ var DiskLayer = class {
|
|
|
4544
4782
|
if (directory.includes("\0")) {
|
|
4545
4783
|
throw new Error("DiskLayer.directory must not contain null bytes.");
|
|
4546
4784
|
}
|
|
4547
|
-
return (0,
|
|
4785
|
+
return (0, import_node_path2.resolve)(directory);
|
|
4548
4786
|
}
|
|
4549
4787
|
normalizeMaxFiles(maxFiles) {
|
|
4550
4788
|
if (maxFiles === void 0) {
|
|
@@ -4568,7 +4806,7 @@ var DiskLayer = class {
|
|
|
4568
4806
|
async readEntryFile(filePath) {
|
|
4569
4807
|
let handle;
|
|
4570
4808
|
try {
|
|
4571
|
-
handle = await
|
|
4809
|
+
handle = await import_node_fs2.promises.open(filePath, "r");
|
|
4572
4810
|
return await this.readHandleWithLimit(handle);
|
|
4573
4811
|
} catch {
|
|
4574
4812
|
await this.safeDelete(filePath);
|
|
@@ -4608,7 +4846,7 @@ var DiskLayer = class {
|
|
|
4608
4846
|
async scanEntries(visitor) {
|
|
4609
4847
|
let entries;
|
|
4610
4848
|
try {
|
|
4611
|
-
entries = await
|
|
4849
|
+
entries = await import_node_fs2.promises.readdir(this.directory);
|
|
4612
4850
|
} catch {
|
|
4613
4851
|
return;
|
|
4614
4852
|
}
|
|
@@ -4624,7 +4862,7 @@ var DiskLayer = class {
|
|
|
4624
4862
|
if (name === void 0) {
|
|
4625
4863
|
return;
|
|
4626
4864
|
}
|
|
4627
|
-
const filePath = (0,
|
|
4865
|
+
const filePath = (0, import_node_path2.join)(this.directory, name);
|
|
4628
4866
|
const raw = await this.readEntryFile(filePath);
|
|
4629
4867
|
if (raw === null) {
|
|
4630
4868
|
continue;
|
|
@@ -4671,7 +4909,7 @@ var DiskLayer = class {
|
|
|
4671
4909
|
}
|
|
4672
4910
|
async safeDelete(filePath) {
|
|
4673
4911
|
try {
|
|
4674
|
-
await
|
|
4912
|
+
await import_node_fs2.promises.unlink(filePath);
|
|
4675
4913
|
} catch {
|
|
4676
4914
|
}
|
|
4677
4915
|
}
|
|
@@ -4689,7 +4927,7 @@ var DiskLayer = class {
|
|
|
4689
4927
|
}
|
|
4690
4928
|
let entries;
|
|
4691
4929
|
try {
|
|
4692
|
-
entries = await
|
|
4930
|
+
entries = await import_node_fs2.promises.readdir(this.directory);
|
|
4693
4931
|
} catch {
|
|
4694
4932
|
return;
|
|
4695
4933
|
}
|
|
@@ -4699,9 +4937,9 @@ var DiskLayer = class {
|
|
|
4699
4937
|
}
|
|
4700
4938
|
const withStats = await Promise.all(
|
|
4701
4939
|
lcFiles.map(async (name) => {
|
|
4702
|
-
const filePath = (0,
|
|
4940
|
+
const filePath = (0, import_node_path2.join)(this.directory, name);
|
|
4703
4941
|
try {
|
|
4704
|
-
const stat = await
|
|
4942
|
+
const stat = await import_node_fs2.promises.stat(filePath);
|
|
4705
4943
|
return { filePath, mtimeMs: stat.mtimeMs };
|
|
4706
4944
|
} catch {
|
|
4707
4945
|
return { filePath, mtimeMs: 0 };
|
|
@@ -4797,44 +5035,19 @@ var MemcachedLayer = class {
|
|
|
4797
5035
|
|
|
4798
5036
|
// src/serialization/MsgpackSerializer.ts
|
|
4799
5037
|
var import_msgpack = require("@msgpack/msgpack");
|
|
4800
|
-
var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
4801
|
-
var MAX_SANITIZE_DEPTH3 = 64;
|
|
4802
|
-
var MAX_SANITIZE_NODES3 = 1e4;
|
|
4803
5038
|
var MsgpackSerializer = class {
|
|
4804
5039
|
serialize(value) {
|
|
4805
5040
|
return Buffer.from((0, import_msgpack.encode)(value));
|
|
4806
5041
|
}
|
|
4807
5042
|
deserialize(payload) {
|
|
4808
|
-
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
4809
|
-
return
|
|
5043
|
+
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
|
|
5044
|
+
return sanitizeStructuredData((0, import_msgpack.decode)(normalized), {
|
|
5045
|
+
label: "MessagePack payload",
|
|
5046
|
+
maxDepth: 64,
|
|
5047
|
+
maxNodes: 1e4
|
|
5048
|
+
});
|
|
4810
5049
|
}
|
|
4811
5050
|
};
|
|
4812
|
-
function sanitizeMsgpackValue(value, depth, state) {
|
|
4813
|
-
state.count += 1;
|
|
4814
|
-
if (state.count > MAX_SANITIZE_NODES3) {
|
|
4815
|
-
throw new Error(`MessagePack payload exceeds max node count of ${MAX_SANITIZE_NODES3}.`);
|
|
4816
|
-
}
|
|
4817
|
-
if (depth > MAX_SANITIZE_DEPTH3) {
|
|
4818
|
-
throw new Error(`MessagePack payload exceeds max depth of ${MAX_SANITIZE_DEPTH3}.`);
|
|
4819
|
-
}
|
|
4820
|
-
if (Array.isArray(value)) {
|
|
4821
|
-
return value.map((entry) => sanitizeMsgpackValue(entry, depth + 1, state));
|
|
4822
|
-
}
|
|
4823
|
-
if (!isPlainObject2(value)) {
|
|
4824
|
-
return value;
|
|
4825
|
-
}
|
|
4826
|
-
const sanitized = {};
|
|
4827
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
4828
|
-
if (DANGEROUS_KEYS2.has(key)) {
|
|
4829
|
-
continue;
|
|
4830
|
-
}
|
|
4831
|
-
sanitized[key] = sanitizeMsgpackValue(entry, depth + 1, state);
|
|
4832
|
-
}
|
|
4833
|
-
return sanitized;
|
|
4834
|
-
}
|
|
4835
|
-
function isPlainObject2(value) {
|
|
4836
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
4837
|
-
}
|
|
4838
5051
|
|
|
4839
5052
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
4840
5053
|
var import_node_crypto2 = require("crypto");
|