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