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.js CHANGED
@@ -22,6 +22,127 @@ import { EventEmitter } from "events";
22
22
 
23
23
  // src/CacheNamespace.ts
24
24
  import { Mutex } from "async-mutex";
25
+
26
+ // src/internal/CacheNamespaceMetrics.ts
27
+ function createEmptyNamespaceMetrics(resetAt = Date.now()) {
28
+ return {
29
+ hits: 0,
30
+ misses: 0,
31
+ fetches: 0,
32
+ sets: 0,
33
+ deletes: 0,
34
+ backfills: 0,
35
+ invalidations: 0,
36
+ staleHits: 0,
37
+ refreshes: 0,
38
+ refreshErrors: 0,
39
+ writeFailures: 0,
40
+ singleFlightWaits: 0,
41
+ negativeCacheHits: 0,
42
+ circuitBreakerTrips: 0,
43
+ degradedOperations: 0,
44
+ hitsByLayer: {},
45
+ missesByLayer: {},
46
+ latencyByLayer: {},
47
+ resetAt
48
+ };
49
+ }
50
+ function cloneNamespaceMetrics(metrics) {
51
+ return {
52
+ ...metrics,
53
+ hitsByLayer: { ...metrics.hitsByLayer },
54
+ missesByLayer: { ...metrics.missesByLayer },
55
+ latencyByLayer: Object.fromEntries(
56
+ Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
57
+ )
58
+ };
59
+ }
60
+ function diffNamespaceMetrics(before, after) {
61
+ const latencyByLayer = Object.fromEntries(
62
+ Object.entries(after.latencyByLayer).map(([layer, value]) => [
63
+ layer,
64
+ {
65
+ avgMs: value.avgMs,
66
+ maxMs: value.maxMs,
67
+ count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
68
+ }
69
+ ])
70
+ );
71
+ return {
72
+ hits: after.hits - before.hits,
73
+ misses: after.misses - before.misses,
74
+ fetches: after.fetches - before.fetches,
75
+ sets: after.sets - before.sets,
76
+ deletes: after.deletes - before.deletes,
77
+ backfills: after.backfills - before.backfills,
78
+ invalidations: after.invalidations - before.invalidations,
79
+ staleHits: after.staleHits - before.staleHits,
80
+ refreshes: after.refreshes - before.refreshes,
81
+ refreshErrors: after.refreshErrors - before.refreshErrors,
82
+ writeFailures: after.writeFailures - before.writeFailures,
83
+ singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
84
+ negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
85
+ circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
86
+ degradedOperations: after.degradedOperations - before.degradedOperations,
87
+ hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
88
+ missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
89
+ latencyByLayer,
90
+ resetAt: after.resetAt
91
+ };
92
+ }
93
+ function addNamespaceMetrics(base, delta) {
94
+ return {
95
+ hits: base.hits + delta.hits,
96
+ misses: base.misses + delta.misses,
97
+ fetches: base.fetches + delta.fetches,
98
+ sets: base.sets + delta.sets,
99
+ deletes: base.deletes + delta.deletes,
100
+ backfills: base.backfills + delta.backfills,
101
+ invalidations: base.invalidations + delta.invalidations,
102
+ staleHits: base.staleHits + delta.staleHits,
103
+ refreshes: base.refreshes + delta.refreshes,
104
+ refreshErrors: base.refreshErrors + delta.refreshErrors,
105
+ writeFailures: base.writeFailures + delta.writeFailures,
106
+ singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
107
+ negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
108
+ circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
109
+ degradedOperations: base.degradedOperations + delta.degradedOperations,
110
+ hitsByLayer: addMetricMap(base.hitsByLayer, delta.hitsByLayer),
111
+ missesByLayer: addMetricMap(base.missesByLayer, delta.missesByLayer),
112
+ latencyByLayer: cloneNamespaceMetrics(delta).latencyByLayer,
113
+ resetAt: base.resetAt
114
+ };
115
+ }
116
+ function computeNamespaceHitRate(metrics) {
117
+ const total = metrics.hits + metrics.misses;
118
+ const overall = total === 0 ? 0 : metrics.hits / total;
119
+ const byLayer = {};
120
+ const layers = /* @__PURE__ */ new Set([...Object.keys(metrics.hitsByLayer), ...Object.keys(metrics.missesByLayer)]);
121
+ for (const layer of layers) {
122
+ const hits = metrics.hitsByLayer[layer] ?? 0;
123
+ const misses = metrics.missesByLayer[layer] ?? 0;
124
+ byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
125
+ }
126
+ return { overall, byLayer };
127
+ }
128
+ function diffMetricMap(before, after) {
129
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
130
+ const result = {};
131
+ for (const key of keys) {
132
+ result[key] = (after[key] ?? 0) - (before[key] ?? 0);
133
+ }
134
+ return result;
135
+ }
136
+ function addMetricMap(base, delta) {
137
+ const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
138
+ const result = {};
139
+ for (const key of keys) {
140
+ result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
141
+ }
142
+ return result;
143
+ }
144
+
145
+ // src/CacheNamespace.ts
25
146
  var CacheNamespace = class _CacheNamespace {
26
147
  constructor(cache, prefix) {
27
148
  this.cache = cache;
@@ -31,7 +152,7 @@ var CacheNamespace = class _CacheNamespace {
31
152
  cache;
32
153
  prefix;
33
154
  static metricsMutexes = /* @__PURE__ */ new WeakMap();
34
- metrics = emptyMetrics();
155
+ metrics = createEmptyNamespaceMetrics();
35
156
  async get(key, fetcher, options) {
36
157
  return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
37
158
  }
@@ -128,19 +249,10 @@ var CacheNamespace = class _CacheNamespace {
128
249
  );
129
250
  }
130
251
  getMetrics() {
131
- return cloneMetrics(this.metrics);
252
+ return cloneNamespaceMetrics(this.metrics);
132
253
  }
133
254
  getHitRate() {
134
- const total = this.metrics.hits + this.metrics.misses;
135
- const overall = total === 0 ? 0 : this.metrics.hits / total;
136
- const byLayer = {};
137
- const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
138
- for (const layer of layers) {
139
- const hits = this.metrics.hitsByLayer[layer] ?? 0;
140
- const misses = this.metrics.missesByLayer[layer] ?? 0;
141
- byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
142
- }
143
- return { overall, byLayer };
255
+ return computeNamespaceHitRate(this.metrics);
144
256
  }
145
257
  /**
146
258
  * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
@@ -181,7 +293,7 @@ var CacheNamespace = class _CacheNamespace {
181
293
  const before = this.cache.getMetrics();
182
294
  const result = await operation();
183
295
  const after = this.cache.getMetrics();
184
- this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
296
+ this.metrics = addNamespaceMetrics(this.metrics, diffNamespaceMetrics(before, after));
185
297
  return result;
186
298
  });
187
299
  }
@@ -195,111 +307,6 @@ var CacheNamespace = class _CacheNamespace {
195
307
  return mutex;
196
308
  }
197
309
  };
198
- function emptyMetrics() {
199
- return {
200
- hits: 0,
201
- misses: 0,
202
- fetches: 0,
203
- sets: 0,
204
- deletes: 0,
205
- backfills: 0,
206
- invalidations: 0,
207
- staleHits: 0,
208
- refreshes: 0,
209
- refreshErrors: 0,
210
- writeFailures: 0,
211
- singleFlightWaits: 0,
212
- negativeCacheHits: 0,
213
- circuitBreakerTrips: 0,
214
- degradedOperations: 0,
215
- hitsByLayer: {},
216
- missesByLayer: {},
217
- latencyByLayer: {},
218
- resetAt: Date.now()
219
- };
220
- }
221
- function cloneMetrics(metrics) {
222
- return {
223
- ...metrics,
224
- hitsByLayer: { ...metrics.hitsByLayer },
225
- missesByLayer: { ...metrics.missesByLayer },
226
- latencyByLayer: Object.fromEntries(
227
- Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
228
- )
229
- };
230
- }
231
- function diffMetrics(before, after) {
232
- const latencyByLayer = Object.fromEntries(
233
- Object.entries(after.latencyByLayer).map(([layer, value]) => [
234
- layer,
235
- {
236
- avgMs: value.avgMs,
237
- maxMs: value.maxMs,
238
- count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
239
- }
240
- ])
241
- );
242
- return {
243
- hits: after.hits - before.hits,
244
- misses: after.misses - before.misses,
245
- fetches: after.fetches - before.fetches,
246
- sets: after.sets - before.sets,
247
- deletes: after.deletes - before.deletes,
248
- backfills: after.backfills - before.backfills,
249
- invalidations: after.invalidations - before.invalidations,
250
- staleHits: after.staleHits - before.staleHits,
251
- refreshes: after.refreshes - before.refreshes,
252
- refreshErrors: after.refreshErrors - before.refreshErrors,
253
- writeFailures: after.writeFailures - before.writeFailures,
254
- singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
255
- negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
256
- circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
257
- degradedOperations: after.degradedOperations - before.degradedOperations,
258
- hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
259
- missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
260
- latencyByLayer,
261
- resetAt: after.resetAt
262
- };
263
- }
264
- function addMetrics(base, delta) {
265
- return {
266
- hits: base.hits + delta.hits,
267
- misses: base.misses + delta.misses,
268
- fetches: base.fetches + delta.fetches,
269
- sets: base.sets + delta.sets,
270
- deletes: base.deletes + delta.deletes,
271
- backfills: base.backfills + delta.backfills,
272
- invalidations: base.invalidations + delta.invalidations,
273
- staleHits: base.staleHits + delta.staleHits,
274
- refreshes: base.refreshes + delta.refreshes,
275
- refreshErrors: base.refreshErrors + delta.refreshErrors,
276
- writeFailures: base.writeFailures + delta.writeFailures,
277
- singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
278
- negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
279
- circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
280
- degradedOperations: base.degradedOperations + delta.degradedOperations,
281
- hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
282
- missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
283
- latencyByLayer: cloneMetrics(delta).latencyByLayer,
284
- resetAt: base.resetAt
285
- };
286
- }
287
- function diffMap(before, after) {
288
- const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
289
- const result = {};
290
- for (const key of keys) {
291
- result[key] = (after[key] ?? 0) - (before[key] ?? 0);
292
- }
293
- return result;
294
- }
295
- function addMap(base, delta) {
296
- const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
297
- const result = {};
298
- for (const key of keys) {
299
- result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
300
- }
301
- return result;
302
- }
303
310
  function validateNamespaceKey(key) {
304
311
  if (key.length === 0) {
305
312
  throw new Error("Namespace prefix must not be empty.");
@@ -365,119 +372,503 @@ var CacheKeyDiscovery = class {
365
372
  );
366
373
  return [...matches];
367
374
  }
368
- async collectKeysMatchingPattern(pattern, maxMatches = false) {
369
- const matches = /* @__PURE__ */ new Set();
370
- if (this.options.tagIndex.forEachKeyMatchingPattern) {
371
- await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
372
- matches.add(key);
373
- this.assertWithinMatchLimit(matches, maxMatches);
374
- });
375
- } else {
376
- for (const key of await this.options.tagIndex.matchPattern(pattern)) {
377
- matches.add(key);
378
- this.assertWithinMatchLimit(matches, maxMatches);
379
- }
375
+ async collectKeysMatchingPattern(pattern, maxMatches = false) {
376
+ const matches = /* @__PURE__ */ new Set();
377
+ if (this.options.tagIndex.forEachKeyMatchingPattern) {
378
+ await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
379
+ matches.add(key);
380
+ this.assertWithinMatchLimit(matches, maxMatches);
381
+ });
382
+ } else {
383
+ for (const key of await this.options.tagIndex.matchPattern(pattern)) {
384
+ matches.add(key);
385
+ this.assertWithinMatchLimit(matches, maxMatches);
386
+ }
387
+ }
388
+ await Promise.all(
389
+ this.options.layers.map(async (layer) => {
390
+ if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
391
+ return;
392
+ }
393
+ try {
394
+ if (layer.forEachKey) {
395
+ await layer.forEachKey(async (key) => {
396
+ if (PatternMatcher.matches(pattern, key)) {
397
+ matches.add(key);
398
+ this.assertWithinMatchLimit(matches, maxMatches);
399
+ }
400
+ });
401
+ return;
402
+ }
403
+ const keys = await layer.keys?.();
404
+ for (const key of keys ?? []) {
405
+ if (PatternMatcher.matches(pattern, key)) {
406
+ matches.add(key);
407
+ this.assertWithinMatchLimit(matches, maxMatches);
408
+ }
409
+ }
410
+ } catch (error) {
411
+ await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
412
+ }
413
+ })
414
+ );
415
+ return [...matches];
416
+ }
417
+ assertWithinMatchLimit(matches, maxMatches) {
418
+ if (maxMatches !== false && matches.size > maxMatches) {
419
+ throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
420
+ }
421
+ }
422
+ };
423
+
424
+ // src/internal/CacheKeySerialization.ts
425
+ var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
426
+ function normalizeForSerialization(value) {
427
+ if (Array.isArray(value)) {
428
+ return value.map((entry) => normalizeForSerialization(entry));
429
+ }
430
+ if (value && typeof value === "object") {
431
+ return Object.keys(value).sort().reduce((normalized, key) => {
432
+ if (DANGEROUS_OBJECT_KEYS.has(key)) {
433
+ return normalized;
434
+ }
435
+ normalized[key] = normalizeForSerialization(value[key]);
436
+ return normalized;
437
+ }, {});
438
+ }
439
+ return value;
440
+ }
441
+ function serializeKeyPart(value) {
442
+ if (typeof value === "string") {
443
+ return `s:${value}`;
444
+ }
445
+ if (typeof value === "number") {
446
+ return `n:${value}`;
447
+ }
448
+ if (typeof value === "boolean") {
449
+ return `b:${value}`;
450
+ }
451
+ return `j:${JSON.stringify(normalizeForSerialization(value))}`;
452
+ }
453
+ function serializeOptions(options) {
454
+ return JSON.stringify(normalizeForSerialization(options) ?? null);
455
+ }
456
+ function createInstanceId() {
457
+ if (globalThis.crypto?.randomUUID) {
458
+ return globalThis.crypto.randomUUID();
459
+ }
460
+ const bytes = new Uint8Array(16);
461
+ if (globalThis.crypto?.getRandomValues) {
462
+ globalThis.crypto.getRandomValues(bytes);
463
+ } else {
464
+ for (let i = 0; i < bytes.length; i += 1) {
465
+ bytes[i] = Math.floor(Math.random() * 256);
466
+ }
467
+ }
468
+ return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
469
+ }
470
+
471
+ // src/internal/CacheStackGeneration.ts
472
+ var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
473
+ function generationPrefix(generation) {
474
+ return generation === void 0 ? "" : `v${generation}:`;
475
+ }
476
+ function qualifyGenerationKey(key, generation) {
477
+ const prefix = generationPrefix(generation);
478
+ return prefix ? `${prefix}${key}` : key;
479
+ }
480
+ function qualifyGenerationPattern(pattern, generation) {
481
+ return qualifyGenerationKey(pattern, generation);
482
+ }
483
+ function stripGenerationPrefix(key, generation) {
484
+ const prefix = generationPrefix(generation);
485
+ if (!prefix || !key.startsWith(prefix)) {
486
+ return key;
487
+ }
488
+ return key.slice(prefix.length);
489
+ }
490
+ function resolveGenerationCleanupTarget({
491
+ previousGeneration,
492
+ nextGeneration,
493
+ generationCleanup
494
+ }) {
495
+ if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
496
+ return null;
497
+ }
498
+ return previousGeneration;
499
+ }
500
+ function resolveGenerationCleanupBatchSize(generationCleanup) {
501
+ if (typeof generationCleanup !== "object" || generationCleanup === null) {
502
+ return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
503
+ }
504
+ return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
505
+ }
506
+ function planGenerationCleanupBatches(keys, generationCleanup) {
507
+ if (keys.length === 0) {
508
+ return [];
509
+ }
510
+ const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
511
+ const batches = [];
512
+ for (let index = 0; index < keys.length; index += batchSize) {
513
+ batches.push(keys.slice(index, index + batchSize));
514
+ }
515
+ return batches;
516
+ }
517
+
518
+ // src/internal/CacheStackInvalidationSupport.ts
519
+ var CacheStackInvalidationSupport = class {
520
+ constructor(options) {
521
+ this.options = options;
522
+ }
523
+ options;
524
+ async collectKeysForTag(tag, maxKeys) {
525
+ const keys = /* @__PURE__ */ new Set();
526
+ if (this.options.tagIndex.forEachKeyForTag) {
527
+ await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
528
+ keys.add(key);
529
+ this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
530
+ });
531
+ return [...keys];
532
+ }
533
+ for (const key of await this.options.tagIndex.keysForTag(tag)) {
534
+ keys.add(key);
535
+ this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
536
+ }
537
+ return [...keys];
538
+ }
539
+ intersectKeys(groups) {
540
+ if (groups.length === 0) {
541
+ return [];
542
+ }
543
+ const [firstGroup, ...rest] = groups;
544
+ const restSets = rest.map((group) => new Set(group));
545
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
546
+ }
547
+ async deleteKeysFromLayers(layers, keys) {
548
+ await Promise.all(
549
+ layers.map(async (layer) => {
550
+ if (this.options.shouldSkipLayer(layer)) {
551
+ return;
552
+ }
553
+ if (layer.deleteMany) {
554
+ try {
555
+ await layer.deleteMany(keys);
556
+ } catch (error) {
557
+ await this.options.handleLayerFailure(layer, "delete", error);
558
+ }
559
+ return;
560
+ }
561
+ await Promise.all(
562
+ keys.map(async (key) => {
563
+ try {
564
+ await layer.delete(key);
565
+ } catch (error) {
566
+ await this.options.handleLayerFailure(layer, "delete", error);
567
+ }
568
+ })
569
+ );
570
+ })
571
+ );
572
+ }
573
+ assertWithinInvalidationKeyLimit(size, maxKeys) {
574
+ if (maxKeys !== false && size > maxKeys) {
575
+ throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
576
+ }
577
+ }
578
+ };
579
+
580
+ // src/internal/CacheStackLayerWriter.ts
581
+ var CacheStackLayerWriter = class {
582
+ constructor(options) {
583
+ this.options = options;
584
+ }
585
+ options;
586
+ async writeAcrossLayers(key, kind, value, writeOptions) {
587
+ const now = Date.now();
588
+ const clearEpoch = this.options.maintenance.currentClearEpoch();
589
+ const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
590
+ const immediateOperations = [];
591
+ const deferredOperations = [];
592
+ for (const layer of this.options.layers) {
593
+ const operation = async () => {
594
+ if (this.options.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
595
+ return;
596
+ }
597
+ if (this.options.shouldSkipLayer(layer)) {
598
+ return;
599
+ }
600
+ const entry = this.buildLayerSetEntry(layer, key, kind, value, writeOptions, now);
601
+ try {
602
+ await layer.set(entry.key, entry.value, entry.ttl);
603
+ } catch (error) {
604
+ await this.options.handleLayerFailure(layer, "write", error);
605
+ }
606
+ };
607
+ if (this.options.shouldWriteBehind(layer)) {
608
+ deferredOperations.push(operation);
609
+ } else {
610
+ immediateOperations.push(operation);
611
+ }
612
+ }
613
+ await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
614
+ await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
615
+ }
616
+ async writeBatch(entries) {
617
+ const now = Date.now();
618
+ const clearEpoch = this.options.maintenance.currentClearEpoch();
619
+ const entryEpochs = new Map(
620
+ entries.map((entry) => [entry.key, this.options.maintenance.currentKeyEpoch(entry.key)])
621
+ );
622
+ const entriesByLayer = /* @__PURE__ */ new Map();
623
+ const immediateOperations = [];
624
+ const deferredOperations = [];
625
+ for (const entry of entries) {
626
+ for (const layer of this.options.layers) {
627
+ if (this.options.shouldSkipLayer(layer)) {
628
+ continue;
629
+ }
630
+ const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
631
+ const bucket = entriesByLayer.get(layer) ?? [];
632
+ bucket.push(layerEntry);
633
+ entriesByLayer.set(layer, bucket);
634
+ }
635
+ }
636
+ for (const [layer, layerEntries] of entriesByLayer.entries()) {
637
+ const operation = async () => {
638
+ if (clearEpoch !== this.options.maintenance.currentClearEpoch()) {
639
+ return;
640
+ }
641
+ const activeEntries = layerEntries.filter(
642
+ (entry) => (entryEpochs.get(entry.key) ?? 0) === this.options.maintenance.currentKeyEpoch(entry.key)
643
+ );
644
+ if (activeEntries.length === 0) {
645
+ return;
646
+ }
647
+ try {
648
+ if (layer.setMany) {
649
+ await layer.setMany(activeEntries);
650
+ return;
651
+ }
652
+ await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
653
+ } catch (error) {
654
+ await this.options.handleLayerFailure(layer, "write", error);
655
+ }
656
+ };
657
+ if (this.options.shouldWriteBehind(layer)) {
658
+ deferredOperations.push(operation);
659
+ } else {
660
+ immediateOperations.push(operation);
661
+ }
662
+ }
663
+ await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
664
+ await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
665
+ return { clearEpoch, entryEpochs };
666
+ }
667
+ async executeLayerOperations(operations, context) {
668
+ if (this.options.writePolicy !== "best-effort") {
669
+ await Promise.all(operations.map((operation) => operation()));
670
+ return;
671
+ }
672
+ const results = await Promise.allSettled(operations.map((operation) => operation()));
673
+ const failures = results.filter((result) => result.status === "rejected");
674
+ if (failures.length === 0) {
675
+ return;
676
+ }
677
+ this.options.onWriteFailures(
678
+ context,
679
+ failures.map((failure) => failure.reason)
680
+ );
681
+ if (failures.length === operations.length) {
682
+ throw new AggregateError(
683
+ failures.map((failure) => failure.reason),
684
+ `${context.action} failed for every cache layer`
685
+ );
686
+ }
687
+ }
688
+ buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
689
+ const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
690
+ const staleWhileRevalidate = this.options.resolveLayerSeconds(
691
+ layer.name,
692
+ writeOptions?.staleWhileRevalidate,
693
+ this.options.globalStaleWhileRevalidate
694
+ );
695
+ const staleIfError = this.options.resolveLayerSeconds(
696
+ layer.name,
697
+ writeOptions?.staleIfError,
698
+ this.options.globalStaleIfError
699
+ );
700
+ const payload = createStoredValueEnvelope({
701
+ kind,
702
+ value,
703
+ freshTtlSeconds: freshTtl,
704
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
705
+ staleIfErrorSeconds: staleIfError,
706
+ now
707
+ });
708
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
709
+ return {
710
+ key,
711
+ value: payload,
712
+ ttl
713
+ };
714
+ }
715
+ };
716
+
717
+ // src/internal/CacheStackMaintenance.ts
718
+ var CacheStackMaintenance = class {
719
+ keyEpochs = /* @__PURE__ */ new Map();
720
+ writeBehindQueue = [];
721
+ writeBehindTimer;
722
+ writeBehindFlushPromise;
723
+ generationCleanupPromise;
724
+ clearEpoch = 0;
725
+ initializeWriteBehindTimer(writeStrategy, options, flush) {
726
+ if (writeStrategy !== "write-behind") {
727
+ return;
728
+ }
729
+ const flushIntervalMs = options?.flushIntervalMs;
730
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
731
+ return;
732
+ }
733
+ this.disposeWriteBehindTimer();
734
+ this.writeBehindTimer = setInterval(() => {
735
+ void flush();
736
+ }, flushIntervalMs);
737
+ this.writeBehindTimer.unref?.();
738
+ }
739
+ disposeWriteBehindTimer() {
740
+ if (!this.writeBehindTimer) {
741
+ return;
380
742
  }
381
- await Promise.all(
382
- this.options.layers.map(async (layer) => {
383
- if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
384
- return;
385
- }
386
- try {
387
- if (layer.forEachKey) {
388
- await layer.forEachKey(async (key) => {
389
- if (PatternMatcher.matches(pattern, key)) {
390
- matches.add(key);
391
- this.assertWithinMatchLimit(matches, maxMatches);
392
- }
393
- });
394
- return;
395
- }
396
- const keys = await layer.keys?.();
397
- for (const key of keys ?? []) {
398
- if (PatternMatcher.matches(pattern, key)) {
399
- matches.add(key);
400
- this.assertWithinMatchLimit(matches, maxMatches);
401
- }
402
- }
403
- } catch (error) {
404
- await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
405
- }
406
- })
407
- );
408
- return [...matches];
743
+ clearInterval(this.writeBehindTimer);
744
+ this.writeBehindTimer = void 0;
409
745
  }
