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
|
@@ -223,6 +223,125 @@ var Mutex = class {
|
|
|
223
223
|
}
|
|
224
224
|
};
|
|
225
225
|
|
|
226
|
+
// ../../src/internal/CacheNamespaceMetrics.ts
|
|
227
|
+
function createEmptyNamespaceMetrics(resetAt = Date.now()) {
|
|
228
|
+
return {
|
|
229
|
+
hits: 0,
|
|
230
|
+
misses: 0,
|
|
231
|
+
fetches: 0,
|
|
232
|
+
sets: 0,
|
|
233
|
+
deletes: 0,
|
|
234
|
+
backfills: 0,
|
|
235
|
+
invalidations: 0,
|
|
236
|
+
staleHits: 0,
|
|
237
|
+
refreshes: 0,
|
|
238
|
+
refreshErrors: 0,
|
|
239
|
+
writeFailures: 0,
|
|
240
|
+
singleFlightWaits: 0,
|
|
241
|
+
negativeCacheHits: 0,
|
|
242
|
+
circuitBreakerTrips: 0,
|
|
243
|
+
degradedOperations: 0,
|
|
244
|
+
hitsByLayer: {},
|
|
245
|
+
missesByLayer: {},
|
|
246
|
+
latencyByLayer: {},
|
|
247
|
+
resetAt
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function cloneNamespaceMetrics(metrics) {
|
|
251
|
+
return {
|
|
252
|
+
...metrics,
|
|
253
|
+
hitsByLayer: { ...metrics.hitsByLayer },
|
|
254
|
+
missesByLayer: { ...metrics.missesByLayer },
|
|
255
|
+
latencyByLayer: Object.fromEntries(
|
|
256
|
+
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
257
|
+
)
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function diffNamespaceMetrics(before, after) {
|
|
261
|
+
const latencyByLayer = Object.fromEntries(
|
|
262
|
+
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
263
|
+
layer,
|
|
264
|
+
{
|
|
265
|
+
avgMs: value.avgMs,
|
|
266
|
+
maxMs: value.maxMs,
|
|
267
|
+
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
268
|
+
}
|
|
269
|
+
])
|
|
270
|
+
);
|
|
271
|
+
return {
|
|
272
|
+
hits: after.hits - before.hits,
|
|
273
|
+
misses: after.misses - before.misses,
|
|
274
|
+
fetches: after.fetches - before.fetches,
|
|
275
|
+
sets: after.sets - before.sets,
|
|
276
|
+
deletes: after.deletes - before.deletes,
|
|
277
|
+
backfills: after.backfills - before.backfills,
|
|
278
|
+
invalidations: after.invalidations - before.invalidations,
|
|
279
|
+
staleHits: after.staleHits - before.staleHits,
|
|
280
|
+
refreshes: after.refreshes - before.refreshes,
|
|
281
|
+
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
282
|
+
writeFailures: after.writeFailures - before.writeFailures,
|
|
283
|
+
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
284
|
+
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
285
|
+
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
286
|
+
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
287
|
+
hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
|
|
288
|
+
missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
|
|
289
|
+
latencyByLayer,
|
|
290
|
+
resetAt: after.resetAt
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function addNamespaceMetrics(base, delta) {
|
|
294
|
+
return {
|
|
295
|
+
hits: base.hits + delta.hits,
|
|
296
|
+
misses: base.misses + delta.misses,
|
|
297
|
+
fetches: base.fetches + delta.fetches,
|
|
298
|
+
sets: base.sets + delta.sets,
|
|
299
|
+
deletes: base.deletes + delta.deletes,
|
|
300
|
+
backfills: base.backfills + delta.backfills,
|
|
301
|
+
invalidations: base.invalidations + delta.invalidations,
|
|
302
|
+
staleHits: base.staleHits + delta.staleHits,
|
|
303
|
+
refreshes: base.refreshes + delta.refreshes,
|
|
304
|
+
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
305
|
+
writeFailures: base.writeFailures + delta.writeFailures,
|
|
306
|
+
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
307
|
+
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
308
|
+
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
309
|
+
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
310
|
+
hitsByLayer: addMetricMap(base.hitsByLayer, delta.hitsByLayer),
|
|
311
|
+
missesByLayer: addMetricMap(base.missesByLayer, delta.missesByLayer),
|
|
312
|
+
latencyByLayer: cloneNamespaceMetrics(delta).latencyByLayer,
|
|
313
|
+
resetAt: base.resetAt
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function computeNamespaceHitRate(metrics) {
|
|
317
|
+
const total = metrics.hits + metrics.misses;
|
|
318
|
+
const overall = total === 0 ? 0 : metrics.hits / total;
|
|
319
|
+
const byLayer = {};
|
|
320
|
+
const layers = /* @__PURE__ */ new Set([...Object.keys(metrics.hitsByLayer), ...Object.keys(metrics.missesByLayer)]);
|
|
321
|
+
for (const layer of layers) {
|
|
322
|
+
const hits = metrics.hitsByLayer[layer] ?? 0;
|
|
323
|
+
const misses = metrics.missesByLayer[layer] ?? 0;
|
|
324
|
+
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
325
|
+
}
|
|
326
|
+
return { overall, byLayer };
|
|
327
|
+
}
|
|
328
|
+
function diffMetricMap(before, after) {
|
|
329
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
330
|
+
const result = {};
|
|
331
|
+
for (const key of keys) {
|
|
332
|
+
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
333
|
+
}
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
function addMetricMap(base, delta) {
|
|
337
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
338
|
+
const result = {};
|
|
339
|
+
for (const key of keys) {
|
|
340
|
+
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
|
|
226
345
|
// ../../src/CacheNamespace.ts
|
|
227
346
|
var CacheNamespace = class _CacheNamespace {
|
|
228
347
|
constructor(cache, prefix) {
|
|
@@ -233,7 +352,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
233
352
|
cache;
|
|
234
353
|
prefix;
|
|
235
354
|
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
236
|
-
metrics =
|
|
355
|
+
metrics = createEmptyNamespaceMetrics();
|
|
237
356
|
async get(key, fetcher, options) {
|
|
238
357
|
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
239
358
|
}
|
|
@@ -330,19 +449,10 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
330
449
|
);
|
|
331
450
|
}
|
|
332
451
|
getMetrics() {
|
|
333
|
-
return
|
|
452
|
+
return cloneNamespaceMetrics(this.metrics);
|
|
334
453
|
}
|
|
335
454
|
getHitRate() {
|
|
336
|
-
|
|
337
|
-
const overall = total === 0 ? 0 : this.metrics.hits / total;
|
|
338
|
-
const byLayer = {};
|
|
339
|
-
const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
|
|
340
|
-
for (const layer of layers) {
|
|
341
|
-
const hits = this.metrics.hitsByLayer[layer] ?? 0;
|
|
342
|
-
const misses = this.metrics.missesByLayer[layer] ?? 0;
|
|
343
|
-
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
344
|
-
}
|
|
345
|
-
return { overall, byLayer };
|
|
455
|
+
return computeNamespaceHitRate(this.metrics);
|
|
346
456
|
}
|
|
347
457
|
/**
|
|
348
458
|
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
@@ -383,7 +493,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
383
493
|
const before = this.cache.getMetrics();
|
|
384
494
|
const result = await operation();
|
|
385
495
|
const after = this.cache.getMetrics();
|
|
386
|
-
this.metrics =
|
|
496
|
+
this.metrics = addNamespaceMetrics(this.metrics, diffNamespaceMetrics(before, after));
|
|
387
497
|
return result;
|
|
388
498
|
});
|
|
389
499
|
}
|
|
@@ -397,111 +507,6 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
397
507
|
return mutex;
|
|
398
508
|
}
|
|
399
509
|
};
|
|
400
|
-
function emptyMetrics() {
|
|
401
|
-
return {
|
|
402
|
-
hits: 0,
|
|
403
|
-
misses: 0,
|
|
404
|
-
fetches: 0,
|
|
405
|
-
sets: 0,
|
|
406
|
-
deletes: 0,
|
|
407
|
-
backfills: 0,
|
|
408
|
-
invalidations: 0,
|
|
409
|
-
staleHits: 0,
|
|
410
|
-
refreshes: 0,
|
|
411
|
-
refreshErrors: 0,
|
|
412
|
-
writeFailures: 0,
|
|
413
|
-
singleFlightWaits: 0,
|
|
414
|
-
negativeCacheHits: 0,
|
|
415
|
-
circuitBreakerTrips: 0,
|
|
416
|
-
degradedOperations: 0,
|
|
417
|
-
hitsByLayer: {},
|
|
418
|
-
missesByLayer: {},
|
|
419
|
-
latencyByLayer: {},
|
|
420
|
-
resetAt: Date.now()
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
function cloneMetrics(metrics) {
|
|
424
|
-
return {
|
|
425
|
-
...metrics,
|
|
426
|
-
hitsByLayer: { ...metrics.hitsByLayer },
|
|
427
|
-
missesByLayer: { ...metrics.missesByLayer },
|
|
428
|
-
latencyByLayer: Object.fromEntries(
|
|
429
|
-
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
430
|
-
)
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
function diffMetrics(before, after) {
|
|
434
|
-
const latencyByLayer = Object.fromEntries(
|
|
435
|
-
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
436
|
-
layer,
|
|
437
|
-
{
|
|
438
|
-
avgMs: value.avgMs,
|
|
439
|
-
maxMs: value.maxMs,
|
|
440
|
-
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
441
|
-
}
|
|
442
|
-
])
|
|
443
|
-
);
|
|
444
|
-
return {
|
|
445
|
-
hits: after.hits - before.hits,
|
|
446
|
-
misses: after.misses - before.misses,
|
|
447
|
-
fetches: after.fetches - before.fetches,
|
|
448
|
-
sets: after.sets - before.sets,
|
|
449
|
-
deletes: after.deletes - before.deletes,
|
|
450
|
-
backfills: after.backfills - before.backfills,
|
|
451
|
-
invalidations: after.invalidations - before.invalidations,
|
|
452
|
-
staleHits: after.staleHits - before.staleHits,
|
|
453
|
-
refreshes: after.refreshes - before.refreshes,
|
|
454
|
-
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
455
|
-
writeFailures: after.writeFailures - before.writeFailures,
|
|
456
|
-
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
457
|
-
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
458
|
-
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
459
|
-
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
460
|
-
hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
|
|
461
|
-
missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
|
|
462
|
-
latencyByLayer,
|
|
463
|
-
resetAt: after.resetAt
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
function addMetrics(base, delta) {
|
|
467
|
-
return {
|
|
468
|
-
hits: base.hits + delta.hits,
|
|
469
|
-
misses: base.misses + delta.misses,
|
|
470
|
-
fetches: base.fetches + delta.fetches,
|
|
471
|
-
sets: base.sets + delta.sets,
|
|
472
|
-
deletes: base.deletes + delta.deletes,
|
|
473
|
-
backfills: base.backfills + delta.backfills,
|
|
474
|
-
invalidations: base.invalidations + delta.invalidations,
|
|
475
|
-
staleHits: base.staleHits + delta.staleHits,
|
|
476
|
-
refreshes: base.refreshes + delta.refreshes,
|
|
477
|
-
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
478
|
-
writeFailures: base.writeFailures + delta.writeFailures,
|
|
479
|
-
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
480
|
-
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
481
|
-
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
482
|
-
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
483
|
-
hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
|
|
484
|
-
missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
|
|
485
|
-
latencyByLayer: cloneMetrics(delta).latencyByLayer,
|
|
486
|
-
resetAt: base.resetAt
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
function diffMap(before, after) {
|
|
490
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
491
|
-
const result = {};
|
|
492
|
-
for (const key of keys) {
|
|
493
|
-
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
494
|
-
}
|
|
495
|
-
return result;
|
|
496
|
-
}
|
|
497
|
-
function addMap(base, delta) {
|
|
498
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
499
|
-
const result = {};
|
|
500
|
-
for (const key of keys) {
|
|
501
|
-
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
502
|
-
}
|
|
503
|
-
return result;
|
|
504
|
-
}
|
|
505
510
|
function validateNamespaceKey(key) {
|
|
506
511
|
if (key.length === 0) {
|
|
507
512
|
throw new Error("Namespace prefix must not be empty.");
|
|
@@ -711,101 +716,781 @@ function createInstanceId() {
|
|
|
711
716
|
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
712
717
|
}
|
|
713
718
|
|
|
714
|
-
// ../../src/internal/
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
return
|
|
719
|
+
// ../../src/internal/CacheStackGeneration.ts
|
|
720
|
+
var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
|
|
721
|
+
function generationPrefix(generation) {
|
|
722
|
+
return generation === void 0 ? "" : `v${generation}:`;
|
|
718
723
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
try {
|
|
723
|
-
await fs.lstat(current);
|
|
724
|
-
return current;
|
|
725
|
-
} catch (error) {
|
|
726
|
-
if (error.code !== "ENOENT") {
|
|
727
|
-
throw error;
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
const parent = path.dirname(current);
|
|
731
|
-
if (parent === current) {
|
|
732
|
-
return current;
|
|
733
|
-
}
|
|
734
|
-
current = parent;
|
|
735
|
-
}
|
|
724
|
+
function qualifyGenerationKey(key, generation) {
|
|
725
|
+
const prefix = generationPrefix(generation);
|
|
726
|
+
return prefix ? `${prefix}${key}` : key;
|
|
736
727
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
728
|
+
function qualifyGenerationPattern(pattern, generation) {
|
|
729
|
+
return qualifyGenerationKey(pattern, generation);
|
|
730
|
+
}
|
|
731
|
+
function stripGenerationPrefix(key, generation) {
|
|
732
|
+
const prefix = generationPrefix(generation);
|
|
733
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
734
|
+
return key;
|
|
740
735
|
}
|
|
741
|
-
|
|
742
|
-
|
|
736
|
+
return key.slice(prefix.length);
|
|
737
|
+
}
|
|
738
|
+
function resolveGenerationCleanupTarget({
|
|
739
|
+
previousGeneration,
|
|
740
|
+
nextGeneration,
|
|
741
|
+
generationCleanup
|
|
742
|
+
}) {
|
|
743
|
+
if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
|
|
744
|
+
return null;
|
|
743
745
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
return resolved;
|
|
746
|
+
return previousGeneration;
|
|
747
|
+
}
|
|
748
|
+
function resolveGenerationCleanupBatchSize(generationCleanup) {
|
|
749
|
+
if (typeof generationCleanup !== "object" || generationCleanup === null) {
|
|
750
|
+
return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
750
751
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
752
|
+
return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
753
|
+
}
|
|
754
|
+
function planGenerationCleanupBatches(keys, generationCleanup) {
|
|
755
|
+
if (keys.length === 0) {
|
|
756
|
+
return [];
|
|
755
757
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
761
|
-
return realTarget;
|
|
758
|
+
const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
|
|
759
|
+
const batches = [];
|
|
760
|
+
for (let index = 0; index < keys.length; index += batchSize) {
|
|
761
|
+
batches.push(keys.slice(index, index + batchSize));
|
|
762
762
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
763
|
+
return batches;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ../../src/internal/CacheStackInvalidationSupport.ts
|
|
767
|
+
var CacheStackInvalidationSupport = class {
|
|
768
|
+
constructor(options) {
|
|
769
|
+
this.options = options;
|
|
768
770
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
771
|
+
options;
|
|
772
|
+
async collectKeysForTag(tag, maxKeys) {
|
|
773
|
+
const keys = /* @__PURE__ */ new Set();
|
|
774
|
+
if (this.options.tagIndex.forEachKeyForTag) {
|
|
775
|
+
await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
776
|
+
keys.add(key);
|
|
777
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
778
|
+
});
|
|
779
|
+
return [...keys];
|
|
780
|
+
}
|
|
781
|
+
for (const key of await this.options.tagIndex.keysForTag(tag)) {
|
|
782
|
+
keys.add(key);
|
|
783
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
784
|
+
}
|
|
785
|
+
return [...keys];
|
|
773
786
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (existing.isSymbolicLink()) {
|
|
778
|
-
throw new Error("filePath must not point to a symbolic link.");
|
|
787
|
+
intersectKeys(groups) {
|
|
788
|
+
if (groups.length === 0) {
|
|
789
|
+
return [];
|
|
779
790
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
791
|
+
const [firstGroup, ...rest] = groups;
|
|
792
|
+
const restSets = rest.map((group) => new Set(group));
|
|
793
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
794
|
+
}
|
|
795
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
796
|
+
await Promise.all(
|
|
797
|
+
layers.map(async (layer) => {
|
|
798
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (layer.deleteMany) {
|
|
802
|
+
try {
|
|
803
|
+
await layer.deleteMany(keys);
|
|
804
|
+
} catch (error) {
|
|
805
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
806
|
+
}
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
await Promise.all(
|
|
810
|
+
keys.map(async (key) => {
|
|
811
|
+
try {
|
|
812
|
+
await layer.delete(key);
|
|
813
|
+
} catch (error) {
|
|
814
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
815
|
+
}
|
|
816
|
+
})
|
|
817
|
+
);
|
|
818
|
+
})
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
assertWithinInvalidationKeyLimit(size, maxKeys) {
|
|
822
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
823
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
// ../../src/internal/StoredValue.ts
|
|
829
|
+
function isStoredValueEnvelope(value) {
|
|
830
|
+
if (typeof value !== "object" || value === null) {
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
const v = value;
|
|
834
|
+
if (v.__layercache !== 1) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
if (v.kind !== "value" && v.kind !== "empty") {
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
850
|
+
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
|
|
869
|
+
if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
function createStoredValueEnvelope(options) {
|
|
884
|
+
const now = options.now ?? Date.now();
|
|
885
|
+
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
886
|
+
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
887
|
+
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
888
|
+
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
889
|
+
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
890
|
+
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
891
|
+
return {
|
|
892
|
+
__layercache: 1,
|
|
893
|
+
kind: options.kind,
|
|
894
|
+
value: options.value,
|
|
895
|
+
freshUntil,
|
|
896
|
+
staleUntil,
|
|
897
|
+
errorUntil,
|
|
898
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
899
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
900
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
function resolveStoredValue(stored, now = Date.now()) {
|
|
904
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
905
|
+
return { state: "fresh", value: stored, stored };
|
|
906
|
+
}
|
|
907
|
+
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
908
|
+
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
909
|
+
}
|
|
910
|
+
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
911
|
+
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
912
|
+
}
|
|
913
|
+
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
914
|
+
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
915
|
+
}
|
|
916
|
+
return { state: "expired", value: null, stored, envelope: stored };
|
|
917
|
+
}
|
|
918
|
+
function unwrapStoredValue(stored) {
|
|
919
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
920
|
+
return stored;
|
|
921
|
+
}
|
|
922
|
+
if (stored.kind === "empty") {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
return stored.value ?? null;
|
|
926
|
+
}
|
|
927
|
+
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
928
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
929
|
+
return void 0;
|
|
930
|
+
}
|
|
931
|
+
const expiry = maxExpiry(stored);
|
|
932
|
+
if (expiry === null) {
|
|
933
|
+
return void 0;
|
|
934
|
+
}
|
|
935
|
+
const remainingMs = expiry - now;
|
|
936
|
+
if (remainingMs <= 0) {
|
|
937
|
+
return 1;
|
|
938
|
+
}
|
|
939
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
940
|
+
}
|
|
941
|
+
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
942
|
+
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
943
|
+
return void 0;
|
|
944
|
+
}
|
|
945
|
+
const remainingMs = stored.freshUntil - now;
|
|
946
|
+
if (remainingMs <= 0) {
|
|
947
|
+
return 0;
|
|
948
|
+
}
|
|
949
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
950
|
+
}
|
|
951
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
952
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
953
|
+
return stored;
|
|
954
|
+
}
|
|
955
|
+
return createStoredValueEnvelope({
|
|
956
|
+
kind: stored.kind,
|
|
957
|
+
value: stored.value,
|
|
958
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
959
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
960
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
961
|
+
now
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
function maxExpiry(stored) {
|
|
965
|
+
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
966
|
+
(value) => value !== null
|
|
967
|
+
);
|
|
968
|
+
if (values.length === 0) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
return Math.max(...values);
|
|
972
|
+
}
|
|
973
|
+
function normalizePositiveSeconds(value) {
|
|
974
|
+
if (!value || value <= 0) {
|
|
975
|
+
return void 0;
|
|
976
|
+
}
|
|
977
|
+
return value;
|
|
978
|
+
}
|
|
979
|
+
function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
|
|
980
|
+
if (value == null) {
|
|
981
|
+
return true;
|
|
982
|
+
}
|
|
983
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// ../../src/internal/CacheStackLayerWriter.ts
|
|
987
|
+
var CacheStackLayerWriter = class {
|
|
988
|
+
constructor(options) {
|
|
989
|
+
this.options = options;
|
|
990
|
+
}
|
|
991
|
+
options;
|
|
992
|
+
async writeAcrossLayers(key, kind, value, writeOptions) {
|
|
993
|
+
const now = Date.now();
|
|
994
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
995
|
+
const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
|
|
996
|
+
const immediateOperations = [];
|
|
997
|
+
const deferredOperations = [];
|
|
998
|
+
for (const layer of this.options.layers) {
|
|
999
|
+
const operation = async () => {
|
|
1000
|
+
if (this.options.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, writeOptions, now);
|
|
1007
|
+
try {
|
|
1008
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
1014
|
+
deferredOperations.push(operation);
|
|
1015
|
+
} else {
|
|
1016
|
+
immediateOperations.push(operation);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
1020
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
1021
|
+
}
|
|
1022
|
+
async writeBatch(entries) {
|
|
1023
|
+
const now = Date.now();
|
|
1024
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
1025
|
+
const entryEpochs = new Map(
|
|
1026
|
+
entries.map((entry) => [entry.key, this.options.maintenance.currentKeyEpoch(entry.key)])
|
|
1027
|
+
);
|
|
1028
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
1029
|
+
const immediateOperations = [];
|
|
1030
|
+
const deferredOperations = [];
|
|
1031
|
+
for (const entry of entries) {
|
|
1032
|
+
for (const layer of this.options.layers) {
|
|
1033
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
1037
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
1038
|
+
bucket.push(layerEntry);
|
|
1039
|
+
entriesByLayer.set(layer, bucket);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
1043
|
+
const operation = async () => {
|
|
1044
|
+
if (clearEpoch !== this.options.maintenance.currentClearEpoch()) {
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const activeEntries = layerEntries.filter(
|
|
1048
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.options.maintenance.currentKeyEpoch(entry.key)
|
|
1049
|
+
);
|
|
1050
|
+
if (activeEntries.length === 0) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
try {
|
|
1054
|
+
if (layer.setMany) {
|
|
1055
|
+
await layer.setMany(activeEntries);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
1064
|
+
deferredOperations.push(operation);
|
|
1065
|
+
} else {
|
|
1066
|
+
immediateOperations.push(operation);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
1070
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
1071
|
+
return { clearEpoch, entryEpochs };
|
|
1072
|
+
}
|
|
1073
|
+
async executeLayerOperations(operations, context) {
|
|
1074
|
+
if (this.options.writePolicy !== "best-effort") {
|
|
1075
|
+
await Promise.all(operations.map((operation) => operation()));
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
1079
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
1080
|
+
if (failures.length === 0) {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
this.options.onWriteFailures(
|
|
1084
|
+
context,
|
|
1085
|
+
failures.map((failure) => failure.reason)
|
|
1086
|
+
);
|
|
1087
|
+
if (failures.length === operations.length) {
|
|
1088
|
+
throw new AggregateError(
|
|
1089
|
+
failures.map((failure) => failure.reason),
|
|
1090
|
+
`${context.action} failed for every cache layer`
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
|
|
1095
|
+
const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
|
|
1096
|
+
const staleWhileRevalidate = this.options.resolveLayerSeconds(
|
|
1097
|
+
layer.name,
|
|
1098
|
+
writeOptions?.staleWhileRevalidate,
|
|
1099
|
+
this.options.globalStaleWhileRevalidate
|
|
1100
|
+
);
|
|
1101
|
+
const staleIfError = this.options.resolveLayerSeconds(
|
|
1102
|
+
layer.name,
|
|
1103
|
+
writeOptions?.staleIfError,
|
|
1104
|
+
this.options.globalStaleIfError
|
|
1105
|
+
);
|
|
1106
|
+
const payload = createStoredValueEnvelope({
|
|
1107
|
+
kind,
|
|
1108
|
+
value,
|
|
1109
|
+
freshTtlSeconds: freshTtl,
|
|
1110
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
1111
|
+
staleIfErrorSeconds: staleIfError,
|
|
1112
|
+
now
|
|
1113
|
+
});
|
|
1114
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1115
|
+
return {
|
|
1116
|
+
key,
|
|
1117
|
+
value: payload,
|
|
1118
|
+
ttl
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
// ../../src/internal/CacheStackMaintenance.ts
|
|
1124
|
+
var CacheStackMaintenance = class {
|
|
1125
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
1126
|
+
writeBehindQueue = [];
|
|
1127
|
+
writeBehindTimer;
|
|
1128
|
+
writeBehindFlushPromise;
|
|
1129
|
+
generationCleanupPromise;
|
|
1130
|
+
clearEpoch = 0;
|
|
1131
|
+
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
1132
|
+
if (writeStrategy !== "write-behind") {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
1136
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
this.disposeWriteBehindTimer();
|
|
1140
|
+
this.writeBehindTimer = setInterval(() => {
|
|
1141
|
+
void flush();
|
|
1142
|
+
}, flushIntervalMs);
|
|
1143
|
+
this.writeBehindTimer.unref?.();
|
|
1144
|
+
}
|
|
1145
|
+
disposeWriteBehindTimer() {
|
|
1146
|
+
if (!this.writeBehindTimer) {
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
clearInterval(this.writeBehindTimer);
|
|
1150
|
+
this.writeBehindTimer = void 0;
|
|
1151
|
+
}
|
|
1152
|
+
beginClearEpoch() {
|
|
1153
|
+
this.clearEpoch += 1;
|
|
1154
|
+
this.keyEpochs.clear();
|
|
1155
|
+
this.writeBehindQueue.length = 0;
|
|
1156
|
+
}
|
|
1157
|
+
currentClearEpoch() {
|
|
1158
|
+
return this.clearEpoch;
|
|
1159
|
+
}
|
|
1160
|
+
currentKeyEpoch(key) {
|
|
1161
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
1162
|
+
}
|
|
1163
|
+
bumpKeyEpochs(keys) {
|
|
1164
|
+
for (const key of keys) {
|
|
1165
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
1169
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
1170
|
+
return true;
|
|
1171
|
+
}
|
|
1172
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
1173
|
+
return true;
|
|
1174
|
+
}
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
async enqueueWriteBehind(operation, options, flushBatch) {
|
|
1178
|
+
this.writeBehindQueue.push(operation);
|
|
1179
|
+
const batchSize = options?.batchSize ?? 100;
|
|
1180
|
+
const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
|
|
1181
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
1182
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
1186
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
async flushWriteBehindQueue(options, flushBatch) {
|
|
1190
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
1191
|
+
await this.writeBehindFlushPromise;
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
const batchSize = options?.batchSize ?? 100;
|
|
1195
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
1196
|
+
this.writeBehindFlushPromise = flushBatch(batch);
|
|
1197
|
+
try {
|
|
1198
|
+
await this.writeBehindFlushPromise;
|
|
1199
|
+
} finally {
|
|
1200
|
+
this.writeBehindFlushPromise = void 0;
|
|
1201
|
+
}
|
|
1202
|
+
if (this.writeBehindQueue.length > 0) {
|
|
1203
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
scheduleGenerationCleanup(generation, task, onError) {
|
|
1207
|
+
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
1208
|
+
onError(generation, error);
|
|
1209
|
+
});
|
|
1210
|
+
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
1211
|
+
if (this.generationCleanupPromise === scheduledTask) {
|
|
1212
|
+
this.generationCleanupPromise = void 0;
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
async waitForGenerationCleanup() {
|
|
1217
|
+
await this.generationCleanupPromise;
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
// ../../src/internal/CacheStackRuntimePolicy.ts
|
|
1222
|
+
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
1223
|
+
return degradedUntil !== void 0 && degradedUntil > now;
|
|
1224
|
+
}
|
|
1225
|
+
function shouldStartBackgroundRefresh({
|
|
1226
|
+
isDisconnecting,
|
|
1227
|
+
hasRefreshInFlight
|
|
1228
|
+
}) {
|
|
1229
|
+
return !isDisconnecting && !hasRefreshInFlight;
|
|
1230
|
+
}
|
|
1231
|
+
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
1232
|
+
if (!gracefulDegradation) {
|
|
1233
|
+
return { degrade: false };
|
|
1234
|
+
}
|
|
1235
|
+
const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1236
|
+
return {
|
|
1237
|
+
degrade: true,
|
|
1238
|
+
degradedUntil: now + retryAfterMs
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
function planFreshReadPolicies({
|
|
1242
|
+
stored,
|
|
1243
|
+
hasFetcher,
|
|
1244
|
+
slidingTtl,
|
|
1245
|
+
refreshAheadSeconds
|
|
1246
|
+
}) {
|
|
1247
|
+
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
1248
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
1249
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
1250
|
+
return {
|
|
1251
|
+
refreshedStored,
|
|
1252
|
+
refreshedStoredTtl,
|
|
1253
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// ../../src/internal/CacheStackSnapshotManager.ts
|
|
1258
|
+
import { constants, promises as fs } from "fs";
|
|
1259
|
+
import path from "path";
|
|
1260
|
+
|
|
1261
|
+
// ../../src/internal/CacheSnapshotFile.ts
|
|
1262
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
|
|
1263
|
+
const relative = path2.relative(realBaseDir, candidatePath);
|
|
1264
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
|
|
1265
|
+
}
|
|
1266
|
+
async function findExistingAncestor(directory, fs2, path2) {
|
|
1267
|
+
let current = directory;
|
|
1268
|
+
while (true) {
|
|
1269
|
+
try {
|
|
1270
|
+
await fs2.lstat(current);
|
|
1271
|
+
return current;
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
if (error.code !== "ENOENT") {
|
|
1274
|
+
throw error;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
const parent = path2.dirname(current);
|
|
1278
|
+
if (parent === current) {
|
|
1279
|
+
return current;
|
|
1280
|
+
}
|
|
1281
|
+
current = parent;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
1285
|
+
if (filePath.length === 0) {
|
|
1286
|
+
throw new Error("filePath must not be empty.");
|
|
1287
|
+
}
|
|
1288
|
+
if (filePath.includes("\0")) {
|
|
1289
|
+
throw new Error("filePath must not contain null bytes.");
|
|
1290
|
+
}
|
|
1291
|
+
const { promises: fs2 } = await import("fs");
|
|
1292
|
+
const path2 = await import("path");
|
|
1293
|
+
const resolved = path2.resolve(filePath);
|
|
1294
|
+
const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
|
|
1295
|
+
if (baseDir === false) {
|
|
1296
|
+
return resolved;
|
|
1297
|
+
}
|
|
1298
|
+
await fs2.mkdir(baseDir, { recursive: true });
|
|
1299
|
+
const realBaseDir = await fs2.realpath(baseDir);
|
|
1300
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
|
|
1301
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1302
|
+
}
|
|
1303
|
+
if (mode === "read") {
|
|
1304
|
+
const realTarget = await fs2.realpath(resolved);
|
|
1305
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
|
|
1306
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1307
|
+
}
|
|
1308
|
+
return realTarget;
|
|
1309
|
+
}
|
|
1310
|
+
const parentDir = path2.dirname(resolved);
|
|
1311
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs2, path2);
|
|
1312
|
+
const realExistingAncestor = await fs2.realpath(existingAncestor);
|
|
1313
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
|
|
1314
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1315
|
+
}
|
|
1316
|
+
await fs2.mkdir(parentDir, { recursive: true });
|
|
1317
|
+
const realParentDir = await fs2.realpath(parentDir);
|
|
1318
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
|
|
1319
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1320
|
+
}
|
|
1321
|
+
const targetPath = path2.join(realParentDir, path2.basename(resolved));
|
|
1322
|
+
try {
|
|
1323
|
+
const existing = await fs2.lstat(targetPath);
|
|
1324
|
+
if (existing.isSymbolicLink()) {
|
|
1325
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
1326
|
+
}
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
if (error.code !== "ENOENT") {
|
|
1329
|
+
throw error;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return targetPath;
|
|
1333
|
+
}
|
|
1334
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
1335
|
+
if (byteLimit === false) {
|
|
1336
|
+
return handle.readFile({ encoding: "utf8" });
|
|
1337
|
+
}
|
|
1338
|
+
const chunks = [];
|
|
1339
|
+
let totalBytes = 0;
|
|
1340
|
+
let position = 0;
|
|
1341
|
+
while (true) {
|
|
1342
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
1343
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
1344
|
+
if (bytesRead === 0) {
|
|
1345
|
+
break;
|
|
1346
|
+
}
|
|
1347
|
+
totalBytes += bytesRead;
|
|
1348
|
+
if (totalBytes > byteLimit) {
|
|
1349
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
1350
|
+
}
|
|
1351
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
1352
|
+
position += bytesRead;
|
|
1353
|
+
}
|
|
1354
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// ../../src/internal/CacheStackSnapshotManager.ts
|
|
1358
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1359
|
+
var CacheStackSnapshotManager = class {
|
|
1360
|
+
constructor(options) {
|
|
1361
|
+
this.options = options;
|
|
1362
|
+
}
|
|
1363
|
+
options;
|
|
1364
|
+
async exportState(maxEntries) {
|
|
1365
|
+
const entries = [];
|
|
1366
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
1367
|
+
entries.push(entry);
|
|
1368
|
+
});
|
|
1369
|
+
return entries;
|
|
1370
|
+
}
|
|
1371
|
+
async importState(entries) {
|
|
1372
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
1373
|
+
key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
|
|
1374
|
+
value: entry.value,
|
|
1375
|
+
ttl: entry.ttl
|
|
1376
|
+
}));
|
|
1377
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
1378
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
1379
|
+
await Promise.all(
|
|
1380
|
+
batch.map(async (entry) => {
|
|
1381
|
+
await Promise.all(this.options.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1382
|
+
await this.options.tagIndex.touch(entry.key);
|
|
1383
|
+
})
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
async persistToFile(filePath, snapshotBaseDir, maxEntries) {
|
|
1388
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1389
|
+
const tempPath = path.join(
|
|
1390
|
+
path.dirname(targetPath),
|
|
1391
|
+
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
1392
|
+
);
|
|
1393
|
+
let handle;
|
|
1394
|
+
try {
|
|
1395
|
+
handle = await fs.open(tempPath, "wx");
|
|
1396
|
+
const openedHandle = handle;
|
|
1397
|
+
await openedHandle.writeFile("[", "utf8");
|
|
1398
|
+
let wroteAny = false;
|
|
1399
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
1400
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
1401
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
1402
|
+
wroteAny = true;
|
|
1403
|
+
});
|
|
1404
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
1405
|
+
await openedHandle.close();
|
|
1406
|
+
handle = void 0;
|
|
1407
|
+
await fs.rename(tempPath, targetPath);
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
await handle?.close().catch(() => void 0);
|
|
1410
|
+
await fs.unlink(tempPath).catch(() => void 0);
|
|
1411
|
+
throw error;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
|
|
1415
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
|
|
1416
|
+
const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
1417
|
+
let raw;
|
|
1418
|
+
try {
|
|
1419
|
+
if (maxBytes !== false) {
|
|
1420
|
+
const stat = await handle.stat();
|
|
1421
|
+
if (stat.size > maxBytes) {
|
|
1422
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
raw = await readUtf8HandleWithLimit(handle, maxBytes);
|
|
1426
|
+
} finally {
|
|
1427
|
+
await handle.close();
|
|
1428
|
+
}
|
|
1429
|
+
let parsed;
|
|
1430
|
+
try {
|
|
1431
|
+
parsed = JSON.parse(raw);
|
|
1432
|
+
} catch (cause) {
|
|
1433
|
+
throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
|
|
1434
|
+
}
|
|
1435
|
+
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1436
|
+
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1437
|
+
}
|
|
1438
|
+
await this.importState(
|
|
1439
|
+
parsed.map((entry) => ({
|
|
1440
|
+
key: entry.key,
|
|
1441
|
+
value: this.sanitizeSnapshotValue(entry.value),
|
|
1442
|
+
ttl: entry.ttl
|
|
1443
|
+
}))
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
1447
|
+
const exported = /* @__PURE__ */ new Set();
|
|
1448
|
+
for (const layer of this.options.layers) {
|
|
1449
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
const visitKey = async (key) => {
|
|
1453
|
+
const exportedKey = this.options.stripQualifiedKey(key);
|
|
1454
|
+
if (exported.has(exportedKey)) {
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
const stored = await this.options.readLayerEntry(layer, key);
|
|
1458
|
+
if (stored === null) {
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
exported.add(exportedKey);
|
|
1462
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
1463
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
1464
|
+
}
|
|
1465
|
+
await visitor({
|
|
1466
|
+
key: exportedKey,
|
|
1467
|
+
value: stored,
|
|
1468
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
1469
|
+
});
|
|
1470
|
+
};
|
|
1471
|
+
if (layer.forEachKey) {
|
|
1472
|
+
await layer.forEachKey(visitKey);
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
const keys = await layer.keys?.();
|
|
1476
|
+
for (const key of keys ?? []) {
|
|
1477
|
+
await visitKey(key);
|
|
1478
|
+
}
|
|
783
1479
|
}
|
|
784
1480
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1481
|
+
isCacheSnapshotEntries(value) {
|
|
1482
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1483
|
+
if (!entry || typeof entry !== "object") {
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
const candidate = entry;
|
|
1487
|
+
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
1488
|
+
});
|
|
790
1489
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
let position = 0;
|
|
794
|
-
while (true) {
|
|
795
|
-
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
796
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
797
|
-
if (bytesRead === 0) {
|
|
798
|
-
break;
|
|
799
|
-
}
|
|
800
|
-
totalBytes += bytesRead;
|
|
801
|
-
if (totalBytes > byteLimit) {
|
|
802
|
-
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
803
|
-
}
|
|
804
|
-
chunks.push(buffer.subarray(0, bytesRead));
|
|
805
|
-
position += bytesRead;
|
|
1490
|
+
sanitizeSnapshotValue(value) {
|
|
1491
|
+
return this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
806
1492
|
}
|
|
807
|
-
|
|
808
|
-
}
|
|
1493
|
+
};
|
|
809
1494
|
|
|
810
1495
|
// ../../src/internal/CacheStackValidation.ts
|
|
811
1496
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
@@ -960,7 +1645,6 @@ var CircuitBreakerManager = class {
|
|
|
960
1645
|
if (!options) {
|
|
961
1646
|
return;
|
|
962
1647
|
}
|
|
963
|
-
this.pruneIfNeeded();
|
|
964
1648
|
const failureThreshold = options.failureThreshold ?? 3;
|
|
965
1649
|
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
966
1650
|
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
|
|
@@ -969,6 +1653,7 @@ var CircuitBreakerManager = class {
|
|
|
969
1653
|
state.openUntil = Date.now() + cooldownMs;
|
|
970
1654
|
}
|
|
971
1655
|
this.breakers.set(key, state);
|
|
1656
|
+
this.pruneIfNeeded();
|
|
972
1657
|
}
|
|
973
1658
|
recordSuccess(key) {
|
|
974
1659
|
this.breakers.delete(key);
|
|
@@ -1034,7 +1719,11 @@ var FetchRateLimiter = class {
|
|
|
1034
1719
|
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
1035
1720
|
nextFetcherBucketId = 0;
|
|
1036
1721
|
drainTimer;
|
|
1722
|
+
isDisposed = false;
|
|
1037
1723
|
async schedule(options, context, task) {
|
|
1724
|
+
if (this.isDisposed) {
|
|
1725
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1726
|
+
}
|
|
1038
1727
|
if (!options) {
|
|
1039
1728
|
return task();
|
|
1040
1729
|
}
|
|
@@ -1057,6 +1746,27 @@ var FetchRateLimiter = class {
|
|
|
1057
1746
|
this.drain();
|
|
1058
1747
|
});
|
|
1059
1748
|
}
|
|
1749
|
+
dispose() {
|
|
1750
|
+
this.isDisposed = true;
|
|
1751
|
+
if (this.drainTimer) {
|
|
1752
|
+
clearTimeout(this.drainTimer);
|
|
1753
|
+
this.drainTimer = void 0;
|
|
1754
|
+
}
|
|
1755
|
+
for (const bucket of this.buckets.values()) {
|
|
1756
|
+
if (bucket.cleanupTimer) {
|
|
1757
|
+
clearTimeout(bucket.cleanupTimer);
|
|
1758
|
+
bucket.cleanupTimer = void 0;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
for (const queue of this.queuesByBucket.values()) {
|
|
1762
|
+
for (const item of queue) {
|
|
1763
|
+
item.reject(new Error("FetchRateLimiter has been disposed."));
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
this.queuesByBucket.clear();
|
|
1767
|
+
this.pendingBuckets.clear();
|
|
1768
|
+
this.buckets.clear();
|
|
1769
|
+
}
|
|
1060
1770
|
normalize(options) {
|
|
1061
1771
|
const maxConcurrent = options.maxConcurrent;
|
|
1062
1772
|
const intervalMs = options.intervalMs;
|
|
@@ -1092,6 +1802,9 @@ var FetchRateLimiter = class {
|
|
|
1092
1802
|
return "global";
|
|
1093
1803
|
}
|
|
1094
1804
|
drain() {
|
|
1805
|
+
if (this.isDisposed) {
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1095
1808
|
if (this.drainTimer) {
|
|
1096
1809
|
clearTimeout(this.drainTimer);
|
|
1097
1810
|
this.drainTimer = void 0;
|
|
@@ -1188,6 +1901,9 @@ var FetchRateLimiter = class {
|
|
|
1188
1901
|
}
|
|
1189
1902
|
}
|
|
1190
1903
|
bucketState(bucketKey) {
|
|
1904
|
+
if (this.isDisposed) {
|
|
1905
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1906
|
+
}
|
|
1191
1907
|
const existing = this.buckets.get(bucketKey);
|
|
1192
1908
|
if (existing) {
|
|
1193
1909
|
return existing;
|
|
@@ -1249,226 +1965,68 @@ var MetricsCollector = class {
|
|
|
1249
1965
|
latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
|
|
1250
1966
|
};
|
|
1251
1967
|
}
|
|
1252
|
-
increment(field, amount = 1) {
|
|
1253
|
-
;
|
|
1254
|
-
this.data[field] += amount;
|
|
1255
|
-
}
|
|
1256
|
-
incrementLayer(map, layerName) {
|
|
1257
|
-
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
1258
|
-
}
|
|
1259
|
-
/**
|
|
1260
|
-
* Records a read latency sample for the given layer.
|
|
1261
|
-
* Maintains a rolling average and max using Welford's online algorithm.
|
|
1262
|
-
*/
|
|
1263
|
-
recordLatency(layerName, durationMs) {
|
|
1264
|
-
const existing = this.data.latencyByLayer[layerName];
|
|
1265
|
-
if (!existing) {
|
|
1266
|
-
this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
existing.count += 1;
|
|
1270
|
-
existing.avgMs += (durationMs - existing.avgMs) / existing.count;
|
|
1271
|
-
if (durationMs > existing.maxMs) {
|
|
1272
|
-
existing.maxMs = durationMs;
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
reset() {
|
|
1276
|
-
this.data = this.empty();
|
|
1277
|
-
}
|
|
1278
|
-
hitRate() {
|
|
1279
|
-
const total = this.data.hits + this.data.misses;
|
|
1280
|
-
const overall = total === 0 ? 0 : this.data.hits / total;
|
|
1281
|
-
const byLayer = {};
|
|
1282
|
-
const allLayers = /* @__PURE__ */ new Set([...Object.keys(this.data.hitsByLayer), ...Object.keys(this.data.missesByLayer)]);
|
|
1283
|
-
for (const layer of allLayers) {
|
|
1284
|
-
const h = this.data.hitsByLayer[layer] ?? 0;
|
|
1285
|
-
const m = this.data.missesByLayer[layer] ?? 0;
|
|
1286
|
-
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
1287
|
-
}
|
|
1288
|
-
return { overall, byLayer };
|
|
1289
|
-
}
|
|
1290
|
-
empty() {
|
|
1291
|
-
return {
|
|
1292
|
-
hits: 0,
|
|
1293
|
-
misses: 0,
|
|
1294
|
-
fetches: 0,
|
|
1295
|
-
sets: 0,
|
|
1296
|
-
deletes: 0,
|
|
1297
|
-
backfills: 0,
|
|
1298
|
-
invalidations: 0,
|
|
1299
|
-
staleHits: 0,
|
|
1300
|
-
refreshes: 0,
|
|
1301
|
-
refreshErrors: 0,
|
|
1302
|
-
writeFailures: 0,
|
|
1303
|
-
singleFlightWaits: 0,
|
|
1304
|
-
negativeCacheHits: 0,
|
|
1305
|
-
circuitBreakerTrips: 0,
|
|
1306
|
-
degradedOperations: 0,
|
|
1307
|
-
hitsByLayer: {},
|
|
1308
|
-
missesByLayer: {},
|
|
1309
|
-
latencyByLayer: {},
|
|
1310
|
-
resetAt: Date.now()
|
|
1311
|
-
};
|
|
1312
|
-
}
|
|
1313
|
-
};
|
|
1314
|
-
|
|
1315
|
-
// ../../src/internal/StoredValue.ts
|
|
1316
|
-
function isStoredValueEnvelope(value) {
|
|
1317
|
-
if (typeof value !== "object" || value === null) {
|
|
1318
|
-
return false;
|
|
1319
|
-
}
|
|
1320
|
-
const v = value;
|
|
1321
|
-
if (v.__layercache !== 1) {
|
|
1322
|
-
return false;
|
|
1323
|
-
}
|
|
1324
|
-
if (v.kind !== "value" && v.kind !== "empty") {
|
|
1325
|
-
return false;
|
|
1326
|
-
}
|
|
1327
|
-
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
1328
|
-
return false;
|
|
1329
|
-
}
|
|
1330
|
-
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
1331
|
-
return false;
|
|
1332
|
-
}
|
|
1333
|
-
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
1334
|
-
return false;
|
|
1335
|
-
}
|
|
1336
|
-
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
1337
|
-
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
1338
|
-
return false;
|
|
1339
|
-
}
|
|
1340
|
-
if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
|
|
1341
|
-
return false;
|
|
1342
|
-
}
|
|
1343
|
-
if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
|
|
1344
|
-
return false;
|
|
1345
|
-
}
|
|
1346
|
-
if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
|
|
1347
|
-
return false;
|
|
1348
|
-
}
|
|
1349
|
-
if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
|
|
1350
|
-
return false;
|
|
1351
|
-
}
|
|
1352
|
-
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
1353
|
-
return false;
|
|
1354
|
-
}
|
|
1355
|
-
const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
|
|
1356
|
-
if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
|
|
1357
|
-
return false;
|
|
1358
|
-
}
|
|
1359
|
-
if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
|
|
1360
|
-
return false;
|
|
1361
|
-
}
|
|
1362
|
-
if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
|
|
1363
|
-
return false;
|
|
1364
|
-
}
|
|
1365
|
-
if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
|
|
1366
|
-
return false;
|
|
1367
|
-
}
|
|
1368
|
-
return true;
|
|
1369
|
-
}
|
|
1370
|
-
function createStoredValueEnvelope(options) {
|
|
1371
|
-
const now = options.now ?? Date.now();
|
|
1372
|
-
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
1373
|
-
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
1374
|
-
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
1375
|
-
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
1376
|
-
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
1377
|
-
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
1378
|
-
return {
|
|
1379
|
-
__layercache: 1,
|
|
1380
|
-
kind: options.kind,
|
|
1381
|
-
value: options.value,
|
|
1382
|
-
freshUntil,
|
|
1383
|
-
staleUntil,
|
|
1384
|
-
errorUntil,
|
|
1385
|
-
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
1386
|
-
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
1387
|
-
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
1388
|
-
};
|
|
1389
|
-
}
|
|
1390
|
-
function resolveStoredValue(stored, now = Date.now()) {
|
|
1391
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1392
|
-
return { state: "fresh", value: stored, stored };
|
|
1393
|
-
}
|
|
1394
|
-
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
1395
|
-
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1396
|
-
}
|
|
1397
|
-
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
1398
|
-
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1399
|
-
}
|
|
1400
|
-
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
1401
|
-
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1402
|
-
}
|
|
1403
|
-
return { state: "expired", value: null, stored, envelope: stored };
|
|
1404
|
-
}
|
|
1405
|
-
function unwrapStoredValue(stored) {
|
|
1406
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1407
|
-
return stored;
|
|
1408
|
-
}
|
|
1409
|
-
if (stored.kind === "empty") {
|
|
1410
|
-
return null;
|
|
1411
|
-
}
|
|
1412
|
-
return stored.value ?? null;
|
|
1413
|
-
}
|
|
1414
|
-
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
1415
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1416
|
-
return void 0;
|
|
1417
|
-
}
|
|
1418
|
-
const expiry = maxExpiry(stored);
|
|
1419
|
-
if (expiry === null) {
|
|
1420
|
-
return void 0;
|
|
1421
|
-
}
|
|
1422
|
-
const remainingMs = expiry - now;
|
|
1423
|
-
if (remainingMs <= 0) {
|
|
1424
|
-
return 1;
|
|
1425
|
-
}
|
|
1426
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
1427
|
-
}
|
|
1428
|
-
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
1429
|
-
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
1430
|
-
return void 0;
|
|
1968
|
+
increment(field, amount = 1) {
|
|
1969
|
+
;
|
|
1970
|
+
this.data[field] += amount;
|
|
1431
1971
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
return 0;
|
|
1972
|
+
incrementLayer(map, layerName) {
|
|
1973
|
+
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
1435
1974
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1975
|
+
/**
|
|
1976
|
+
* Records a read latency sample for the given layer.
|
|
1977
|
+
* Maintains a rolling average and max using Welford's online algorithm.
|
|
1978
|
+
*/
|
|
1979
|
+
recordLatency(layerName, durationMs) {
|
|
1980
|
+
const existing = this.data.latencyByLayer[layerName];
|
|
1981
|
+
if (!existing) {
|
|
1982
|
+
this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
existing.count += 1;
|
|
1986
|
+
existing.avgMs += (durationMs - existing.avgMs) / existing.count;
|
|
1987
|
+
if (durationMs > existing.maxMs) {
|
|
1988
|
+
existing.maxMs = durationMs;
|
|
1989
|
+
}
|
|
1441
1990
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
value: stored.value,
|
|
1445
|
-
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
1446
|
-
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
1447
|
-
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
1448
|
-
now
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
function maxExpiry(stored) {
|
|
1452
|
-
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
1453
|
-
(value) => value !== null
|
|
1454
|
-
);
|
|
1455
|
-
if (values.length === 0) {
|
|
1456
|
-
return null;
|
|
1991
|
+
reset() {
|
|
1992
|
+
this.data = this.empty();
|
|
1457
1993
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1994
|
+
hitRate() {
|
|
1995
|
+
const total = this.data.hits + this.data.misses;
|
|
1996
|
+
const overall = total === 0 ? 0 : this.data.hits / total;
|
|
1997
|
+
const byLayer = {};
|
|
1998
|
+
const allLayers = /* @__PURE__ */ new Set([...Object.keys(this.data.hitsByLayer), ...Object.keys(this.data.missesByLayer)]);
|
|
1999
|
+
for (const layer of allLayers) {
|
|
2000
|
+
const h = this.data.hitsByLayer[layer] ?? 0;
|
|
2001
|
+
const m = this.data.missesByLayer[layer] ?? 0;
|
|
2002
|
+
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
2003
|
+
}
|
|
2004
|
+
return { overall, byLayer };
|
|
1463
2005
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
2006
|
+
empty() {
|
|
2007
|
+
return {
|
|
2008
|
+
hits: 0,
|
|
2009
|
+
misses: 0,
|
|
2010
|
+
fetches: 0,
|
|
2011
|
+
sets: 0,
|
|
2012
|
+
deletes: 0,
|
|
2013
|
+
backfills: 0,
|
|
2014
|
+
invalidations: 0,
|
|
2015
|
+
staleHits: 0,
|
|
2016
|
+
refreshes: 0,
|
|
2017
|
+
refreshErrors: 0,
|
|
2018
|
+
writeFailures: 0,
|
|
2019
|
+
singleFlightWaits: 0,
|
|
2020
|
+
negativeCacheHits: 0,
|
|
2021
|
+
circuitBreakerTrips: 0,
|
|
2022
|
+
degradedOperations: 0,
|
|
2023
|
+
hitsByLayer: {},
|
|
2024
|
+
missesByLayer: {},
|
|
2025
|
+
latencyByLayer: {},
|
|
2026
|
+
resetAt: Date.now()
|
|
2027
|
+
};
|
|
1469
2028
|
}
|
|
1470
|
-
|
|
1471
|
-
}
|
|
2029
|
+
};
|
|
1472
2030
|
|
|
1473
2031
|
// ../../src/internal/TtlResolver.ts
|
|
1474
2032
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
@@ -1804,19 +2362,19 @@ var TagIndex = class {
|
|
|
1804
2362
|
if (!this.knownKeys.delete(key)) {
|
|
1805
2363
|
return;
|
|
1806
2364
|
}
|
|
1807
|
-
const
|
|
2365
|
+
const path2 = [];
|
|
1808
2366
|
let node = this.root;
|
|
1809
2367
|
for (const character of key) {
|
|
1810
2368
|
const child = node.children.get(character);
|
|
1811
2369
|
if (!child) {
|
|
1812
2370
|
return;
|
|
1813
2371
|
}
|
|
1814
|
-
|
|
2372
|
+
path2.push([node, character]);
|
|
1815
2373
|
node = child;
|
|
1816
2374
|
}
|
|
1817
2375
|
node.terminal = false;
|
|
1818
|
-
for (let index =
|
|
1819
|
-
const entry =
|
|
2376
|
+
for (let index = path2.length - 1; index >= 0; index -= 1) {
|
|
2377
|
+
const entry = path2[index];
|
|
1820
2378
|
if (!entry) {
|
|
1821
2379
|
continue;
|
|
1822
2380
|
}
|
|
@@ -1830,39 +2388,31 @@ var TagIndex = class {
|
|
|
1830
2388
|
}
|
|
1831
2389
|
};
|
|
1832
2390
|
|
|
1833
|
-
// ../../src/
|
|
1834
|
-
var
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
}
|
|
1840
|
-
deserialize(payload) {
|
|
1841
|
-
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1842
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1843
|
-
}
|
|
1844
|
-
};
|
|
1845
|
-
var MAX_SANITIZE_DEPTH = 200;
|
|
1846
|
-
function sanitizeJsonValue(value, depth, state) {
|
|
2391
|
+
// ../../src/internal/StructuredDataSanitizer.ts
|
|
2392
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2393
|
+
function sanitizeStructuredData(value, options) {
|
|
2394
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
2395
|
+
}
|
|
2396
|
+
function sanitizeValue(value, depth, state, options) {
|
|
1847
2397
|
state.count += 1;
|
|
1848
|
-
if (state.count >
|
|
1849
|
-
throw new Error(
|
|
2398
|
+
if (state.count > options.maxNodes) {
|
|
2399
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
1850
2400
|
}
|
|
1851
|
-
if (depth >
|
|
1852
|
-
throw new Error(
|
|
2401
|
+
if (depth > options.maxDepth) {
|
|
2402
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
1853
2403
|
}
|
|
1854
2404
|
if (Array.isArray(value)) {
|
|
1855
|
-
return value.map((entry) =>
|
|
2405
|
+
return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
|
|
1856
2406
|
}
|
|
1857
2407
|
if (!isPlainObject(value)) {
|
|
1858
2408
|
return value;
|
|
1859
2409
|
}
|
|
1860
|
-
const sanitized = {};
|
|
2410
|
+
const sanitized = options.createObject?.() ?? {};
|
|
1861
2411
|
for (const [key, entry] of Object.entries(value)) {
|
|
1862
|
-
if (
|
|
2412
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
1863
2413
|
continue;
|
|
1864
2414
|
}
|
|
1865
|
-
sanitized[key] =
|
|
2415
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
1866
2416
|
}
|
|
1867
2417
|
return sanitized;
|
|
1868
2418
|
}
|
|
@@ -1870,6 +2420,21 @@ function isPlainObject(value) {
|
|
|
1870
2420
|
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1871
2421
|
}
|
|
1872
2422
|
|
|
2423
|
+
// ../../src/serialization/JsonSerializer.ts
|
|
2424
|
+
var JsonSerializer = class {
|
|
2425
|
+
serialize(value) {
|
|
2426
|
+
return JSON.stringify(value);
|
|
2427
|
+
}
|
|
2428
|
+
deserialize(payload) {
|
|
2429
|
+
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
2430
|
+
return sanitizeStructuredData(JSON.parse(normalized), {
|
|
2431
|
+
label: "JSON payload",
|
|
2432
|
+
maxDepth: 200,
|
|
2433
|
+
maxNodes: 1e4
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2437
|
+
|
|
1873
2438
|
// ../../src/stampede/StampedeGuard.ts
|
|
1874
2439
|
var StampedeGuard = class {
|
|
1875
2440
|
mutexes = /* @__PURE__ */ new Map();
|
|
@@ -1913,7 +2478,6 @@ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
|
1913
2478
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1914
2479
|
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1915
2480
|
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1916
|
-
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1917
2481
|
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
1918
2482
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1919
2483
|
var DebugLogger = class {
|
|
@@ -1970,6 +2534,35 @@ var CacheStack = class extends EventEmitter {
|
|
|
1970
2534
|
await this.handleLayerFailure(layer, operation, error);
|
|
1971
2535
|
}
|
|
1972
2536
|
});
|
|
2537
|
+
this.invalidation = new CacheStackInvalidationSupport({
|
|
2538
|
+
tagIndex: this.tagIndex,
|
|
2539
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2540
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
2541
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2542
|
+
}
|
|
2543
|
+
});
|
|
2544
|
+
this.layerWriter = new CacheStackLayerWriter({
|
|
2545
|
+
layers: this.layers,
|
|
2546
|
+
maintenance: this.maintenance,
|
|
2547
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2548
|
+
shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
|
|
2549
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
2550
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2551
|
+
},
|
|
2552
|
+
enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
|
|
2553
|
+
resolveFreshTtl: this.resolveFreshTtl.bind(this),
|
|
2554
|
+
resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
|
|
2555
|
+
globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
|
|
2556
|
+
globalStaleIfError: this.options.staleIfError,
|
|
2557
|
+
writePolicy: this.options.writePolicy,
|
|
2558
|
+
onWriteFailures: (context, failures) => {
|
|
2559
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2560
|
+
this.logger.debug?.("write-failure", {
|
|
2561
|
+
...context,
|
|
2562
|
+
failures: failures.map((failure) => this.formatError(failure))
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
1973
2566
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
1974
2567
|
this.logger.warn?.(
|
|
1975
2568
|
"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."
|
|
@@ -1985,6 +2578,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
1985
2578
|
"broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
|
|
1986
2579
|
);
|
|
1987
2580
|
}
|
|
2581
|
+
this.snapshots = new CacheStackSnapshotManager({
|
|
2582
|
+
layers: this.layers,
|
|
2583
|
+
tagIndex: this.tagIndex,
|
|
2584
|
+
snapshotSerializer: this.snapshotSerializer,
|
|
2585
|
+
readLayerEntry: this.readLayerEntry.bind(this),
|
|
2586
|
+
qualifyKey: this.qualifyKey.bind(this),
|
|
2587
|
+
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
2588
|
+
validateCacheKey,
|
|
2589
|
+
formatError: this.formatError.bind(this)
|
|
2590
|
+
});
|
|
1988
2591
|
this.initializeWriteBehind(options.writeBehind);
|
|
1989
2592
|
this.startup = this.initialize();
|
|
1990
2593
|
}
|
|
@@ -2000,17 +2603,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
2000
2603
|
keyDiscovery;
|
|
2001
2604
|
fetchRateLimiter = new FetchRateLimiter();
|
|
2002
2605
|
snapshotSerializer = new JsonSerializer();
|
|
2606
|
+
invalidation;
|
|
2607
|
+
layerWriter;
|
|
2608
|
+
snapshots;
|
|
2003
2609
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
2004
2610
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2005
|
-
|
|
2611
|
+
maintenance = new CacheStackMaintenance();
|
|
2006
2612
|
ttlResolver;
|
|
2007
2613
|
circuitBreakerManager;
|
|
2614
|
+
nextOperationId = 0;
|
|
2008
2615
|
currentGeneration;
|
|
2009
|
-
writeBehindQueue = [];
|
|
2010
|
-
writeBehindTimer;
|
|
2011
|
-
writeBehindFlushPromise;
|
|
2012
|
-
generationCleanupPromise;
|
|
2013
|
-
clearEpoch = 0;
|
|
2014
2616
|
isDisconnecting = false;
|
|
2015
2617
|
disconnectPromise;
|
|
2016
2618
|
/**
|
|
@@ -2020,10 +2622,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
2020
2622
|
* and no `fetcher` is provided.
|
|
2021
2623
|
*/
|
|
2022
2624
|
async get(key, fetcher, options) {
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2625
|
+
return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
|
|
2626
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2627
|
+
this.validateWriteOptions(options);
|
|
2628
|
+
await this.awaitStartup("get");
|
|
2629
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
2630
|
+
});
|
|
2027
2631
|
}
|
|
2028
2632
|
async getPrepared(normalizedKey, fetcher, options) {
|
|
2029
2633
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
@@ -2145,28 +2749,32 @@ var CacheStack = class extends EventEmitter {
|
|
|
2145
2749
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
2146
2750
|
*/
|
|
2147
2751
|
async set(key, value, options) {
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2752
|
+
await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
|
|
2753
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2754
|
+
this.validateWriteOptions(options);
|
|
2755
|
+
await this.awaitStartup("set");
|
|
2756
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
2757
|
+
});
|
|
2152
2758
|
}
|
|
2153
2759
|
/**
|
|
2154
2760
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
2155
2761
|
*/
|
|
2156
2762
|
async delete(key) {
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2763
|
+
await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
|
|
2764
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2765
|
+
await this.awaitStartup("delete");
|
|
2766
|
+
await this.deleteKeys([normalizedKey]);
|
|
2767
|
+
await this.publishInvalidation({
|
|
2768
|
+
scope: "key",
|
|
2769
|
+
keys: [normalizedKey],
|
|
2770
|
+
sourceId: this.instanceId,
|
|
2771
|
+
operation: "delete"
|
|
2772
|
+
});
|
|
2165
2773
|
});
|
|
2166
2774
|
}
|
|
2167
2775
|
async clear() {
|
|
2168
2776
|
await this.awaitStartup("clear");
|
|
2169
|
-
this.beginClearEpoch();
|
|
2777
|
+
this.maintenance.beginClearEpoch();
|
|
2170
2778
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
2171
2779
|
await this.tagIndex.clear();
|
|
2172
2780
|
this.ttlResolver.clearProfiles();
|
|
@@ -2194,95 +2802,99 @@ var CacheStack = class extends EventEmitter {
|
|
|
2194
2802
|
});
|
|
2195
2803
|
}
|
|
2196
2804
|
async mget(entries) {
|
|
2197
|
-
this.
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2805
|
+
return this.observeOperation("layercache.mget", void 0, async () => {
|
|
2806
|
+
this.assertActive("mget");
|
|
2807
|
+
if (entries.length === 0) {
|
|
2808
|
+
return [];
|
|
2809
|
+
}
|
|
2810
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2811
|
+
...entry,
|
|
2812
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2813
|
+
}));
|
|
2814
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2815
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
2816
|
+
if (!canFastPath) {
|
|
2817
|
+
await this.awaitStartup("mget");
|
|
2818
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
2819
|
+
return Promise.all(
|
|
2820
|
+
normalizedEntries.map((entry) => {
|
|
2821
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
2822
|
+
const existing = pendingReads.get(entry.key);
|
|
2823
|
+
if (!existing) {
|
|
2824
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2825
|
+
pendingReads.set(entry.key, {
|
|
2826
|
+
promise,
|
|
2827
|
+
fetch: entry.fetch,
|
|
2828
|
+
optionsSignature
|
|
2829
|
+
});
|
|
2830
|
+
return promise;
|
|
2831
|
+
}
|
|
2832
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
2833
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
2834
|
+
}
|
|
2835
|
+
return existing.promise;
|
|
2836
|
+
})
|
|
2837
|
+
);
|
|
2838
|
+
}
|
|
2208
2839
|
await this.awaitStartup("mget");
|
|
2209
|
-
const
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2840
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2841
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
2842
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
2843
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2844
|
+
const entry = normalizedEntries[index];
|
|
2845
|
+
if (!entry) continue;
|
|
2846
|
+
const key = entry.key;
|
|
2847
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
2848
|
+
indexes.push(index);
|
|
2849
|
+
indexesByKey.set(key, indexes);
|
|
2850
|
+
pending.add(key);
|
|
2851
|
+
}
|
|
2852
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2853
|
+
const layer = this.layers[layerIndex];
|
|
2854
|
+
if (!layer) continue;
|
|
2855
|
+
const keys = [...pending];
|
|
2856
|
+
if (keys.length === 0) {
|
|
2857
|
+
break;
|
|
2858
|
+
}
|
|
2859
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2860
|
+
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2861
|
+
const key = keys[offset];
|
|
2862
|
+
const stored = values[offset];
|
|
2863
|
+
if (!key || stored === null) {
|
|
2864
|
+
continue;
|
|
2222
2865
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2866
|
+
const resolved = resolveStoredValue(stored);
|
|
2867
|
+
if (resolved.state === "expired") {
|
|
2868
|
+
await layer.delete(key);
|
|
2869
|
+
continue;
|
|
2225
2870
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
const pending = /* @__PURE__ */ new Set();
|
|
2232
|
-
const indexesByKey = /* @__PURE__ */ new Map();
|
|
2233
|
-
const resultsByKey = /* @__PURE__ */ new Map();
|
|
2234
|
-
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2235
|
-
const entry = normalizedEntries[index];
|
|
2236
|
-
if (!entry) continue;
|
|
2237
|
-
const key = entry.key;
|
|
2238
|
-
const indexes = indexesByKey.get(key) ?? [];
|
|
2239
|
-
indexes.push(index);
|
|
2240
|
-
indexesByKey.set(key, indexes);
|
|
2241
|
-
pending.add(key);
|
|
2242
|
-
}
|
|
2243
|
-
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2244
|
-
const layer = this.layers[layerIndex];
|
|
2245
|
-
if (!layer) continue;
|
|
2246
|
-
const keys = [...pending];
|
|
2247
|
-
if (keys.length === 0) {
|
|
2248
|
-
break;
|
|
2249
|
-
}
|
|
2250
|
-
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2251
|
-
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2252
|
-
const key = keys[offset];
|
|
2253
|
-
const stored = values[offset];
|
|
2254
|
-
if (!key || stored === null) {
|
|
2255
|
-
continue;
|
|
2871
|
+
await this.tagIndex.touch(key);
|
|
2872
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
2873
|
+
resultsByKey.set(key, resolved.value);
|
|
2874
|
+
pending.delete(key);
|
|
2875
|
+
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
2256
2876
|
}
|
|
2257
|
-
const resolved = resolveStoredValue(stored);
|
|
2258
|
-
if (resolved.state === "expired") {
|
|
2259
|
-
await layer.delete(key);
|
|
2260
|
-
continue;
|
|
2261
|
-
}
|
|
2262
|
-
await this.tagIndex.touch(key);
|
|
2263
|
-
await this.backfill(key, stored, layerIndex - 1);
|
|
2264
|
-
resultsByKey.set(key, resolved.value);
|
|
2265
|
-
pending.delete(key);
|
|
2266
|
-
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
2267
2877
|
}
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2878
|
+
if (pending.size > 0) {
|
|
2879
|
+
for (const key of pending) {
|
|
2880
|
+
await this.tagIndex.remove(key);
|
|
2881
|
+
this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
|
|
2882
|
+
}
|
|
2273
2883
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2884
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
2885
|
+
});
|
|
2276
2886
|
}
|
|
2277
2887
|
async mset(entries) {
|
|
2278
|
-
this.
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2888
|
+
await this.observeOperation("layercache.mset", void 0, async () => {
|
|
2889
|
+
this.assertActive("mset");
|
|
2890
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2891
|
+
...entry,
|
|
2892
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2893
|
+
}));
|
|
2894
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2895
|
+
await this.awaitStartup("mset");
|
|
2896
|
+
await this.writeBatch(normalizedEntries);
|
|
2897
|
+
});
|
|
2286
2898
|
}
|
|
2287
2899
|
async warm(entries, options = {}) {
|
|
2288
2900
|
this.assertActive("warm");
|
|
@@ -2335,40 +2947,50 @@ var CacheStack = class extends EventEmitter {
|
|
|
2335
2947
|
return new CacheNamespace(this, prefix);
|
|
2336
2948
|
}
|
|
2337
2949
|
async invalidateByTag(tag) {
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2950
|
+
await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
|
|
2951
|
+
validateTag(tag);
|
|
2952
|
+
await this.awaitStartup("invalidateByTag");
|
|
2953
|
+
const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
|
|
2954
|
+
await this.deleteKeys(keys);
|
|
2955
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2956
|
+
});
|
|
2343
2957
|
}
|
|
2344
2958
|
async invalidateByTags(tags, mode = "any") {
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2959
|
+
await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
|
|
2960
|
+
if (tags.length === 0) {
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
validateTags(tags);
|
|
2964
|
+
await this.awaitStartup("invalidateByTags");
|
|
2965
|
+
const keysByTag = await Promise.all(
|
|
2966
|
+
tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
|
|
2967
|
+
);
|
|
2968
|
+
const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2969
|
+
this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
|
|
2970
|
+
await this.deleteKeys(keys);
|
|
2971
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2972
|
+
});
|
|
2355
2973
|
}
|
|
2356
2974
|
async invalidateByPattern(pattern) {
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
this.
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2975
|
+
await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
|
|
2976
|
+
validatePattern(pattern);
|
|
2977
|
+
await this.awaitStartup("invalidateByPattern");
|
|
2978
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2979
|
+
this.qualifyPattern(pattern),
|
|
2980
|
+
this.invalidationMaxKeys()
|
|
2981
|
+
);
|
|
2982
|
+
await this.deleteKeys(keys);
|
|
2983
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2984
|
+
});
|
|
2365
2985
|
}
|
|
2366
2986
|
async invalidateByPrefix(prefix) {
|
|
2367
|
-
await this.
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2987
|
+
await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
|
|
2988
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
2989
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2990
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
2991
|
+
await this.deleteKeys(keys);
|
|
2992
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2993
|
+
});
|
|
2372
2994
|
}
|
|
2373
2995
|
getMetrics() {
|
|
2374
2996
|
return this.metricsCollector.snapshot;
|
|
@@ -2424,9 +3046,15 @@ var CacheStack = class extends EventEmitter {
|
|
|
2424
3046
|
bumpGeneration(nextGeneration) {
|
|
2425
3047
|
const current = this.currentGeneration ?? 0;
|
|
2426
3048
|
const previousGeneration = this.currentGeneration;
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
3049
|
+
const updatedGeneration = nextGeneration ?? current + 1;
|
|
3050
|
+
const generationToCleanup = resolveGenerationCleanupTarget({
|
|
3051
|
+
previousGeneration,
|
|
3052
|
+
nextGeneration: updatedGeneration,
|
|
3053
|
+
generationCleanup: this.options.generationCleanup
|
|
3054
|
+
});
|
|
3055
|
+
this.currentGeneration = updatedGeneration;
|
|
3056
|
+
if (generationToCleanup !== null) {
|
|
3057
|
+
this.scheduleGenerationCleanup(generationToCleanup);
|
|
2430
3058
|
}
|
|
2431
3059
|
return this.currentGeneration;
|
|
2432
3060
|
}
|
|
@@ -2473,95 +3101,19 @@ var CacheStack = class extends EventEmitter {
|
|
|
2473
3101
|
}
|
|
2474
3102
|
async exportState() {
|
|
2475
3103
|
await this.awaitStartup("exportState");
|
|
2476
|
-
|
|
2477
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2478
|
-
entries.push(entry);
|
|
2479
|
-
});
|
|
2480
|
-
return entries;
|
|
3104
|
+
return this.snapshots.exportState(this.snapshotMaxEntries());
|
|
2481
3105
|
}
|
|
2482
3106
|
async importState(entries) {
|
|
2483
3107
|
await this.awaitStartup("importState");
|
|
2484
|
-
|
|
2485
|
-
key: this.qualifyKey(validateCacheKey(entry.key)),
|
|
2486
|
-
value: entry.value,
|
|
2487
|
-
ttl: entry.ttl
|
|
2488
|
-
}));
|
|
2489
|
-
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
2490
|
-
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
2491
|
-
await Promise.all(
|
|
2492
|
-
batch.map(async (entry) => {
|
|
2493
|
-
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2494
|
-
await this.tagIndex.touch(entry.key);
|
|
2495
|
-
})
|
|
2496
|
-
);
|
|
2497
|
-
}
|
|
3108
|
+
await this.snapshots.importState(entries);
|
|
2498
3109
|
}
|
|
2499
3110
|
async persistToFile(filePath) {
|
|
2500
3111
|
this.assertActive("persistToFile");
|
|
2501
|
-
|
|
2502
|
-
const path = await import("path");
|
|
2503
|
-
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
2504
|
-
const tempPath = path.join(
|
|
2505
|
-
path.dirname(targetPath),
|
|
2506
|
-
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
2507
|
-
);
|
|
2508
|
-
let handle;
|
|
2509
|
-
try {
|
|
2510
|
-
handle = await fs.open(tempPath, "wx");
|
|
2511
|
-
const openedHandle = handle;
|
|
2512
|
-
await openedHandle.writeFile("[", "utf8");
|
|
2513
|
-
let wroteAny = false;
|
|
2514
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2515
|
-
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
2516
|
-
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
2517
|
-
wroteAny = true;
|
|
2518
|
-
});
|
|
2519
|
-
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
2520
|
-
await openedHandle.close();
|
|
2521
|
-
handle = void 0;
|
|
2522
|
-
await fs.rename(tempPath, targetPath);
|
|
2523
|
-
} catch (error) {
|
|
2524
|
-
await handle?.close().catch(() => void 0);
|
|
2525
|
-
await fs.unlink(tempPath).catch(() => void 0);
|
|
2526
|
-
throw error;
|
|
2527
|
-
}
|
|
3112
|
+
await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
|
|
2528
3113
|
}
|
|
2529
3114
|
async restoreFromFile(filePath) {
|
|
2530
3115
|
this.assertActive("restoreFromFile");
|
|
2531
|
-
|
|
2532
|
-
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
2533
|
-
const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2534
|
-
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
2535
|
-
let raw;
|
|
2536
|
-
try {
|
|
2537
|
-
if (snapshotMaxBytes !== false) {
|
|
2538
|
-
const stat = await handle.stat();
|
|
2539
|
-
if (stat.size > snapshotMaxBytes) {
|
|
2540
|
-
throw new Error(
|
|
2541
|
-
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
2542
|
-
);
|
|
2543
|
-
}
|
|
2544
|
-
}
|
|
2545
|
-
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
2546
|
-
} finally {
|
|
2547
|
-
await handle.close();
|
|
2548
|
-
}
|
|
2549
|
-
let parsed;
|
|
2550
|
-
try {
|
|
2551
|
-
parsed = JSON.parse(raw);
|
|
2552
|
-
} catch (cause) {
|
|
2553
|
-
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
2554
|
-
}
|
|
2555
|
-
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
2556
|
-
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
2557
|
-
}
|
|
2558
|
-
await this.importState(
|
|
2559
|
-
parsed.map((entry) => ({
|
|
2560
|
-
key: entry.key,
|
|
2561
|
-
value: this.sanitizeSnapshotValue(entry.value),
|
|
2562
|
-
ttl: entry.ttl
|
|
2563
|
-
}))
|
|
2564
|
-
);
|
|
3116
|
+
await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
|
|
2565
3117
|
}
|
|
2566
3118
|
async disconnect() {
|
|
2567
3119
|
if (!this.disconnectPromise) {
|
|
@@ -2570,12 +3122,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
2570
3122
|
await this.startup;
|
|
2571
3123
|
await this.unsubscribeInvalidation?.();
|
|
2572
3124
|
await this.flushWriteBehindQueue();
|
|
2573
|
-
await this.
|
|
3125
|
+
await this.maintenance.waitForGenerationCleanup();
|
|
2574
3126
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
this.writeBehindTimer = void 0;
|
|
2578
|
-
}
|
|
3127
|
+
this.maintenance.disposeWriteBehindTimer();
|
|
3128
|
+
this.fetchRateLimiter.dispose();
|
|
2579
3129
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
2580
3130
|
})();
|
|
2581
3131
|
}
|
|
@@ -2651,13 +3201,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
2651
3201
|
if (!this.shouldNegativeCache(options)) {
|
|
2652
3202
|
return null;
|
|
2653
3203
|
}
|
|
2654
|
-
if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
3204
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2655
3205
|
this.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
2656
3206
|
key,
|
|
2657
3207
|
expectedClearEpoch,
|
|
2658
|
-
clearEpoch: this.
|
|
3208
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2659
3209
|
expectedKeyEpoch,
|
|
2660
|
-
keyEpoch: this.currentKeyEpoch(key)
|
|
3210
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2661
3211
|
});
|
|
2662
3212
|
return null;
|
|
2663
3213
|
}
|
|
@@ -2673,13 +3223,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
2673
3223
|
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2674
3224
|
}
|
|
2675
3225
|
}
|
|
2676
|
-
if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
3226
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2677
3227
|
this.logger.debug?.("skip-store-after-invalidation", {
|
|
2678
3228
|
key,
|
|
2679
3229
|
expectedClearEpoch,
|
|
2680
|
-
clearEpoch: this.
|
|
3230
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2681
3231
|
expectedKeyEpoch,
|
|
2682
|
-
keyEpoch: this.currentKeyEpoch(key)
|
|
3232
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2683
3233
|
});
|
|
2684
3234
|
return fetched;
|
|
2685
3235
|
}
|
|
@@ -2687,10 +3237,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
2687
3237
|
return fetched;
|
|
2688
3238
|
}
|
|
2689
3239
|
async storeEntry(key, kind, value, options) {
|
|
2690
|
-
const clearEpoch = this.
|
|
2691
|
-
const keyEpoch = this.currentKeyEpoch(key);
|
|
2692
|
-
await this.writeAcrossLayers(key, kind, value, options);
|
|
2693
|
-
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
3240
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3241
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3242
|
+
await this.layerWriter.writeAcrossLayers(key, kind, value, options);
|
|
3243
|
+
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2694
3244
|
return;
|
|
2695
3245
|
}
|
|
2696
3246
|
if (options?.tags) {
|
|
@@ -2706,57 +3256,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
2706
3256
|
}
|
|
2707
3257
|
}
|
|
2708
3258
|
async writeBatch(entries) {
|
|
2709
|
-
const
|
|
2710
|
-
|
|
2711
|
-
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
|
|
2712
|
-
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2713
|
-
const immediateOperations = [];
|
|
2714
|
-
const deferredOperations = [];
|
|
2715
|
-
for (const entry of entries) {
|
|
2716
|
-
for (const layer of this.layers) {
|
|
2717
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2718
|
-
continue;
|
|
2719
|
-
}
|
|
2720
|
-
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
2721
|
-
const bucket = entriesByLayer.get(layer) ?? [];
|
|
2722
|
-
bucket.push(layerEntry);
|
|
2723
|
-
entriesByLayer.set(layer, bucket);
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2727
|
-
const operation = async () => {
|
|
2728
|
-
if (clearEpoch !== this.clearEpoch) {
|
|
2729
|
-
return;
|
|
2730
|
-
}
|
|
2731
|
-
const activeEntries = layerEntries.filter(
|
|
2732
|
-
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
|
|
2733
|
-
);
|
|
2734
|
-
if (activeEntries.length === 0) {
|
|
2735
|
-
return;
|
|
2736
|
-
}
|
|
2737
|
-
try {
|
|
2738
|
-
if (layer.setMany) {
|
|
2739
|
-
await layer.setMany(activeEntries);
|
|
2740
|
-
return;
|
|
2741
|
-
}
|
|
2742
|
-
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2743
|
-
} catch (error) {
|
|
2744
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
2745
|
-
}
|
|
2746
|
-
};
|
|
2747
|
-
if (this.shouldWriteBehind(layer)) {
|
|
2748
|
-
deferredOperations.push(operation);
|
|
2749
|
-
} else {
|
|
2750
|
-
immediateOperations.push(operation);
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2754
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2755
|
-
if (clearEpoch !== this.clearEpoch) {
|
|
3259
|
+
const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
|
|
3260
|
+
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2756
3261
|
return;
|
|
2757
3262
|
}
|
|
2758
3263
|
for (const entry of entries) {
|
|
2759
|
-
if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
3264
|
+
if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2760
3265
|
continue;
|
|
2761
3266
|
}
|
|
2762
3267
|
if (entry.options?.tags) {
|
|
@@ -2858,58 +3363,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2858
3363
|
this.emit("backfill", { key, layer: layer.name });
|
|
2859
3364
|
}
|
|
2860
3365
|
}
|
|
2861
|
-
async writeAcrossLayers(key, kind, value, options) {
|
|
2862
|
-
const now = Date.now();
|
|
2863
|
-
const clearEpoch = this.clearEpoch;
|
|
2864
|
-
const keyEpoch = this.currentKeyEpoch(key);
|
|
2865
|
-
const immediateOperations = [];
|
|
2866
|
-
const deferredOperations = [];
|
|
2867
|
-
for (const layer of this.layers) {
|
|
2868
|
-
const operation = async () => {
|
|
2869
|
-
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2870
|
-
return;
|
|
2871
|
-
}
|
|
2872
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2873
|
-
return;
|
|
2874
|
-
}
|
|
2875
|
-
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
2876
|
-
try {
|
|
2877
|
-
await layer.set(entry.key, entry.value, entry.ttl);
|
|
2878
|
-
} catch (error) {
|
|
2879
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
2880
|
-
}
|
|
2881
|
-
};
|
|
2882
|
-
if (this.shouldWriteBehind(layer)) {
|
|
2883
|
-
deferredOperations.push(operation);
|
|
2884
|
-
} else {
|
|
2885
|
-
immediateOperations.push(operation);
|
|
2886
|
-
}
|
|
2887
|
-
}
|
|
2888
|
-
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
2889
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2890
|
-
}
|
|
2891
|
-
async executeLayerOperations(operations, context) {
|
|
2892
|
-
if (this.options.writePolicy !== "best-effort") {
|
|
2893
|
-
await Promise.all(operations.map((operation) => operation()));
|
|
2894
|
-
return;
|
|
2895
|
-
}
|
|
2896
|
-
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
2897
|
-
const failures = results.filter((result) => result.status === "rejected");
|
|
2898
|
-
if (failures.length === 0) {
|
|
2899
|
-
return;
|
|
2900
|
-
}
|
|
2901
|
-
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2902
|
-
this.logger.debug?.("write-failure", {
|
|
2903
|
-
...context,
|
|
2904
|
-
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
2905
|
-
});
|
|
2906
|
-
if (failures.length === operations.length) {
|
|
2907
|
-
throw new AggregateError(
|
|
2908
|
-
failures.map((failure) => failure.reason),
|
|
2909
|
-
`${context.action} failed for every cache layer`
|
|
2910
|
-
);
|
|
2911
|
-
}
|
|
2912
|
-
}
|
|
2913
3366
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
2914
3367
|
return this.ttlResolver.resolveFreshTtl(
|
|
2915
3368
|
key,
|
|
@@ -2929,11 +3382,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
2929
3382
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
2930
3383
|
}
|
|
2931
3384
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
2932
|
-
if (
|
|
3385
|
+
if (!shouldStartBackgroundRefresh({
|
|
3386
|
+
isDisconnecting: this.isDisconnecting,
|
|
3387
|
+
hasRefreshInFlight: this.backgroundRefreshes.has(key)
|
|
3388
|
+
})) {
|
|
2933
3389
|
return;
|
|
2934
3390
|
}
|
|
2935
|
-
const clearEpoch = this.
|
|
2936
|
-
const keyEpoch = this.currentKeyEpoch(key);
|
|
3391
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3392
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2937
3393
|
const refresh = (async () => {
|
|
2938
3394
|
this.metricsCollector.increment("refreshes");
|
|
2939
3395
|
try {
|
|
@@ -2971,8 +3427,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
2971
3427
|
if (keys.length === 0) {
|
|
2972
3428
|
return;
|
|
2973
3429
|
}
|
|
2974
|
-
this.bumpKeyEpochs(keys);
|
|
2975
|
-
await this.deleteKeysFromLayers(this.layers, keys);
|
|
3430
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
3431
|
+
await this.invalidation.deleteKeysFromLayers(this.layers, keys);
|
|
2976
3432
|
for (const key of keys) {
|
|
2977
3433
|
await this.tagIndex.remove(key);
|
|
2978
3434
|
this.ttlResolver.deleteProfile(key);
|
|
@@ -2995,7 +3451,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2995
3451
|
}
|
|
2996
3452
|
const localLayers = this.layers.filter((layer) => layer.isLocal);
|
|
2997
3453
|
if (message.scope === "clear") {
|
|
2998
|
-
this.beginClearEpoch();
|
|
3454
|
+
this.maintenance.beginClearEpoch();
|
|
2999
3455
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
3000
3456
|
await this.tagIndex.clear();
|
|
3001
3457
|
this.ttlResolver.clearProfiles();
|
|
@@ -3003,8 +3459,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
3003
3459
|
return;
|
|
3004
3460
|
}
|
|
3005
3461
|
const keys = message.keys ?? [];
|
|
3006
|
-
this.bumpKeyEpochs(keys);
|
|
3007
|
-
await this.deleteKeysFromLayers(localLayers, keys);
|
|
3462
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
3463
|
+
await this.invalidation.deleteKeysFromLayers(localLayers, keys);
|
|
3008
3464
|
if (message.operation !== "write") {
|
|
3009
3465
|
for (const key of keys) {
|
|
3010
3466
|
await this.tagIndex.remove(key);
|
|
@@ -3061,35 +3517,47 @@ var CacheStack = class extends EventEmitter {
|
|
|
3061
3517
|
shouldBroadcastL1Invalidation() {
|
|
3062
3518
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
3063
3519
|
}
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3520
|
+
async observeOperation(name, attributes, execute) {
|
|
3521
|
+
const id = this.nextOperationId;
|
|
3522
|
+
this.nextOperationId += 1;
|
|
3523
|
+
this.emit("operation-start", { id, name, attributes });
|
|
3524
|
+
try {
|
|
3525
|
+
const result = await execute();
|
|
3526
|
+
this.emit("operation-end", {
|
|
3527
|
+
id,
|
|
3528
|
+
name,
|
|
3529
|
+
attributes,
|
|
3530
|
+
success: true,
|
|
3531
|
+
result: result === null ? "null" : void 0
|
|
3532
|
+
});
|
|
3533
|
+
return result;
|
|
3534
|
+
} catch (error) {
|
|
3535
|
+
this.emit("operation-end", {
|
|
3536
|
+
id,
|
|
3537
|
+
name,
|
|
3538
|
+
attributes,
|
|
3539
|
+
success: false,
|
|
3540
|
+
error
|
|
3541
|
+
});
|
|
3542
|
+
throw error;
|
|
3543
|
+
}
|
|
3070
3544
|
}
|
|
3071
3545
|
scheduleGenerationCleanup(generation) {
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
this.generationCleanupPromise = void 0;
|
|
3546
|
+
this.maintenance.scheduleGenerationCleanup(
|
|
3547
|
+
generation,
|
|
3548
|
+
async (generationToClean) => this.cleanupGeneration(generationToClean),
|
|
3549
|
+
(failedGeneration, error) => {
|
|
3550
|
+
this.logger.warn?.("generation-cleanup-error", {
|
|
3551
|
+
generation: failedGeneration,
|
|
3552
|
+
error: this.formatError(error)
|
|
3553
|
+
});
|
|
3081
3554
|
}
|
|
3082
|
-
|
|
3555
|
+
);
|
|
3083
3556
|
}
|
|
3084
3557
|
async cleanupGeneration(generation) {
|
|
3085
3558
|
const prefix = `v${generation}:`;
|
|
3086
3559
|
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
3087
|
-
|
|
3088
|
-
return;
|
|
3089
|
-
}
|
|
3090
|
-
const batchSize = this.generationCleanupBatchSize();
|
|
3091
|
-
for (let index = 0; index < keys.length; index += batchSize) {
|
|
3092
|
-
const batch = keys.slice(index, index + batchSize);
|
|
3560
|
+
for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
|
|
3093
3561
|
await this.deleteKeys(batch);
|
|
3094
3562
|
await this.publishInvalidation({
|
|
3095
3563
|
scope: "keys",
|
|
@@ -3100,161 +3568,43 @@ var CacheStack = class extends EventEmitter {
|
|
|
3100
3568
|
}
|
|
3101
3569
|
}
|
|
3102
3570
|
initializeWriteBehind(options) {
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
return;
|
|
3109
|
-
}
|
|
3110
|
-
this.writeBehindTimer = setInterval(() => {
|
|
3111
|
-
void this.flushWriteBehindQueue();
|
|
3112
|
-
}, flushIntervalMs);
|
|
3113
|
-
this.writeBehindTimer.unref?.();
|
|
3571
|
+
this.maintenance.initializeWriteBehindTimer(
|
|
3572
|
+
this.options.writeStrategy,
|
|
3573
|
+
options,
|
|
3574
|
+
this.flushWriteBehindQueue.bind(this)
|
|
3575
|
+
);
|
|
3114
3576
|
}
|
|
3115
3577
|
shouldWriteBehind(layer) {
|
|
3116
3578
|
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
3117
3579
|
}
|
|
3118
|
-
beginClearEpoch() {
|
|
3119
|
-
this.clearEpoch += 1;
|
|
3120
|
-
this.keyEpochs.clear();
|
|
3121
|
-
this.writeBehindQueue.length = 0;
|
|
3122
|
-
}
|
|
3123
|
-
currentKeyEpoch(key) {
|
|
3124
|
-
return this.keyEpochs.get(key) ?? 0;
|
|
3125
|
-
}
|
|
3126
|
-
bumpKeyEpochs(keys) {
|
|
3127
|
-
for (const key of keys) {
|
|
3128
|
-
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
3129
|
-
}
|
|
3130
|
-
}
|
|
3131
|
-
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
3132
|
-
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
3133
|
-
return true;
|
|
3134
|
-
}
|
|
3135
|
-
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
3136
|
-
return true;
|
|
3137
|
-
}
|
|
3138
|
-
return false;
|
|
3139
|
-
}
|
|
3140
3580
|
async enqueueWriteBehind(operation) {
|
|
3141
|
-
this.
|
|
3142
|
-
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
3143
|
-
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
3144
|
-
if (this.writeBehindQueue.length >= batchSize) {
|
|
3145
|
-
await this.flushWriteBehindQueue();
|
|
3146
|
-
return;
|
|
3147
|
-
}
|
|
3148
|
-
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
3149
|
-
await this.flushWriteBehindQueue();
|
|
3150
|
-
}
|
|
3581
|
+
await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
3151
3582
|
}
|
|
3152
3583
|
async flushWriteBehindQueue() {
|
|
3153
|
-
|
|
3154
|
-
|
|
3584
|
+
await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
3585
|
+
}
|
|
3586
|
+
async runWriteBehindBatch(batch) {
|
|
3587
|
+
const results = await Promise.allSettled(batch.map((operation) => operation()));
|
|
3588
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
3589
|
+
if (failures.length === 0) {
|
|
3155
3590
|
return;
|
|
3156
3591
|
}
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
if (failures.length > 0) {
|
|
3163
|
-
this.metricsCollector.increment("writeFailures", failures.length);
|
|
3164
|
-
this.logger.error?.("write-behind-flush-failure", {
|
|
3165
|
-
failed: failures.length,
|
|
3166
|
-
total: batch.length,
|
|
3167
|
-
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
3168
|
-
});
|
|
3169
|
-
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
3170
|
-
}
|
|
3171
|
-
})();
|
|
3172
|
-
await this.writeBehindFlushPromise;
|
|
3173
|
-
this.writeBehindFlushPromise = void 0;
|
|
3174
|
-
if (this.writeBehindQueue.length > 0) {
|
|
3175
|
-
await this.flushWriteBehindQueue();
|
|
3176
|
-
}
|
|
3177
|
-
}
|
|
3178
|
-
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
3179
|
-
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
3180
|
-
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
3181
|
-
layer.name,
|
|
3182
|
-
options?.staleWhileRevalidate,
|
|
3183
|
-
this.options.staleWhileRevalidate
|
|
3184
|
-
);
|
|
3185
|
-
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
3186
|
-
const payload = createStoredValueEnvelope({
|
|
3187
|
-
kind,
|
|
3188
|
-
value,
|
|
3189
|
-
freshTtlSeconds: freshTtl,
|
|
3190
|
-
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
3191
|
-
staleIfErrorSeconds: staleIfError,
|
|
3192
|
-
now
|
|
3592
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
3593
|
+
this.logger.error?.("write-behind-flush-failure", {
|
|
3594
|
+
failed: failures.length,
|
|
3595
|
+
total: batch.length,
|
|
3596
|
+
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
3193
3597
|
});
|
|
3194
|
-
|
|
3195
|
-
return {
|
|
3196
|
-
key,
|
|
3197
|
-
value: payload,
|
|
3198
|
-
ttl
|
|
3199
|
-
};
|
|
3200
|
-
}
|
|
3201
|
-
intersectKeys(groups) {
|
|
3202
|
-
if (groups.length === 0) {
|
|
3203
|
-
return [];
|
|
3204
|
-
}
|
|
3205
|
-
const [firstGroup, ...rest] = groups;
|
|
3206
|
-
if (!firstGroup) {
|
|
3207
|
-
return [];
|
|
3208
|
-
}
|
|
3209
|
-
const restSets = rest.map((group) => new Set(group));
|
|
3210
|
-
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
3598
|
+
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
3211
3599
|
}
|
|
3212
3600
|
qualifyKey(key) {
|
|
3213
|
-
|
|
3214
|
-
return prefix ? `${prefix}${key}` : key;
|
|
3601
|
+
return qualifyGenerationKey(key, this.currentGeneration);
|
|
3215
3602
|
}
|
|
3216
3603
|
qualifyPattern(pattern) {
|
|
3217
|
-
|
|
3218
|
-
return prefix ? `${prefix}${pattern}` : pattern;
|
|
3604
|
+
return qualifyGenerationPattern(pattern, this.currentGeneration);
|
|
3219
3605
|
}
|
|
3220
3606
|
stripQualifiedKey(key) {
|
|
3221
|
-
|
|
3222
|
-
if (!prefix || !key.startsWith(prefix)) {
|
|
3223
|
-
return key;
|
|
3224
|
-
}
|
|
3225
|
-
return key.slice(prefix.length);
|
|
3226
|
-
}
|
|
3227
|
-
generationPrefix() {
|
|
3228
|
-
if (this.currentGeneration === void 0) {
|
|
3229
|
-
return "";
|
|
3230
|
-
}
|
|
3231
|
-
return `v${this.currentGeneration}:`;
|
|
3232
|
-
}
|
|
3233
|
-
async deleteKeysFromLayers(layers, keys) {
|
|
3234
|
-
await Promise.all(
|
|
3235
|
-
layers.map(async (layer) => {
|
|
3236
|
-
if (this.shouldSkipLayer(layer)) {
|
|
3237
|
-
return;
|
|
3238
|
-
}
|
|
3239
|
-
if (layer.deleteMany) {
|
|
3240
|
-
try {
|
|
3241
|
-
await layer.deleteMany(keys);
|
|
3242
|
-
} catch (error) {
|
|
3243
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
3244
|
-
}
|
|
3245
|
-
return;
|
|
3246
|
-
}
|
|
3247
|
-
await Promise.all(
|
|
3248
|
-
keys.map(async (key) => {
|
|
3249
|
-
try {
|
|
3250
|
-
await layer.delete(key);
|
|
3251
|
-
} catch (error) {
|
|
3252
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
3253
|
-
}
|
|
3254
|
-
})
|
|
3255
|
-
);
|
|
3256
|
-
})
|
|
3257
|
-
);
|
|
3607
|
+
return stripGenerationPrefix(key, this.currentGeneration);
|
|
3258
3608
|
}
|
|
3259
3609
|
validateConfiguration() {
|
|
3260
3610
|
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
@@ -3319,37 +3669,38 @@ var CacheStack = class extends EventEmitter {
|
|
|
3319
3669
|
this.assertActive(operation);
|
|
3320
3670
|
}
|
|
3321
3671
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
3322
|
-
const
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3672
|
+
const plan = planFreshReadPolicies({
|
|
3673
|
+
stored: hit.stored,
|
|
3674
|
+
hasFetcher: Boolean(fetcher),
|
|
3675
|
+
slidingTtl: options?.slidingTtl ?? false,
|
|
3676
|
+
refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
3677
|
+
});
|
|
3678
|
+
if (plan.refreshedStored) {
|
|
3327
3679
|
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
3328
3680
|
const layer = this.layers[index];
|
|
3329
3681
|
if (!layer || this.shouldSkipLayer(layer)) {
|
|
3330
3682
|
continue;
|
|
3331
3683
|
}
|
|
3332
3684
|
try {
|
|
3333
|
-
await layer.set(key,
|
|
3685
|
+
await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
|
|
3334
3686
|
} catch (error) {
|
|
3335
3687
|
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
3336
3688
|
}
|
|
3337
3689
|
}
|
|
3338
3690
|
}
|
|
3339
|
-
if (fetcher &&
|
|
3691
|
+
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
3340
3692
|
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
3341
3693
|
}
|
|
3342
3694
|
}
|
|
3343
3695
|
shouldSkipLayer(layer) {
|
|
3344
|
-
|
|
3345
|
-
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
3696
|
+
return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
|
|
3346
3697
|
}
|
|
3347
3698
|
async handleLayerFailure(layer, operation, error) {
|
|
3348
|
-
|
|
3699
|
+
const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
|
|
3700
|
+
if (!recovery.degrade) {
|
|
3349
3701
|
throw error;
|
|
3350
3702
|
}
|
|
3351
|
-
|
|
3352
|
-
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
3703
|
+
this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
|
|
3353
3704
|
this.metricsCollector.increment("degradedOperations");
|
|
3354
3705
|
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
3355
3706
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
@@ -3385,18 +3736,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
3385
3736
|
this.emit("error", { operation, ...context });
|
|
3386
3737
|
}
|
|
3387
3738
|
}
|
|
3388
|
-
isCacheSnapshotEntries(value) {
|
|
3389
|
-
return Array.isArray(value) && value.every((entry) => {
|
|
3390
|
-
if (!entry || typeof entry !== "object") {
|
|
3391
|
-
return false;
|
|
3392
|
-
}
|
|
3393
|
-
const candidate = entry;
|
|
3394
|
-
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
3395
|
-
});
|
|
3396
|
-
}
|
|
3397
|
-
sanitizeSnapshotValue(value) {
|
|
3398
|
-
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
3399
|
-
}
|
|
3400
3739
|
snapshotMaxBytes() {
|
|
3401
3740
|
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
3402
3741
|
}
|
|
@@ -3406,62 +3745,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
3406
3745
|
invalidationMaxKeys() {
|
|
3407
3746
|
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
3408
3747
|
}
|
|
3409
|
-
async collectKeysForTag(tag) {
|
|
3410
|
-
const keys = /* @__PURE__ */ new Set();
|
|
3411
|
-
if (this.tagIndex.forEachKeyForTag) {
|
|
3412
|
-
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
3413
|
-
keys.add(key);
|
|
3414
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3415
|
-
});
|
|
3416
|
-
return [...keys];
|
|
3417
|
-
}
|
|
3418
|
-
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3419
|
-
keys.add(key);
|
|
3420
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3421
|
-
}
|
|
3422
|
-
return [...keys];
|
|
3423
|
-
}
|
|
3424
|
-
assertWithinInvalidationKeyLimit(size) {
|
|
3425
|
-
const maxKeys = this.invalidationMaxKeys();
|
|
3426
|
-
if (maxKeys !== false && size > maxKeys) {
|
|
3427
|
-
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
3428
|
-
}
|
|
3429
|
-
}
|
|
3430
|
-
async visitExportEntries(maxEntries, visitor) {
|
|
3431
|
-
const exported = /* @__PURE__ */ new Set();
|
|
3432
|
-
for (const layer of this.layers) {
|
|
3433
|
-
if (!layer.keys && !layer.forEachKey) {
|
|
3434
|
-
continue;
|
|
3435
|
-
}
|
|
3436
|
-
const visitKey = async (key) => {
|
|
3437
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
3438
|
-
if (exported.has(exportedKey)) {
|
|
3439
|
-
return;
|
|
3440
|
-
}
|
|
3441
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
3442
|
-
if (stored === null) {
|
|
3443
|
-
return;
|
|
3444
|
-
}
|
|
3445
|
-
exported.add(exportedKey);
|
|
3446
|
-
if (maxEntries !== false && exported.size > maxEntries) {
|
|
3447
|
-
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
3448
|
-
}
|
|
3449
|
-
await visitor({
|
|
3450
|
-
key: exportedKey,
|
|
3451
|
-
value: stored,
|
|
3452
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
3453
|
-
});
|
|
3454
|
-
};
|
|
3455
|
-
if (layer.forEachKey) {
|
|
3456
|
-
await layer.forEachKey(visitKey);
|
|
3457
|
-
continue;
|
|
3458
|
-
}
|
|
3459
|
-
const keys = await layer.keys?.();
|
|
3460
|
-
for (const key of keys ?? []) {
|
|
3461
|
-
await visitKey(key);
|
|
3462
|
-
}
|
|
3463
|
-
}
|
|
3464
|
-
}
|
|
3465
3748
|
};
|
|
3466
3749
|
|
|
3467
3750
|
// src/module.ts
|