layercache 1.2.6 → 1.2.7

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.");
@@ -557,6 +564,187 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
557
564
  return Buffer.concat(chunks).toString("utf8");
558
565
  }
559
566
 
567
+ // src/internal/CacheStackGeneration.ts
568
+ var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
569
+ function generationPrefix(generation) {
570
+ return generation === void 0 ? "" : `v${generation}:`;
571
+ }
572
+ function qualifyGenerationKey(key, generation) {
573
+ const prefix = generationPrefix(generation);
574
+ return prefix ? `${prefix}${key}` : key;
575
+ }
576
+ function qualifyGenerationPattern(pattern, generation) {
577
+ return qualifyGenerationKey(pattern, generation);
578
+ }
579
+ function stripGenerationPrefix(key, generation) {
580
+ const prefix = generationPrefix(generation);
581
+ if (!prefix || !key.startsWith(prefix)) {
582
+ return key;
583
+ }
584
+ return key.slice(prefix.length);
585
+ }
586
+ function resolveGenerationCleanupTarget({
587
+ previousGeneration,
588
+ nextGeneration,
589
+ generationCleanup
590
+ }) {
591
+ if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
592
+ return null;
593
+ }
594
+ return previousGeneration;
595
+ }
596
+ function resolveGenerationCleanupBatchSize(generationCleanup) {
597
+ if (typeof generationCleanup !== "object" || generationCleanup === null) {
598
+ return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
599
+ }
600
+ return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
601
+ }
602
+ function planGenerationCleanupBatches(keys, generationCleanup) {
603
+ if (keys.length === 0) {
604
+ return [];
605
+ }
606
+ const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
607
+ const batches = [];
608
+ for (let index = 0; index < keys.length; index += batchSize) {
609
+ batches.push(keys.slice(index, index + batchSize));
610
+ }
611
+ return batches;
612
+ }
613
+
614
+ // src/internal/CacheStackMaintenance.ts
615
+ var CacheStackMaintenance = class {
616
+ keyEpochs = /* @__PURE__ */ new Map();
617
+ writeBehindQueue = [];
618
+ writeBehindTimer;
619
+ writeBehindFlushPromise;
620
+ generationCleanupPromise;
621
+ clearEpoch = 0;
622
+ initializeWriteBehindTimer(writeStrategy, options, flush) {
623
+ if (writeStrategy !== "write-behind") {
624
+ return;
625
+ }
626
+ const flushIntervalMs = options?.flushIntervalMs;
627
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
628
+ return;
629
+ }
630
+ this.disposeWriteBehindTimer();
631
+ this.writeBehindTimer = setInterval(() => {
632
+ void flush();
633
+ }, flushIntervalMs);
634
+ this.writeBehindTimer.unref?.();
635
+ }
636
+ disposeWriteBehindTimer() {
637
+ if (!this.writeBehindTimer) {
638
+ return;
639
+ }
640
+ clearInterval(this.writeBehindTimer);
641
+ this.writeBehindTimer = void 0;
642
+ }
643
+ beginClearEpoch() {
644
+ this.clearEpoch += 1;
645
+ this.keyEpochs.clear();
646
+ this.writeBehindQueue.length = 0;
647
+ }
648
+ currentClearEpoch() {
649
+ return this.clearEpoch;
650
+ }
651
+ currentKeyEpoch(key) {
652
+ return this.keyEpochs.get(key) ?? 0;
653
+ }
654
+ bumpKeyEpochs(keys) {
655
+ for (const key of keys) {
656
+ this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
657
+ }
658
+ }
659
+ isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
660
+ if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
661
+ return true;
662
+ }
663
+ if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
664
+ return true;
665
+ }
666
+ return false;
667
+ }
668
+ async enqueueWriteBehind(operation, options, flushBatch) {
669
+ this.writeBehindQueue.push(operation);
670
+ const batchSize = options?.batchSize ?? 100;
671
+ const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
672
+ if (this.writeBehindQueue.length >= batchSize) {
673
+ await this.flushWriteBehindQueue(options, flushBatch);
674
+ return;
675
+ }
676
+ if (this.writeBehindQueue.length >= maxQueueSize) {
677
+ await this.flushWriteBehindQueue(options, flushBatch);
678
+ }
679
+ }
680
+ async flushWriteBehindQueue(options, flushBatch) {
681
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
682
+ await this.writeBehindFlushPromise;
683
+ return;
684
+ }
685
+ const batchSize = options?.batchSize ?? 100;
686
+ const batch = this.writeBehindQueue.splice(0, batchSize);
687
+ this.writeBehindFlushPromise = flushBatch(batch);
688
+ try {
689
+ await this.writeBehindFlushPromise;
690
+ } finally {
691
+ this.writeBehindFlushPromise = void 0;
692
+ }
693
+ if (this.writeBehindQueue.length > 0) {
694
+ await this.flushWriteBehindQueue(options, flushBatch);
695
+ }
696
+ }
697
+ scheduleGenerationCleanup(generation, task, onError) {
698
+ const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
699
+ onError(generation, error);
700
+ });
701
+ this.generationCleanupPromise = scheduledTask.finally(() => {
702
+ if (this.generationCleanupPromise === scheduledTask) {
703
+ this.generationCleanupPromise = void 0;
704
+ }
705
+ });
706
+ }
707
+ async waitForGenerationCleanup() {
708
+ await this.generationCleanupPromise;
709
+ }
710
+ };
711
+
712
+ // src/internal/CacheStackRuntimePolicy.ts
713
+ function shouldSkipLayer(degradedUntil, now = Date.now()) {
714
+ return degradedUntil !== void 0 && degradedUntil > now;
715
+ }
716
+ function shouldStartBackgroundRefresh({
717
+ isDisconnecting,
718
+ hasRefreshInFlight
719
+ }) {
720
+ return !isDisconnecting && !hasRefreshInFlight;
721
+ }
722
+ function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
723
+ if (!gracefulDegradation) {
724
+ return { degrade: false };
725
+ }
726
+ const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
727
+ return {
728
+ degrade: true,
729
+ degradedUntil: now + retryAfterMs
730
+ };
731
+ }
732
+ function planFreshReadPolicies({
733
+ stored,
734
+ hasFetcher,
735
+ slidingTtl,
736
+ refreshAheadSeconds
737
+ }) {
738
+ const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
739
+ const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
740
+ const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
741
+ return {
742
+ refreshedStored,
743
+ refreshedStoredTtl,
744
+ shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
745
+ };
746
+ }
747
+
560
748
  // src/internal/CacheStackValidation.ts