410
- assertWithinMatchLimit(matches, maxMatches) {
411
- if (maxMatches !== false && matches.size > maxMatches) {
412
- throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
746
+ beginClearEpoch() {
747
+ this.clearEpoch += 1;
748
+ this.keyEpochs.clear();
749
+ this.writeBehindQueue.length = 0;
750
+ }
751
+ currentClearEpoch() {
752
+ return this.clearEpoch;
753
+ }
754
+ currentKeyEpoch(key) {
755
+ return this.keyEpochs.get(key) ?? 0;
756
+ }
757
+ bumpKeyEpochs(keys) {
758
+ for (const key of keys) {
759
+ this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
413
760
  }
414
761
  }
415
- };
416
-
417
- // src/internal/CacheKeySerialization.ts
418
- var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
419
- function normalizeForSerialization(value) {
420
- if (Array.isArray(value)) {
421
- return value.map((entry) => normalizeForSerialization(entry));
762
+ isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
763
+ if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
764
+ return true;
765
+ }
766
+ if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
767
+ return true;
768
+ }
769
+ return false;
422
770
  }
423
- if (value && typeof value === "object") {
424
- return Object.keys(value).sort().reduce((normalized, key) => {
425
- if (DANGEROUS_OBJECT_KEYS.has(key)) {
426
- return normalized;
427
- }
428
- normalized[key] = normalizeForSerialization(value[key]);
429
- return normalized;
430
- }, {});
771
+ async enqueueWriteBehind(operation, options, flushBatch) {
772
+ this.writeBehindQueue.push(operation);
773
+ const batchSize = options?.batchSize ?? 100;
774
+ const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
775
+ if (this.writeBehindQueue.length >= batchSize) {
776
+ await this.flushWriteBehindQueue(options, flushBatch);
777
+ return;
778
+ }
779
+ if (this.writeBehindQueue.length >= maxQueueSize) {
780
+ await this.flushWriteBehindQueue(options, flushBatch);
781
+ }
431
782
  }
432
- return value;
433
- }
434
- function serializeKeyPart(value) {
435
- if (typeof value === "string") {
436
- return `s:${value}`;
783
+ async flushWriteBehindQueue(options, flushBatch) {
784
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
785
+ await this.writeBehindFlushPromise;
786
+ return;
787
+ }
788
+ const batchSize = options?.batchSize ?? 100;
789
+ const batch = this.writeBehindQueue.splice(0, batchSize);
790
+ this.writeBehindFlushPromise = flushBatch(batch);
791
+ try {
792
+ await this.writeBehindFlushPromise;
793
+ } finally {
794
+ this.writeBehindFlushPromise = void 0;
795
+ }
796
+ if (this.writeBehindQueue.length > 0) {
797
+ await this.flushWriteBehindQueue(options, flushBatch);
798
+ }
437
799
  }
438
- if (typeof value === "number") {
439
- return `n:${value}`;
800
+ scheduleGenerationCleanup(generation, task, onError) {
801
+ const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
802
+ onError(generation, error);
803
+ });
804
+ this.generationCleanupPromise = scheduledTask.finally(() => {
805
+ if (this.generationCleanupPromise === scheduledTask) {
806
+ this.generationCleanupPromise = void 0;
807
+ }
808
+ });
440
809
  }
