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.
@@ -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 = emptyMetrics();
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 cloneMetrics(this.metrics);
488
+ return cloneNamespaceMetrics(this.metrics);
370
489
  }
371
490
  getHitRate() {
372
- const total = this.metrics.hits + this.metrics.misses;
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 = addMetrics(this.metrics, diffMetrics(before, after));
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/CacheSnapshotFile.ts
751
- function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
752
- const relative = path.relative(realBaseDir, candidatePath);
753
- return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
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
- async function findExistingAncestor(directory, fs, path) {
756
- let current = directory;
757
- while (true) {
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
- async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
774
- if (filePath.length === 0) {
775
- throw new Error("filePath must not be empty.");
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
- if (filePath.includes("\0")) {
778
- throw new Error("filePath must not contain null bytes.");
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
- const { promises: fs } = await import("fs");
781
- const path = await import("path");
782
- const resolved = path.resolve(filePath);
783
- const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
784
- if (baseDir === false) {
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
- await fs.mkdir(baseDir, { recursive: true });
788
- const realBaseDir = await fs.realpath(baseDir);
789
- if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
790
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
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
- if (mode === "read") {
793
- const realTarget = await fs.realpath(resolved);
794
- if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
795
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
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
- const parentDir = path.dirname(resolved);
800
- const existingAncestor = await findExistingAncestor(parentDir, fs, path);
801
- const realExistingAncestor = await fs.realpath(existingAncestor);
802
- if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
803
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
799
+ return batches;
800
+ }
801
+
802
+ // ../../src/internal/CacheStackInvalidationSupport.ts
803
+ var CacheStackInvalidationSupport = class {
804
+ constructor(options) {
805
+ this.options = options;
804
806
  }
805
- await fs.mkdir(parentDir, { recursive: true });
806
- const realParentDir = await fs.realpath(parentDir);
807
- if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
808
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
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
- const targetPath = path.join(realParentDir, path.basename(resolved));
811
- try {
812
- const existing = await fs.lstat(targetPath);
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
- } catch (error) {
817
- if (error.code !== "ENOENT") {
818
- throw error;
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
- return targetPath;
822
- }
823
- async function readUtf8HandleWithLimit(handle, byteLimit) {
824
- if (byteLimit === false) {
825
- return handle.readFile({ encoding: "utf8" });
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
- const chunks = [];
828
- let totalBytes = 0;
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
- return Buffer.concat(chunks).toString("utf8");
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
- return Math.max(1, Math.ceil(remainingMs / 1e3));
1463
- }
1464
- function remainingFreshTtlSeconds(stored, now = Date.now()) {
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
- const remainingMs = stored.freshUntil - now;
1469
- if (remainingMs <= 0) {
1470
- return 0;
2008
+ incrementLayer(map, layerName) {
2009
+ this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
1471
2010
  }
1472
- return Math.max(1, Math.ceil(remainingMs / 1e3));
1473
- }
1474
- function refreshStoredEnvelope(stored, now = Date.now()) {
1475
- if (!isStoredValueEnvelope(stored)) {
1476
- return stored;
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
- return createStoredValueEnvelope({
1479
- kind: stored.kind,
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
- return Math.max(...values);
1495
- }
1496
- function normalizePositiveSeconds(value) {
1497
- if (!value || value <= 0) {
1498
- return void 0;
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
- return value;
1501
- }
1502
- function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
1503
- if (value == null) {
1504
- return true;
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
- return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
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 path = [];
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
- path.push([node, character]);
2408
+ path2.push([node, character]);
1851
2409
  node = child;
1852
2410
  }
1853
2411
  node.terminal = false;
1854
- for (let index = path.length - 1; index >= 0; index -= 1) {
1855
- const entry = path[index];
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/serialization/JsonSerializer.ts
1870
- var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1871
- var MAX_SANITIZE_NODES = 1e4;
1872
- var JsonSerializer = class {
1873
- serialize(value) {
1874
- return JSON.stringify(value);
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 > MAX_SANITIZE_NODES) {
1885
- throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
2434
+ if (state.count > options.maxNodes) {
2435
+ throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
1886
2436
  }
1887
- if (depth > MAX_SANITIZE_DEPTH) {
1888
- throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
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) => sanitizeJsonValue(entry, depth + 1, state));
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 (DANGEROUS_JSON_KEYS.has(key)) {
2448
+ if (DANGEROUS_KEYS.has(key)) {
1899
2449
  continue;
1900
2450
  }
1901
- sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
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
- keyEpochs = /* @__PURE__ */ new Map();
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
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2060
- this.validateWriteOptions(options);
2061
- await this.awaitStartup("get");
2062
- return this.getPrepared(normalizedKey, fetcher, options);
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
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2185
- this.validateWriteOptions(options);
2186
- await this.awaitStartup("set");
2187
- await this.storeEntry(normalizedKey, "value", value, options);
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
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2194
- await this.awaitStartup("delete");
2195
- await this.deleteKeys([normalizedKey]);
2196
- await this.publishInvalidation({
2197
- scope: "key",
2198
- keys: [normalizedKey],
2199
- sourceId: this.instanceId,
2200
- operation: "delete"
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.assertActive("mget");
2234
- if (entries.length === 0) {
2235
- return [];
2236
- }
2237
- const normalizedEntries = entries.map((entry) => ({
2238
- ...entry,
2239
- key: this.qualifyKey(validateCacheKey(entry.key))
2240
- }));
2241
- normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2242
- const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
2243
- if (!canFastPath) {
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 pendingReads = /* @__PURE__ */ new Map();
2246
- return Promise.all(
2247
- normalizedEntries.map((entry) => {
2248
- const optionsSignature = serializeOptions(entry.options);
2249
- const existing = pendingReads.get(entry.key);
2250
- if (!existing) {
2251
- const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
2252
- pendingReads.set(entry.key, {
2253
- promise,
2254
- fetch: entry.fetch,
2255
- optionsSignature
2256
- });
2257
- return promise;
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
- if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
2260
- throw new Error(`mget received conflicting entries for key "${entry.key}".`);
2902
+ const resolved = resolveStoredValue(stored);
2903
+ if (resolved.state === "expired") {
2904
+ await layer.delete(key);
2905
+ continue;
2261
2906
  }
2262
- return existing.promise;
2263
- })
2264
- );
2265
- }
2266
- await this.awaitStartup("mget");
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
- if (pending.size > 0) {
2306
- for (const key of pending) {
2307
- await this.tagIndex.remove(key);
2308
- this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
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
- return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2920
+ return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2921
+ });
2312
2922
  }
2313
2923
  async mset(entries) {
2314
- this.assertActive("mset");
2315
- const normalizedEntries = entries.map((entry) => ({
2316
- ...entry,
2317
- key: this.qualifyKey(validateCacheKey(entry.key))
2318
- }));
2319
- normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2320
- await this.awaitStartup("mset");
2321
- await this.writeBatch(normalizedEntries);
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
- validateTag(tag);
2375
- await this.awaitStartup("invalidateByTag");
2376
- const keys = await this.collectKeysForTag(tag);
2377
- await this.deleteKeys(keys);
2378
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
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
- if (tags.length === 0) {
2382
- return;
2383
- }
2384
- validateTags(tags);
2385
- await this.awaitStartup("invalidateByTags");
2386
- const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
2387
- const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
2388
- this.assertWithinInvalidationKeyLimit(keys.length);
2389
- await this.deleteKeys(keys);
2390
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
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
- validatePattern(pattern);
2394
- await this.awaitStartup("invalidateByPattern");
2395
- const keys = await this.keyDiscovery.collectKeysMatchingPattern(
2396
- this.qualifyPattern(pattern),
2397
- this.invalidationMaxKeys()
2398
- );
2399
- await this.deleteKeys(keys);
2400
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
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.awaitStartup("invalidateByPrefix");
2404
- const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
2405
- const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
2406
- await this.deleteKeys(keys);
2407
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
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
- this.currentGeneration = nextGeneration ?? current + 1;
2464
- if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
2465
- this.scheduleGenerationCleanup(previousGeneration);
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
- const entries = [];
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
- const normalizedEntries = entries.map((entry) => ({
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
- const { promises: fs } = await import("fs");
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
- const { promises: fs, constants } = await import("fs");
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.generationCleanupPromise;
3161
+ await this.maintenance.waitForGenerationCleanup();
2610
3162
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
2611
- if (this.writeBehindTimer) {
2612
- clearInterval(this.writeBehindTimer);
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.clearEpoch,
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.clearEpoch,
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.clearEpoch;
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 now = Date.now();
2746
- const clearEpoch = this.clearEpoch;
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 (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
3421
+ if (!shouldStartBackgroundRefresh({
3422
+ isDisconnecting: this.isDisconnecting,
3423
+ hasRefreshInFlight: this.backgroundRefreshes.has(key)
3424
+ })) {
2969
3425
  return;
2970
3426
  }
2971
- const clearEpoch = this.clearEpoch;
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
- shouldCleanupGenerations() {
3101
- return Boolean(this.options.generationCleanup);
3102
- }
3103
- generationCleanupBatchSize() {
3104
- const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
3105
- return configured ?? 500;
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
- const task = (this.generationCleanupPromise ?? Promise.resolve()).then(() => this.cleanupGeneration(generation)).catch((error) => {
3109
- this.logger.warn?.("generation-cleanup-error", {
3110
- generation,
3111
- error: this.formatError(error)
3112
- });
3113
- });
3114
- this.generationCleanupPromise = task.finally(() => {
3115
- if (this.generationCleanupPromise === task) {
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
- if (keys.length === 0) {
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
- if (this.options.writeStrategy !== "write-behind") {
3140
- return;
3141
- }
3142
- const flushIntervalMs = options?.flushIntervalMs;
3143
- if (!flushIntervalMs || flushIntervalMs <= 0) {
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.writeBehindQueue.push(operation);
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
- if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
3190
- await this.writeBehindFlushPromise;
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
- const batchSize = this.options.writeBehind?.batchSize ?? 100;
3194
- const batch = this.writeBehindQueue.splice(0, batchSize);
3195
- this.writeBehindFlushPromise = (async () => {
3196
- const results = await Promise.allSettled(batch.map((operation) => operation()));
3197
- const failures = results.filter((result) => result.status === "rejected");
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
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
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
- const prefix = this.generationPrefix();
3250
- return prefix ? `${prefix}${key}` : key;
3637
+ return qualifyGenerationKey(key, this.currentGeneration);
3251
3638
  }
3252
3639
  qualifyPattern(pattern) {
3253
- const prefix = this.generationPrefix();
3254
- return prefix ? `${prefix}${pattern}` : pattern;
3640
+ return qualifyGenerationPattern(pattern, this.currentGeneration);
3255
3641
  }
3256
3642
  stripQualifiedKey(key) {
3257
- const prefix = this.generationPrefix();
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 refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
3359
- const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
3360
- if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
3361
- const refreshed = refreshStoredEnvelope(hit.stored);
3362
- const ttl = remainingStoredTtlSeconds(refreshed);
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, refreshed, ttl);
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 && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
3727
+ if (fetcher && plan.shouldScheduleBackgroundRefresh) {
3376
3728
  this.scheduleBackgroundRefresh(key, fetcher, options);
3377
3729
  }
3378
3730
  }
3379
3731
  shouldSkipLayer(layer) {
3380
- const degradedUntil = this.layerDegradedUntil.get(layer.name);
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
- if (!this.isGracefulDegradationEnabled()) {
3735
+ const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
3736
+ if (!recovery.degrade) {
3385
3737
  throw error;
3386
3738
  }
3387
- const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
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