layercache 1.2.6 → 1.2.8

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