441
- if (typeof value === "boolean") {
442
- return `b:${value}`;
810
+ async waitForGenerationCleanup() {
811
+ await this.generationCleanupPromise;
443
812
  }
444
- return `j:${JSON.stringify(normalizeForSerialization(value))}`;
813
+ };
814
+
815
+ // src/internal/CacheStackRuntimePolicy.ts
816
+ function shouldSkipLayer(degradedUntil, now = Date.now()) {
817
+ return degradedUntil !== void 0 && degradedUntil > now;
445
818
  }
446
- function serializeOptions(options) {
447
- return JSON.stringify(normalizeForSerialization(options) ?? null);
819
+ function shouldStartBackgroundRefresh({
820
+ isDisconnecting,
821
+ hasRefreshInFlight
822
+ }) {
823
+ return !isDisconnecting && !hasRefreshInFlight;
448
824
  }
449
- function createInstanceId() {
450
- if (globalThis.crypto?.randomUUID) {
451
- return globalThis.crypto.randomUUID();
452
- }
453
- const bytes = new Uint8Array(16);
454
- if (globalThis.crypto?.getRandomValues) {
455
- globalThis.crypto.getRandomValues(bytes);
456
- } else {
457
- for (let i = 0; i < bytes.length; i += 1) {
458
- bytes[i] = Math.floor(Math.random() * 256);
459
- }
825
+ function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
826
+ if (!gracefulDegradation) {
827
+ return { degrade: false };
460
828
  }
461
- return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
829
+ const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
830
+ return {
831
+ degrade: true,
832
+ degradedUntil: now + retryAfterMs
833
+ };
834
+ }
835
+ function planFreshReadPolicies({
836
+ stored,
837
+ hasFetcher,
838
+ slidingTtl,
839
+ refreshAheadSeconds
840
+ }) {
841
+ const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
842
+ const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
843
+ const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
844
+ return {
845
+ refreshedStored,
846
+ refreshedStoredTtl,
847
+ shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
848
+ };
462
849
  }
463
850
 
851
+ // src/internal/CacheStackSnapshotManager.ts
852
+ import { constants, promises as fs } from "fs";
853
+ import path from "path";
854
+
464
855
  // src/internal/CacheSnapshotFile.ts
465
- function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
466
- const relative = path.relative(realBaseDir, candidatePath);
467
- return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
856
+ function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
857
+ const relative = path2.relative(realBaseDir, candidatePath);
858
+ return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
468
859
  }
469
- async function findExistingAncestor(directory, fs2, path) {
860
+ async function findExistingAncestor(directory, fs3, path2) {
470
861
  let current = directory;
471
862
  while (true) {
472
863
  try {
473
- await fs2.lstat(current);
864
+ await fs3.lstat(current);
474
865
  return current;
475
866
  } catch (error) {
476
867
  if (error.code !== "ENOENT") {
477
868
  throw error;
478
869
  }
479
870
  }
480
- const parent = path.dirname(current);
871
+ const parent = path2.dirname(current);
481
872
  if (parent === current) {
482
873
  return current;
483
874
  }
@@ -491,39 +882,39 @@ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = p
491
882
  if (filePath.includes("\0")) {
492
883
  throw new Error("filePath must not contain null bytes.");
493
884
  }
494
- const { promises: fs2 } = await import("fs");
495
- const path = await import("path");
496
- const resolved = path.resolve(filePath);
497
- const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
885
+ const { promises: fs3 } = await import("fs");
886
+ const path2 = await import("path");
887
+ const resolved = path2.resolve(filePath);
888
+ const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
498
889
  if (baseDir === false) {
499
890
  return resolved;
500
891
  }
501
- await fs2.mkdir(baseDir, { recursive: true });
502
- const realBaseDir = await fs2.realpath(baseDir);
503
- if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
892
+ await fs3.mkdir(baseDir, { recursive: true });
893
+ const realBaseDir = await fs3.realpath(baseDir);
894
+ if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
504
895
  throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
505
896
  }
506
897
  if (mode === "read") {
507
- const realTarget = await fs2.realpath(resolved);
508
- if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
898
+ const realTarget = await fs3.realpath(resolved);
899
+ if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
509
900
  throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
510
901
  }
511
902
  return realTarget;
512
903
  }
513
- const parentDir = path.dirname(resolved);
514
- const existingAncestor = await findExistingAncestor(parentDir, fs2, path);
515
- const realExistingAncestor = await fs2.realpath(existingAncestor);
516
- if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
904
+ const parentDir = path2.dirname(resolved);
905
+ const existingAncestor = await findExistingAncestor(parentDir, fs3, path2);
906
+ const realExistingAncestor = await fs3.realpath(existingAncestor);
907
+ if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
517
908
  throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
518
909
  }
519
- await fs2.mkdir(parentDir, { recursive: true });
520
- const realParentDir = await fs2.realpath(parentDir);
521
- if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
910
+ await fs3.mkdir(parentDir, { recursive: true });
911
+ const realParentDir = await fs3.realpath(parentDir);
912
+ if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
522
913
  throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
523
914
  }
524
- const targetPath = path.join(realParentDir, path.basename(resolved));
915
+ const targetPath = path2.join(realParentDir, path2.basename(resolved));
525
916
  try {
526
- const existing = await fs2.lstat(targetPath);
917
+ const existing = await fs3.lstat(targetPath);
527
918
  if (existing.isSymbolicLink()) {
528
919
  throw new Error("filePath must not point to a symbolic link.");
529
920
  }
@@ -557,6 +948,144 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
557
948
  return Buffer.concat(chunks).toString("utf8");
558
949
  }
559
950
 