561
749
  var MAX_CACHE_KEY_LENGTH = 1024;
562
750
  var MAX_PATTERN_LENGTH = 1024;
@@ -710,7 +898,6 @@ var CircuitBreakerManager = class {
710
898
  if (!options) {
711
899
  return;
712
900
  }
713
- this.pruneIfNeeded();
714
901
  const failureThreshold = options.failureThreshold ?? 3;
715
902
  const cooldownMs = options.cooldownMs ?? 3e4;
716
903
  const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
@@ -719,6 +906,7 @@ var CircuitBreakerManager = class {
719
906
  state.openUntil = Date.now() + cooldownMs;
720
907
  }
721
908
  this.breakers.set(key, state);
909
+ this.pruneIfNeeded();
722
910
  }
723
911
  recordSuccess(key) {
724
912
  this.breakers.delete(key);
@@ -1346,15 +1534,10 @@ var CacheStack = class extends EventEmitter {
1346
1534
  snapshotSerializer = new JsonSerializer();
1347
1535
  backgroundRefreshes = /* @__PURE__ */ new Map();
1348
1536
  layerDegradedUntil = /* @__PURE__ */ new Map();
1349
- keyEpochs = /* @__PURE__ */ new Map();
1537
+ maintenance = new CacheStackMaintenance();
1350
1538
  ttlResolver;
1351
1539
  circuitBreakerManager;
1352
1540
  currentGeneration;
1353
- writeBehindQueue = [];
1354
- writeBehindTimer;
1355
- writeBehindFlushPromise;
1356
- generationCleanupPromise;
1357
- clearEpoch = 0;
1358
1541
  isDisconnecting = false;
1359
1542
  disconnectPromise;
1360
1543
  /**
@@ -1510,7 +1693,7 @@ var CacheStack = class extends EventEmitter {
1510
1693
  }
1511
1694
  async clear() {
1512
1695
  await this.awaitStartup("clear");
1513
- this.beginClearEpoch();
1696
+ this.maintenance.beginClearEpoch();
1514
1697
  await Promise.all(this.layers.map((layer) => layer.clear()));
1515
1698
  await this.tagIndex.clear();
1516
1699
  this.ttlResolver.clearProfiles();
@@ -1768,9 +1951,15 @@ var CacheStack = class extends EventEmitter {
1768
1951
  bumpGeneration(nextGeneration) {
1769
1952
  const current = this.currentGeneration ?? 0;
1770
1953
  const previousGeneration = this.currentGeneration;
1771
- this.currentGeneration = nextGeneration ?? current + 1;
1772
- if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
1773
- this.scheduleGenerationCleanup(previousGeneration);
1954
+ const updatedGeneration = nextGeneration ?? current + 1;
1955
+ const generationToCleanup = resolveGenerationCleanupTarget({
1956
+ previousGeneration,
1957
+ nextGeneration: updatedGeneration,
1958
+ generationCleanup: this.options.generationCleanup
1959
+ });
1960
+ this.currentGeneration = updatedGeneration;
1961
+ if (generationToCleanup !== null) {
1962
+ this.scheduleGenerationCleanup(generationToCleanup);
1774
1963
  }
1775
1964
  return this.currentGeneration;
1776
1965
  }
@@ -1914,12 +2103,9 @@ var CacheStack = class extends EventEmitter {
1914
2103
  await this.startup;
1915
2104
  await this.unsubscribeInvalidation?.();
1916
2105
  await this.flushWriteBehindQueue();
1917
- await this.generationCleanupPromise;
2106
+ await this.maintenance.waitForGenerationCleanup();
1918
2107
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
1919
- if (this.writeBehindTimer) {
1920
- clearInterval(this.writeBehindTimer);
1921
- this.writeBehindTimer = void 0;
1922
- }
2108
+ this.maintenance.disposeWriteBehindTimer();
1923
2109
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
1924
2110
  })();
1925
2111
  }
@@ -1995,13 +2181,13 @@ var CacheStack = class extends EventEmitter {
1995
2181
  if (!this.shouldNegativeCache(options)) {
1996
2182
  return null;
1997
2183
  }
1998
- if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2184
+ if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
1999
2185
  this.logger.debug?.("skip-negative-store-after-invalidation", {
2000
2186
  key,
2001
2187
  expectedClearEpoch,
2002
- clearEpoch: this.clearEpoch,
2188
+ clearEpoch: this.maintenance.currentClearEpoch(),
2003
2189
  expectedKeyEpoch,
2004
- keyEpoch: this.currentKeyEpoch(key)
2190
+ keyEpoch: this.maintenance.currentKeyEpoch(key)
2005
2191
  });
2006
2192
  return null;
2007
2193
  }
@@ -2017,13 +2203,13 @@ var CacheStack = class extends EventEmitter {
2017
2203
  this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
2018
2204
  }
2019
2205
  }
2020
- if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2206
+ if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2021
2207
  this.logger.debug?.("skip-store-after-invalidation", {
2022
2208
  key,
2023
2209
  expectedClearEpoch,
2024
- clearEpoch: this.clearEpoch,
2210
+ clearEpoch: this.maintenance.currentClearEpoch(),
2025
2211
  expectedKeyEpoch,
2026
- keyEpoch: this.currentKeyEpoch(key)
2212
+ keyEpoch: this.maintenance.currentKeyEpoch(key)
2027
2213
  });
2028
2214
  return fetched;
2029
2215
  }
@@ -2031,10 +2217,10 @@ var CacheStack = class extends EventEmitter {
2031
2217
  return fetched;
2032
2218
  }
2033
2219
  async storeEntry(key, kind, value, options) {
2034
- const clearEpoch = this.clearEpoch;
2035
- const keyEpoch = this.currentKeyEpoch(key);
2220
+ const clearEpoch = this.maintenance.currentClearEpoch();
2221
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2036
2222
  await this.writeAcrossLayers(key, kind, value, options);
2037
- if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2223
+ if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2038
2224
  return;
2039
2225
  }
2040
2226
  if (options?.tags) {
@@ -2051,8 +2237,8 @@ var CacheStack = class extends EventEmitter {
2051
2237
  }
2052
2238
  async writeBatch(entries) {
2053
2239
  const now = Date.now();
2054
- const clearEpoch = this.clearEpoch;
2055
- const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
2240
+ const clearEpoch = this.maintenance.currentClearEpoch();
2241
+ const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
2056
2242
  const entriesByLayer = /* @__PURE__ */ new Map();
2057
2243
  const immediateOperations = [];
2058
2244
  const deferredOperations = [];
@@ -2069,11 +2255,11 @@ var CacheStack = class extends EventEmitter {
2069
2255
  }
2070
2256
  for (const [layer, layerEntries] of entriesByLayer.entries()) {
2071
2257
  const operation = async () => {
2072
- if (clearEpoch !== this.clearEpoch) {
2258
+ if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2073
2259
  return;
2074
2260
  }
2075
2261
  const activeEntries = layerEntries.filter(
2076
- (entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
2262
+ (entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
2077
2263
  );
2078
2264
  if (activeEntries.length === 0) {
2079
2265
  return;
@@ -2096,11 +2282,11 @@ var CacheStack = class extends EventEmitter {
2096
2282
  }
2097
2283
  await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
2098
2284
  await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
2099
- if (clearEpoch !== this.clearEpoch) {
2285
+ if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2100
2286
  return;
2101
2287
  }
2102
2288
  for (const entry of entries) {
2103
- if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2289
+ if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2104
2290
  continue;
2105
2291
  }
2106
2292
  if (entry.options?.tags) {
@@ -2204,13 +2390,13 @@ var CacheStack = class extends EventEmitter {
2204
2390
  }
2205
2391
  async writeAcrossLayers(key, kind, value, options) {
2206
2392
  const now = Date.now();
2207
- const clearEpoch = this.clearEpoch;
2208
- const keyEpoch = this.currentKeyEpoch(key);
2393
+ const clearEpoch = this.maintenance.currentClearEpoch();
2394
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2209
2395
  const immediateOperations = [];
2210
2396
  const deferredOperations = [];
2211
2397
  for (const layer of this.layers) {
2212
2398
  const operation = async () => {
2213
- if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2399
+ if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2214
2400
  return;
2215
2401
  }
2216
2402
  if (this.shouldSkipLayer(layer)) {
@@ -2273,11 +2459,14 @@ var CacheStack = class extends EventEmitter {
2273
2459
  return options?.negativeCache ?? this.options.negativeCaching ?? false;
2274
2460
  }
2275
2461
  scheduleBackgroundRefresh(key, fetcher, options) {
2276
- if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
2462
+ if (!shouldStartBackgroundRefresh({
2463
+ isDisconnecting: this.isDisconnecting,
2464
+ hasRefreshInFlight: this.backgroundRefreshes.has(key)
2465
+ })) {
2277
2466
  return;
2278
2467
  }
2279
- const clearEpoch = this.clearEpoch;
2280
- const keyEpoch = this.currentKeyEpoch(key);
2468
+ const clearEpoch = this.maintenance.currentClearEpoch();
2469
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2281
2470
  const refresh = (async () => {
2282
2471
  this.metricsCollector.increment("refreshes");
2283
2472
  try {
@@ -2315,7 +2504,7 @@ var CacheStack = class extends EventEmitter {
2315
2504
  if (keys.length === 0) {
2316
2505
  return;
2317
2506
  }
2318
- this.bumpKeyEpochs(keys);
2507
+ this.maintenance.bumpKeyEpochs(keys);
2319
2508
  await this.deleteKeysFromLayers(this.layers, keys);
2320
2509
  for (const key of keys) {
2321
2510
  await this.tagIndex.remove(key);
@@ -2339,7 +2528,7 @@ var CacheStack = class extends EventEmitter {
2339
2528
  }
2340
2529
  const localLayers = this.layers.filter((layer) => layer.isLocal);
2341
2530
  if (message.scope === "clear") {
2342
- this.beginClearEpoch();
2531
+ this.maintenance.beginClearEpoch();
2343
2532
  await Promise.all(localLayers.map((layer) => layer.clear()));
2344
2533
  await this.tagIndex.clear();
2345
2534
  this.ttlResolver.clearProfiles();
@@ -2347,7 +2536,7 @@ var CacheStack = class extends EventEmitter {
2347
2536
  return;
2348
2537
  }
2349
2538
  const keys = message.keys ?? [];
2350
- this.bumpKeyEpochs(keys);
2539
+ this.maintenance.bumpKeyEpochs(keys);
2351
2540
  await this.deleteKeysFromLayers(localLayers, keys);
2352
2541
  if (message.operation !== "write") {
2353
2542
  for (const key of keys) {
@@ -2405,35 +2594,22 @@ var CacheStack = class extends EventEmitter {
2405
2594
  shouldBroadcastL1Invalidation() {
2406
2595
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
2407
2596
  }
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;
2414
- }
2415
2597
  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;
2598
+ this.maintenance.scheduleGenerationCleanup(
2599
+ generation,
2600
+ async (generationToClean) => this.cleanupGeneration(generationToClean),
2601
+ (failedGeneration, error) => {
2602
+ this.logger.warn?.("generation-cleanup-error", {
2603
+ generation: failedGeneration,
2604
+ error: this.formatError(error)
2605
+ });
2425
2606
  }
2426
- });
2607
+ );
2427
2608
  }
2428
2609
  async cleanupGeneration(generation) {
2429
2610
  const prefix = `v${generation}:`;
2430
2611
  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);
2612
+ for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
2437
2613
  await this.deleteKeys(batch);
2438
2614
  await this.publishInvalidation({
2439
2615
  scope: "keys",
@@ -2444,80 +2620,34 @@ var CacheStack = class extends EventEmitter {
2444
2620
  }
2445
2621
  }
2446
2622
  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?.();
2623
+ this.maintenance.initializeWriteBehindTimer(
2624
+ this.options.writeStrategy,
2625
+ options,
2626
+ this.flushWriteBehindQueue.bind(this)
2627
+ );
2458
2628
  }
2459
2629
  shouldWriteBehind(layer) {
2460
2630
  return this.options.writeStrategy === "write-behind" && !layer.isLocal;
2461
2631
  }
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
2632
  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
- }
2633
+ await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
2495
2634
  }
2496
2635
  async flushWriteBehindQueue() {
2497
- if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
2498
- await this.writeBehindFlushPromise;
2636
+ await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
2637
+ }
2638
+ async runWriteBehindBatch(batch) {
2639
+ const results = await Promise.allSettled(batch.map((operation) => operation()));
2640
+ const failures = results.filter((result) => result.status === "rejected");
2641
+ if (failures.length === 0) {
2499
2642
  return;
2500
2643
  }
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
- }
2644
+ this.metricsCollector.increment("writeFailures", failures.length);
2645
+ this.logger.error?.("write-behind-flush-failure", {
2646
+ failed: failures.length,
2647
+ total: batch.length,
2648
+ errors: failures.map((failure) => this.formatError(failure.reason))
2649
+ });
2650
+ this.emitError("write-behind", { failed: failures.length, total: batch.length });
2521
2651
  }
2522
2652
  buildLayerSetEntry(layer, key, kind, value, options, now) {
2523
2653
  const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
@@ -2547,32 +2677,17 @@ var CacheStack = class extends EventEmitter {
2547
2677
  return [];
2548
2678
  }
2549
2679
  const [firstGroup, ...rest] = groups;
2550
- if (!firstGroup) {
2551
- return [];
2552
- }
2553
2680
  const restSets = rest.map((group) => new Set(group));
2554
2681
  return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
2555
2682
  }
2556
2683
  qualifyKey(key) {
2557
- const prefix = this.generationPrefix();
2558
- return prefix ? `${prefix}${key}` : key;
2684
+ return qualifyGenerationKey(key, this.currentGeneration);
2559
2685
  }
2560
2686
  qualifyPattern(pattern) {
2561
- const prefix = this.generationPrefix();
2562
- return prefix ? `${prefix}${pattern}` : pattern;
2687
+ return qualifyGenerationPattern(pattern, this.currentGeneration);
2563
2688
  }
2564
2689
  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}:`;
2690
+ return stripGenerationPrefix(key, this.currentGeneration);
2576
2691
  }
2577
2692
  async deleteKeysFromLayers(layers, keys) {
2578
2693
  await Promise.all(
@@ -2663,37 +2778,38 @@ var CacheStack = class extends EventEmitter {
2663
2778
  this.assertActive(operation);
2664
2779
  }
2665
2780
  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);
2781
+ const plan = planFreshReadPolicies({
2782
+ stored: hit.stored,
2783
+ hasFetcher: Boolean(fetcher),
2784
+ slidingTtl: options?.slidingTtl ?? false,
2785
+ refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
2786
+ });
2787
+ if (plan.refreshedStored) {
2671
2788
  for (let index = 0; index <= hit.layerIndex; index += 1) {
2672
2789
  const layer = this.layers[index];
2673
2790
  if (!layer || this.shouldSkipLayer(layer)) {
2674
2791
  continue;
2675
2792
  }
2676
2793
  try {
2677
- await layer.set(key, refreshed, ttl);
2794
+ await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
2678
2795
  } catch (error) {
2679
2796
  await this.handleLayerFailure(layer, "sliding-ttl", error);
2680
2797
  }
2681
2798
  }
2682
2799
  }
2683
- if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
2800
+ if (fetcher && plan.shouldScheduleBackgroundRefresh) {
2684
2801
  this.scheduleBackgroundRefresh(key, fetcher, options);
2685
2802
  }
2686
2803
  }
2687
2804
  shouldSkipLayer(layer) {
2688
- const degradedUntil = this.layerDegradedUntil.get(layer.name);
2689
- return degradedUntil !== void 0 && degradedUntil > Date.now();
2805
+ return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
2690
2806
  }
2691
2807
  async handleLayerFailure(layer, operation, error) {
2692
- if (!this.isGracefulDegradationEnabled()) {
2808
+ const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
2809
+ if (!recovery.degrade) {
2693
2810
  throw error;
2694
2811
  }
2695
- const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
2696
- this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
2812
+ this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
2697
2813
  this.metricsCollector.increment("degradedOperations");
2698
2814
  this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
2699
2815
  this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
@@ -3897,7 +4013,7 @@ var MsgpackSerializer = class {
3897
4013
  return Buffer.from(encode(value));
3898
4014
  }
3899
4015
  deserialize(payload) {
3900
- const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
4016
+ const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
3901
4017
  return sanitizeMsgpackValue(decode(normalized), 0, { count: 0 });
3902
4018
  }
3903
4019
  };