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
|
@@ -259,6 +259,125 @@ var Mutex = class {
|
|
|
259
259
|
}
|
|
260
260
|
};
|
|
261
261
|
|
|
262
|
+
// ../../src/internal/CacheNamespaceMetrics.ts
|
|
263
|
+
function createEmptyNamespaceMetrics(resetAt = Date.now()) {
|
|
264
|
+
return {
|
|
265
|
+
hits: 0,
|
|
266
|
+
misses: 0,
|
|
267
|
+
fetches: 0,
|
|
268
|
+
sets: 0,
|
|
269
|
+
deletes: 0,
|
|
270
|
+
backfills: 0,
|
|
271
|
+
invalidations: 0,
|
|
272
|
+
staleHits: 0,
|
|
273
|
+
refreshes: 0,
|
|
274
|
+
refreshErrors: 0,
|
|
275
|
+
writeFailures: 0,
|
|
276
|
+
singleFlightWaits: 0,
|
|
277
|
+
negativeCacheHits: 0,
|
|
278
|
+
circuitBreakerTrips: 0,
|
|
279
|
+
degradedOperations: 0,
|
|
280
|
+
hitsByLayer: {},
|
|
281
|
+
missesByLayer: {},
|
|
282
|
+
latencyByLayer: {},
|
|
283
|
+
resetAt
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function cloneNamespaceMetrics(metrics) {
|
|
287
|
+
return {
|
|
288
|
+
...metrics,
|
|
289
|
+
hitsByLayer: { ...metrics.hitsByLayer },
|
|
290
|
+
missesByLayer: { ...metrics.missesByLayer },
|
|
291
|
+
latencyByLayer: Object.fromEntries(
|
|
292
|
+
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
293
|
+
)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function diffNamespaceMetrics(before, after) {
|
|
297
|
+
const latencyByLayer = Object.fromEntries(
|
|
298
|
+
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
299
|
+
layer,
|
|
300
|
+
{
|
|
301
|
+
avgMs: value.avgMs,
|
|
302
|
+
maxMs: value.maxMs,
|
|
303
|
+
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
304
|
+
}
|
|
305
|
+
])
|
|
306
|
+
);
|
|
307
|
+
return {
|
|
308
|
+
hits: after.hits - before.hits,
|
|
309
|
+
misses: after.misses - before.misses,
|
|
310
|
+
fetches: after.fetches - before.fetches,
|
|
311
|
+
sets: after.sets - before.sets,
|
|
312
|
+
deletes: after.deletes - before.deletes,
|
|
313
|
+
backfills: after.backfills - before.backfills,
|
|
314
|
+
invalidations: after.invalidations - before.invalidations,
|
|
315
|
+
staleHits: after.staleHits - before.staleHits,
|
|
316
|
+
refreshes: after.refreshes - before.refreshes,
|
|
317
|
+
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
318
|
+
writeFailures: after.writeFailures - before.writeFailures,
|
|
319
|
+
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
320
|
+
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
321
|
+
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
322
|
+
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
323
|
+
hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
|
|
324
|
+
missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
|
|
325
|
+
latencyByLayer,
|
|
326
|
+
resetAt: after.resetAt
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function addNamespaceMetrics(base, delta) {
|
|
330
|
+
return {
|
|
331
|
+
hits: base.hits + delta.hits,
|
|
332
|
+
misses: base.misses + delta.misses,
|
|
333
|
+
fetches: base.fetches + delta.fetches,
|
|
334
|
+
sets: base.sets + delta.sets,
|
|
335
|
+
deletes: base.deletes + delta.deletes,
|
|
336
|
+
backfills: base.backfills + delta.backfills,
|
|
337
|
+
invalidations: base.invalidations + delta.invalidations,
|
|
338
|
+
staleHits: base.staleHits + delta.staleHits,
|
|
339
|
+
refreshes: base.refreshes + delta.refreshes,
|
|
340
|
+
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
341
|
+
writeFailures: base.writeFailures + delta.writeFailures,
|
|
342
|
+
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
343
|
+
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
344
|
+
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
345
|
+
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
346
|
+
hitsByLayer: addMetricMap(base.hitsByLayer, delta.hitsByLayer),
|
|
347
|
+
missesByLayer: addMetricMap(base.missesByLayer, delta.missesByLayer),
|
|
348
|
+
latencyByLayer: cloneNamespaceMetrics(delta).latencyByLayer,
|
|
349
|
+
resetAt: base.resetAt
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function computeNamespaceHitRate(metrics) {
|
|
353
|
+
const total = metrics.hits + metrics.misses;
|
|
354
|
+
const overall = total === 0 ? 0 : metrics.hits / total;
|
|
355
|
+
const byLayer = {};
|
|
356
|
+
const layers = /* @__PURE__ */ new Set([...Object.keys(metrics.hitsByLayer), ...Object.keys(metrics.missesByLayer)]);
|
|
357
|
+
for (const layer of layers) {
|
|
358
|
+
const hits = metrics.hitsByLayer[layer] ?? 0;
|
|
359
|
+
const misses = metrics.missesByLayer[layer] ?? 0;
|
|
360
|
+
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
361
|
+
}
|
|
362
|
+
return { overall, byLayer };
|
|
363
|
+
}
|
|
364
|
+
function diffMetricMap(before, after) {
|
|
365
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
366
|
+
const result = {};
|
|
367
|
+
for (const key of keys) {
|
|
368
|
+
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
369
|
+
}
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
function addMetricMap(base, delta) {
|
|
373
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
374
|
+
const result = {};
|
|
375
|
+
for (const key of keys) {
|
|
376
|
+
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
|
|
262
381
|
// ../../src/CacheNamespace.ts
|
|
263
382
|
var CacheNamespace = class _CacheNamespace {
|
|
264
383
|
constructor(cache, prefix) {
|
|
@@ -269,7 +388,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
269
388
|
cache;
|
|
270
389
|
prefix;
|
|
271
390
|
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
272
|
-
metrics =
|
|
391
|
+
metrics = createEmptyNamespaceMetrics();
|
|
273
392
|
async get(key, fetcher, options) {
|
|
274
393
|
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
275
394
|
}
|
|
@@ -366,19 +485,10 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
366
485
|
);
|
|
367
486
|
}
|
|
368
487
|
getMetrics() {
|
|
369
|
-
return
|
|
488
|
+
return cloneNamespaceMetrics(this.metrics);
|
|
370
489
|
}
|
|
371
490
|
getHitRate() {
|
|
372
|
-
|
|
373
|
-
const overall = total === 0 ? 0 : this.metrics.hits / total;
|
|
374
|
-
const byLayer = {};
|
|
375
|
-
const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
|
|
376
|
-
for (const layer of layers) {
|
|
377
|
-
const hits = this.metrics.hitsByLayer[layer] ?? 0;
|
|
378
|
-
const misses = this.metrics.missesByLayer[layer] ?? 0;
|
|
379
|
-
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
380
|
-
}
|
|
381
|
-
return { overall, byLayer };
|
|
491
|
+
return computeNamespaceHitRate(this.metrics);
|
|
382
492
|
}
|
|
383
493
|
/**
|
|
384
494
|
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
@@ -419,7 +529,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
419
529
|
const before = this.cache.getMetrics();
|
|
420
530
|
const result = await operation();
|
|
421
531
|
const after = this.cache.getMetrics();
|
|
422
|
-
this.metrics =
|
|
532
|
+
this.metrics = addNamespaceMetrics(this.metrics, diffNamespaceMetrics(before, after));
|
|
423
533
|
return result;
|
|
424
534
|
});
|
|
425
535
|
}
|
|
@@ -433,111 +543,6 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
433
543
|
return mutex;
|
|
434
544
|
}
|
|
435
545
|
};
|
|
436
|
-
function emptyMetrics() {
|
|
437
|
-
return {
|
|
438
|
-
hits: 0,
|
|
439
|
-
misses: 0,
|
|
440
|
-
fetches: 0,
|
|
441
|
-
sets: 0,
|
|
442
|
-
deletes: 0,
|
|
443
|
-
backfills: 0,
|
|
444
|
-
invalidations: 0,
|
|
445
|
-
staleHits: 0,
|
|
446
|
-
refreshes: 0,
|
|
447
|
-
refreshErrors: 0,
|
|
448
|
-
writeFailures: 0,
|
|
449
|
-
singleFlightWaits: 0,
|
|
450
|
-
negativeCacheHits: 0,
|
|
451
|
-
circuitBreakerTrips: 0,
|
|
452
|
-
degradedOperations: 0,
|
|
453
|
-
hitsByLayer: {},
|
|
454
|
-
missesByLayer: {},
|
|
455
|
-
latencyByLayer: {},
|
|
456
|
-
resetAt: Date.now()
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
function cloneMetrics(metrics) {
|
|
460
|
-
return {
|
|
461
|
-
...metrics,
|
|
462
|
-
hitsByLayer: { ...metrics.hitsByLayer },
|
|
463
|
-
missesByLayer: { ...metrics.missesByLayer },
|
|
464
|
-
latencyByLayer: Object.fromEntries(
|
|
465
|
-
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
466
|
-
)
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
function diffMetrics(before, after) {
|
|
470
|
-
const latencyByLayer = Object.fromEntries(
|
|
471
|
-
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
472
|
-
layer,
|
|
473
|
-
{
|
|
474
|
-
avgMs: value.avgMs,
|
|
475
|
-
maxMs: value.maxMs,
|
|
476
|
-
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
477
|
-
}
|
|
478
|
-
])
|
|
479
|
-
);
|
|
480
|
-
return {
|
|
481
|
-
hits: after.hits - before.hits,
|
|
482
|
-
misses: after.misses - before.misses,
|
|
483
|
-
fetches: after.fetches - before.fetches,
|
|
484
|
-
sets: after.sets - before.sets,
|
|
485
|
-
deletes: after.deletes - before.deletes,
|
|
486
|
-
backfills: after.backfills - before.backfills,
|
|
487
|
-
invalidations: after.invalidations - before.invalidations,
|
|
488
|
-
staleHits: after.staleHits - before.staleHits,
|
|
489
|
-
refreshes: after.refreshes - before.refreshes,
|
|
490
|
-
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
491
|
-
writeFailures: after.writeFailures - before.writeFailures,
|
|
492
|
-
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
493
|
-
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
494
|
-
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
495
|
-
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
496
|
-
hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
|
|
497
|
-
missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
|
|
498
|
-
latencyByLayer,
|
|
499
|
-
resetAt: after.resetAt
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
function addMetrics(base, delta) {
|
|
503
|
-
return {
|
|
504
|
-
hits: base.hits + delta.hits,
|
|
505
|
-
misses: base.misses + delta.misses,
|
|
506
|
-
fetches: base.fetches + delta.fetches,
|
|
507
|
-
sets: base.sets + delta.sets,
|
|
508
|
-
deletes: base.deletes + delta.deletes,
|
|
509
|
-
backfills: base.backfills + delta.backfills,
|
|
510
|
-
invalidations: base.invalidations + delta.invalidations,
|
|
511
|
-
staleHits: base.staleHits + delta.staleHits,
|
|
512
|
-
refreshes: base.refreshes + delta.refreshes,
|
|
513
|
-
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
514
|
-
writeFailures: base.writeFailures + delta.writeFailures,
|
|
515
|
-
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
516
|
-
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
517
|
-
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
518
|
-
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
519
|
-
hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
|
|
520
|
-
missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
|
|
521
|
-
latencyByLayer: cloneMetrics(delta).latencyByLayer,
|
|
522
|
-
resetAt: base.resetAt
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
function diffMap(before, after) {
|
|
526
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
527
|
-
const result = {};
|
|
528
|
-
for (const key of keys) {
|
|
529
|
-
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
530
|
-
}
|
|
531
|
-
return result;
|
|
532
|
-
}
|
|
533
|
-
function addMap(base, delta) {
|
|
534
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
535
|
-
const result = {};
|
|
536
|
-
for (const key of keys) {
|
|
537
|
-
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
538
|
-
}
|
|
539
|
-
return result;
|
|
540
|
-
}
|
|
541
546
|
function validateNamespaceKey(key) {
|
|
542
547
|
if (key.length === 0) {
|
|
543
548
|
throw new Error("Namespace prefix must not be empty.");
|
|
@@ -747,101 +752,781 @@ function createInstanceId() {
|
|
|
747
752
|
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
748
753
|
}
|
|
749
754
|
|
|
750
|
-
// ../../src/internal/
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
return
|
|
755
|
+
// ../../src/internal/CacheStackGeneration.ts
|
|
756
|
+
var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
|
|
757
|
+
function generationPrefix(generation) {
|
|
758
|
+
return generation === void 0 ? "" : `v${generation}:`;
|
|
754
759
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
try {
|
|
759
|
-
await fs.lstat(current);
|
|
760
|
-
return current;
|
|
761
|
-
} catch (error) {
|
|
762
|
-
if (error.code !== "ENOENT") {
|
|
763
|
-
throw error;
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
const parent = path.dirname(current);
|
|
767
|
-
if (parent === current) {
|
|
768
|
-
return current;
|
|
769
|
-
}
|
|
770
|
-
current = parent;
|
|
771
|
-
}
|
|
760
|
+
function qualifyGenerationKey(key, generation) {
|
|
761
|
+
const prefix = generationPrefix(generation);
|
|
762
|
+
return prefix ? `${prefix}${key}` : key;
|
|
772
763
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
764
|
+
function qualifyGenerationPattern(pattern, generation) {
|
|
765
|
+
return qualifyGenerationKey(pattern, generation);
|
|
766
|
+
}
|
|
767
|
+
function stripGenerationPrefix(key, generation) {
|
|
768
|
+
const prefix = generationPrefix(generation);
|
|
769
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
770
|
+
return key;
|
|
776
771
|
}
|
|
777
|
-
|
|
778
|
-
|
|
772
|
+
return key.slice(prefix.length);
|
|
773
|
+
}
|
|
774
|
+
function resolveGenerationCleanupTarget({
|
|
775
|
+
previousGeneration,
|
|
776
|
+
nextGeneration,
|
|
777
|
+
generationCleanup
|
|
778
|
+
}) {
|
|
779
|
+
if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
|
|
780
|
+
return null;
|
|
779
781
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
return resolved;
|
|
782
|
+
return previousGeneration;
|
|
783
|
+
}
|
|
784
|
+
function resolveGenerationCleanupBatchSize(generationCleanup) {
|
|
785
|
+
if (typeof generationCleanup !== "object" || generationCleanup === null) {
|
|
786
|
+
return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
786
787
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
788
|
+
return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
789
|
+
}
|
|
790
|
+
function planGenerationCleanupBatches(keys, generationCleanup) {
|
|
791
|
+
if (keys.length === 0) {
|
|
792
|
+
return [];
|
|
791
793
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}
|
|
797
|
-
return realTarget;
|
|
794
|
+
const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
|
|
795
|
+
const batches = [];
|
|
796
|
+
for (let index = 0; index < keys.length; index += batchSize) {
|
|
797
|
+
batches.push(keys.slice(index, index + batchSize));
|
|
798
798
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
799
|
+
return batches;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ../../src/internal/CacheStackInvalidationSupport.ts
|
|
803
|
+
var CacheStackInvalidationSupport = class {
|
|
804
|
+
constructor(options) {
|
|
805
|
+
this.options = options;
|
|
804
806
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
807
|
+
options;
|
|
808
|
+
async collectKeysForTag(tag, maxKeys) {
|
|
809
|
+
const keys = /* @__PURE__ */ new Set();
|
|
810
|
+
if (this.options.tagIndex.forEachKeyForTag) {
|
|
811
|
+
await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
812
|
+
keys.add(key);
|
|
813
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
814
|
+
});
|
|
815
|
+
return [...keys];
|
|
816
|
+
}
|
|
817
|
+
for (const key of await this.options.tagIndex.keysForTag(tag)) {
|
|
818
|
+
keys.add(key);
|
|
819
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
820
|
+
}
|
|
821
|
+
return [...keys];
|
|
809
822
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if (existing.isSymbolicLink()) {
|
|
814
|
-
throw new Error("filePath must not point to a symbolic link.");
|
|
823
|
+
intersectKeys(groups) {
|
|
824
|
+
if (groups.length === 0) {
|
|
825
|
+
return [];
|
|
815
826
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
827
|
+
const [firstGroup, ...rest] = groups;
|
|
828
|
+
const restSets = rest.map((group) => new Set(group));
|
|
829
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
830
|
+
}
|
|
831
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
832
|
+
await Promise.all(
|
|
833
|
+
layers.map(async (layer) => {
|
|
834
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (layer.deleteMany) {
|
|
838
|
+
try {
|
|
839
|
+
await layer.deleteMany(keys);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
await Promise.all(
|
|
846
|
+
keys.map(async (key) => {
|
|
847
|
+
try {
|
|
848
|
+
await layer.delete(key);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
851
|
+
}
|
|
852
|
+
})
|
|
853
|
+
);
|
|
854
|
+
})
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
assertWithinInvalidationKeyLimit(size, maxKeys) {
|
|
858
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
859
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
// ../../src/internal/StoredValue.ts
|
|
865
|
+
function isStoredValueEnvelope(value) {
|
|
866
|
+
if (typeof value !== "object" || value === null) {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
const v = value;
|
|
870
|
+
if (v.__layercache !== 1) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
if (v.kind !== "value" && v.kind !== "empty") {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
886
|
+
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
|
|
905
|
+
if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
function createStoredValueEnvelope(options) {
|
|
920
|
+
const now = options.now ?? Date.now();
|
|
921
|
+
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
922
|
+
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
923
|
+
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
924
|
+
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
925
|
+
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
926
|
+
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
927
|
+
return {
|
|
928
|
+
__layercache: 1,
|
|
929
|
+
kind: options.kind,
|
|
930
|
+
value: options.value,
|
|
931
|
+
freshUntil,
|
|
932
|
+
staleUntil,
|
|
933
|
+
errorUntil,
|
|
934
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
935
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
936
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function resolveStoredValue(stored, now = Date.now()) {
|
|
940
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
941
|
+
return { state: "fresh", value: stored, stored };
|
|
942
|
+
}
|
|
943
|
+
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
944
|
+
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
945
|
+
}
|
|
946
|
+
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
947
|
+
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
948
|
+
}
|
|
949
|
+
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
950
|
+
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
951
|
+
}
|
|
952
|
+
return { state: "expired", value: null, stored, envelope: stored };
|
|
953
|
+
}
|
|
954
|
+
function unwrapStoredValue(stored) {
|
|
955
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
956
|
+
return stored;
|
|
957
|
+
}
|
|
958
|
+
if (stored.kind === "empty") {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
return stored.value ?? null;
|
|
962
|
+
}
|
|
963
|
+
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
964
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
965
|
+
return void 0;
|
|
966
|
+
}
|
|
967
|
+
const expiry = maxExpiry(stored);
|
|
968
|
+
if (expiry === null) {
|
|
969
|
+
return void 0;
|
|
970
|
+
}
|
|
971
|
+
const remainingMs = expiry - now;
|
|
972
|
+
if (remainingMs <= 0) {
|
|
973
|
+
return 1;
|
|
974
|
+
}
|
|
975
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
976
|
+
}
|
|
977
|
+
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
978
|
+
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
979
|
+
return void 0;
|
|
980
|
+
}
|
|
981
|
+
const remainingMs = stored.freshUntil - now;
|
|
982
|
+
if (remainingMs <= 0) {
|
|
983
|
+
return 0;
|
|
984
|
+
}
|
|
985
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
986
|
+
}
|
|
987
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
988
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
989
|
+
return stored;
|
|
990
|
+
}
|
|
991
|
+
return createStoredValueEnvelope({
|
|
992
|
+
kind: stored.kind,
|
|
993
|
+
value: stored.value,
|
|
994
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
995
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
996
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
997
|
+
now
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
function maxExpiry(stored) {
|
|
1001
|
+
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
1002
|
+
(value) => value !== null
|
|
1003
|
+
);
|
|
1004
|
+
if (values.length === 0) {
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
return Math.max(...values);
|
|
1008
|
+
}
|
|
1009
|
+
function normalizePositiveSeconds(value) {
|
|
1010
|
+
if (!value || value <= 0) {
|
|
1011
|
+
return void 0;
|
|
1012
|
+
}
|
|
1013
|
+
return value;
|
|
1014
|
+
}
|
|
1015
|
+
function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
|
|
1016
|
+
if (value == null) {
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// ../../src/internal/CacheStackLayerWriter.ts
|
|
1023
|
+
var CacheStackLayerWriter = class {
|
|
1024
|
+
constructor(options) {
|
|
1025
|
+
this.options = options;
|
|
1026
|
+
}
|
|
1027
|
+
options;
|
|
1028
|
+
async writeAcrossLayers(key, kind, value, writeOptions) {
|
|
1029
|
+
const now = Date.now();
|
|
1030
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
1031
|
+
const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
|
|
1032
|
+
const immediateOperations = [];
|
|
1033
|
+
const deferredOperations = [];
|
|
1034
|
+
for (const layer of this.options.layers) {
|
|
1035
|
+
const operation = async () => {
|
|
1036
|
+
if (this.options.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, writeOptions, now);
|
|
1043
|
+
try {
|
|
1044
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
1050
|
+
deferredOperations.push(operation);
|
|
1051
|
+
} else {
|
|
1052
|
+
immediateOperations.push(operation);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
1056
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
1057
|
+
}
|
|
1058
|
+
async writeBatch(entries) {
|
|
1059
|
+
const now = Date.now();
|
|
1060
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
1061
|
+
const entryEpochs = new Map(
|
|
1062
|
+
entries.map((entry) => [entry.key, this.options.maintenance.currentKeyEpoch(entry.key)])
|
|
1063
|
+
);
|
|
1064
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
1065
|
+
const immediateOperations = [];
|
|
1066
|
+
const deferredOperations = [];
|
|
1067
|
+
for (const entry of entries) {
|
|
1068
|
+
for (const layer of this.options.layers) {
|
|
1069
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
1073
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
1074
|
+
bucket.push(layerEntry);
|
|
1075
|
+
entriesByLayer.set(layer, bucket);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
1079
|
+
const operation = async () => {
|
|
1080
|
+
if (clearEpoch !== this.options.maintenance.currentClearEpoch()) {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const activeEntries = layerEntries.filter(
|
|
1084
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.options.maintenance.currentKeyEpoch(entry.key)
|
|
1085
|
+
);
|
|
1086
|
+
if (activeEntries.length === 0) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
try {
|
|
1090
|
+
if (layer.setMany) {
|
|
1091
|
+
await layer.setMany(activeEntries);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
if (this.options.shouldWriteBehind(layer)) {
|
|
1100
|
+
deferredOperations.push(operation);
|
|
1101
|
+
} else {
|
|
1102
|
+
immediateOperations.push(operation);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
1106
|
+
await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
|
|
1107
|
+
return { clearEpoch, entryEpochs };
|
|
1108
|
+
}
|
|
1109
|
+
async executeLayerOperations(operations, context) {
|
|
1110
|
+
if (this.options.writePolicy !== "best-effort") {
|
|
1111
|
+
await Promise.all(operations.map((operation) => operation()));
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
1115
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
1116
|
+
if (failures.length === 0) {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
this.options.onWriteFailures(
|
|
1120
|
+
context,
|
|
1121
|
+
failures.map((failure) => failure.reason)
|
|
1122
|
+
);
|
|
1123
|
+
if (failures.length === operations.length) {
|
|
1124
|
+
throw new AggregateError(
|
|
1125
|
+
failures.map((failure) => failure.reason),
|
|
1126
|
+
`${context.action} failed for every cache layer`
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
|
|
1131
|
+
const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
|
|
1132
|
+
const staleWhileRevalidate = this.options.resolveLayerSeconds(
|
|
1133
|
+
layer.name,
|
|
1134
|
+
writeOptions?.staleWhileRevalidate,
|
|
1135
|
+
this.options.globalStaleWhileRevalidate
|
|
1136
|
+
);
|
|
1137
|
+
const staleIfError = this.options.resolveLayerSeconds(
|
|
1138
|
+
layer.name,
|
|
1139
|
+
writeOptions?.staleIfError,
|
|
1140
|
+
this.options.globalStaleIfError
|
|
1141
|
+
);
|
|
1142
|
+
const payload = createStoredValueEnvelope({
|
|
1143
|
+
kind,
|
|
1144
|
+
value,
|
|
1145
|
+
freshTtlSeconds: freshTtl,
|
|
1146
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
1147
|
+
staleIfErrorSeconds: staleIfError,
|
|
1148
|
+
now
|
|
1149
|
+
});
|
|
1150
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1151
|
+
return {
|
|
1152
|
+
key,
|
|
1153
|
+
value: payload,
|
|
1154
|
+
ttl
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
// ../../src/internal/CacheStackMaintenance.ts
|
|
1160
|
+
var CacheStackMaintenance = class {
|
|
1161
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
1162
|
+
writeBehindQueue = [];
|
|
1163
|
+
writeBehindTimer;
|
|
1164
|
+
writeBehindFlushPromise;
|
|
1165
|
+
generationCleanupPromise;
|
|
1166
|
+
clearEpoch = 0;
|
|
1167
|
+
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
1168
|
+
if (writeStrategy !== "write-behind") {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
1172
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
this.disposeWriteBehindTimer();
|
|
1176
|
+
this.writeBehindTimer = setInterval(() => {
|
|
1177
|
+
void flush();
|
|
1178
|
+
}, flushIntervalMs);
|
|
1179
|
+
this.writeBehindTimer.unref?.();
|
|
1180
|
+
}
|
|
1181
|
+
disposeWriteBehindTimer() {
|
|
1182
|
+
if (!this.writeBehindTimer) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
clearInterval(this.writeBehindTimer);
|
|
1186
|
+
this.writeBehindTimer = void 0;
|
|
1187
|
+
}
|
|
1188
|
+
beginClearEpoch() {
|
|
1189
|
+
this.clearEpoch += 1;
|
|
1190
|
+
this.keyEpochs.clear();
|
|
1191
|
+
this.writeBehindQueue.length = 0;
|
|
1192
|
+
}
|
|
1193
|
+
currentClearEpoch() {
|
|
1194
|
+
return this.clearEpoch;
|
|
1195
|
+
}
|
|
1196
|
+
currentKeyEpoch(key) {
|
|
1197
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
1198
|
+
}
|
|
1199
|
+
bumpKeyEpochs(keys) {
|
|
1200
|
+
for (const key of keys) {
|
|
1201
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
1205
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
1206
|
+
return true;
|
|
1207
|
+
}
|
|
1208
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
return false;
|
|
1212
|
+
}
|
|
1213
|
+
async enqueueWriteBehind(operation, options, flushBatch) {
|
|
1214
|
+
this.writeBehindQueue.push(operation);
|
|
1215
|
+
const batchSize = options?.batchSize ?? 100;
|
|
1216
|
+
const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
|
|
1217
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
1218
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
1222
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
async flushWriteBehindQueue(options, flushBatch) {
|
|
1226
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
1227
|
+
await this.writeBehindFlushPromise;
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const batchSize = options?.batchSize ?? 100;
|
|
1231
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
1232
|
+
this.writeBehindFlushPromise = flushBatch(batch);
|
|
1233
|
+
try {
|
|
1234
|
+
await this.writeBehindFlushPromise;
|
|
1235
|
+
} finally {
|
|
1236
|
+
this.writeBehindFlushPromise = void 0;
|
|
1237
|
+
}
|
|
1238
|
+
if (this.writeBehindQueue.length > 0) {
|
|
1239
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
scheduleGenerationCleanup(generation, task, onError) {
|
|
1243
|
+
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
1244
|
+
onError(generation, error);
|
|
1245
|
+
});
|
|
1246
|
+
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
1247
|
+
if (this.generationCleanupPromise === scheduledTask) {
|
|
1248
|
+
this.generationCleanupPromise = void 0;
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
async waitForGenerationCleanup() {
|
|
1253
|
+
await this.generationCleanupPromise;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
// ../../src/internal/CacheStackRuntimePolicy.ts
|
|
1258
|
+
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
1259
|
+
return degradedUntil !== void 0 && degradedUntil > now;
|
|
1260
|
+
}
|
|
1261
|
+
function shouldStartBackgroundRefresh({
|
|
1262
|
+
isDisconnecting,
|
|
1263
|
+
hasRefreshInFlight
|
|
1264
|
+
}) {
|
|
1265
|
+
return !isDisconnecting && !hasRefreshInFlight;
|
|
1266
|
+
}
|
|
1267
|
+
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
1268
|
+
if (!gracefulDegradation) {
|
|
1269
|
+
return { degrade: false };
|
|
1270
|
+
}
|
|
1271
|
+
const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1272
|
+
return {
|
|
1273
|
+
degrade: true,
|
|
1274
|
+
degradedUntil: now + retryAfterMs
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
function planFreshReadPolicies({
|
|
1278
|
+
stored,
|
|
1279
|
+
hasFetcher,
|
|
1280
|
+
slidingTtl,
|
|
1281
|
+
refreshAheadSeconds
|
|
1282
|
+
}) {
|
|
1283
|
+
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
1284
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
1285
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
1286
|
+
return {
|
|
1287
|
+
refreshedStored,
|
|
1288
|
+
refreshedStoredTtl,
|
|
1289
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// ../../src/internal/CacheStackSnapshotManager.ts
|
|
1294
|
+
var import_node_fs = require("fs");
|
|
1295
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
1296
|
+
|
|
1297
|
+
// ../../src/internal/CacheSnapshotFile.ts
|
|
1298
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
|
|
1299
|
+
const relative = path2.relative(realBaseDir, candidatePath);
|
|
1300
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
|
|
1301
|
+
}
|
|
1302
|
+
async function findExistingAncestor(directory, fs2, path2) {
|
|
1303
|
+
let current = directory;
|
|
1304
|
+
while (true) {
|
|
1305
|
+
try {
|
|
1306
|
+
await fs2.lstat(current);
|
|
1307
|
+
return current;
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
if (error.code !== "ENOENT") {
|
|
1310
|
+
throw error;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const parent = path2.dirname(current);
|
|
1314
|
+
if (parent === current) {
|
|
1315
|
+
return current;
|
|
1316
|
+
}
|
|
1317
|
+
current = parent;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
1321
|
+
if (filePath.length === 0) {
|
|
1322
|
+
throw new Error("filePath must not be empty.");
|
|
1323
|
+
}
|
|
1324
|
+
if (filePath.includes("\0")) {
|
|
1325
|
+
throw new Error("filePath must not contain null bytes.");
|
|
1326
|
+
}
|
|
1327
|
+
const { promises: fs2 } = await import("fs");
|
|
1328
|
+
const path2 = await import("path");
|
|
1329
|
+
const resolved = path2.resolve(filePath);
|
|
1330
|
+
const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
|
|
1331
|
+
if (baseDir === false) {
|
|
1332
|
+
return resolved;
|
|
1333
|
+
}
|
|
1334
|
+
await fs2.mkdir(baseDir, { recursive: true });
|
|
1335
|
+
const realBaseDir = await fs2.realpath(baseDir);
|
|
1336
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
|
|
1337
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1338
|
+
}
|
|
1339
|
+
if (mode === "read") {
|
|
1340
|
+
const realTarget = await fs2.realpath(resolved);
|
|
1341
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
|
|
1342
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1343
|
+
}
|
|
1344
|
+
return realTarget;
|
|
1345
|
+
}
|
|
1346
|
+
const parentDir = path2.dirname(resolved);
|
|
1347
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs2, path2);
|
|
1348
|
+
const realExistingAncestor = await fs2.realpath(existingAncestor);
|
|
1349
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
|
|
1350
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1351
|
+
}
|
|
1352
|
+
await fs2.mkdir(parentDir, { recursive: true });
|
|
1353
|
+
const realParentDir = await fs2.realpath(parentDir);
|
|
1354
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
|
|
1355
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
1356
|
+
}
|
|
1357
|
+
const targetPath = path2.join(realParentDir, path2.basename(resolved));
|
|
1358
|
+
try {
|
|
1359
|
+
const existing = await fs2.lstat(targetPath);
|
|
1360
|
+
if (existing.isSymbolicLink()) {
|
|
1361
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
1362
|
+
}
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
if (error.code !== "ENOENT") {
|
|
1365
|
+
throw error;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return targetPath;
|
|
1369
|
+
}
|
|
1370
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
1371
|
+
if (byteLimit === false) {
|
|
1372
|
+
return handle.readFile({ encoding: "utf8" });
|
|
1373
|
+
}
|
|
1374
|
+
const chunks = [];
|
|
1375
|
+
let totalBytes = 0;
|
|
1376
|
+
let position = 0;
|
|
1377
|
+
while (true) {
|
|
1378
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
1379
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
1380
|
+
if (bytesRead === 0) {
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
totalBytes += bytesRead;
|
|
1384
|
+
if (totalBytes > byteLimit) {
|
|
1385
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
1386
|
+
}
|
|
1387
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
1388
|
+
position += bytesRead;
|
|
1389
|
+
}
|
|
1390
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// ../../src/internal/CacheStackSnapshotManager.ts
|
|
1394
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1395
|
+
var CacheStackSnapshotManager = class {
|
|
1396
|
+
constructor(options) {
|
|
1397
|
+
this.options = options;
|
|
1398
|
+
}
|
|
1399
|
+
options;
|
|
1400
|
+
async exportState(maxEntries) {
|
|
1401
|
+
const entries = [];
|
|
1402
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
1403
|
+
entries.push(entry);
|
|
1404
|
+
});
|
|
1405
|
+
return entries;
|
|
1406
|
+
}
|
|
1407
|
+
async importState(entries) {
|
|
1408
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
1409
|
+
key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
|
|
1410
|
+
value: entry.value,
|
|
1411
|
+
ttl: entry.ttl
|
|
1412
|
+
}));
|
|
1413
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
1414
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
1415
|
+
await Promise.all(
|
|
1416
|
+
batch.map(async (entry) => {
|
|
1417
|
+
await Promise.all(this.options.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1418
|
+
await this.options.tagIndex.touch(entry.key);
|
|
1419
|
+
})
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
async persistToFile(filePath, snapshotBaseDir, maxEntries) {
|
|
1424
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1425
|
+
const tempPath = import_node_path.default.join(
|
|
1426
|
+
import_node_path.default.dirname(targetPath),
|
|
1427
|
+
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
1428
|
+
);
|
|
1429
|
+
let handle;
|
|
1430
|
+
try {
|
|
1431
|
+
handle = await import_node_fs.promises.open(tempPath, "wx");
|
|
1432
|
+
const openedHandle = handle;
|
|
1433
|
+
await openedHandle.writeFile("[", "utf8");
|
|
1434
|
+
let wroteAny = false;
|
|
1435
|
+
await this.visitExportEntries(maxEntries, async (entry) => {
|
|
1436
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
1437
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
1438
|
+
wroteAny = true;
|
|
1439
|
+
});
|
|
1440
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
1441
|
+
await openedHandle.close();
|
|
1442
|
+
handle = void 0;
|
|
1443
|
+
await import_node_fs.promises.rename(tempPath, targetPath);
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
await handle?.close().catch(() => void 0);
|
|
1446
|
+
await import_node_fs.promises.unlink(tempPath).catch(() => void 0);
|
|
1447
|
+
throw error;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
|
|
1451
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
|
|
1452
|
+
const handle = await import_node_fs.promises.open(validatedPath, import_node_fs.constants.O_RDONLY | (import_node_fs.constants.O_NOFOLLOW ?? 0));
|
|
1453
|
+
let raw;
|
|
1454
|
+
try {
|
|
1455
|
+
if (maxBytes !== false) {
|
|
1456
|
+
const stat = await handle.stat();
|
|
1457
|
+
if (stat.size > maxBytes) {
|
|
1458
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
raw = await readUtf8HandleWithLimit(handle, maxBytes);
|
|
1462
|
+
} finally {
|
|
1463
|
+
await handle.close();
|
|
1464
|
+
}
|
|
1465
|
+
let parsed;
|
|
1466
|
+
try {
|
|
1467
|
+
parsed = JSON.parse(raw);
|
|
1468
|
+
} catch (cause) {
|
|
1469
|
+
throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
|
|
1470
|
+
}
|
|
1471
|
+
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
1472
|
+
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
1473
|
+
}
|
|
1474
|
+
await this.importState(
|
|
1475
|
+
parsed.map((entry) => ({
|
|
1476
|
+
key: entry.key,
|
|
1477
|
+
value: this.sanitizeSnapshotValue(entry.value),
|
|
1478
|
+
ttl: entry.ttl
|
|
1479
|
+
}))
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
1483
|
+
const exported = /* @__PURE__ */ new Set();
|
|
1484
|
+
for (const layer of this.options.layers) {
|
|
1485
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
const visitKey = async (key) => {
|
|
1489
|
+
const exportedKey = this.options.stripQualifiedKey(key);
|
|
1490
|
+
if (exported.has(exportedKey)) {
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
const stored = await this.options.readLayerEntry(layer, key);
|
|
1494
|
+
if (stored === null) {
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
exported.add(exportedKey);
|
|
1498
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
1499
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
1500
|
+
}
|
|
1501
|
+
await visitor({
|
|
1502
|
+
key: exportedKey,
|
|
1503
|
+
value: stored,
|
|
1504
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
1505
|
+
});
|
|
1506
|
+
};
|
|
1507
|
+
if (layer.forEachKey) {
|
|
1508
|
+
await layer.forEachKey(visitKey);
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
const keys = await layer.keys?.();
|
|
1512
|
+
for (const key of keys ?? []) {
|
|
1513
|
+
await visitKey(key);
|
|
1514
|
+
}
|
|
819
1515
|
}
|
|
820
1516
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1517
|
+
isCacheSnapshotEntries(value) {
|
|
1518
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1519
|
+
if (!entry || typeof entry !== "object") {
|
|
1520
|
+
return false;
|
|
1521
|
+
}
|
|
1522
|
+
const candidate = entry;
|
|
1523
|
+
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
1524
|
+
});
|
|
826
1525
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
let position = 0;
|
|
830
|
-
while (true) {
|
|
831
|
-
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
832
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
833
|
-
if (bytesRead === 0) {
|
|
834
|
-
break;
|
|
835
|
-
}
|
|
836
|
-
totalBytes += bytesRead;
|
|
837
|
-
if (totalBytes > byteLimit) {
|
|
838
|
-
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
839
|
-
}
|
|
840
|
-
chunks.push(buffer.subarray(0, bytesRead));
|
|
841
|
-
position += bytesRead;
|
|
1526
|
+
sanitizeSnapshotValue(value) {
|
|
1527
|
+
return this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
842
1528
|
}
|
|
843
|
-
|
|
844
|
-
}
|
|
1529
|
+
};
|
|
845
1530
|
|
|
846
1531
|
// ../../src/internal/CacheStackValidation.ts
|
|
847
1532
|
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
@@ -996,7 +1681,6 @@ var CircuitBreakerManager = class {
|
|
|
996
1681
|
if (!options) {
|
|
997
1682
|
return;
|
|
998
1683
|
}
|
|
999
|
-
this.pruneIfNeeded();
|
|
1000
1684
|
const failureThreshold = options.failureThreshold ?? 3;
|
|
1001
1685
|
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
1002
1686
|
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
|
|
@@ -1005,6 +1689,7 @@ var CircuitBreakerManager = class {
|
|
|
1005
1689
|
state.openUntil = Date.now() + cooldownMs;
|
|
1006
1690
|
}
|
|
1007
1691
|
this.breakers.set(key, state);
|
|
1692
|
+
this.pruneIfNeeded();
|
|
1008
1693
|
}
|
|
1009
1694
|
recordSuccess(key) {
|
|
1010
1695
|
this.breakers.delete(key);
|
|
@@ -1070,7 +1755,11 @@ var FetchRateLimiter = class {
|
|
|
1070
1755
|
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
1071
1756
|
nextFetcherBucketId = 0;
|
|
1072
1757
|
drainTimer;
|
|
1758
|
+
isDisposed = false;
|
|
1073
1759
|
async schedule(options, context, task) {
|
|
1760
|
+
if (this.isDisposed) {
|
|
1761
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1762
|
+
}
|
|
1074
1763
|
if (!options) {
|
|
1075
1764
|
return task();
|
|
1076
1765
|
}
|
|
@@ -1093,6 +1782,27 @@ var FetchRateLimiter = class {
|
|
|
1093
1782
|
this.drain();
|
|
1094
1783
|
});
|
|
1095
1784
|
}
|
|
1785
|
+
dispose() {
|
|
1786
|
+
this.isDisposed = true;
|
|
1787
|
+
if (this.drainTimer) {
|
|
1788
|
+
clearTimeout(this.drainTimer);
|
|
1789
|
+
this.drainTimer = void 0;
|
|
1790
|
+
}
|
|
1791
|
+
for (const bucket of this.buckets.values()) {
|
|
1792
|
+
if (bucket.cleanupTimer) {
|
|
1793
|
+
clearTimeout(bucket.cleanupTimer);
|
|
1794
|
+
bucket.cleanupTimer = void 0;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
for (const queue of this.queuesByBucket.values()) {
|
|
1798
|
+
for (const item of queue) {
|
|
1799
|
+
item.reject(new Error("FetchRateLimiter has been disposed."));
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
this.queuesByBucket.clear();
|
|
1803
|
+
this.pendingBuckets.clear();
|
|
1804
|
+
this.buckets.clear();
|
|
1805
|
+
}
|
|
1096
1806
|
normalize(options) {
|
|
1097
1807
|
const maxConcurrent = options.maxConcurrent;
|
|
1098
1808
|
const intervalMs = options.intervalMs;
|
|
@@ -1128,6 +1838,9 @@ var FetchRateLimiter = class {
|
|
|
1128
1838
|
return "global";
|
|
1129
1839
|
}
|
|
1130
1840
|
drain() {
|
|
1841
|
+
if (this.isDisposed) {
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1131
1844
|
if (this.drainTimer) {
|
|
1132
1845
|
clearTimeout(this.drainTimer);
|
|
1133
1846
|
this.drainTimer = void 0;
|
|
@@ -1224,6 +1937,9 @@ var FetchRateLimiter = class {
|
|
|
1224
1937
|
}
|
|
1225
1938
|
}
|
|
1226
1939
|
bucketState(bucketKey) {
|
|
1940
|
+
if (this.isDisposed) {
|
|
1941
|
+
throw new Error("FetchRateLimiter has been disposed.");
|
|
1942
|
+
}
|
|
1227
1943
|
const existing = this.buckets.get(bucketKey);
|
|
1228
1944
|
if (existing) {
|
|
1229
1945
|
return existing;
|
|
@@ -1283,228 +1999,70 @@ var MetricsCollector = class {
|
|
|
1283
1999
|
hitsByLayer: { ...this.data.hitsByLayer },
|
|
1284
2000
|
missesByLayer: { ...this.data.missesByLayer },
|
|
1285
2001
|
latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
increment(field, amount = 1) {
|
|
1289
|
-
;
|
|
1290
|
-
this.data[field] += amount;
|
|
1291
|
-
}
|
|
1292
|
-
incrementLayer(map, layerName) {
|
|
1293
|
-
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
1294
|
-
}
|
|
1295
|
-
/**
|
|
1296
|
-
* Records a read latency sample for the given layer.
|
|
1297
|
-
* Maintains a rolling average and max using Welford's online algorithm.
|
|
1298
|
-
*/
|
|
1299
|
-
recordLatency(layerName, durationMs) {
|
|
1300
|
-
const existing = this.data.latencyByLayer[layerName];
|
|
1301
|
-
if (!existing) {
|
|
1302
|
-
this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
1303
|
-
return;
|
|
1304
|
-
}
|
|
1305
|
-
existing.count += 1;
|
|
1306
|
-
existing.avgMs += (durationMs - existing.avgMs) / existing.count;
|
|
1307
|
-
if (durationMs > existing.maxMs) {
|
|
1308
|
-
existing.maxMs = durationMs;
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
reset() {
|
|
1312
|
-
this.data = this.empty();
|
|
1313
|
-
}
|
|
1314
|
-
hitRate() {
|
|
1315
|
-
const total = this.data.hits + this.data.misses;
|
|
1316
|
-
const overall = total === 0 ? 0 : this.data.hits / total;
|
|
1317
|
-
const byLayer = {};
|
|
1318
|
-
const allLayers = /* @__PURE__ */ new Set([...Object.keys(this.data.hitsByLayer), ...Object.keys(this.data.missesByLayer)]);
|
|
1319
|
-
for (const layer of allLayers) {
|
|
1320
|
-
const h = this.data.hitsByLayer[layer] ?? 0;
|
|
1321
|
-
const m = this.data.missesByLayer[layer] ?? 0;
|
|
1322
|
-
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
1323
|
-
}
|
|
1324
|
-
return { overall, byLayer };
|
|
1325
|
-
}
|
|
1326
|
-
empty() {
|
|
1327
|
-
return {
|
|
1328
|
-
hits: 0,
|
|
1329
|
-
misses: 0,
|
|
1330
|
-
fetches: 0,
|
|
1331
|
-
sets: 0,
|
|
1332
|
-
deletes: 0,
|
|
1333
|
-
backfills: 0,
|
|
1334
|
-
invalidations: 0,
|
|
1335
|
-
staleHits: 0,
|
|
1336
|
-
refreshes: 0,
|
|
1337
|
-
refreshErrors: 0,
|
|
1338
|
-
writeFailures: 0,
|
|
1339
|
-
singleFlightWaits: 0,
|
|
1340
|
-
negativeCacheHits: 0,
|
|
1341
|
-
circuitBreakerTrips: 0,
|
|
1342
|
-
degradedOperations: 0,
|
|
1343
|
-
hitsByLayer: {},
|
|
1344
|
-
missesByLayer: {},
|
|
1345
|
-
latencyByLayer: {},
|
|
1346
|
-
resetAt: Date.now()
|
|
1347
|
-
};
|
|
1348
|
-
}
|
|
1349
|
-
};
|
|
1350
|
-
|
|
1351
|
-
// ../../src/internal/StoredValue.ts
|
|
1352
|
-
function isStoredValueEnvelope(value) {
|
|
1353
|
-
if (typeof value !== "object" || value === null) {
|
|
1354
|
-
return false;
|
|
1355
|
-
}
|
|
1356
|
-
const v = value;
|
|
1357
|
-
if (v.__layercache !== 1) {
|
|
1358
|
-
return false;
|
|
1359
|
-
}
|
|
1360
|
-
if (v.kind !== "value" && v.kind !== "empty") {
|
|
1361
|
-
return false;
|
|
1362
|
-
}
|
|
1363
|
-
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
1364
|
-
return false;
|
|
1365
|
-
}
|
|
1366
|
-
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
1367
|
-
return false;
|
|
1368
|
-
}
|
|
1369
|
-
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
1370
|
-
return false;
|
|
1371
|
-
}
|
|
1372
|
-
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
1373
|
-
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
1374
|
-
return false;
|
|
1375
|
-
}
|
|
1376
|
-
if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
|
|
1377
|
-
return false;
|
|
1378
|
-
}
|
|
1379
|
-
if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
|
|
1380
|
-
return false;
|
|
1381
|
-
}
|
|
1382
|
-
if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
|
|
1383
|
-
return false;
|
|
1384
|
-
}
|
|
1385
|
-
if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
|
|
1386
|
-
return false;
|
|
1387
|
-
}
|
|
1388
|
-
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
1389
|
-
return false;
|
|
1390
|
-
}
|
|
1391
|
-
const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
|
|
1392
|
-
if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
|
|
1393
|
-
return false;
|
|
1394
|
-
}
|
|
1395
|
-
if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
|
|
1396
|
-
return false;
|
|
1397
|
-
}
|
|
1398
|
-
if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
|
|
1399
|
-
return false;
|
|
1400
|
-
}
|
|
1401
|
-
if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
|
|
1402
|
-
return false;
|
|
1403
|
-
}
|
|
1404
|
-
return true;
|
|
1405
|
-
}
|
|
1406
|
-
function createStoredValueEnvelope(options) {
|
|
1407
|
-
const now = options.now ?? Date.now();
|
|
1408
|
-
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
1409
|
-
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
1410
|
-
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
1411
|
-
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
1412
|
-
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
1413
|
-
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
1414
|
-
return {
|
|
1415
|
-
__layercache: 1,
|
|
1416
|
-
kind: options.kind,
|
|
1417
|
-
value: options.value,
|
|
1418
|
-
freshUntil,
|
|
1419
|
-
staleUntil,
|
|
1420
|
-
errorUntil,
|
|
1421
|
-
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
1422
|
-
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
1423
|
-
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
function resolveStoredValue(stored, now = Date.now()) {
|
|
1427
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1428
|
-
return { state: "fresh", value: stored, stored };
|
|
1429
|
-
}
|
|
1430
|
-
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
1431
|
-
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1432
|
-
}
|
|
1433
|
-
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
1434
|
-
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1435
|
-
}
|
|
1436
|
-
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
1437
|
-
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1438
|
-
}
|
|
1439
|
-
return { state: "expired", value: null, stored, envelope: stored };
|
|
1440
|
-
}
|
|
1441
|
-
function unwrapStoredValue(stored) {
|
|
1442
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1443
|
-
return stored;
|
|
1444
|
-
}
|
|
1445
|
-
if (stored.kind === "empty") {
|
|
1446
|
-
return null;
|
|
1447
|
-
}
|
|
1448
|
-
return stored.value ?? null;
|
|
1449
|
-
}
|
|
1450
|
-
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
1451
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1452
|
-
return void 0;
|
|
1453
|
-
}
|
|
1454
|
-
const expiry = maxExpiry(stored);
|
|
1455
|
-
if (expiry === null) {
|
|
1456
|
-
return void 0;
|
|
1457
|
-
}
|
|
1458
|
-
const remainingMs = expiry - now;
|
|
1459
|
-
if (remainingMs <= 0) {
|
|
1460
|
-
return 1;
|
|
2002
|
+
};
|
|
1461
2003
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
1466
|
-
return void 0;
|
|
2004
|
+
increment(field, amount = 1) {
|
|
2005
|
+
;
|
|
2006
|
+
this.data[field] += amount;
|
|
1467
2007
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
return 0;
|
|
2008
|
+
incrementLayer(map, layerName) {
|
|
2009
|
+
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
1471
2010
|
}
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
2011
|
+
/**
|
|
2012
|
+
* Records a read latency sample for the given layer.
|
|
2013
|
+
* Maintains a rolling average and max using Welford's online algorithm.
|
|
2014
|
+
*/
|
|
2015
|
+
recordLatency(layerName, durationMs) {
|
|
2016
|
+
const existing = this.data.latencyByLayer[layerName];
|
|
2017
|
+
if (!existing) {
|
|
2018
|
+
this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
existing.count += 1;
|
|
2022
|
+
existing.avgMs += (durationMs - existing.avgMs) / existing.count;
|
|
2023
|
+
if (durationMs > existing.maxMs) {
|
|
2024
|
+
existing.maxMs = durationMs;
|
|
2025
|
+
}
|
|
1477
2026
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
value: stored.value,
|
|
1481
|
-
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
1482
|
-
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
1483
|
-
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
1484
|
-
now
|
|
1485
|
-
});
|
|
1486
|
-
}
|
|
1487
|
-
function maxExpiry(stored) {
|
|
1488
|
-
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
1489
|
-
(value) => value !== null
|
|
1490
|
-
);
|
|
1491
|
-
if (values.length === 0) {
|
|
1492
|
-
return null;
|
|
2027
|
+
reset() {
|
|
2028
|
+
this.data = this.empty();
|
|
1493
2029
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
2030
|
+
hitRate() {
|
|
2031
|
+
const total = this.data.hits + this.data.misses;
|
|
2032
|
+
const overall = total === 0 ? 0 : this.data.hits / total;
|
|
2033
|
+
const byLayer = {};
|
|
2034
|
+
const allLayers = /* @__PURE__ */ new Set([...Object.keys(this.data.hitsByLayer), ...Object.keys(this.data.missesByLayer)]);
|
|
2035
|
+
for (const layer of allLayers) {
|
|
2036
|
+
const h = this.data.hitsByLayer[layer] ?? 0;
|
|
2037
|
+
const m = this.data.missesByLayer[layer] ?? 0;
|
|
2038
|
+
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
2039
|
+
}
|
|
2040
|
+
return { overall, byLayer };
|
|
1499
2041
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
2042
|
+
empty() {
|
|
2043
|
+
return {
|
|
2044
|
+
hits: 0,
|
|
2045
|
+
misses: 0,
|
|
2046
|
+
fetches: 0,
|
|
2047
|
+
sets: 0,
|
|
2048
|
+
deletes: 0,
|
|
2049
|
+
backfills: 0,
|
|
2050
|
+
invalidations: 0,
|
|
2051
|
+
staleHits: 0,
|
|
2052
|
+
refreshes: 0,
|
|
2053
|
+
refreshErrors: 0,
|
|
2054
|
+
writeFailures: 0,
|
|
2055
|
+
singleFlightWaits: 0,
|
|
2056
|
+
negativeCacheHits: 0,
|
|
2057
|
+
circuitBreakerTrips: 0,
|
|
2058
|
+
degradedOperations: 0,
|
|
2059
|
+
hitsByLayer: {},
|
|
2060
|
+
missesByLayer: {},
|
|
2061
|
+
latencyByLayer: {},
|
|
2062
|
+
resetAt: Date.now()
|
|
2063
|
+
};
|
|
1505
2064
|
}
|
|
1506
|
-
|
|
1507
|
-
}
|
|
2065
|
+
};
|
|
1508
2066
|
|
|
1509
2067
|
// ../../src/internal/TtlResolver.ts
|
|
1510
2068
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
@@ -1840,19 +2398,19 @@ var TagIndex = class {
|
|
|
1840
2398
|
if (!this.knownKeys.delete(key)) {
|
|
1841
2399
|
return;
|
|
1842
2400
|
}
|
|
1843
|
-
const
|
|
2401
|
+
const path2 = [];
|
|
1844
2402
|
let node = this.root;
|
|
1845
2403
|
for (const character of key) {
|
|
1846
2404
|
const child = node.children.get(character);
|
|
1847
2405
|
if (!child) {
|
|
1848
2406
|
return;
|
|
1849
2407
|
}
|
|
1850
|
-
|
|
2408
|
+
path2.push([node, character]);
|
|
1851
2409
|
node = child;
|
|
1852
2410
|
}
|
|
1853
2411
|
node.terminal = false;
|
|
1854
|
-
for (let index =
|
|
1855
|
-
const entry =
|
|
2412
|
+
for (let index = path2.length - 1; index >= 0; index -= 1) {
|
|
2413
|
+
const entry = path2[index];
|
|
1856
2414
|
if (!entry) {
|
|
1857
2415
|
continue;
|
|
1858
2416
|
}
|
|
@@ -1866,39 +2424,31 @@ var TagIndex = class {
|
|
|
1866
2424
|
}
|
|
1867
2425
|
};
|
|
1868
2426
|
|
|
1869
|
-
// ../../src/
|
|
1870
|
-
var
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
}
|
|
1876
|
-
deserialize(payload) {
|
|
1877
|
-
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1878
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1879
|
-
}
|
|
1880
|
-
};
|
|
1881
|
-
var MAX_SANITIZE_DEPTH = 200;
|
|
1882
|
-
function sanitizeJsonValue(value, depth, state) {
|
|
2427
|
+
// ../../src/internal/StructuredDataSanitizer.ts
|
|
2428
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2429
|
+
function sanitizeStructuredData(value, options) {
|
|
2430
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
2431
|
+
}
|
|
2432
|
+
function sanitizeValue(value, depth, state, options) {
|
|
1883
2433
|
state.count += 1;
|
|
1884
|
-
if (state.count >
|
|
1885
|
-
throw new Error(
|
|
2434
|
+
if (state.count > options.maxNodes) {
|
|
2435
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
1886
2436
|
}
|
|
1887
|
-
if (depth >
|
|
1888
|
-
throw new Error(
|
|
2437
|
+
if (depth > options.maxDepth) {
|
|
2438
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
1889
2439
|
}
|
|
1890
2440
|
if (Array.isArray(value)) {
|
|
1891
|
-
return value.map((entry) =>
|
|
2441
|
+
return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
|
|
1892
2442
|
}
|
|
1893
2443
|
if (!isPlainObject(value)) {
|
|
1894
2444
|
return value;
|
|
1895
2445
|
}
|
|
1896
|
-
const sanitized = {};
|
|
2446
|
+
const sanitized = options.createObject?.() ?? {};
|
|
1897
2447
|
for (const [key, entry] of Object.entries(value)) {
|
|
1898
|
-
if (
|
|
2448
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
1899
2449
|
continue;
|
|
1900
2450
|
}
|
|
1901
|
-
sanitized[key] =
|
|
2451
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
1902
2452
|
}
|
|
1903
2453
|
return sanitized;
|
|
1904
2454
|
}
|
|
@@ -1906,6 +2456,21 @@ function isPlainObject(value) {
|
|
|
1906
2456
|
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1907
2457
|
}
|
|
1908
2458
|
|
|
2459
|
+
// ../../src/serialization/JsonSerializer.ts
|
|
2460
|
+
var JsonSerializer = class {
|
|
2461
|
+
serialize(value) {
|
|
2462
|
+
return JSON.stringify(value);
|
|
2463
|
+
}
|
|
2464
|
+
deserialize(payload) {
|
|
2465
|
+
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
2466
|
+
return sanitizeStructuredData(JSON.parse(normalized), {
|
|
2467
|
+
label: "JSON payload",
|
|
2468
|
+
maxDepth: 200,
|
|
2469
|
+
maxNodes: 1e4
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
};
|
|
2473
|
+
|
|
1909
2474
|
// ../../src/stampede/StampedeGuard.ts
|
|
1910
2475
|
var StampedeGuard = class {
|
|
1911
2476
|
mutexes = /* @__PURE__ */ new Map();
|
|
@@ -1949,7 +2514,6 @@ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
|
1949
2514
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1950
2515
|
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1951
2516
|
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1952
|
-
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1953
2517
|
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
1954
2518
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1955
2519
|
var DebugLogger = class {
|
|
@@ -2006,6 +2570,35 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2006
2570
|
await this.handleLayerFailure(layer, operation, error);
|
|
2007
2571
|
}
|
|
2008
2572
|
});
|
|
2573
|
+
this.invalidation = new CacheStackInvalidationSupport({
|
|
2574
|
+
tagIndex: this.tagIndex,
|
|
2575
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2576
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
2577
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2578
|
+
}
|
|
2579
|
+
});
|
|
2580
|
+
this.layerWriter = new CacheStackLayerWriter({
|
|
2581
|
+
layers: this.layers,
|
|
2582
|
+
maintenance: this.maintenance,
|
|
2583
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2584
|
+
shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
|
|
2585
|
+
handleLayerFailure: async (layer, operation, error) => {
|
|
2586
|
+
await this.handleLayerFailure(layer, operation, error);
|
|
2587
|
+
},
|
|
2588
|
+
enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
|
|
2589
|
+
resolveFreshTtl: this.resolveFreshTtl.bind(this),
|
|
2590
|
+
resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
|
|
2591
|
+
globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
|
|
2592
|
+
globalStaleIfError: this.options.staleIfError,
|
|
2593
|
+
writePolicy: this.options.writePolicy,
|
|
2594
|
+
onWriteFailures: (context, failures) => {
|
|
2595
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2596
|
+
this.logger.debug?.("write-failure", {
|
|
2597
|
+
...context,
|
|
2598
|
+
failures: failures.map((failure) => this.formatError(failure))
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
});
|
|
2009
2602
|
if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
|
|
2010
2603
|
this.logger.warn?.(
|
|
2011
2604
|
"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."
|
|
@@ -2021,6 +2614,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2021
2614
|
"broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
|
|
2022
2615
|
);
|
|
2023
2616
|
}
|
|
2617
|
+
this.snapshots = new CacheStackSnapshotManager({
|
|
2618
|
+
layers: this.layers,
|
|
2619
|
+
tagIndex: this.tagIndex,
|
|
2620
|
+
snapshotSerializer: this.snapshotSerializer,
|
|
2621
|
+
readLayerEntry: this.readLayerEntry.bind(this),
|
|
2622
|
+
qualifyKey: this.qualifyKey.bind(this),
|
|
2623
|
+
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
2624
|
+
validateCacheKey,
|
|
2625
|
+
formatError: this.formatError.bind(this)
|
|
2626
|
+
});
|
|
2024
2627
|
this.initializeWriteBehind(options.writeBehind);
|
|
2025
2628
|
this.startup = this.initialize();
|
|
2026
2629
|
}
|
|
@@ -2036,17 +2639,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2036
2639
|
keyDiscovery;
|
|
2037
2640
|
fetchRateLimiter = new FetchRateLimiter();
|
|
2038
2641
|
snapshotSerializer = new JsonSerializer();
|
|
2642
|
+
invalidation;
|
|
2643
|
+
layerWriter;
|
|
2644
|
+
snapshots;
|
|
2039
2645
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
2040
2646
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2041
|
-
|
|
2647
|
+
maintenance = new CacheStackMaintenance();
|
|
2042
2648
|
ttlResolver;
|
|
2043
2649
|
circuitBreakerManager;
|
|
2650
|
+
nextOperationId = 0;
|
|
2044
2651
|
currentGeneration;
|
|
2045
|
-
writeBehindQueue = [];
|
|
2046
|
-
writeBehindTimer;
|
|
2047
|
-
writeBehindFlushPromise;
|
|
2048
|
-
generationCleanupPromise;
|
|
2049
|
-
clearEpoch = 0;
|
|
2050
2652
|
isDisconnecting = false;
|
|
2051
2653
|
disconnectPromise;
|
|
2052
2654
|
/**
|
|
@@ -2056,10 +2658,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2056
2658
|
* and no `fetcher` is provided.
|
|
2057
2659
|
*/
|
|
2058
2660
|
async get(key, fetcher, options) {
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2661
|
+
return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
|
|
2662
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2663
|
+
this.validateWriteOptions(options);
|
|
2664
|
+
await this.awaitStartup("get");
|
|
2665
|
+
return this.getPrepared(normalizedKey, fetcher, options);
|
|
2666
|
+
});
|
|
2063
2667
|
}
|
|
2064
2668
|
async getPrepared(normalizedKey, fetcher, options) {
|
|
2065
2669
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
@@ -2181,28 +2785,32 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2181
2785
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
2182
2786
|
*/
|
|
2183
2787
|
async set(key, value, options) {
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2788
|
+
await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
|
|
2789
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2790
|
+
this.validateWriteOptions(options);
|
|
2791
|
+
await this.awaitStartup("set");
|
|
2792
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
2793
|
+
});
|
|
2188
2794
|
}
|
|
2189
2795
|
/**
|
|
2190
2796
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
2191
2797
|
*/
|
|
2192
2798
|
async delete(key) {
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2799
|
+
await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
|
|
2800
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2801
|
+
await this.awaitStartup("delete");
|
|
2802
|
+
await this.deleteKeys([normalizedKey]);
|
|
2803
|
+
await this.publishInvalidation({
|
|
2804
|
+
scope: "key",
|
|
2805
|
+
keys: [normalizedKey],
|
|
2806
|
+
sourceId: this.instanceId,
|
|
2807
|
+
operation: "delete"
|
|
2808
|
+
});
|
|
2201
2809
|
});
|
|
2202
2810
|
}
|
|
2203
2811
|
async clear() {
|
|
2204
2812
|
await this.awaitStartup("clear");
|
|
2205
|
-
this.beginClearEpoch();
|
|
2813
|
+
this.maintenance.beginClearEpoch();
|
|
2206
2814
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
2207
2815
|
await this.tagIndex.clear();
|
|
2208
2816
|
this.ttlResolver.clearProfiles();
|
|
@@ -2230,95 +2838,99 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2230
2838
|
});
|
|
2231
2839
|
}
|
|
2232
2840
|
async mget(entries) {
|
|
2233
|
-
this.
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2841
|
+
return this.observeOperation("layercache.mget", void 0, async () => {
|
|
2842
|
+
this.assertActive("mget");
|
|
2843
|
+
if (entries.length === 0) {
|
|
2844
|
+
return [];
|
|
2845
|
+
}
|
|
2846
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2847
|
+
...entry,
|
|
2848
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2849
|
+
}));
|
|
2850
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2851
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
2852
|
+
if (!canFastPath) {
|
|
2853
|
+
await this.awaitStartup("mget");
|
|
2854
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
2855
|
+
return Promise.all(
|
|
2856
|
+
normalizedEntries.map((entry) => {
|
|
2857
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
2858
|
+
const existing = pendingReads.get(entry.key);
|
|
2859
|
+
if (!existing) {
|
|
2860
|
+
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2861
|
+
pendingReads.set(entry.key, {
|
|
2862
|
+
promise,
|
|
2863
|
+
fetch: entry.fetch,
|
|
2864
|
+
optionsSignature
|
|
2865
|
+
});
|
|
2866
|
+
return promise;
|
|
2867
|
+
}
|
|
2868
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
2869
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
2870
|
+
}
|
|
2871
|
+
return existing.promise;
|
|
2872
|
+
})
|
|
2873
|
+
);
|
|
2874
|
+
}
|
|
2244
2875
|
await this.awaitStartup("mget");
|
|
2245
|
-
const
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2876
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2877
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
2878
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
2879
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2880
|
+
const entry = normalizedEntries[index];
|
|
2881
|
+
if (!entry) continue;
|
|
2882
|
+
const key = entry.key;
|
|
2883
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
2884
|
+
indexes.push(index);
|
|
2885
|
+
indexesByKey.set(key, indexes);
|
|
2886
|
+
pending.add(key);
|
|
2887
|
+
}
|
|
2888
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2889
|
+
const layer = this.layers[layerIndex];
|
|
2890
|
+
if (!layer) continue;
|
|
2891
|
+
const keys = [...pending];
|
|
2892
|
+
if (keys.length === 0) {
|
|
2893
|
+
break;
|
|
2894
|
+
}
|
|
2895
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2896
|
+
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2897
|
+
const key = keys[offset];
|
|
2898
|
+
const stored = values[offset];
|
|
2899
|
+
if (!key || stored === null) {
|
|
2900
|
+
continue;
|
|
2258
2901
|
}
|
|
2259
|
-
|
|
2260
|
-
|
|
2902
|
+
const resolved = resolveStoredValue(stored);
|
|
2903
|
+
if (resolved.state === "expired") {
|
|
2904
|
+
await layer.delete(key);
|
|
2905
|
+
continue;
|
|
2261
2906
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
const pending = /* @__PURE__ */ new Set();
|
|
2268
|
-
const indexesByKey = /* @__PURE__ */ new Map();
|
|
2269
|
-
const resultsByKey = /* @__PURE__ */ new Map();
|
|
2270
|
-
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
2271
|
-
const entry = normalizedEntries[index];
|
|
2272
|
-
if (!entry) continue;
|
|
2273
|
-
const key = entry.key;
|
|
2274
|
-
const indexes = indexesByKey.get(key) ?? [];
|
|
2275
|
-
indexes.push(index);
|
|
2276
|
-
indexesByKey.set(key, indexes);
|
|
2277
|
-
pending.add(key);
|
|
2278
|
-
}
|
|
2279
|
-
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2280
|
-
const layer = this.layers[layerIndex];
|
|
2281
|
-
if (!layer) continue;
|
|
2282
|
-
const keys = [...pending];
|
|
2283
|
-
if (keys.length === 0) {
|
|
2284
|
-
break;
|
|
2285
|
-
}
|
|
2286
|
-
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2287
|
-
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2288
|
-
const key = keys[offset];
|
|
2289
|
-
const stored = values[offset];
|
|
2290
|
-
if (!key || stored === null) {
|
|
2291
|
-
continue;
|
|
2292
|
-
}
|
|
2293
|
-
const resolved = resolveStoredValue(stored);
|
|
2294
|
-
if (resolved.state === "expired") {
|
|
2295
|
-
await layer.delete(key);
|
|
2296
|
-
continue;
|
|
2907
|
+
await this.tagIndex.touch(key);
|
|
2908
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
2909
|
+
resultsByKey.set(key, resolved.value);
|
|
2910
|
+
pending.delete(key);
|
|
2911
|
+
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
2297
2912
|
}
|
|
2298
|
-
await this.tagIndex.touch(key);
|
|
2299
|
-
await this.backfill(key, stored, layerIndex - 1);
|
|
2300
|
-
resultsByKey.set(key, resolved.value);
|
|
2301
|
-
pending.delete(key);
|
|
2302
|
-
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
2303
2913
|
}
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2914
|
+
if (pending.size > 0) {
|
|
2915
|
+
for (const key of pending) {
|
|
2916
|
+
await this.tagIndex.remove(key);
|
|
2917
|
+
this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
|
|
2918
|
+
}
|
|
2309
2919
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2920
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
2921
|
+
});
|
|
2312
2922
|
}
|
|
2313
2923
|
async mset(entries) {
|
|
2314
|
-
this.
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2924
|
+
await this.observeOperation("layercache.mset", void 0, async () => {
|
|
2925
|
+
this.assertActive("mset");
|
|
2926
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2927
|
+
...entry,
|
|
2928
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
2929
|
+
}));
|
|
2930
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
2931
|
+
await this.awaitStartup("mset");
|
|
2932
|
+
await this.writeBatch(normalizedEntries);
|
|
2933
|
+
});
|
|
2322
2934
|
}
|
|
2323
2935
|
async warm(entries, options = {}) {
|
|
2324
2936
|
this.assertActive("warm");
|
|
@@ -2371,40 +2983,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2371
2983
|
return new CacheNamespace(this, prefix);
|
|
2372
2984
|
}
|
|
2373
2985
|
async invalidateByTag(tag) {
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2986
|
+
await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
|
|
2987
|
+
validateTag(tag);
|
|
2988
|
+
await this.awaitStartup("invalidateByTag");
|
|
2989
|
+
const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
|
|
2990
|
+
await this.deleteKeys(keys);
|
|
2991
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2992
|
+
});
|
|
2379
2993
|
}
|
|
2380
2994
|
async invalidateByTags(tags, mode = "any") {
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2995
|
+
await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
|
|
2996
|
+
if (tags.length === 0) {
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
validateTags(tags);
|
|
3000
|
+
await this.awaitStartup("invalidateByTags");
|
|
3001
|
+
const keysByTag = await Promise.all(
|
|
3002
|
+
tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
|
|
3003
|
+
);
|
|
3004
|
+
const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
3005
|
+
this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
|
|
3006
|
+
await this.deleteKeys(keys);
|
|
3007
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3008
|
+
});
|
|
2391
3009
|
}
|
|
2392
3010
|
async invalidateByPattern(pattern) {
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
this.
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
3011
|
+
await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
|
|
3012
|
+
validatePattern(pattern);
|
|
3013
|
+
await this.awaitStartup("invalidateByPattern");
|
|
3014
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
3015
|
+
this.qualifyPattern(pattern),
|
|
3016
|
+
this.invalidationMaxKeys()
|
|
3017
|
+
);
|
|
3018
|
+
await this.deleteKeys(keys);
|
|
3019
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3020
|
+
});
|
|
2401
3021
|
}
|
|
2402
3022
|
async invalidateByPrefix(prefix) {
|
|
2403
|
-
await this.
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
3023
|
+
await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
|
|
3024
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
3025
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
3026
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
3027
|
+
await this.deleteKeys(keys);
|
|
3028
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3029
|
+
});
|
|
2408
3030
|
}
|
|
2409
3031
|
getMetrics() {
|
|
2410
3032
|
return this.metricsCollector.snapshot;
|
|
@@ -2460,9 +3082,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2460
3082
|
bumpGeneration(nextGeneration) {
|
|
2461
3083
|
const current = this.currentGeneration ?? 0;
|
|
2462
3084
|
const previousGeneration = this.currentGeneration;
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
3085
|
+
const updatedGeneration = nextGeneration ?? current + 1;
|
|
3086
|
+
const generationToCleanup = resolveGenerationCleanupTarget({
|
|
3087
|
+
previousGeneration,
|
|
3088
|
+
nextGeneration: updatedGeneration,
|
|
3089
|
+
generationCleanup: this.options.generationCleanup
|
|
3090
|
+
});
|
|
3091
|
+
this.currentGeneration = updatedGeneration;
|
|
3092
|
+
if (generationToCleanup !== null) {
|
|
3093
|
+
this.scheduleGenerationCleanup(generationToCleanup);
|
|
2466
3094
|
}
|
|
2467
3095
|
return this.currentGeneration;
|
|
2468
3096
|
}
|
|
@@ -2509,95 +3137,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2509
3137
|
}
|
|
2510
3138
|
async exportState() {
|
|
2511
3139
|
await this.awaitStartup("exportState");
|
|
2512
|
-
|
|
2513
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2514
|
-
entries.push(entry);
|
|
2515
|
-
});
|
|
2516
|
-
return entries;
|
|
3140
|
+
return this.snapshots.exportState(this.snapshotMaxEntries());
|
|
2517
3141
|
}
|
|
2518
3142
|
async importState(entries) {
|
|
2519
3143
|
await this.awaitStartup("importState");
|
|
2520
|
-
|
|
2521
|
-
key: this.qualifyKey(validateCacheKey(entry.key)),
|
|
2522
|
-
value: entry.value,
|
|
2523
|
-
ttl: entry.ttl
|
|
2524
|
-
}));
|
|
2525
|
-
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
2526
|
-
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
2527
|
-
await Promise.all(
|
|
2528
|
-
batch.map(async (entry) => {
|
|
2529
|
-
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2530
|
-
await this.tagIndex.touch(entry.key);
|
|
2531
|
-
})
|
|
2532
|
-
);
|
|
2533
|
-
}
|
|
3144
|
+
await this.snapshots.importState(entries);
|
|
2534
3145
|
}
|
|
2535
3146
|
async persistToFile(filePath) {
|
|
2536
3147
|
this.assertActive("persistToFile");
|
|
2537
|
-
|
|
2538
|
-
const path = await import("path");
|
|
2539
|
-
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
2540
|
-
const tempPath = path.join(
|
|
2541
|
-
path.dirname(targetPath),
|
|
2542
|
-
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
2543
|
-
);
|
|
2544
|
-
let handle;
|
|
2545
|
-
try {
|
|
2546
|
-
handle = await fs.open(tempPath, "wx");
|
|
2547
|
-
const openedHandle = handle;
|
|
2548
|
-
await openedHandle.writeFile("[", "utf8");
|
|
2549
|
-
let wroteAny = false;
|
|
2550
|
-
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2551
|
-
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
2552
|
-
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
2553
|
-
wroteAny = true;
|
|
2554
|
-
});
|
|
2555
|
-
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
2556
|
-
await openedHandle.close();
|
|
2557
|
-
handle = void 0;
|
|
2558
|
-
await fs.rename(tempPath, targetPath);
|
|
2559
|
-
} catch (error) {
|
|
2560
|
-
await handle?.close().catch(() => void 0);
|
|
2561
|
-
await fs.unlink(tempPath).catch(() => void 0);
|
|
2562
|
-
throw error;
|
|
2563
|
-
}
|
|
3148
|
+
await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
|
|
2564
3149
|
}
|
|
2565
3150
|
async restoreFromFile(filePath) {
|
|
2566
3151
|
this.assertActive("restoreFromFile");
|
|
2567
|
-
|
|
2568
|
-
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
2569
|
-
const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2570
|
-
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
2571
|
-
let raw;
|
|
2572
|
-
try {
|
|
2573
|
-
if (snapshotMaxBytes !== false) {
|
|
2574
|
-
const stat = await handle.stat();
|
|
2575
|
-
if (stat.size > snapshotMaxBytes) {
|
|
2576
|
-
throw new Error(
|
|
2577
|
-
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
2578
|
-
);
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
|
-
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
2582
|
-
} finally {
|
|
2583
|
-
await handle.close();
|
|
2584
|
-
}
|
|
2585
|
-
let parsed;
|
|
2586
|
-
try {
|
|
2587
|
-
parsed = JSON.parse(raw);
|
|
2588
|
-
} catch (cause) {
|
|
2589
|
-
throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
|
|
2590
|
-
}
|
|
2591
|
-
if (!this.isCacheSnapshotEntries(parsed)) {
|
|
2592
|
-
throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
|
|
2593
|
-
}
|
|
2594
|
-
await this.importState(
|
|
2595
|
-
parsed.map((entry) => ({
|
|
2596
|
-
key: entry.key,
|
|
2597
|
-
value: this.sanitizeSnapshotValue(entry.value),
|
|
2598
|
-
ttl: entry.ttl
|
|
2599
|
-
}))
|
|
2600
|
-
);
|
|
3152
|
+
await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
|
|
2601
3153
|
}
|
|
2602
3154
|
async disconnect() {
|
|
2603
3155
|
if (!this.disconnectPromise) {
|
|
@@ -2606,12 +3158,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2606
3158
|
await this.startup;
|
|
2607
3159
|
await this.unsubscribeInvalidation?.();
|
|
2608
3160
|
await this.flushWriteBehindQueue();
|
|
2609
|
-
await this.
|
|
3161
|
+
await this.maintenance.waitForGenerationCleanup();
|
|
2610
3162
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
this.writeBehindTimer = void 0;
|
|
2614
|
-
}
|
|
3163
|
+
this.maintenance.disposeWriteBehindTimer();
|
|
3164
|
+
this.fetchRateLimiter.dispose();
|
|
2615
3165
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
2616
3166
|
})();
|
|
2617
3167
|
}
|
|
@@ -2687,13 +3237,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2687
3237
|
if (!this.shouldNegativeCache(options)) {
|
|
2688
3238
|
return null;
|
|
2689
3239
|
}
|
|
2690
|
-
if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
3240
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2691
3241
|
this.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
2692
3242
|
key,
|
|
2693
3243
|
expectedClearEpoch,
|
|
2694
|
-
clearEpoch: this.
|
|
3244
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2695
3245
|
expectedKeyEpoch,
|
|
2696
|
-
keyEpoch: this.currentKeyEpoch(key)
|
|
3246
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2697
3247
|
});
|
|
2698
3248
|
return null;
|
|
2699
3249
|
}
|
|
@@ -2709,13 +3259,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2709
3259
|
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2710
3260
|
}
|
|
2711
3261
|
}
|
|
2712
|
-
if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
3262
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2713
3263
|
this.logger.debug?.("skip-store-after-invalidation", {
|
|
2714
3264
|
key,
|
|
2715
3265
|
expectedClearEpoch,
|
|
2716
|
-
clearEpoch: this.
|
|
3266
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2717
3267
|
expectedKeyEpoch,
|
|
2718
|
-
keyEpoch: this.currentKeyEpoch(key)
|
|
3268
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2719
3269
|
});
|
|
2720
3270
|
return fetched;
|
|
2721
3271
|
}
|
|
@@ -2723,10 +3273,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2723
3273
|
return fetched;
|
|
2724
3274
|
}
|
|
2725
3275
|
async storeEntry(key, kind, value, options) {
|
|
2726
|
-
const clearEpoch = this.
|
|
2727
|
-
const keyEpoch = this.currentKeyEpoch(key);
|
|
2728
|
-
await this.writeAcrossLayers(key, kind, value, options);
|
|
2729
|
-
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
3276
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3277
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3278
|
+
await this.layerWriter.writeAcrossLayers(key, kind, value, options);
|
|
3279
|
+
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2730
3280
|
return;
|
|
2731
3281
|
}
|
|
2732
3282
|
if (options?.tags) {
|
|
@@ -2742,57 +3292,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2742
3292
|
}
|
|
2743
3293
|
}
|
|
2744
3294
|
async writeBatch(entries) {
|
|
2745
|
-
const
|
|
2746
|
-
|
|
2747
|
-
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
|
|
2748
|
-
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2749
|
-
const immediateOperations = [];
|
|
2750
|
-
const deferredOperations = [];
|
|
2751
|
-
for (const entry of entries) {
|
|
2752
|
-
for (const layer of this.layers) {
|
|
2753
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2754
|
-
continue;
|
|
2755
|
-
}
|
|
2756
|
-
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
2757
|
-
const bucket = entriesByLayer.get(layer) ?? [];
|
|
2758
|
-
bucket.push(layerEntry);
|
|
2759
|
-
entriesByLayer.set(layer, bucket);
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2763
|
-
const operation = async () => {
|
|
2764
|
-
if (clearEpoch !== this.clearEpoch) {
|
|
2765
|
-
return;
|
|
2766
|
-
}
|
|
2767
|
-
const activeEntries = layerEntries.filter(
|
|
2768
|
-
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
|
|
2769
|
-
);
|
|
2770
|
-
if (activeEntries.length === 0) {
|
|
2771
|
-
return;
|
|
2772
|
-
}
|
|
2773
|
-
try {
|
|
2774
|
-
if (layer.setMany) {
|
|
2775
|
-
await layer.setMany(activeEntries);
|
|
2776
|
-
return;
|
|
2777
|
-
}
|
|
2778
|
-
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2779
|
-
} catch (error) {
|
|
2780
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
2781
|
-
}
|
|
2782
|
-
};
|
|
2783
|
-
if (this.shouldWriteBehind(layer)) {
|
|
2784
|
-
deferredOperations.push(operation);
|
|
2785
|
-
} else {
|
|
2786
|
-
immediateOperations.push(operation);
|
|
2787
|
-
}
|
|
2788
|
-
}
|
|
2789
|
-
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2790
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2791
|
-
if (clearEpoch !== this.clearEpoch) {
|
|
3295
|
+
const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
|
|
3296
|
+
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2792
3297
|
return;
|
|
2793
3298
|
}
|
|
2794
3299
|
for (const entry of entries) {
|
|
2795
|
-
if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
3300
|
+
if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2796
3301
|
continue;
|
|
2797
3302
|
}
|
|
2798
3303
|
if (entry.options?.tags) {
|
|
@@ -2894,58 +3399,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2894
3399
|
this.emit("backfill", { key, layer: layer.name });
|
|
2895
3400
|
}
|
|
2896
3401
|
}
|
|
2897
|
-
async writeAcrossLayers(key, kind, value, options) {
|
|
2898
|
-
const now = Date.now();
|
|
2899
|
-
const clearEpoch = this.clearEpoch;
|
|
2900
|
-
const keyEpoch = this.currentKeyEpoch(key);
|
|
2901
|
-
const immediateOperations = [];
|
|
2902
|
-
const deferredOperations = [];
|
|
2903
|
-
for (const layer of this.layers) {
|
|
2904
|
-
const operation = async () => {
|
|
2905
|
-
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2906
|
-
return;
|
|
2907
|
-
}
|
|
2908
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2909
|
-
return;
|
|
2910
|
-
}
|
|
2911
|
-
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
2912
|
-
try {
|
|
2913
|
-
await layer.set(entry.key, entry.value, entry.ttl);
|
|
2914
|
-
} catch (error) {
|
|
2915
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
2916
|
-
}
|
|
2917
|
-
};
|
|
2918
|
-
if (this.shouldWriteBehind(layer)) {
|
|
2919
|
-
deferredOperations.push(operation);
|
|
2920
|
-
} else {
|
|
2921
|
-
immediateOperations.push(operation);
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
2925
|
-
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2926
|
-
}
|
|
2927
|
-
async executeLayerOperations(operations, context) {
|
|
2928
|
-
if (this.options.writePolicy !== "best-effort") {
|
|
2929
|
-
await Promise.all(operations.map((operation) => operation()));
|
|
2930
|
-
return;
|
|
2931
|
-
}
|
|
2932
|
-
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
2933
|
-
const failures = results.filter((result) => result.status === "rejected");
|
|
2934
|
-
if (failures.length === 0) {
|
|
2935
|
-
return;
|
|
2936
|
-
}
|
|
2937
|
-
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2938
|
-
this.logger.debug?.("write-failure", {
|
|
2939
|
-
...context,
|
|
2940
|
-
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
2941
|
-
});
|
|
2942
|
-
if (failures.length === operations.length) {
|
|
2943
|
-
throw new AggregateError(
|
|
2944
|
-
failures.map((failure) => failure.reason),
|
|
2945
|
-
`${context.action} failed for every cache layer`
|
|
2946
|
-
);
|
|
2947
|
-
}
|
|
2948
|
-
}
|
|
2949
3402
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
2950
3403
|
return this.ttlResolver.resolveFreshTtl(
|
|
2951
3404
|
key,
|
|
@@ -2965,11 +3418,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2965
3418
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
2966
3419
|
}
|
|
2967
3420
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
2968
|
-
if (
|
|
3421
|
+
if (!shouldStartBackgroundRefresh({
|
|
3422
|
+
isDisconnecting: this.isDisconnecting,
|
|
3423
|
+
hasRefreshInFlight: this.backgroundRefreshes.has(key)
|
|
3424
|
+
})) {
|
|
2969
3425
|
return;
|
|
2970
3426
|
}
|
|
2971
|
-
const clearEpoch = this.
|
|
2972
|
-
const keyEpoch = this.currentKeyEpoch(key);
|
|
3427
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3428
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2973
3429
|
const refresh = (async () => {
|
|
2974
3430
|
this.metricsCollector.increment("refreshes");
|
|
2975
3431
|
try {
|
|
@@ -3007,8 +3463,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3007
3463
|
if (keys.length === 0) {
|
|
3008
3464
|
return;
|
|
3009
3465
|
}
|
|
3010
|
-
this.bumpKeyEpochs(keys);
|
|
3011
|
-
await this.deleteKeysFromLayers(this.layers, keys);
|
|
3466
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
3467
|
+
await this.invalidation.deleteKeysFromLayers(this.layers, keys);
|
|
3012
3468
|
for (const key of keys) {
|
|
3013
3469
|
await this.tagIndex.remove(key);
|
|
3014
3470
|
this.ttlResolver.deleteProfile(key);
|
|
@@ -3031,7 +3487,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3031
3487
|
}
|
|
3032
3488
|
const localLayers = this.layers.filter((layer) => layer.isLocal);
|
|
3033
3489
|
if (message.scope === "clear") {
|
|
3034
|
-
this.beginClearEpoch();
|
|
3490
|
+
this.maintenance.beginClearEpoch();
|
|
3035
3491
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
3036
3492
|
await this.tagIndex.clear();
|
|
3037
3493
|
this.ttlResolver.clearProfiles();
|
|
@@ -3039,8 +3495,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3039
3495
|
return;
|
|
3040
3496
|
}
|
|
3041
3497
|
const keys = message.keys ?? [];
|
|
3042
|
-
this.bumpKeyEpochs(keys);
|
|
3043
|
-
await this.deleteKeysFromLayers(localLayers, keys);
|
|
3498
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
3499
|
+
await this.invalidation.deleteKeysFromLayers(localLayers, keys);
|
|
3044
3500
|
if (message.operation !== "write") {
|
|
3045
3501
|
for (const key of keys) {
|
|
3046
3502
|
await this.tagIndex.remove(key);
|
|
@@ -3097,35 +3553,47 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3097
3553
|
shouldBroadcastL1Invalidation() {
|
|
3098
3554
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
3099
3555
|
}
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3556
|
+
async observeOperation(name, attributes, execute) {
|
|
3557
|
+
const id = this.nextOperationId;
|
|
3558
|
+
this.nextOperationId += 1;
|
|
3559
|
+
this.emit("operation-start", { id, name, attributes });
|
|
3560
|
+
try {
|
|
3561
|
+
const result = await execute();
|
|
3562
|
+
this.emit("operation-end", {
|
|
3563
|
+
id,
|
|
3564
|
+
name,
|
|
3565
|
+
attributes,
|
|
3566
|
+
success: true,
|
|
3567
|
+
result: result === null ? "null" : void 0
|
|
3568
|
+
});
|
|
3569
|
+
return result;
|
|
3570
|
+
} catch (error) {
|
|
3571
|
+
this.emit("operation-end", {
|
|
3572
|
+
id,
|
|
3573
|
+
name,
|
|
3574
|
+
attributes,
|
|
3575
|
+
success: false,
|
|
3576
|
+
error
|
|
3577
|
+
});
|
|
3578
|
+
throw error;
|
|
3579
|
+
}
|
|
3106
3580
|
}
|
|
3107
3581
|
scheduleGenerationCleanup(generation) {
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
this.generationCleanupPromise = void 0;
|
|
3582
|
+
this.maintenance.scheduleGenerationCleanup(
|
|
3583
|
+
generation,
|
|
3584
|
+
async (generationToClean) => this.cleanupGeneration(generationToClean),
|
|
3585
|
+
(failedGeneration, error) => {
|
|
3586
|
+
this.logger.warn?.("generation-cleanup-error", {
|
|
3587
|
+
generation: failedGeneration,
|
|
3588
|
+
error: this.formatError(error)
|
|
3589
|
+
});
|
|
3117
3590
|
}
|
|
3118
|
-
|
|
3591
|
+
);
|
|
3119
3592
|
}
|
|
3120
3593
|
async cleanupGeneration(generation) {
|
|
3121
3594
|
const prefix = `v${generation}:`;
|
|
3122
3595
|
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
3123
|
-
|
|
3124
|
-
return;
|
|
3125
|
-
}
|
|
3126
|
-
const batchSize = this.generationCleanupBatchSize();
|
|
3127
|
-
for (let index = 0; index < keys.length; index += batchSize) {
|
|
3128
|
-
const batch = keys.slice(index, index + batchSize);
|
|
3596
|
+
for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
|
|
3129
3597
|
await this.deleteKeys(batch);
|
|
3130
3598
|
await this.publishInvalidation({
|
|
3131
3599
|
scope: "keys",
|
|
@@ -3136,161 +3604,43 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3136
3604
|
}
|
|
3137
3605
|
}
|
|
3138
3606
|
initializeWriteBehind(options) {
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
return;
|
|
3145
|
-
}
|
|
3146
|
-
this.writeBehindTimer = setInterval(() => {
|
|
3147
|
-
void this.flushWriteBehindQueue();
|
|
3148
|
-
}, flushIntervalMs);
|
|
3149
|
-
this.writeBehindTimer.unref?.();
|
|
3607
|
+
this.maintenance.initializeWriteBehindTimer(
|
|
3608
|
+
this.options.writeStrategy,
|
|
3609
|
+
options,
|
|
3610
|
+
this.flushWriteBehindQueue.bind(this)
|
|
3611
|
+
);
|
|
3150
3612
|
}
|
|
3151
3613
|
shouldWriteBehind(layer) {
|
|
3152
3614
|
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
3153
3615
|
}
|
|
3154
|
-
beginClearEpoch() {
|
|
3155
|
-
this.clearEpoch += 1;
|
|
3156
|
-
this.keyEpochs.clear();
|
|
3157
|
-
this.writeBehindQueue.length = 0;
|
|
3158
|
-
}
|
|
3159
|
-
currentKeyEpoch(key) {
|
|
3160
|
-
return this.keyEpochs.get(key) ?? 0;
|
|
3161
|
-
}
|
|
3162
|
-
bumpKeyEpochs(keys) {
|
|
3163
|
-
for (const key of keys) {
|
|
3164
|
-
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
3165
|
-
}
|
|
3166
|
-
}
|
|
3167
|
-
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
3168
|
-
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
3169
|
-
return true;
|
|
3170
|
-
}
|
|
3171
|
-
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
3172
|
-
return true;
|
|
3173
|
-
}
|
|
3174
|
-
return false;
|
|
3175
|
-
}
|
|
3176
3616
|
async enqueueWriteBehind(operation) {
|
|
3177
|
-
this.
|
|
3178
|
-
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
3179
|
-
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
3180
|
-
if (this.writeBehindQueue.length >= batchSize) {
|
|
3181
|
-
await this.flushWriteBehindQueue();
|
|
3182
|
-
return;
|
|
3183
|
-
}
|
|
3184
|
-
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
3185
|
-
await this.flushWriteBehindQueue();
|
|
3186
|
-
}
|
|
3617
|
+
await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
3187
3618
|
}
|
|
3188
3619
|
async flushWriteBehindQueue() {
|
|
3189
|
-
|
|
3190
|
-
|
|
3620
|
+
await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
3621
|
+
}
|
|
3622
|
+
async runWriteBehindBatch(batch) {
|
|
3623
|
+
const results = await Promise.allSettled(batch.map((operation) => operation()));
|
|
3624
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
3625
|
+
if (failures.length === 0) {
|
|
3191
3626
|
return;
|
|
3192
3627
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
if (failures.length > 0) {
|
|
3199
|
-
this.metricsCollector.increment("writeFailures", failures.length);
|
|
3200
|
-
this.logger.error?.("write-behind-flush-failure", {
|
|
3201
|
-
failed: failures.length,
|
|
3202
|
-
total: batch.length,
|
|
3203
|
-
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
3204
|
-
});
|
|
3205
|
-
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
3206
|
-
}
|
|
3207
|
-
})();
|
|
3208
|
-
await this.writeBehindFlushPromise;
|
|
3209
|
-
this.writeBehindFlushPromise = void 0;
|
|
3210
|
-
if (this.writeBehindQueue.length > 0) {
|
|
3211
|
-
await this.flushWriteBehindQueue();
|
|
3212
|
-
}
|
|
3213
|
-
}
|
|
3214
|
-
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
3215
|
-
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
3216
|
-
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
3217
|
-
layer.name,
|
|
3218
|
-
options?.staleWhileRevalidate,
|
|
3219
|
-
this.options.staleWhileRevalidate
|
|
3220
|
-
);
|
|
3221
|
-
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
3222
|
-
const payload = createStoredValueEnvelope({
|
|
3223
|
-
kind,
|
|
3224
|
-
value,
|
|
3225
|
-
freshTtlSeconds: freshTtl,
|
|
3226
|
-
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
3227
|
-
staleIfErrorSeconds: staleIfError,
|
|
3228
|
-
now
|
|
3628
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
3629
|
+
this.logger.error?.("write-behind-flush-failure", {
|
|
3630
|
+
failed: failures.length,
|
|
3631
|
+
total: batch.length,
|
|
3632
|
+
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
3229
3633
|
});
|
|
3230
|
-
|
|
3231
|
-
return {
|
|
3232
|
-
key,
|
|
3233
|
-
value: payload,
|
|
3234
|
-
ttl
|
|
3235
|
-
};
|
|
3236
|
-
}
|
|
3237
|
-
intersectKeys(groups) {
|
|
3238
|
-
if (groups.length === 0) {
|
|
3239
|
-
return [];
|
|
3240
|
-
}
|
|
3241
|
-
const [firstGroup, ...rest] = groups;
|
|
3242
|
-
if (!firstGroup) {
|
|
3243
|
-
return [];
|
|
3244
|
-
}
|
|
3245
|
-
const restSets = rest.map((group) => new Set(group));
|
|
3246
|
-
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
3634
|
+
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
3247
3635
|
}
|
|
3248
3636
|
qualifyKey(key) {
|
|
3249
|
-
|
|
3250
|
-
return prefix ? `${prefix}${key}` : key;
|
|
3637
|
+
return qualifyGenerationKey(key, this.currentGeneration);
|
|
3251
3638
|
}
|
|
3252
3639
|
qualifyPattern(pattern) {
|
|
3253
|
-
|
|
3254
|
-
return prefix ? `${prefix}${pattern}` : pattern;
|
|
3640
|
+
return qualifyGenerationPattern(pattern, this.currentGeneration);
|
|
3255
3641
|
}
|
|
3256
3642
|
stripQualifiedKey(key) {
|
|
3257
|
-
|
|
3258
|
-
if (!prefix || !key.startsWith(prefix)) {
|
|
3259
|
-
return key;
|
|
3260
|
-
}
|
|
3261
|
-
return key.slice(prefix.length);
|
|
3262
|
-
}
|
|
3263
|
-
generationPrefix() {
|
|
3264
|
-
if (this.currentGeneration === void 0) {
|
|
3265
|
-
return "";
|
|
3266
|
-
}
|
|
3267
|
-
return `v${this.currentGeneration}:`;
|
|
3268
|
-
}
|
|
3269
|
-
async deleteKeysFromLayers(layers, keys) {
|
|
3270
|
-
await Promise.all(
|
|
3271
|
-
layers.map(async (layer) => {
|
|
3272
|
-
if (this.shouldSkipLayer(layer)) {
|
|
3273
|
-
return;
|
|
3274
|
-
}
|
|
3275
|
-
if (layer.deleteMany) {
|
|
3276
|
-
try {
|
|
3277
|
-
await layer.deleteMany(keys);
|
|
3278
|
-
} catch (error) {
|
|
3279
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
3280
|
-
}
|
|
3281
|
-
return;
|
|
3282
|
-
}
|
|
3283
|
-
await Promise.all(
|
|
3284
|
-
keys.map(async (key) => {
|
|
3285
|
-
try {
|
|
3286
|
-
await layer.delete(key);
|
|
3287
|
-
} catch (error) {
|
|
3288
|
-
await this.handleLayerFailure(layer, "delete", error);
|
|
3289
|
-
}
|
|
3290
|
-
})
|
|
3291
|
-
);
|
|
3292
|
-
})
|
|
3293
|
-
);
|
|
3643
|
+
return stripGenerationPrefix(key, this.currentGeneration);
|
|
3294
3644
|
}
|
|
3295
3645
|
validateConfiguration() {
|
|
3296
3646
|
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
@@ -3355,37 +3705,38 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3355
3705
|
this.assertActive(operation);
|
|
3356
3706
|
}
|
|
3357
3707
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
3358
|
-
const
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3708
|
+
const plan = planFreshReadPolicies({
|
|
3709
|
+
stored: hit.stored,
|
|
3710
|
+
hasFetcher: Boolean(fetcher),
|
|
3711
|
+
slidingTtl: options?.slidingTtl ?? false,
|
|
3712
|
+
refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
3713
|
+
});
|
|
3714
|
+
if (plan.refreshedStored) {
|
|
3363
3715
|
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
3364
3716
|
const layer = this.layers[index];
|
|
3365
3717
|
if (!layer || this.shouldSkipLayer(layer)) {
|
|
3366
3718
|
continue;
|
|
3367
3719
|
}
|
|
3368
3720
|
try {
|
|
3369
|
-
await layer.set(key,
|
|
3721
|
+
await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
|
|
3370
3722
|
} catch (error) {
|
|
3371
3723
|
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
3372
3724
|
}
|
|
3373
3725
|
}
|
|
3374
3726
|
}
|
|
3375
|
-
if (fetcher &&
|
|
3727
|
+
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
3376
3728
|
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
3377
3729
|
}
|
|
3378
3730
|
}
|
|
3379
3731
|
shouldSkipLayer(layer) {
|
|
3380
|
-
|
|
3381
|
-
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
3732
|
+
return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
|
|
3382
3733
|
}
|
|
3383
3734
|
async handleLayerFailure(layer, operation, error) {
|
|
3384
|
-
|
|
3735
|
+
const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
|
|
3736
|
+
if (!recovery.degrade) {
|
|
3385
3737
|
throw error;
|
|
3386
3738
|
}
|
|
3387
|
-
|
|
3388
|
-
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
3739
|
+
this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
|
|
3389
3740
|
this.metricsCollector.increment("degradedOperations");
|
|
3390
3741
|
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
3391
3742
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
@@ -3421,18 +3772,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3421
3772
|
this.emit("error", { operation, ...context });
|
|
3422
3773
|
}
|
|
3423
3774
|
}
|
|
3424
|
-
isCacheSnapshotEntries(value) {
|
|
3425
|
-
return Array.isArray(value) && value.every((entry) => {
|
|
3426
|
-
if (!entry || typeof entry !== "object") {
|
|
3427
|
-
return false;
|
|
3428
|
-
}
|
|
3429
|
-
const candidate = entry;
|
|
3430
|
-
return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
|
|
3431
|
-
});
|
|
3432
|
-
}
|
|
3433
|
-
sanitizeSnapshotValue(value) {
|
|
3434
|
-
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
3435
|
-
}
|
|
3436
3775
|
snapshotMaxBytes() {
|
|
3437
3776
|
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
3438
3777
|
}
|
|
@@ -3442,62 +3781,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3442
3781
|
invalidationMaxKeys() {
|
|
3443
3782
|
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
3444
3783
|
}
|
|
3445
|
-
async collectKeysForTag(tag) {
|
|
3446
|
-
const keys = /* @__PURE__ */ new Set();
|
|
3447
|
-
if (this.tagIndex.forEachKeyForTag) {
|
|
3448
|
-
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
3449
|
-
keys.add(key);
|
|
3450
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3451
|
-
});
|
|
3452
|
-
return [...keys];
|
|
3453
|
-
}
|
|
3454
|
-
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3455
|
-
keys.add(key);
|
|
3456
|
-
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3457
|
-
}
|
|
3458
|
-
return [...keys];
|
|
3459
|
-
}
|
|
3460
|
-
assertWithinInvalidationKeyLimit(size) {
|
|
3461
|
-
const maxKeys = this.invalidationMaxKeys();
|
|
3462
|
-
if (maxKeys !== false && size > maxKeys) {
|
|
3463
|
-
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
3464
|
-
}
|
|
3465
|
-
}
|
|
3466
|
-
async visitExportEntries(maxEntries, visitor) {
|
|
3467
|
-
const exported = /* @__PURE__ */ new Set();
|
|
3468
|
-
for (const layer of this.layers) {
|
|
3469
|
-
if (!layer.keys && !layer.forEachKey) {
|
|
3470
|
-
continue;
|
|
3471
|
-
}
|
|
3472
|
-
const visitKey = async (key) => {
|
|
3473
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
3474
|
-
if (exported.has(exportedKey)) {
|
|
3475
|
-
return;
|
|
3476
|
-
}
|
|
3477
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
3478
|
-
if (stored === null) {
|
|
3479
|
-
return;
|
|
3480
|
-
}
|
|
3481
|
-
exported.add(exportedKey);
|
|
3482
|
-
if (maxEntries !== false && exported.size > maxEntries) {
|
|
3483
|
-
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
3484
|
-
}
|
|
3485
|
-
await visitor({
|
|
3486
|
-
key: exportedKey,
|
|
3487
|
-
value: stored,
|
|
3488
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
3489
|
-
});
|
|
3490
|
-
};
|
|
3491
|
-
if (layer.forEachKey) {
|
|
3492
|
-
await layer.forEachKey(visitKey);
|
|
3493
|
-
continue;
|
|
3494
|
-
}
|
|
3495
|
-
const keys = await layer.keys?.();
|
|
3496
|
-
for (const key of keys ?? []) {
|
|
3497
|
-
await visitKey(key);
|
|
3498
|
-
}
|
|
3499
|
-
}
|
|
3500
|
-
}
|
|
3501
3784
|
};
|
|
3502
3785
|
|
|
3503
3786
|
// src/module.ts
|