951
+ // src/internal/CacheStackSnapshotManager.ts
952
+ var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
953
+ var CacheStackSnapshotManager = class {
954
+ constructor(options) {
955
+ this.options = options;
956
+ }
957
+ options;
958
+ async exportState(maxEntries) {
959
+ const entries = [];
960
+ await this.visitExportEntries(maxEntries, async (entry) => {
961
+ entries.push(entry);
962
+ });
963
+ return entries;
964
+ }
965
+ async importState(entries) {
966
+ const normalizedEntries = entries.map((entry) => ({
967
+ key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
968
+ value: entry.value,
969
+ ttl: entry.ttl
970
+ }));
971
+ for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
972
+ const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
973
+ await Promise.all(
974
+ batch.map(async (entry) => {
975
+ await Promise.all(this.options.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
976
+ await this.options.tagIndex.touch(entry.key);
977
+ })
978
+ );
979
+ }
980
+ }
981
+ async persistToFile(filePath, snapshotBaseDir, maxEntries) {
982
+ const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
983
+ const tempPath = path.join(
984
+ path.dirname(targetPath),
985
+ `.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
986
+ );
987
+ let handle;
988
+ try {
989
+ handle = await fs.open(tempPath, "wx");
990
+ const openedHandle = handle;
991
+ await openedHandle.writeFile("[", "utf8");
992
+ let wroteAny = false;
993
+ await this.visitExportEntries(maxEntries, async (entry) => {
994
+ await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
995
+ await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
996
+ wroteAny = true;
997
+ });
998
+ await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
999
+ await openedHandle.close();
1000
+ handle = void 0;
1001
+ await fs.rename(tempPath, targetPath);
1002
+ } catch (error) {
1003
+ await handle?.close().catch(() => void 0);
1004
+ await fs.unlink(tempPath).catch(() => void 0);
1005
+ throw error;
1006
+ }
1007
+ }
1008
+ async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
1009
+ const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
1010
+ const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
1011
+ let raw;
1012
+ try {
1013
+ if (maxBytes !== false) {
1014
+ const stat = await handle.stat();
1015
+ if (stat.size > maxBytes) {
1016
+ throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
1017
+ }
1018
+ }
1019
+ raw = await readUtf8HandleWithLimit(handle, maxBytes);
1020
+ } finally {
1021
+ await handle.close();
1022
+ }
1023
+ let parsed;
1024
+ try {
1025
+ parsed = JSON.parse(raw);
1026
+ } catch (cause) {
1027
+ throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
1028
+ }
1029
+ if (!this.isCacheSnapshotEntries(parsed)) {
1030
+ throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
1031
+ }
1032
+ await this.importState(
1033
+ parsed.map((entry) => ({
1034
+ key: entry.key,
1035
+ value: this.sanitizeSnapshotValue(entry.value),
1036
+ ttl: entry.ttl
1037
+ }))
1038
+ );
1039
+ }
1040
+ async visitExportEntries(maxEntries, visitor) {
1041
+ const exported = /* @__PURE__ */ new Set();
1042
+ for (const layer of this.options.layers) {
1043
+ if (!layer.keys && !layer.forEachKey) {
1044
+ continue;
1045
+ }
1046
+ const visitKey = async (key) => {
1047
+ const exportedKey = this.options.stripQualifiedKey(key);
1048
+ if (exported.has(exportedKey)) {
1049
+ return;
1050
+ }
1051
+ const stored = await this.options.readLayerEntry(layer, key);
1052
+ if (stored === null) {
1053
+ return;
1054
+ }
1055
+ exported.add(exportedKey);
1056
+ if (maxEntries !== false && exported.size > maxEntries) {
1057
+ throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
1058
+ }
1059
+ await visitor({
1060
+ key: exportedKey,
1061
+ value: stored,
1062
+ ttl: remainingStoredTtlSeconds(stored)
1063
+ });
1064
+ };
1065
+ if (layer.forEachKey) {
1066
+ await layer.forEachKey(visitKey);
1067
+ continue;
1068
+ }
1069
+ const keys = await layer.keys?.();
1070
+ for (const key of keys ?? []) {
1071
+ await visitKey(key);
1072
+ }
1073
+ }
1074
+ }
1075
+ isCacheSnapshotEntries(value) {
1076
+ return Array.isArray(value) && value.every((entry) => {
1077
+ if (!entry || typeof entry !== "object") {
1078
+ return false;
1079
+ }
1080
+ const candidate = entry;
1081
+ return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
1082
+ });
1083
+ }
1084
+ sanitizeSnapshotValue(value) {
1085
+ return this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
1086
+ }
1087
+ };
1088
+
560
1089
  // src/internal/CacheStackValidation.ts
561
1090
  var MAX_CACHE_KEY_LENGTH = 1024;
562
1091
  var MAX_PATTERN_LENGTH = 1024;
@@ -710,7 +1239,6 @@ var CircuitBreakerManager = class {
710
1239
  if (!options) {
711
1240
  return;
712
1241
  }
713
- this.pruneIfNeeded();
714
1242
  const failureThreshold = options.failureThreshold ?? 3;
715
1243
  const cooldownMs = options.cooldownMs ?? 3e4;
716
1244
  const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
@@ -719,6 +1247,7 @@ var CircuitBreakerManager = class {
719
1247
  state.openUntil = Date.now() + cooldownMs;
720
1248
  }
721
1249
  this.breakers.set(key, state);
1250
+ this.pruneIfNeeded();
722
1251
  }
723
1252
  recordSuccess(key) {
724
1253
  this.breakers.delete(key);
@@ -784,7 +1313,11 @@ var FetchRateLimiter = class {
784
1313
  fetcherBuckets = /* @__PURE__ */ new WeakMap();
785
1314
  nextFetcherBucketId = 0;
786
1315
  drainTimer;
1316
+ isDisposed = false;
787
1317
  async schedule(options, context, task) {
1318
+ if (this.isDisposed) {
1319
+ throw new Error("FetchRateLimiter has been disposed.");
1320
+ }
788
1321
  if (!options) {
789
1322
  return task();
790
1323
  }
@@ -807,6 +1340,27 @@ var FetchRateLimiter = class {
807
1340
  this.drain();
808
1341
  });
809
1342
  }
1343
+ dispose() {
1344
+ this.isDisposed = true;
1345
+ if (this.drainTimer) {
1346
+ clearTimeout(this.drainTimer);
1347
+ this.drainTimer = void 0;
1348
+ }
1349
+ for (const bucket of this.buckets.values()) {
1350
+ if (bucket.cleanupTimer) {
1351
+ clearTimeout(bucket.cleanupTimer);
1352
+ bucket.cleanupTimer = void 0;
1353
+ }
1354
+ }
1355
+ for (const queue of this.queuesByBucket.values()) {
1356
+ for (const item of queue) {
1357
+ item.reject(new Error("FetchRateLimiter has been disposed."));
1358
+ }
1359
+ }
1360
+ this.queuesByBucket.clear();
1361
+ this.pendingBuckets.clear();
1362
+ this.buckets.clear();
1363
+ }
810
1364
  normalize(options) {
811
1365
  const maxConcurrent = options.maxConcurrent;
812
1366
  const intervalMs = options.intervalMs;
@@ -842,6 +1396,9 @@ var FetchRateLimiter = class {
842
1396
  return "global";
843
1397
  }
844
1398
  drain() {
1399
+ if (this.isDisposed) {
1400
+ return;
1401
+ }
845
1402
  if (this.drainTimer) {
846
1403
  clearTimeout(this.drainTimer);
847
1404
  this.drainTimer = void 0;
@@ -938,6 +1495,9 @@ var FetchRateLimiter = class {
938
1495
  }
939
1496
  }
940
1497
  bucketState(bucketKey) {
1498
+ if (this.isDisposed) {
1499
+ throw new Error("FetchRateLimiter has been disposed.");
1500
+ }
941
1501
  const existing = this.buckets.get(bucketKey);
942
1502
  if (existing) {
943
1503
  return existing;
@@ -1173,39 +1733,31 @@ var TtlResolver = class {
1173
1733
  }
1174
1734
  };
1175
1735
 
1176
- // src/serialization/JsonSerializer.ts
1177
- var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1178
- var MAX_SANITIZE_NODES = 1e4;
1179
- var JsonSerializer = class {
1180
- serialize(value) {
1181
- return JSON.stringify(value);
1182
- }
1183
- deserialize(payload) {
1184
- const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
1185
- return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
1186
- }
1187
- };
1188
- var MAX_SANITIZE_DEPTH = 200;
1189
- function sanitizeJsonValue(value, depth, state) {
1736
+ // src/internal/StructuredDataSanitizer.ts
1737
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1738
+ function sanitizeStructuredData(value, options) {
1739
+ return sanitizeValue(value, 0, { count: 0 }, options);
1740
+ }
1741
+ function sanitizeValue(value, depth, state, options) {
1190
1742
  state.count += 1;
1191
- if (state.count > MAX_SANITIZE_NODES) {
1192
- throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
1743
+ if (state.count > options.maxNodes) {
1744
+ throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
1193
1745
  }
1194
- if (depth > MAX_SANITIZE_DEPTH) {
1195
- throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
1746
+ if (depth > options.maxDepth) {
1747
+ throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
1196
1748
  }
1197
1749
  if (Array.isArray(value)) {
1198
- return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
1750
+ return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
1199
1751
  }
1200
1752
  if (!isPlainObject(value)) {
1201
1753
  return value;
1202
1754
  }
1203
- const sanitized = {};
1755
+ const sanitized = options.createObject?.() ?? {};
1204
1756
  for (const [key, entry] of Object.entries(value)) {
1205
- if (DANGEROUS_JSON_KEYS.has(key)) {
1757
+ if (DANGEROUS_KEYS.has(key)) {
1206
1758
  continue;
1207
1759
  }
1208
- sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
1760
+ sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
1209
1761
  }
1210
1762
  return sanitized;
1211
1763
  }
@@ -1213,6 +1765,21 @@ function isPlainObject(value) {
1213
1765
  return Object.prototype.toString.call(value) === "[object Object]";
1214
1766
  }
1215
1767
 
1768
+ // src/serialization/JsonSerializer.ts
1769
+ var JsonSerializer = class {
1770
+ serialize(value) {
1771
+ return JSON.stringify(value);
1772
+ }
1773
+ deserialize(payload) {
1774
+ const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
1775
+ return sanitizeStructuredData(JSON.parse(normalized), {
1776
+ label: "JSON payload",
1777
+ maxDepth: 200,
1778
+ maxNodes: 1e4
1779
+ });
1780
+ }
1781
+ };
1782
+
1216
1783
  // src/stampede/StampedeGuard.ts
1217
1784
  import { Mutex as Mutex2 } from "async-mutex";
1218
1785
  var StampedeGuard = class {
@@ -1257,7 +1824,6 @@ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1257
1824
  var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1258
1825
  var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
1259
1826
  var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
1260
- var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
1261
1827
  var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
1262
1828
  var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
1263
1829
  var DebugLogger = class {
@@ -1314,6 +1880,35 @@ var CacheStack = class extends EventEmitter {
1314
1880
  await this.handleLayerFailure(layer, operation, error);
1315
1881
  }
1316
1882
  });
1883
+ this.invalidation = new CacheStackInvalidationSupport({
1884
+ tagIndex: this.tagIndex,
1885
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
1886
+ handleLayerFailure: async (layer, operation, error) => {
1887
+ await this.handleLayerFailure(layer, operation, error);
1888
+ }
1889
+ });
1890
+ this.layerWriter = new CacheStackLayerWriter({
1891
+ layers: this.layers,
1892
+ maintenance: this.maintenance,
1893
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
1894
+ shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
1895
+ handleLayerFailure: async (layer, operation, error) => {
1896
+ await this.handleLayerFailure(layer, operation, error);
1897
+ },
1898
+ enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
1899
+ resolveFreshTtl: this.resolveFreshTtl.bind(this),
1900
+ resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
1901
+ globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
1902
+ globalStaleIfError: this.options.staleIfError,
1903
+ writePolicy: this.options.writePolicy,
1904
+ onWriteFailures: (context, failures) => {
1905
+ this.metricsCollector.increment("writeFailures", failures.length);
1906
+ this.logger.debug?.("write-failure", {
1907
+ ...context,
1908
+ failures: failures.map((failure) => this.formatError(failure))
1909
+ });
1910
+ }
1911
+ });
1317
1912
  if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
1318
1913
  this.logger.warn?.(
1319
1914
  "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."
@@ -1329,6 +1924,16 @@ var CacheStack = class extends EventEmitter {
1329
1924
  "broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
1330
1925
  );
1331
1926
  }
1927
+ this.snapshots = new CacheStackSnapshotManager({
1928
+ layers: this.layers,
1929
+ tagIndex: this.tagIndex,
1930
+ snapshotSerializer: this.snapshotSerializer,
1931
+ readLayerEntry: this.readLayerEntry.bind(this),
1932
+ qualifyKey: this.qualifyKey.bind(this),
1933
+ stripQualifiedKey: this.stripQualifiedKey.bind(this),
1934
+ validateCacheKey,
1935
+ formatError: this.formatError.bind(this)
1936
+ });
1332
1937
  this.initializeWriteBehind(options.writeBehind);
1333
1938
  this.startup = this.initialize();
1334
1939
  }
@@ -1344,17 +1949,16 @@ var CacheStack = class extends EventEmitter {
1344
1949
  keyDiscovery;
1345
1950
  fetchRateLimiter = new FetchRateLimiter();
1346
1951
  snapshotSerializer = new JsonSerializer();
1952
+ invalidation;
1953
+ layerWriter;
1954
+ snapshots;
1347
1955
  backgroundRefreshes = /* @__PURE__ */ new Map();
1348
1956
  layerDegradedUntil = /* @__PURE__ */ new Map();
1349
- keyEpochs = /* @__PURE__ */ new Map();
1957
+ maintenance = new CacheStackMaintenance();
1350
1958
  ttlResolver;
1351
1959
  circuitBreakerManager;
1960
+ nextOperationId = 0;
1352
1961
  currentGeneration;
1353
- writeBehindQueue = [];
1354
- writeBehindTimer;
1355
- writeBehindFlushPromise;
1356
- generationCleanupPromise;
1357
- clearEpoch = 0;
1358
1962
  isDisconnecting = false;
1359
1963
  disconnectPromise;
1360
1964
  /**
@@ -1364,10 +1968,12 @@ var CacheStack = class extends EventEmitter {
1364
1968
  * and no `fetcher` is provided.
1365
1969
  */
1366
1970
  async get(key, fetcher, options) {
1367
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
1368
- this.validateWriteOptions(options);
1369
- await this.awaitStartup("get");
1370
- return this.getPrepared(normalizedKey, fetcher, options);
1971
+ return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
1972
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
1973
+ this.validateWriteOptions(options);
1974
+ await this.awaitStartup("get");
1975
+ return this.getPrepared(normalizedKey, fetcher, options);
1976
+ });
1371
1977
  }
1372
1978
  async getPrepared(normalizedKey, fetcher, options) {
1373
1979
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
@@ -1489,28 +2095,32 @@ var CacheStack = class extends EventEmitter {
1489
2095
  * Stores a value in all cache layers. Overwrites any existing value.
1490
2096
  */
1491
2097
  async set(key, value, options) {
1492
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
1493
- this.validateWriteOptions(options);
1494
- await this.awaitStartup("set");
1495
- await this.storeEntry(normalizedKey, "value", value, options);
2098
+ await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
2099
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2100
+ this.validateWriteOptions(options);
2101
+ await this.awaitStartup("set");
2102
+ await this.storeEntry(normalizedKey, "value", value, options);
2103
+ });
1496
2104
  }
1497
2105
  /**
1498
2106
  * Deletes the key from all layers and publishes an invalidation message.
1499
2107
  */
1500
2108
  async delete(key) {
1501
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
1502
- await this.awaitStartup("delete");
1503
- await this.deleteKeys([normalizedKey]);
1504
- await this.publishInvalidation({
1505
- scope: "key",
1506
- keys: [normalizedKey],
1507
- sourceId: this.instanceId,
1508
- operation: "delete"
2109
+ await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
2110
+ const normalizedKey = this.qualifyKey(validateCacheKey(key));
2111
+ await this.awaitStartup("delete");
2112
+ await this.deleteKeys([normalizedKey]);
2113
+ await this.publishInvalidation({
2114
+ scope: "key",
2115
+ keys: [normalizedKey],
2116
+ sourceId: this.instanceId,
2117
+ operation: "delete"
2118
+ });
1509
2119
  });
1510
2120
  }
1511
2121
  async clear() {
1512
2122
  await this.awaitStartup("clear");
1513
- this.beginClearEpoch();
2123
+ this.maintenance.beginClearEpoch();
1514
2124
  await Promise.all(this.layers.map((layer) => layer.clear()));
1515
2125
  await this.tagIndex.clear();
1516
2126
  this.ttlResolver.clearProfiles();
@@ -1538,95 +2148,99 @@ var CacheStack = class extends EventEmitter {
1538
2148
  });
1539
2149
  }
1540
2150
  async mget(entries) {
1541
- this.assertActive("mget");
1542
- if (entries.length === 0) {
1543
- return [];
1544
- }
1545
- const normalizedEntries = entries.map((entry) => ({
1546
- ...entry,
1547
- key: this.qualifyKey(validateCacheKey(entry.key))
1548
- }));
1549
- normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1550
- const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
1551
- if (!canFastPath) {
2151
+ return this.observeOperation("layercache.mget", void 0, async () => {
2152
+ this.assertActive("mget");
2153
+ if (entries.length === 0) {
2154
+ return [];
2155
+ }
2156
+ const normalizedEntries = entries.map((entry) => ({
2157
+ ...entry,
2158
+ key: this.qualifyKey(validateCacheKey(entry.key))
2159
+ }));
2160
+ normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2161
+ const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
2162
+ if (!canFastPath) {
2163
+ await this.awaitStartup("mget");
2164
+ const pendingReads = /* @__PURE__ */ new Map();
2165
+ return Promise.all(
2166
+ normalizedEntries.map((entry) => {
2167
+ const optionsSignature = serializeOptions(entry.options);
2168
+ const existing = pendingReads.get(entry.key);
2169
+ if (!existing) {
2170
+ const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
2171
+ pendingReads.set(entry.key, {
2172
+ promise,
2173
+ fetch: entry.fetch,
2174
+ optionsSignature
2175
+ });
2176
+ return promise;
2177
+ }
2178
+ if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
2179
+ throw new Error(`mget received conflicting entries for key "${entry.key}".`);
2180
+ }
2181
+ return existing.promise;
2182
+ })
2183
+ );
2184
+ }
1552
2185
  await this.awaitStartup("mget");
1553
- const pendingReads = /* @__PURE__ */ new Map();
1554
- return Promise.all(
1555
- normalizedEntries.map((entry) => {
1556
- const optionsSignature = serializeOptions(entry.options);
1557
- const existing = pendingReads.get(entry.key);
1558
- if (!existing) {
1559
- const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
1560
- pendingReads.set(entry.key, {
1561
- promise,
1562
- fetch: entry.fetch,
1563
- optionsSignature
1564
- });
1565
- return promise;
1566
- }
1567
- if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
1568
- throw new Error(`mget received conflicting entries for key "${entry.key}".`);
1569
- }
1570
- return existing.promise;
1571
- })
1572
- );
1573
- }
1574
- await this.awaitStartup("mget");
1575
- const pending = /* @__PURE__ */ new Set();
1576
- const indexesByKey = /* @__PURE__ */ new Map();
1577
- const resultsByKey = /* @__PURE__ */ new Map();
1578
- for (let index = 0; index < normalizedEntries.length; index += 1) {
1579
- const entry = normalizedEntries[index];
1580
- if (!entry) continue;
1581
- const key = entry.key;
1582
- const indexes = indexesByKey.get(key) ?? [];
1583
- indexes.push(index);
1584
- indexesByKey.set(key, indexes);
1585
- pending.add(key);
1586
- }
1587
- for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
1588
- const layer = this.layers[layerIndex];
1589
- if (!layer) continue;
1590
- const keys = [...pending];
1591
- if (keys.length === 0) {
1592
- break;
2186
+ const pending = /* @__PURE__ */ new Set();
2187
+ const indexesByKey = /* @__PURE__ */ new Map();
2188
+ const resultsByKey = /* @__PURE__ */ new Map();
2189
+ for (let index = 0; index < normalizedEntries.length; index += 1) {
2190
+ const entry = normalizedEntries[index];
2191
+ if (!entry) continue;
2192
+ const key = entry.key;
2193
+ const indexes = indexesByKey.get(key) ?? [];
2194
+ indexes.push(index);
2195
+ indexesByKey.set(key, indexes);
2196
+ pending.add(key);
1593
2197
  }
1594
- const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
1595
- for (let offset = 0; offset < values.length; offset += 1) {
1596
- const key = keys[offset];
1597
- const stored = values[offset];
1598
- if (!key || stored === null) {
1599
- continue;
2198
+ for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
2199
+ const layer = this.layers[layerIndex];
2200
+ if (!layer) continue;
2201
+ const keys = [...pending];
2202
+ if (keys.length === 0) {
2203
+ break;
1600
2204
  }
1601
- const resolved = resolveStoredValue(stored);
1602
- if (resolved.state === "expired") {
1603
- await layer.delete(key);
1604
- continue;
2205
+ const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
2206
+ for (let offset = 0; offset < values.length; offset += 1) {
2207
+ const key = keys[offset];
2208
+ const stored = values[offset];
2209
+ if (!key || stored === null) {
2210
+ continue;
2211
+ }
2212
+ const resolved = resolveStoredValue(stored);
2213
+ if (resolved.state === "expired") {
2214
+ await layer.delete(key);
2215
+ continue;
2216
+ }
2217
+ await this.tagIndex.touch(key);
2218
+ await this.backfill(key, stored, layerIndex - 1);
2219
+ resultsByKey.set(key, resolved.value);
2220
+ pending.delete(key);
2221
+ this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
1605
2222
  }
1606
- await this.tagIndex.touch(key);
1607
- await this.backfill(key, stored, layerIndex - 1);
1608
- resultsByKey.set(key, resolved.value);
1609
- pending.delete(key);
1610
- this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
1611
2223
  }
1612
- }
1613
- if (pending.size > 0) {
1614
- for (const key of pending) {
1615
- await this.tagIndex.remove(key);
1616
- this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
2224
+ if (pending.size > 0) {
2225
+ for (const key of pending) {
2226
+ await this.tagIndex.remove(key);
2227
+ this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
2228
+ }
1617
2229
  }
1618
- }
1619
- return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2230
+ return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2231
+ });
1620
2232
  }
1621
2233
  async mset(entries) {
1622
- this.assertActive("mset");
1623
- const normalizedEntries = entries.map((entry) => ({
1624
- ...entry,
1625
- key: this.qualifyKey(validateCacheKey(entry.key))
1626
- }));
1627
- normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1628
- await this.awaitStartup("mset");
1629
- await this.writeBatch(normalizedEntries);
2234
+ await this.observeOperation("layercache.mset", void 0, async () => {
2235
+ this.assertActive("mset");
2236
+ const normalizedEntries = entries.map((entry) => ({
2237
+ ...entry,
2238
+ key: this.qualifyKey(validateCacheKey(entry.key))
2239
+ }));
2240
+ normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2241
+ await this.awaitStartup("mset");
2242
+ await this.writeBatch(normalizedEntries);
2243
+ });
1630
2244
  }
1631
2245
  async warm(entries, options = {}) {
1632
2246
  this.assertActive("warm");
@@ -1679,40 +2293,50 @@ var CacheStack = class extends EventEmitter {
1679
2293
  return new CacheNamespace(this, prefix);
1680
2294
  }
1681
2295
  async invalidateByTag(tag) {
1682
- validateTag(tag);
1683
- await this.awaitStartup("invalidateByTag");
1684
- const keys = await this.collectKeysForTag(tag);
1685
- await this.deleteKeys(keys);
1686
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2296
+ await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
2297
+ validateTag(tag);
2298
+ await this.awaitStartup("invalidateByTag");
2299
+ const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
2300
+ await this.deleteKeys(keys);
2301
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2302
+ });
1687
2303
  }
1688
2304
  async invalidateByTags(tags, mode = "any") {
1689
- if (tags.length === 0) {
1690
- return;
1691
- }
1692
- validateTags(tags);
1693
- await this.awaitStartup("invalidateByTags");
1694
- const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
1695
- const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
1696
- this.assertWithinInvalidationKeyLimit(keys.length);
1697
- await this.deleteKeys(keys);
1698
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2305
+ await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
2306
+ if (tags.length === 0) {
2307
+ return;
2308
+ }
2309
+ validateTags(tags);
2310
+ await this.awaitStartup("invalidateByTags");
2311
+ const keysByTag = await Promise.all(
2312
+ tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
2313
+ );
2314
+ const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
2315
+ this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
2316
+ await this.deleteKeys(keys);
2317
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2318
+ });
1699
2319
  }
1700
2320
  async invalidateByPattern(pattern) {
1701
- validatePattern(pattern);
1702
- await this.awaitStartup("invalidateByPattern");
1703
- const keys = await this.keyDiscovery.collectKeysMatchingPattern(
1704
- this.qualifyPattern(pattern),
1705
- this.invalidationMaxKeys()
1706
- );
1707
- await this.deleteKeys(keys);
1708
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2321
+ await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
2322
+ validatePattern(pattern);
2323
+ await this.awaitStartup("invalidateByPattern");
2324
+ const keys = await this.keyDiscovery.collectKeysMatchingPattern(
2325
+ this.qualifyPattern(pattern),
2326
+ this.invalidationMaxKeys()
2327
+ );
2328
+ await this.deleteKeys(keys);
2329
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2330
+ });
1709
2331
  }
1710
2332
  async invalidateByPrefix(prefix) {
1711
- await this.awaitStartup("invalidateByPrefix");
1712
- const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
1713
- const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
1714
- await this.deleteKeys(keys);
1715
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2333
+ await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
2334
+ await this.awaitStartup("invalidateByPrefix");
2335
+ const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
2336
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
2337
+ await this.deleteKeys(keys);
2338
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2339
+ });
1716
2340
  }
1717
2341
  getMetrics() {
1718
2342
  return this.metricsCollector.snapshot;
@@ -1768,9 +2392,15 @@ var CacheStack = class extends EventEmitter {
1768
2392
  bumpGeneration(nextGeneration) {
1769
2393
  const current = this.currentGeneration ?? 0;
1770
2394
  const previousGeneration = this.currentGeneration;
1771
- this.currentGeneration = nextGeneration ?? current + 1;
1772
- if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
1773
- this.scheduleGenerationCleanup(previousGeneration);
2395
+ const updatedGeneration = nextGeneration ?? current + 1;
2396
+ const generationToCleanup = resolveGenerationCleanupTarget({
2397
+ previousGeneration,
2398
+ nextGeneration: updatedGeneration,
2399
+ generationCleanup: this.options.generationCleanup
2400
+ });
2401
+ this.currentGeneration = updatedGeneration;
2402
+ if (generationToCleanup !== null) {
2403
+ this.scheduleGenerationCleanup(generationToCleanup);
1774
2404
  }
1775
2405
  return this.currentGeneration;
1776
2406
  }
@@ -1817,95 +2447,19 @@ var CacheStack = class extends EventEmitter {
1817
2447
  }
1818
2448
  async exportState() {
1819
2449
  await this.awaitStartup("exportState");
1820
- const entries = [];
1821
- await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
1822
- entries.push(entry);
1823
- });
1824
- return entries;
2450
+ return this.snapshots.exportState(this.snapshotMaxEntries());
1825
2451
  }
1826
2452
  async importState(entries) {
1827
2453
  await this.awaitStartup("importState");
1828
- const normalizedEntries = entries.map((entry) => ({
1829
- key: this.qualifyKey(validateCacheKey(entry.key)),
1830
- value: entry.value,
1831
- ttl: entry.ttl
1832
- }));
1833
- for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
1834
- const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
1835
- await Promise.all(
1836
- batch.map(async (entry) => {
1837
- await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
1838
- await this.tagIndex.touch(entry.key);
1839
- })
1840
- );
1841
- }
2454
+ await this.snapshots.importState(entries);
1842
2455
  }
1843
2456
  async persistToFile(filePath) {
1844
2457
  this.assertActive("persistToFile");
1845
- const { promises: fs2 } = await import("fs");
1846
- const path = await import("path");
1847
- const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
1848
- const tempPath = path.join(
1849
- path.dirname(targetPath),
1850
- `.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
1851
- );
1852
- let handle;
1853
- try {
1854
- handle = await fs2.open(tempPath, "wx");
1855
- const openedHandle = handle;
1856
- await openedHandle.writeFile("[", "utf8");
1857
- let wroteAny = false;
1858
- await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
1859
- await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
1860
- await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
1861
- wroteAny = true;
1862
- });
1863
- await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
1864
- await openedHandle.close();
1865
- handle = void 0;
1866
- await fs2.rename(tempPath, targetPath);
1867
- } catch (error) {
1868
- await handle?.close().catch(() => void 0);
1869
- await fs2.unlink(tempPath).catch(() => void 0);
1870
- throw error;
1871
- }
2458
+ await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
1872
2459
  }
1873
2460
  async restoreFromFile(filePath) {
1874
2461
  this.assertActive("restoreFromFile");
1875
- const { promises: fs2, constants } = await import("fs");
1876
- const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
1877
- const handle = await fs2.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
1878
- const snapshotMaxBytes = this.snapshotMaxBytes();
1879
- let raw;
1880
- try {
1881
- if (snapshotMaxBytes !== false) {
1882
- const stat = await handle.stat();
1883
- if (stat.size > snapshotMaxBytes) {
1884
- throw new Error(
1885
- `Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
1886
- );
1887
- }
1888
- }
1889
- raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
1890
- } finally {
1891
- await handle.close();
1892
- }
1893
- let parsed;
1894
- try {
1895
- parsed = JSON.parse(raw);
1896
- } catch (cause) {
1897
- throw new Error(`Invalid snapshot file: could not parse JSON (${this.formatError(cause)})`);
1898
- }
1899
- if (!this.isCacheSnapshotEntries(parsed)) {
1900
- throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
1901
- }
1902
- await this.importState(
1903
- parsed.map((entry) => ({
1904
- key: entry.key,
1905
- value: this.sanitizeSnapshotValue(entry.value),
1906
- ttl: entry.ttl
1907
- }))
1908
- );
2462
+ await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
1909
2463
  }
1910
2464
  async disconnect() {
1911
2465
  if (!this.disconnectPromise) {
@@ -1914,12 +2468,10 @@ var CacheStack = class extends EventEmitter {
1914
2468
  await this.startup;
1915
2469
  await this.unsubscribeInvalidation?.();
1916
2470
  await this.flushWriteBehindQueue();
1917
- await this.generationCleanupPromise;
2471
+ await this.maintenance.waitForGenerationCleanup();
1918
2472
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
1919
- if (this.writeBehindTimer) {
1920
- clearInterval(this.writeBehindTimer);
1921
- this.writeBehindTimer = void 0;
1922
- }
2473
+ this.maintenance.disposeWriteBehindTimer();
2474
+ this.fetchRateLimiter.dispose();
1923
2475
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
1924
2476
  })();
1925
2477
  }
@@ -1995,13 +2547,13 @@ var CacheStack = class extends EventEmitter {
1995
2547
  if (!this.shouldNegativeCache(options)) {
1996
2548
  return null;
1997
2549
  }
1998
- if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2550
+ if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
1999
2551
  this.logger.debug?.("skip-negative-store-after-invalidation", {
2000
2552
  key,
2001
2553
  expectedClearEpoch,
2002
- clearEpoch: this.clearEpoch,
2554
+ clearEpoch: this.maintenance.currentClearEpoch(),
2003
2555
  expectedKeyEpoch,
2004
- keyEpoch: this.currentKeyEpoch(key)
2556
+ keyEpoch: this.maintenance.currentKeyEpoch(key)
2005
2557
  });
2006
2558
  return null;
2007
2559
  }
@@ -2017,13 +2569,13 @@ var CacheStack = class extends EventEmitter {
2017
2569
  this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
2018
2570
  }
2019
2571
  }
2020
- if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2572
+ if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2021
2573
  this.logger.debug?.("skip-store-after-invalidation", {
2022
2574
  key,
2023
2575
  expectedClearEpoch,
2024
- clearEpoch: this.clearEpoch,
2576
+ clearEpoch: this.maintenance.currentClearEpoch(),
2025
2577
  expectedKeyEpoch,
2026
- keyEpoch: this.currentKeyEpoch(key)
2578
+ keyEpoch: this.maintenance.currentKeyEpoch(key)
2027
2579
  });
2028
2580
  return fetched;
2029
2581
  }
@@ -2031,10 +2583,10 @@ var CacheStack = class extends EventEmitter {
2031
2583
  return fetched;
2032
2584
  }
2033
2585
  async storeEntry(key, kind, value, options) {
2034
- const clearEpoch = this.clearEpoch;
2035
- const keyEpoch = this.currentKeyEpoch(key);
2036
- await this.writeAcrossLayers(key, kind, value, options);
2037
- if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2586
+ const clearEpoch = this.maintenance.currentClearEpoch();
2587
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2588
+ await this.layerWriter.writeAcrossLayers(key, kind, value, options);
2589
+ if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2038
2590
  return;
2039
2591
  }
2040
2592
  if (options?.tags) {
@@ -2050,57 +2602,12 @@ var CacheStack = class extends EventEmitter {
2050
2602
  }
2051
2603
  }
2052
2604
  async writeBatch(entries) {
2053
- const now = Date.now();
2054
- const clearEpoch = this.clearEpoch;
2055
- const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
2056
- const entriesByLayer = /* @__PURE__ */ new Map();
2057
- const immediateOperations = [];
2058
- const deferredOperations = [];
2059
- for (const entry of entries) {
2060
- for (const layer of this.layers) {
2061
- if (this.shouldSkipLayer(layer)) {
2062
- continue;
2063
- }
2064
- const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
2065
- const bucket = entriesByLayer.get(layer) ?? [];
2066
- bucket.push(layerEntry);
2067
- entriesByLayer.set(layer, bucket);
2068
- }
2069
- }
2070
- for (const [layer, layerEntries] of entriesByLayer.entries()) {
2071
- const operation = async () => {
2072
- if (clearEpoch !== this.clearEpoch) {
2073
- return;
2074
- }
2075
- const activeEntries = layerEntries.filter(
2076
- (entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
2077
- );
2078
- if (activeEntries.length === 0) {
2079
- return;
2080
- }
2081
- try {
2082
- if (layer.setMany) {
2083
- await layer.setMany(activeEntries);
2084
- return;
2085
- }
2086
- await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
2087
- } catch (error) {
2088
- await this.handleLayerFailure(layer, "write", error);
2089
- }
2090
- };
2091
- if (this.shouldWriteBehind(layer)) {
2092
- deferredOperations.push(operation);
2093
- } else {
2094
- immediateOperations.push(operation);
2095
- }
2096
- }
2097
- await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
2098
- await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
2099
- if (clearEpoch !== this.clearEpoch) {
2605
+ const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
2606
+ if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2100
2607
  return;
2101
2608
  }
2102
2609
  for (const entry of entries) {
2103
- if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2610
+ if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2104
2611
  continue;
2105
2612
  }
2106
2613
  if (entry.options?.tags) {
@@ -2181,77 +2688,25 @@ var CacheStack = class extends EventEmitter {
2181
2688
  return this.handleLayerFailure(layer, "read", error);
2182
2689
  }
2183
2690
  }
2184
- async backfill(key, stored, upToIndex, options) {
2185
- if (upToIndex < 0) {
2186
- return;
2187
- }
2188
- for (let index = 0; index <= upToIndex; index += 1) {
2189
- const layer = this.layers[index];
2190
- if (!layer || this.shouldSkipLayer(layer)) {
2191
- continue;
2192
- }
2193
- const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
2194
- try {
2195
- await layer.set(key, stored, ttl);
2196
- } catch (error) {
2197
- await this.handleLayerFailure(layer, "backfill", error);
2198
- continue;
2199
- }
2200
- this.metricsCollector.increment("backfills");
2201
- this.logger.debug?.("backfill", { key, layer: layer.name });
2202
- this.emit("backfill", { key, layer: layer.name });
2203
- }
2204
- }
2205
- async writeAcrossLayers(key, kind, value, options) {
2206
- const now = Date.now();
2207
- const clearEpoch = this.clearEpoch;
2208
- const keyEpoch = this.currentKeyEpoch(key);
2209
- const immediateOperations = [];
2210
- const deferredOperations = [];
2211
- for (const layer of this.layers) {
2212
- const operation = async () => {
2213
- if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2214
- return;
2215
- }
2216
- if (this.shouldSkipLayer(layer)) {
2217
- return;
2218
- }
2219
- const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
2220
- try {
2221
- await layer.set(entry.key, entry.value, entry.ttl);
2222
- } catch (error) {
2223
- await this.handleLayerFailure(layer, "write", error);
2224
- }
2225
- };
2226
- if (this.shouldWriteBehind(layer)) {
2227
- deferredOperations.push(operation);
2228
- } else {
2229
- immediateOperations.push(operation);
2230
- }
2231
- }
2232
- await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
2233
- await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
2234
- }
2235
- async executeLayerOperations(operations, context) {
2236
- if (this.options.writePolicy !== "best-effort") {
2237
- await Promise.all(operations.map((operation) => operation()));
2238
- return;
2239
- }
2240
- const results = await Promise.allSettled(operations.map((operation) => operation()));
2241
- const failures = results.filter((result) => result.status === "rejected");
2242
- if (failures.length === 0) {
2691
+ async backfill(key, stored, upToIndex, options) {
2692
+ if (upToIndex < 0) {
2243
2693
  return;
2244
2694
  }
2245
- this.metricsCollector.increment("writeFailures", failures.length);
2246
- this.logger.debug?.("write-failure", {
2247
- ...context,
2248
- failures: failures.map((failure) => this.formatError(failure.reason))
2249
- });
2250
- if (failures.length === operations.length) {
2251
- throw new AggregateError(
2252
- failures.map((failure) => failure.reason),
2253
- `${context.action} failed for every cache layer`
2254
- );
2695
+ for (let index = 0; index <= upToIndex; index += 1) {
2696
+ const layer = this.layers[index];
2697
+ if (!layer || this.shouldSkipLayer(layer)) {
2698
+ continue;
2699
+ }
2700
+ const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
2701
+ try {
2702
+ await layer.set(key, stored, ttl);
2703
+ } catch (error) {
2704
+ await this.handleLayerFailure(layer, "backfill", error);
2705
+ continue;
2706
+ }
2707
+ this.metricsCollector.increment("backfills");
2708
+ this.logger.debug?.("backfill", { key, layer: layer.name });
2709
+ this.emit("backfill", { key, layer: layer.name });
2255
2710
  }
2256
2711
  }
2257
2712
  resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
@@ -2273,11 +2728,14 @@ var CacheStack = class extends EventEmitter {
2273
2728
  return options?.negativeCache ?? this.options.negativeCaching ?? false;
2274
2729
  }
2275
2730
  scheduleBackgroundRefresh(key, fetcher, options) {
2276
- if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
2731
+ if (!shouldStartBackgroundRefresh({
2732
+ isDisconnecting: this.isDisconnecting,
2733
+ hasRefreshInFlight: this.backgroundRefreshes.has(key)
2734
+ })) {
2277
2735
  return;
2278
2736
  }
2279
- const clearEpoch = this.clearEpoch;
2280
- const keyEpoch = this.currentKeyEpoch(key);
2737
+ const clearEpoch = this.maintenance.currentClearEpoch();
2738
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2281
2739
  const refresh = (async () => {
2282
2740
  this.metricsCollector.increment("refreshes");
2283
2741
  try {
@@ -2315,8 +2773,8 @@ var CacheStack = class extends EventEmitter {
2315
2773
  if (keys.length === 0) {
2316
2774
  return;
2317
2775
  }
2318
- this.bumpKeyEpochs(keys);
2319
- await this.deleteKeysFromLayers(this.layers, keys);
2776
+ this.maintenance.bumpKeyEpochs(keys);
2777
+ await this.invalidation.deleteKeysFromLayers(this.layers, keys);
2320
2778
  for (const key of keys) {
2321
2779
  await this.tagIndex.remove(key);
2322
2780
  this.ttlResolver.deleteProfile(key);
@@ -2339,7 +2797,7 @@ var CacheStack = class extends EventEmitter {
2339
2797
  }
2340
2798
  const localLayers = this.layers.filter((layer) => layer.isLocal);
2341
2799
  if (message.scope === "clear") {
2342
- this.beginClearEpoch();
2800
+ this.maintenance.beginClearEpoch();
2343
2801
  await Promise.all(localLayers.map((layer) => layer.clear()));
2344
2802
  await this.tagIndex.clear();
2345
2803
  this.ttlResolver.clearProfiles();
@@ -2347,8 +2805,8 @@ var CacheStack = class extends EventEmitter {
2347
2805
  return;
2348
2806
  }
2349
2807
  const keys = message.keys ?? [];
2350
- this.bumpKeyEpochs(keys);
2351
- await this.deleteKeysFromLayers(localLayers, keys);
2808
+ this.maintenance.bumpKeyEpochs(keys);
2809
+ await this.invalidation.deleteKeysFromLayers(localLayers, keys);
2352
2810
  if (message.operation !== "write") {
2353
2811
  for (const key of keys) {
2354
2812
  await this.tagIndex.remove(key);
@@ -2405,35 +2863,47 @@ var CacheStack = class extends EventEmitter {
2405
2863
  shouldBroadcastL1Invalidation() {
2406
2864
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
2407
2865
  }
2408
- shouldCleanupGenerations() {
2409
- return Boolean(this.options.generationCleanup);
2410
- }
2411
- generationCleanupBatchSize() {
2412
- const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
2413
- return configured ?? 500;
2866
+ async observeOperation(name, attributes, execute) {
2867
+ const id = this.nextOperationId;
2868
+ this.nextOperationId += 1;
2869
+ this.emit("operation-start", { id, name, attributes });
2870
+ try {
2871
+ const result = await execute();
2872
+ this.emit("operation-end", {
2873
+ id,
2874
+ name,
2875
+ attributes,
2876
+ success: true,
2877
+ result: result === null ? "null" : void 0
2878
+ });
2879
+ return result;
2880
+ } catch (error) {
2881
+ this.emit("operation-end", {
2882
+ id,
2883
+ name,
2884
+ attributes,
2885
+ success: false,
2886
+ error
2887
+ });
2888
+ throw error;
2889
+ }
2414
2890
  }
2415
2891
  scheduleGenerationCleanup(generation) {
2416
- const task = (this.generationCleanupPromise ?? Promise.resolve()).then(() => this.cleanupGeneration(generation)).catch((error) => {
2417
- this.logger.warn?.("generation-cleanup-error", {
2418
- generation,
2419
- error: this.formatError(error)
2420
- });
2421
- });
2422
- this.generationCleanupPromise = task.finally(() => {
2423
- if (this.generationCleanupPromise === task) {
2424
- this.generationCleanupPromise = void 0;
2892
+ this.maintenance.scheduleGenerationCleanup(
2893
+ generation,
2894
+ async (generationToClean) => this.cleanupGeneration(generationToClean),
2895
+ (failedGeneration, error) => {
2896
+ this.logger.warn?.("generation-cleanup-error", {
2897
+ generation: failedGeneration,
2898
+ error: this.formatError(error)
2899
+ });
2425
2900
  }
2426
- });
2901
+ );
2427
2902
  }
2428
2903
  async cleanupGeneration(generation) {
2429
2904
  const prefix = `v${generation}:`;
2430
2905
  const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
2431
- if (keys.length === 0) {
2432
- return;
2433
- }
2434
- const batchSize = this.generationCleanupBatchSize();
2435
- for (let index = 0; index < keys.length; index += batchSize) {
2436
- const batch = keys.slice(index, index + batchSize);
2906
+ for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
2437
2907
  await this.deleteKeys(batch);
2438
2908
  await this.publishInvalidation({
2439
2909
  scope: "keys",
@@ -2444,161 +2914,43 @@ var CacheStack = class extends EventEmitter {
2444
2914
  }
2445
2915
  }
2446
2916
  initializeWriteBehind(options) {
2447
- if (this.options.writeStrategy !== "write-behind") {
2448
- return;
2449
- }
2450
- const flushIntervalMs = options?.flushIntervalMs;
2451
- if (!flushIntervalMs || flushIntervalMs <= 0) {
2452
- return;
2453
- }
2454
- this.writeBehindTimer = setInterval(() => {
2455
- void this.flushWriteBehindQueue();
2456
- }, flushIntervalMs);
2457
- this.writeBehindTimer.unref?.();
2917
+ this.maintenance.initializeWriteBehindTimer(
2918
+ this.options.writeStrategy,
2919
+ options,
2920
+ this.flushWriteBehindQueue.bind(this)
2921
+ );
2458
2922
  }
2459
2923
  shouldWriteBehind(layer) {
2460
2924
  return this.options.writeStrategy === "write-behind" && !layer.isLocal;
2461
2925
  }
2462
- beginClearEpoch() {
2463
- this.clearEpoch += 1;
2464
- this.keyEpochs.clear();
2465
- this.writeBehindQueue.length = 0;
2466
- }
2467
- currentKeyEpoch(key) {
2468
- return this.keyEpochs.get(key) ?? 0;
2469
- }
2470
- bumpKeyEpochs(keys) {
2471
- for (const key of keys) {
2472
- this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
2473
- }
2474
- }
2475
- isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
2476
- if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
2477
- return true;
2478
- }
2479
- if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
2480
- return true;
2481
- }
2482
- return false;
2483
- }
2484
2926
  async enqueueWriteBehind(operation) {
2485
- this.writeBehindQueue.push(operation);
2486
- const batchSize = this.options.writeBehind?.batchSize ?? 100;
2487
- const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
2488
- if (this.writeBehindQueue.length >= batchSize) {
2489
- await this.flushWriteBehindQueue();
2490
- return;
2491
- }
2492
- if (this.writeBehindQueue.length >= maxQueueSize) {
2493
- await this.flushWriteBehindQueue();
2494
- }
2927
+ await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
2495
2928
  }
2496
2929
  async flushWriteBehindQueue() {
2497
- if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
2498
- await this.writeBehindFlushPromise;
2930
+ await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
2931
+ }
2932
+ async runWriteBehindBatch(batch) {
2933
+ const results = await Promise.allSettled(batch.map((operation) => operation()));
2934
+ const failures = results.filter((result) => result.status === "rejected");
2935
+ if (failures.length === 0) {
2499
2936
  return;
2500
2937
  }
2501
- const batchSize = this.options.writeBehind?.batchSize ?? 100;
2502
- const batch = this.writeBehindQueue.splice(0, batchSize);
2503
- this.writeBehindFlushPromise = (async () => {
2504
- const results = await Promise.allSettled(batch.map((operation) => operation()));
2505
- const failures = results.filter((result) => result.status === "rejected");
2506
- if (failures.length > 0) {
2507
- this.metricsCollector.increment("writeFailures", failures.length);
2508
- this.logger.error?.("write-behind-flush-failure", {
2509
- failed: failures.length,
2510
- total: batch.length,
2511
- errors: failures.map((failure) => this.formatError(failure.reason))
2512
- });
2513
- this.emitError("write-behind", { failed: failures.length, total: batch.length });
2514
- }
2515
- })();
2516
- await this.writeBehindFlushPromise;
2517
- this.writeBehindFlushPromise = void 0;
2518
- if (this.writeBehindQueue.length > 0) {
2519
- await this.flushWriteBehindQueue();
2520
- }
2521
- }
2522
- buildLayerSetEntry(layer, key, kind, value, options, now) {
2523
- const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
2524
- const staleWhileRevalidate = this.resolveLayerSeconds(
2525
- layer.name,
2526
- options?.staleWhileRevalidate,
2527
- this.options.staleWhileRevalidate
2528
- );
2529
- const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
2530
- const payload = createStoredValueEnvelope({
2531
- kind,
2532
- value,
2533
- freshTtlSeconds: freshTtl,
2534
- staleWhileRevalidateSeconds: staleWhileRevalidate,
2535
- staleIfErrorSeconds: staleIfError,
2536
- now
2938
+ this.metricsCollector.increment("writeFailures", failures.length);
2939
+ this.logger.error?.("write-behind-flush-failure", {
2940
+ failed: failures.length,
2941
+ total: batch.length,
2942
+ errors: failures.map((failure) => this.formatError(failure.reason))
2537
2943
  });
2538
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
2539
- return {
2540
- key,
2541
- value: payload,
2542
- ttl
2543
- };
2544
- }
2545
- intersectKeys(groups) {
2546
- if (groups.length === 0) {
2547
- return [];
2548
- }
2549
- const [firstGroup, ...rest] = groups;
2550
- if (!firstGroup) {
2551
- return [];
2552
- }
2553
- const restSets = rest.map((group) => new Set(group));
2554
- return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
2944
+ this.emitError("write-behind", { failed: failures.length, total: batch.length });
2555
2945
  }
2556
2946
  qualifyKey(key) {
2557
- const prefix = this.generationPrefix();
2558
- return prefix ? `${prefix}${key}` : key;
2947
+ return qualifyGenerationKey(key, this.currentGeneration);
2559
2948
  }
2560
2949
  qualifyPattern(pattern) {
2561
- const prefix = this.generationPrefix();
2562
- return prefix ? `${prefix}${pattern}` : pattern;
2950
+ return qualifyGenerationPattern(pattern, this.currentGeneration);
2563
2951
  }
2564
2952
  stripQualifiedKey(key) {
2565
- const prefix = this.generationPrefix();
2566
- if (!prefix || !key.startsWith(prefix)) {
2567
- return key;
2568
- }
2569
- return key.slice(prefix.length);
2570
- }
2571
- generationPrefix() {
2572
- if (this.currentGeneration === void 0) {
2573
- return "";
2574
- }
2575
- return `v${this.currentGeneration}:`;
2576
- }
2577
- async deleteKeysFromLayers(layers, keys) {
2578
- await Promise.all(
2579
- layers.map(async (layer) => {
2580
- if (this.shouldSkipLayer(layer)) {
2581
- return;
2582
- }
2583
- if (layer.deleteMany) {
2584
- try {
2585
- await layer.deleteMany(keys);
2586
- } catch (error) {
2587
- await this.handleLayerFailure(layer, "delete", error);
2588
- }
2589
- return;
2590
- }
2591
- await Promise.all(
2592
- keys.map(async (key) => {
2593
- try {
2594
- await layer.delete(key);
2595
- } catch (error) {
2596
- await this.handleLayerFailure(layer, "delete", error);
2597
- }
2598
- })
2599
- );
2600
- })
2601
- );
2953
+ return stripGenerationPrefix(key, this.currentGeneration);
2602
2954
  }
2603
2955
  validateConfiguration() {
2604
2956
  if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
@@ -2663,37 +3015,38 @@ var CacheStack = class extends EventEmitter {
2663
3015
  this.assertActive(operation);
2664
3016
  }
2665
3017
  async applyFreshReadPolicies(key, hit, options, fetcher) {
2666
- const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
2667
- const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
2668
- if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
2669
- const refreshed = refreshStoredEnvelope(hit.stored);
2670
- const ttl = remainingStoredTtlSeconds(refreshed);
3018
+ const plan = planFreshReadPolicies({
3019
+ stored: hit.stored,
3020
+ hasFetcher: Boolean(fetcher),
3021
+ slidingTtl: options?.slidingTtl ?? false,
3022
+ refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
3023
+ });
3024
+ if (plan.refreshedStored) {
2671
3025
  for (let index = 0; index <= hit.layerIndex; index += 1) {
2672
3026
  const layer = this.layers[index];
2673
3027
  if (!layer || this.shouldSkipLayer(layer)) {
2674
3028
  continue;
2675
3029
  }
2676
3030
  try {
2677
- await layer.set(key, refreshed, ttl);
3031
+ await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
2678
3032
  } catch (error) {
2679
3033
  await this.handleLayerFailure(layer, "sliding-ttl", error);
2680
3034
  }
2681
3035
  }
2682
3036
  }
2683
- if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
3037
+ if (fetcher && plan.shouldScheduleBackgroundRefresh) {
2684
3038
  this.scheduleBackgroundRefresh(key, fetcher, options);
2685
3039
  }
2686
3040
  }
2687
3041
  shouldSkipLayer(layer) {
2688
- const degradedUntil = this.layerDegradedUntil.get(layer.name);
2689
- return degradedUntil !== void 0 && degradedUntil > Date.now();
3042
+ return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
2690
3043
  }
2691
3044
  async handleLayerFailure(layer, operation, error) {
2692
- if (!this.isGracefulDegradationEnabled()) {
3045
+ const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
3046
+ if (!recovery.degrade) {
2693
3047
  throw error;
2694
3048
  }
2695
- const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
2696
- this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
3049
+ this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
2697
3050
  this.metricsCollector.increment("degradedOperations");
2698
3051
  this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
2699
3052
  this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
@@ -2729,18 +3082,6 @@ var CacheStack = class extends EventEmitter {
2729
3082
  this.emit("error", { operation, ...context });
2730
3083
  }
2731
3084
  }
2732
- isCacheSnapshotEntries(value) {
2733
- return Array.isArray(value) && value.every((entry) => {
2734
- if (!entry || typeof entry !== "object") {
2735
- return false;
2736
- }
2737
- const candidate = entry;
2738
- return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
2739
- });
2740
- }
2741
- sanitizeSnapshotValue(value) {
2742
- return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
2743
- }
2744
3085
  snapshotMaxBytes() {
2745
3086
  return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
2746
3087
  }
@@ -2750,62 +3091,6 @@ var CacheStack = class extends EventEmitter {
2750
3091
  invalidationMaxKeys() {
2751
3092
  return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
2752
3093
  }
2753
- async collectKeysForTag(tag) {
2754
- const keys = /* @__PURE__ */ new Set();
2755
- if (this.tagIndex.forEachKeyForTag) {
2756
- await this.tagIndex.forEachKeyForTag(tag, async (key) => {
2757
- keys.add(key);
2758
- this.assertWithinInvalidationKeyLimit(keys.size);
2759
- });
2760
- return [...keys];
2761
- }
2762
- for (const key of await this.tagIndex.keysForTag(tag)) {
2763
- keys.add(key);
2764
- this.assertWithinInvalidationKeyLimit(keys.size);
2765
- }
2766
- return [...keys];
2767
- }
2768
- assertWithinInvalidationKeyLimit(size) {
2769
- const maxKeys = this.invalidationMaxKeys();
2770
- if (maxKeys !== false && size > maxKeys) {
2771
- throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
2772
- }
2773
- }
2774
- async visitExportEntries(maxEntries, visitor) {
2775
- const exported = /* @__PURE__ */ new Set();
2776
- for (const layer of this.layers) {
2777
- if (!layer.keys && !layer.forEachKey) {
2778
- continue;
2779
- }
2780
- const visitKey = async (key) => {
2781
- const exportedKey = this.stripQualifiedKey(key);
2782
- if (exported.has(exportedKey)) {
2783
- return;
2784
- }
2785
- const stored = await this.readLayerEntry(layer, key);
2786
- if (stored === null) {
2787
- return;
2788
- }
2789
- exported.add(exportedKey);
2790
- if (maxEntries !== false && exported.size > maxEntries) {
2791
- throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
2792
- }
2793
- await visitor({
2794
- key: exportedKey,
2795
- value: stored,
2796
- ttl: remainingStoredTtlSeconds(stored)
2797
- });
2798
- };
2799
- if (layer.forEachKey) {
2800
- await layer.forEachKey(visitKey);
2801
- continue;
2802
- }
2803
- const keys = await layer.keys?.();
2804
- for (const key of keys ?? []) {
2805
- await visitKey(key);
2806
- }
2807
- }
2808
- }
2809
3094
  };
2810
3095
 
2811
3096
  // src/invalidation/RedisInvalidationBus.ts
@@ -2847,7 +3132,12 @@ var RedisInvalidationBus = class {
2847
3132
  async dispatchToHandlers(payload) {
2848
3133
  let message;
2849
3134
  try {
2850
- const parsed = sanitizeJsonValue2(JSON.parse(payload));
3135
+ const parsed = sanitizeStructuredData(JSON.parse(payload), {
3136
+ label: "Invalidation payload",
3137
+ maxDepth: 64,
3138
+ maxNodes: 1e4,
3139
+ createObject: () => /* @__PURE__ */ Object.create(null)
3140
+ });
2851
3141
  if (!this.isInvalidationMessage(parsed)) {
2852
3142
  throw new Error("Invalid invalidation payload shape.");
2853
3143
  }
@@ -2884,31 +3174,6 @@ var RedisInvalidationBus = class {
2884
3174
  console.error(`[layercache] ${message}`, error);
2885
3175
  }
2886
3176
  };
2887
- var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2888
- var MAX_SANITIZE_DEPTH2 = 64;
2889
- var MAX_SANITIZE_NODES2 = 1e4;
2890
- function sanitizeJsonValue2(value, depth = 0, state = { count: 0 }) {
2891
- state.count += 1;
2892
- if (state.count > MAX_SANITIZE_NODES2) {
2893
- throw new Error(`Invalidation payload exceeds max node count of ${MAX_SANITIZE_NODES2}.`);
2894
- }
2895
- if (depth > MAX_SANITIZE_DEPTH2) {
2896
- throw new Error(`Invalidation payload exceeds max depth of ${MAX_SANITIZE_DEPTH2}.`);
2897
- }
2898
- if (Array.isArray(value)) {
2899
- return value.map((entry) => sanitizeJsonValue2(entry, depth + 1, state));
2900
- }
2901
- if (value && typeof value === "object") {
2902
- const result = /* @__PURE__ */ Object.create(null);
2903
- for (const key of Object.keys(value)) {
2904
- if (!DANGEROUS_KEYS.has(key)) {
2905
- result[key] = sanitizeJsonValue2(value[key], depth + 1, state);
2906
- }
2907
- }
2908
- return result;
2909
- }
2910
- return value;
2911
- }
2912
3177
 
2913
3178
  // src/http/createCacheStatsHandler.ts
2914
3179
  function createCacheStatsHandler(cache, options = {}) {
@@ -3053,64 +3318,37 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
3053
3318
 
3054
3319
  // src/integrations/opentelemetry.ts
3055
3320
  function createOpenTelemetryPlugin(cache, tracer) {
3056
- const originals = {
3057
- get: cache.get.bind(cache),
3058
- set: cache.set.bind(cache),
3059
- delete: cache.delete.bind(cache),
3060
- mget: cache.mget.bind(cache),
3061
- mset: cache.mset.bind(cache),
3062
- invalidateByTag: cache.invalidateByTag.bind(cache),
3063
- invalidateByTags: cache.invalidateByTags.bind(cache),
3064
- invalidateByPattern: cache.invalidateByPattern.bind(cache),
3065
- invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
3321
+ const spans = /* @__PURE__ */ new Map();
3322
+ const onStart = (event) => {
3323
+ spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
3066
3324
  };
3067
- cache.get = instrument("layercache.get", tracer, originals.get, (args) => ({
3068
- "layercache.key": String(args[0] ?? "")
3069
- }));
3070
- cache.set = instrument("layercache.set", tracer, originals.set, (args) => ({
3071
- "layercache.key": String(args[0] ?? "")
3072
- }));
3073
- cache.delete = instrument("layercache.delete", tracer, originals.delete, (args) => ({
3074
- "layercache.key": String(args[0] ?? "")
3075
- }));
3076
- cache.mget = instrument("layercache.mget", tracer, originals.mget);
3077
- cache.mset = instrument("layercache.mset", tracer, originals.mset);
3078
- cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
3079
- cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
3080
- cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
3081
- cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
3082
- return {
3083
- uninstall() {
3084
- cache.get = originals.get;
3085
- cache.set = originals.set;
3086
- cache.delete = originals.delete;
3087
- cache.mget = originals.mget;
3088
- cache.mset = originals.mset;
3089
- cache.invalidateByTag = originals.invalidateByTag;
3090
- cache.invalidateByTags = originals.invalidateByTags;
3091
- cache.invalidateByPattern = originals.invalidateByPattern;
3092
- cache.invalidateByPrefix = originals.invalidateByPrefix;
3325
+ const onEnd = (event) => {
3326
+ const span = spans.get(event.id);
3327
+ if (!span) {
3328
+ return;
3093
3329
  }
3330
+ spans.delete(event.id);
3331
+ span.setAttribute?.("layercache.success", event.success);
3332
+ if (event.result) {
3333
+ span.setAttribute?.("layercache.result", event.result);
3334
+ }
3335
+ if (event.error !== void 0) {
3336
+ span.recordException?.(event.error);
3337
+ }
3338
+ span.end();
3094
3339
  };
3095
- }
3096
- function instrument(name, tracer, method, attributes) {
3097
- return (async (...args) => {
3098
- const span = tracer.startSpan(name, { attributes: attributes?.(args) });
3099
- try {
3100
- const result = await method(...args);
3101
- span.setAttribute?.("layercache.success", true);
3102
- if (result === null) {
3103
- span.setAttribute?.("layercache.result", "null");
3340
+ cache.on("operation-start", onStart);
3341
+ cache.on("operation-end", onEnd);
3342
+ return {
3343
+ uninstall() {
3344
+ cache.off("operation-start", onStart);
3345
+ cache.off("operation-end", onEnd);
3346
+ for (const span of spans.values()) {
3347
+ span.end();
3104
3348
  }
3105
- return result;
3106
- } catch (error) {
3107
- span.setAttribute?.("layercache.success", false);
3108
- span.recordException?.(error);
3109
- throw error;
3110
- } finally {
3111
- span.end();
3349
+ spans.clear();
3112
3350
  }
3113
- });
3351
+ };
3114
3352
  }
3115
3353
 
3116
3354
  // src/integrations/trpc.ts
@@ -3474,7 +3712,7 @@ var RedisLayer = class {
3474
3712
 
3475
3713
  // src/layers/DiskLayer.ts
3476
3714
  import { createHash } from "crypto";
3477
- import { promises as fs } from "fs";
3715
+ import { promises as fs2 } from "fs";
3478
3716
  import { join, resolve } from "path";
3479
3717
  var FILE_SCAN_CONCURRENCY = 32;
3480
3718
  var DiskLayer = class {
@@ -3518,7 +3756,7 @@ var DiskLayer = class {
3518
3756
  }
3519
3757
  async set(key, value, ttl = this.defaultTtl) {
3520
3758
  await this.enqueueWrite(async () => {
3521
- await fs.mkdir(this.directory, { recursive: true });
3759
+ await fs2.mkdir(this.directory, { recursive: true });
3522
3760
  const entry = {
3523
3761
  key,
3524
3762
  value,
@@ -3528,8 +3766,8 @@ var DiskLayer = class {
3528
3766
  const targetPath = this.keyToPath(key);
3529
3767
  const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
3530
3768
  try {
3531
- await fs.writeFile(tempPath, payload);
3532
- await fs.rename(tempPath, targetPath);
3769
+ await fs2.writeFile(tempPath, payload);
3770
+ await fs2.rename(tempPath, targetPath);
3533
3771
  } catch (error) {
3534
3772
  await this.safeDelete(tempPath);
3535
3773
  throw error;
@@ -3583,7 +3821,7 @@ var DiskLayer = class {
3583
3821
  await this.enqueueWrite(async () => {
3584
3822
  let entries;
3585
3823
  try {
3586
- entries = await fs.readdir(this.directory);
3824
+ entries = await fs2.readdir(this.directory);
3587
3825
  } catch {
3588
3826
  return;
3589
3827
  }
@@ -3617,7 +3855,7 @@ var DiskLayer = class {
3617
3855
  }
3618
3856
  async ping() {
3619
3857
  try {
3620
- await fs.mkdir(this.directory, { recursive: true });
3858
+ await fs2.mkdir(this.directory, { recursive: true });
3621
3859
  return true;
3622
3860
  } catch {
3623
3861
  return false;
@@ -3660,7 +3898,7 @@ var DiskLayer = class {
3660
3898
  async readEntryFile(filePath) {
3661
3899
  let handle;
3662
3900
  try {
3663
- handle = await fs.open(filePath, "r");
3901
+ handle = await fs2.open(filePath, "r");
3664
3902
  return await this.readHandleWithLimit(handle);
3665
3903
  } catch {
3666
3904
  await this.safeDelete(filePath);
@@ -3700,7 +3938,7 @@ var DiskLayer = class {
3700
3938
  async scanEntries(visitor) {
3701
3939
  let entries;
3702
3940
  try {
3703
- entries = await fs.readdir(this.directory);
3941
+ entries = await fs2.readdir(this.directory);
3704
3942
  } catch {
3705
3943
  return;
3706
3944
  }
@@ -3763,7 +4001,7 @@ var DiskLayer = class {
3763
4001
  }
3764
4002
  async safeDelete(filePath) {
3765
4003
  try {
3766
- await fs.unlink(filePath);
4004
+ await fs2.unlink(filePath);
3767
4005
  } catch {
3768
4006
  }
3769
4007
  }
@@ -3781,7 +4019,7 @@ var DiskLayer = class {
3781
4019
  }
3782
4020
  let entries;
3783
4021
  try {
3784
- entries = await fs.readdir(this.directory);
4022
+ entries = await fs2.readdir(this.directory);
3785
4023
  } catch {
3786
4024
  return;
3787
4025
  }
@@ -3793,7 +4031,7 @@ var DiskLayer = class {
3793
4031
  lcFiles.map(async (name) => {
3794
4032
  const filePath = join(this.directory, name);
3795
4033
  try {
3796
- const stat = await fs.stat(filePath);
4034
+ const stat = await fs2.stat(filePath);
3797
4035
  return { filePath, mtimeMs: stat.mtimeMs };
3798
4036
  } catch {
3799
4037
  return { filePath, mtimeMs: 0 };
@@ -3889,44 +4127,19 @@ var MemcachedLayer = class {
3889
4127
 
3890
4128
  // src/serialization/MsgpackSerializer.ts
3891
4129
  import { decode, encode } from "@msgpack/msgpack";
3892
- var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
3893
- var MAX_SANITIZE_DEPTH3 = 64;
3894
- var MAX_SANITIZE_NODES3 = 1e4;
3895
4130
  var MsgpackSerializer = class {
3896
4131
  serialize(value) {
3897
4132
  return Buffer.from(encode(value));
3898
4133
  }
3899
4134
  deserialize(payload) {
3900
- const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
3901
- return sanitizeMsgpackValue(decode(normalized), 0, { count: 0 });
4135
+ const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
4136
+ return sanitizeStructuredData(decode(normalized), {
4137
+ label: "MessagePack payload",
4138
+ maxDepth: 64,
4139
+ maxNodes: 1e4
4140
+ });
3902
4141
  }
3903
4142
  };
3904
- function sanitizeMsgpackValue(value, depth, state) {
3905
- state.count += 1;
3906
- if (state.count > MAX_SANITIZE_NODES3) {
3907
- throw new Error(`MessagePack payload exceeds max node count of ${MAX_SANITIZE_NODES3}.`);
3908
- }
3909
- if (depth > MAX_SANITIZE_DEPTH3) {
3910
- throw new Error(`MessagePack payload exceeds max depth of ${MAX_SANITIZE_DEPTH3}.`);
3911
- }
3912
- if (Array.isArray(value)) {
3913
- return value.map((entry) => sanitizeMsgpackValue(entry, depth + 1, state));
3914
- }
3915
- if (!isPlainObject2(value)) {
3916
- return value;
3917
- }
3918
- const sanitized = {};
3919
- for (const [key, entry] of Object.entries(value)) {
3920
- if (DANGEROUS_KEYS2.has(key)) {
3921
- continue;
3922
- }
3923
- sanitized[key] = sanitizeMsgpackValue(entry, depth + 1, state);
3924
- }
3925
- return sanitized;
3926
- }
3927
- function isPlainObject2(value) {
3928
- return Object.prototype.toString.call(value) === "[object Object]";
3929
- }
3930
4143
 
3931
4144
  // src/singleflight/RedisSingleFlightCoordinator.ts
3932
4145
  import { randomUUID } from "crypto";