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.cjs CHANGED
@@ -62,6 +62,127 @@ var import_node_events = require("events");
62
62
 
63
63
  // src/CacheNamespace.ts
64
64
  var import_async_mutex = require("async-mutex");
65
+
66
+ // src/internal/CacheNamespaceMetrics.ts
67
+ function createEmptyNamespaceMetrics(resetAt = Date.now()) {
68
+ return {
69
+ hits: 0,
70
+ misses: 0,
71
+ fetches: 0,
72
+ sets: 0,
73
+ deletes: 0,
74
+ backfills: 0,
75
+ invalidations: 0,
76
+ staleHits: 0,
77
+ refreshes: 0,
78
+ refreshErrors: 0,
79
+ writeFailures: 0,
80
+ singleFlightWaits: 0,
81
+ negativeCacheHits: 0,
82
+ circuitBreakerTrips: 0,
83
+ degradedOperations: 0,
84
+ hitsByLayer: {},
85
+ missesByLayer: {},
86
+ latencyByLayer: {},
87
+ resetAt
88
+ };
89
+ }
90
+ function cloneNamespaceMetrics(metrics) {
91
+ return {
92
+ ...metrics,
93
+ hitsByLayer: { ...metrics.hitsByLayer },
94
+ missesByLayer: { ...metrics.missesByLayer },
95
+ latencyByLayer: Object.fromEntries(
96
+ Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
97
+ )
98
+ };
99
+ }
100
+ function diffNamespaceMetrics(before, after) {
101
+ const latencyByLayer = Object.fromEntries(
102
+ Object.entries(after.latencyByLayer).map(([layer, value]) => [
103
+ layer,
104
+ {
105
+ avgMs: value.avgMs,
106
+ maxMs: value.maxMs,
107
+ count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
108
+ }
109
+ ])
110
+ );
111
+ return {
112
+ hits: after.hits - before.hits,
113
+ misses: after.misses - before.misses,
114
+ fetches: after.fetches - before.fetches,
115
+ sets: after.sets - before.sets,
116
+ deletes: after.deletes - before.deletes,
117
+ backfills: after.backfills - before.backfills,
118
+ invalidations: after.invalidations - before.invalidations,
119
+ staleHits: after.staleHits - before.staleHits,
120
+ refreshes: after.refreshes - before.refreshes,
121
+ refreshErrors: after.refreshErrors - before.refreshErrors,
122
+ writeFailures: after.writeFailures - before.writeFailures,
123
+ singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
124
+ negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
125
+ circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
126
+ degradedOperations: after.degradedOperations - before.degradedOperations,
127
+ hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
128
+ missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
129
+ latencyByLayer,
130
+ resetAt: after.resetAt
131
+ };
132
+ }
133
+ function addNamespaceMetrics(base, delta) {
134
+ return {
135
+ hits: base.hits + delta.hits,
136
+ misses: base.misses + delta.misses,
137
+ fetches: base.fetches + delta.fetches,
138
+ sets: base.sets + delta.sets,
139
+ deletes: base.deletes + delta.deletes,
140
+ backfills: base.backfills + delta.backfills,
141
+ invalidations: base.invalidations + delta.invalidations,
142
+ staleHits: base.staleHits + delta.staleHits,
143
+ refreshes: base.refreshes + delta.refreshes,
144
+ refreshErrors: base.refreshErrors + delta.refreshErrors,
145
+ writeFailures: base.writeFailures + delta.writeFailures,
146
+ singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
147
+ negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
148
+ circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
149
+ degradedOperations: base.degradedOperations + delta.degradedOperations,
150
+ hitsByLayer: addMetricMap(base.hitsByLayer, delta.hitsByLayer),
151
+ missesByLayer: addMetricMap(base.missesByLayer, delta.missesByLayer),
152
+ latencyByLayer: cloneNamespaceMetrics(delta).latencyByLayer,
153
+ resetAt: base.resetAt
154
+ };
155
+ }
156
+ function computeNamespaceHitRate(metrics) {
157
+ const total = metrics.hits + metrics.misses;
158
+ const overall = total === 0 ? 0 : metrics.hits / total;
159
+ const byLayer = {};
160
+ const layers = /* @__PURE__ */ new Set([...Object.keys(metrics.hitsByLayer), ...Object.keys(metrics.missesByLayer)]);
161
+ for (const layer of layers) {
162
+ const hits = metrics.hitsByLayer[layer] ?? 0;
163
+ const misses = metrics.missesByLayer[layer] ?? 0;
164
+ byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
165
+ }
166
+ return { overall, byLayer };
167
+ }
168
+ function diffMetricMap(before, after) {
169
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
170
+ const result = {};
171
+ for (const key of keys) {
172
+ result[key] = (after[key] ?? 0) - (before[key] ?? 0);
173
+ }
174
+ return result;
175
+ }
176
+ function addMetricMap(base, delta) {
177
+ const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
178
+ const result = {};
179
+ for (const key of keys) {
180
+ result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
181
+ }
182
+ return result;
183
+ }
184
+
185
+ // src/CacheNamespace.ts
65
186
  var CacheNamespace = class _CacheNamespace {
66
187
  constructor(cache, prefix) {
67
188
  this.cache = cache;
@@ -71,7 +192,7 @@ var CacheNamespace = class _CacheNamespace {
71
192
  cache;
72
193
  prefix;
73
194
  static metricsMutexes = /* @__PURE__ */ new WeakMap();
74
- metrics = emptyMetrics();
195
+ metrics = createEmptyNamespaceMetrics();
75
196
  async get(key, fetcher, options) {
76
197
  return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
77
198
  }
@@ -168,19 +289,10 @@ var CacheNamespace = class _CacheNamespace {
168
289
  );
169
290
  }
170
291
  getMetrics() {
171
- return cloneMetrics(this.metrics);
292
+ return cloneNamespaceMetrics(this.metrics);
172
293
  }
173
294
  getHitRate() {
174
- const total = this.metrics.hits + this.metrics.misses;
175
- const overall = total === 0 ? 0 : this.metrics.hits / total;
176
- const byLayer = {};
177
- const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
178
- for (const layer of layers) {
179
- const hits = this.metrics.hitsByLayer[layer] ?? 0;
180
- const misses = this.metrics.missesByLayer[layer] ?? 0;
181
- byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
182
- }
183
- return { overall, byLayer };
295
+ return computeNamespaceHitRate(this.metrics);
184
296
  }
185
297
  /**
186
298
  * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
@@ -221,7 +333,7 @@ var CacheNamespace = class _CacheNamespace {
221
333
  const before = this.cache.getMetrics();
222
334
  const result = await operation();
223
335
  const after = this.cache.getMetrics();
224
- this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
336
+ this.metrics = addNamespaceMetrics(this.metrics, diffNamespaceMetrics(before, after));
225
337
  return result;
226
338
  });
227
339
  }
@@ -235,111 +347,6 @@ var CacheNamespace = class _CacheNamespace {
235
347
  return mutex;
236
348
  }
237
349
  };
238
- function emptyMetrics() {
239
- return {
240
- hits: 0,
241
- misses: 0,
242
- fetches: 0,
243
- sets: 0,
244
- deletes: 0,
245
- backfills: 0,
246
- invalidations: 0,
247
- staleHits: 0,
248
- refreshes: 0,
249
- refreshErrors: 0,
250
- writeFailures: 0,
251
- singleFlightWaits: 0,
252
- negativeCacheHits: 0,
253
- circuitBreakerTrips: 0,
254
- degradedOperations: 0,
255
- hitsByLayer: {},
256
- missesByLayer: {},
257
- latencyByLayer: {},
258
- resetAt: Date.now()
259
- };
260
- }
261
- function cloneMetrics(metrics) {
262
- return {
263
- ...metrics,
264
- hitsByLayer: { ...metrics.hitsByLayer },
265
- missesByLayer: { ...metrics.missesByLayer },
266
- latencyByLayer: Object.fromEntries(
267
- Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
268
- )
269
- };
270
- }
271
- function diffMetrics(before, after) {
272
- const latencyByLayer = Object.fromEntries(
273
- Object.entries(after.latencyByLayer).map(([layer, value]) => [
274
- layer,
275
- {
276
- avgMs: value.avgMs,
277
- maxMs: value.maxMs,
278
- count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
279
- }
280
- ])
281
- );
282
- return {
283
- hits: after.hits - before.hits,
284
- misses: after.misses - before.misses,
285
- fetches: after.fetches - before.fetches,
286
- sets: after.sets - before.sets,
287
- deletes: after.deletes - before.deletes,
288
- backfills: after.backfills - before.backfills,
289
- invalidations: after.invalidations - before.invalidations,
290
- staleHits: after.staleHits - before.staleHits,
291
- refreshes: after.refreshes - before.refreshes,
292
- refreshErrors: after.refreshErrors - before.refreshErrors,
293
- writeFailures: after.writeFailures - before.writeFailures,
294
- singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
295
- negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
296
- circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
297
- degradedOperations: after.degradedOperations - before.degradedOperations,
298
- hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
299
- missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
300
- latencyByLayer,
301
- resetAt: after.resetAt
302
- };
303
- }
304
- function addMetrics(base, delta) {
305
- return {
306
- hits: base.hits + delta.hits,
307
- misses: base.misses + delta.misses,
308
- fetches: base.fetches + delta.fetches,
309
- sets: base.sets + delta.sets,
310
- deletes: base.deletes + delta.deletes,
311
- backfills: base.backfills + delta.backfills,
312
- invalidations: base.invalidations + delta.invalidations,
313
- staleHits: base.staleHits + delta.staleHits,
314
- refreshes: base.refreshes + delta.refreshes,
315
- refreshErrors: base.refreshErrors + delta.refreshErrors,
316
- writeFailures: base.writeFailures + delta.writeFailures,
317
- singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
318
- negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
319
- circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
320
- degradedOperations: base.degradedOperations + delta.degradedOperations,
321
- hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
322
- missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
323
- latencyByLayer: cloneMetrics(delta).latencyByLayer,
324
- resetAt: base.resetAt
325
- };
326
- }
327
- function diffMap(before, after) {
328
- const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
329
- const result = {};
330
- for (const key of keys) {
331
- result[key] = (after[key] ?? 0) - (before[key] ?? 0);
332
- }
333
- return result;
334
- }
335
- function addMap(base, delta) {
336
- const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
337
- const result = {};
338
- for (const key of keys) {
339
- result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
340
- }
341
- return result;
342
- }
343
350
  function validateNamespaceKey(key) {
344
351
  if (key.length === 0) {
345
352
  throw new Error("Namespace prefix must not be empty.");
@@ -642,7 +649,346 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
642
649
  chunks.push(buffer.subarray(0, bytesRead));
643
650
  position += bytesRead;
644
651
  }
645
- return Buffer.concat(chunks).toString("utf8");
652
+ return Buffer.concat(chunks).toString("utf8");
653
+ }
654
+
655
+ // src/internal/CacheStackGeneration.ts
656
+ var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
657
+ function generationPrefix(generation) {
658
+ return generation === void 0 ? "" : `v${generation}:`;
659
+ }
660
+ function qualifyGenerationKey(key, generation) {
661
+ const prefix = generationPrefix(generation);
662
+ return prefix ? `${prefix}${key}` : key;
663
+ }
664
+ function qualifyGenerationPattern(pattern, generation) {
665
+ return qualifyGenerationKey(pattern, generation);
666
+ }
667
+ function stripGenerationPrefix(key, generation) {
668
+ const prefix = generationPrefix(generation);
669
+ if (!prefix || !key.startsWith(prefix)) {
670
+ return key;
671
+ }
672
+ return key.slice(prefix.length);
673
+ }
674
+ function resolveGenerationCleanupTarget({
675
+ previousGeneration,
676
+ nextGeneration,
677
+ generationCleanup
678
+ }) {
679
+ if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
680
+ return null;
681
+ }
682
+ return previousGeneration;
683
+ }
684
+ function resolveGenerationCleanupBatchSize(generationCleanup) {
685
+ if (typeof generationCleanup !== "object" || generationCleanup === null) {
686
+ return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
687
+ }
688
+ return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
689
+ }
690
+ function planGenerationCleanupBatches(keys, generationCleanup) {
691
+ if (keys.length === 0) {
692
+ return [];
693
+ }
694
+ const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
695
+ const batches = [];
696
+ for (let index = 0; index < keys.length; index += batchSize) {
697
+ batches.push(keys.slice(index, index + batchSize));
698
+ }
699
+ return batches;
700
+ }
701
+
702
+ // src/internal/CacheStackMaintenance.ts
703
+ var CacheStackMaintenance = class {
704
+ keyEpochs = /* @__PURE__ */ new Map();
705
+ writeBehindQueue = [];
706
+ writeBehindTimer;
707
+ writeBehindFlushPromise;
708
+ generationCleanupPromise;
709
+ clearEpoch = 0;
710
+ initializeWriteBehindTimer(writeStrategy, options, flush) {
711
+ if (writeStrategy !== "write-behind") {
712
+ return;
713
+ }
714
+ const flushIntervalMs = options?.flushIntervalMs;
715
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
716
+ return;
717
+ }
718
+ this.disposeWriteBehindTimer();
719
+ this.writeBehindTimer = setInterval(() => {
720
+ void flush();
721
+ }, flushIntervalMs);
722
+ this.writeBehindTimer.unref?.();
723
+ }
724
+ disposeWriteBehindTimer() {
725
+ if (!this.writeBehindTimer) {
726
+ return;
727
+ }
728
+ clearInterval(this.writeBehindTimer);
729
+ this.writeBehindTimer = void 0;
730
+ }
731
+ beginClearEpoch() {
732
+ this.clearEpoch += 1;
733
+ this.keyEpochs.clear();
734
+ this.writeBehindQueue.length = 0;
735
+ }
736
+ currentClearEpoch() {
737
+ return this.clearEpoch;
738
+ }
739
+ currentKeyEpoch(key) {
740
+ return this.keyEpochs.get(key) ?? 0;
741
+ }
742
+ bumpKeyEpochs(keys) {
743
+ for (const key of keys) {
744
+ this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
745
+ }
746
+ }
747
+ isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
748
+ if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
749
+ return true;
750
+ }
751
+ if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
752
+ return true;
753
+ }
754
+ return false;
755
+ }
756
+ async enqueueWriteBehind(operation, options, flushBatch) {
757
+ this.writeBehindQueue.push(operation);
758
+ const batchSize = options?.batchSize ?? 100;
759
+ const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
760
+ if (this.writeBehindQueue.length >= batchSize) {
761
+ await this.flushWriteBehindQueue(options, flushBatch);
762
+ return;
763
+ }
764
+ if (this.writeBehindQueue.length >= maxQueueSize) {
765
+ await this.flushWriteBehindQueue(options, flushBatch);
766
+ }
767
+ }
768
+ async flushWriteBehindQueue(options, flushBatch) {
769
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
770
+ await this.writeBehindFlushPromise;
771
+ return;
772
+ }
773
+ const batchSize = options?.batchSize ?? 100;
774
+ const batch = this.writeBehindQueue.splice(0, batchSize);
775
+ this.writeBehindFlushPromise = flushBatch(batch);
776
+ try {
777
+ await this.writeBehindFlushPromise;
778
+ } finally {
779
+ this.writeBehindFlushPromise = void 0;
780
+ }
781
+ if (this.writeBehindQueue.length > 0) {
782
+ await this.flushWriteBehindQueue(options, flushBatch);
783
+ }
784
+ }
785
+ scheduleGenerationCleanup(generation, task, onError) {
786
+ const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
787
+ onError(generation, error);
788
+ });
789
+ this.generationCleanupPromise = scheduledTask.finally(() => {
790
+ if (this.generationCleanupPromise === scheduledTask) {
791
+ this.generationCleanupPromise = void 0;
792
+ }
793
+ });
794
+ }
795
+ async waitForGenerationCleanup() {
796
+ await this.generationCleanupPromise;
797
+ }
798
+ };
799
+
800
+ // src/internal/StoredValue.ts
801
+ function isStoredValueEnvelope(value) {
802
+ if (typeof value !== "object" || value === null) {
803
+ return false;
804
+ }
805
+ const v = value;
806
+ if (v.__layercache !== 1) {
807
+ return false;
808
+ }
809
+ if (v.kind !== "value" && v.kind !== "empty") {
810
+ return false;
811
+ }
812
+ if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
813
+ return false;
814
+ }
815
+ if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
816
+ return false;
817
+ }
818
+ if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
819
+ return false;
820
+ }
821
+ const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
822
+ if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
823
+ return false;
824
+ }
825
+ if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
826
+ return false;
827
+ }
828
+ if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
829
+ return false;
830
+ }
831
+ if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
832
+ return false;
833
+ }
834
+ if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
835
+ return false;
836
+ }
837
+ if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
838
+ return false;
839
+ }
840
+ const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
841
+ if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
842
+ return false;
843
+ }
844
+ if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
845
+ return false;
846
+ }
847
+ if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
848
+ return false;
849
+ }
850
+ if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
851
+ return false;
852
+ }
853
+ return true;
854
+ }
855
+ function createStoredValueEnvelope(options) {
856
+ const now = options.now ?? Date.now();
857
+ const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
858
+ const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
859
+ const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
860
+ const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
861
+ const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
862
+ const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
863
+ return {
864
+ __layercache: 1,
865
+ kind: options.kind,
866
+ value: options.value,
867
+ freshUntil,
868
+ staleUntil,
869
+ errorUntil,
870
+ freshTtlSeconds: freshTtlSeconds ?? null,
871
+ staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
872
+ staleIfErrorSeconds: staleIfErrorSeconds ?? null
873
+ };
874
+ }
875
+ function resolveStoredValue(stored, now = Date.now()) {
876
+ if (!isStoredValueEnvelope(stored)) {
877
+ return { state: "fresh", value: stored, stored };
878
+ }
879
+ if (stored.freshUntil === null || stored.freshUntil > now) {
880
+ return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
881
+ }
882
+ if (stored.staleUntil !== null && stored.staleUntil > now) {
883
+ return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
884
+ }
885
+ if (stored.errorUntil !== null && stored.errorUntil > now) {
886
+ return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
887
+ }
888
+ return { state: "expired", value: null, stored, envelope: stored };
889
+ }
890
+ function unwrapStoredValue(stored) {
891
+ if (!isStoredValueEnvelope(stored)) {
892
+ return stored;
893
+ }
894
+ if (stored.kind === "empty") {
895
+ return null;
896
+ }
897
+ return stored.value ?? null;
898
+ }
899
+ function remainingStoredTtlSeconds(stored, now = Date.now()) {
900
+ if (!isStoredValueEnvelope(stored)) {
901
+ return void 0;
902
+ }
903
+ const expiry = maxExpiry(stored);
904
+ if (expiry === null) {
905
+ return void 0;
906
+ }
907
+ const remainingMs = expiry - now;
908
+ if (remainingMs <= 0) {
909
+ return 1;
910
+ }
911
+ return Math.max(1, Math.ceil(remainingMs / 1e3));
912
+ }
913
+ function remainingFreshTtlSeconds(stored, now = Date.now()) {
914
+ if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
915
+ return void 0;
916
+ }
917
+ const remainingMs = stored.freshUntil - now;
918
+ if (remainingMs <= 0) {
919
+ return 0;
920
+ }
921
+ return Math.max(1, Math.ceil(remainingMs / 1e3));
922
+ }
923
+ function refreshStoredEnvelope(stored, now = Date.now()) {
924
+ if (!isStoredValueEnvelope(stored)) {
925
+ return stored;
926
+ }
927
+ return createStoredValueEnvelope({
928
+ kind: stored.kind,
929
+ value: stored.value,
930
+ freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
931
+ staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
932
+ staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
933
+ now
934
+ });
935
+ }
936
+ function maxExpiry(stored) {
937
+ const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
938
+ (value) => value !== null
939
+ );
940
+ if (values.length === 0) {
941
+ return null;
942
+ }
943
+ return Math.max(...values);
944
+ }
945
+ function normalizePositiveSeconds(value) {
946
+ if (!value || value <= 0) {
947
+ return void 0;
948
+ }
949
+ return value;
950
+ }
951
+ function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
952
+ if (value == null) {
953
+ return true;
954
+ }
955
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
956
+ }
957
+
958
+ // src/internal/CacheStackRuntimePolicy.ts
959
+ function shouldSkipLayer(degradedUntil, now = Date.now()) {
960
+ return degradedUntil !== void 0 && degradedUntil > now;
961
+ }
962
+ function shouldStartBackgroundRefresh({
963
+ isDisconnecting,
964
+ hasRefreshInFlight
965
+ }) {
966
+ return !isDisconnecting && !hasRefreshInFlight;
967
+ }
968
+ function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
969
+ if (!gracefulDegradation) {
970
+ return { degrade: false };
971
+ }
972
+ const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
973
+ return {
974
+ degrade: true,
975
+ degradedUntil: now + retryAfterMs
976
+ };
977
+ }
978
+ function planFreshReadPolicies({
979
+ stored,
980
+ hasFetcher,
981
+ slidingTtl,
982
+ refreshAheadSeconds
983
+ }) {
984
+ const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
985
+ const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
986
+ const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
987
+ return {
988
+ refreshedStored,
989
+ refreshedStoredTtl,
990
+ shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
991
+ };
646
992
  }
647
993
 
648
994
  // src/internal/CacheStackValidation.ts
@@ -798,7 +1144,6 @@ var CircuitBreakerManager = class {
798
1144
  if (!options) {
799
1145
  return;
800
1146
  }
801
- this.pruneIfNeeded();
802
1147
  const failureThreshold = options.failureThreshold ?? 3;
803
1148
  const cooldownMs = options.cooldownMs ?? 3e4;
804
1149
  const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
@@ -807,6 +1152,7 @@ var CircuitBreakerManager = class {
807
1152
  state.openUntil = Date.now() + cooldownMs;
808
1153
  }
809
1154
  this.breakers.set(key, state);
1155
+ this.pruneIfNeeded();
810
1156
  }
811
1157
  recordSuccess(key) {
812
1158
  this.breakers.delete(key);
@@ -1150,164 +1496,6 @@ var MetricsCollector = class {
1150
1496
  }
1151
1497
  };
1152
1498
 
1153
- // src/internal/StoredValue.ts
1154
- function isStoredValueEnvelope(value) {
1155
- if (typeof value !== "object" || value === null) {
1156
- return false;
1157
- }
1158
- const v = value;
1159
- if (v.__layercache !== 1) {
1160
- return false;
1161
- }
1162
- if (v.kind !== "value" && v.kind !== "empty") {
1163
- return false;
1164
- }
1165
- if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
1166
- return false;
1167
- }
1168
- if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
1169
- return false;
1170
- }
1171
- if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
1172
- return false;
1173
- }
1174
- const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
1175
- if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
1176
- return false;
1177
- }
1178
- if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
1179
- return false;
1180
- }
1181
- if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
1182
- return false;
1183
- }
1184
- if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
1185
- return false;
1186
- }
1187
- if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
1188
- return false;
1189
- }
1190
- if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
1191
- return false;
1192
- }
1193
- const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
1194
- if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
1195
- return false;
1196
- }
1197
- if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
1198
- return false;
1199
- }
1200
- if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
1201
- return false;
1202
- }
1203
- if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
1204
- return false;
1205
- }
1206
- return true;
1207
- }
1208
- function createStoredValueEnvelope(options) {
1209
- const now = options.now ?? Date.now();
1210
- const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
1211
- const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
1212
- const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
1213
- const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
1214
- const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
1215
- const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
1216
- return {
1217
- __layercache: 1,
1218
- kind: options.kind,
1219
- value: options.value,
1220
- freshUntil,
1221
- staleUntil,
1222
- errorUntil,
1223
- freshTtlSeconds: freshTtlSeconds ?? null,
1224
- staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
1225
- staleIfErrorSeconds: staleIfErrorSeconds ?? null
1226
- };
1227
- }
1228
- function resolveStoredValue(stored, now = Date.now()) {
1229
- if (!isStoredValueEnvelope(stored)) {
1230
- return { state: "fresh", value: stored, stored };
1231
- }
1232
- if (stored.freshUntil === null || stored.freshUntil > now) {
1233
- return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
1234
- }
1235
- if (stored.staleUntil !== null && stored.staleUntil > now) {
1236
- return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
1237
- }
1238
- if (stored.errorUntil !== null && stored.errorUntil > now) {
1239
- return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
1240
- }
1241
- return { state: "expired", value: null, stored, envelope: stored };
1242
- }
1243
- function unwrapStoredValue(stored) {
1244
- if (!isStoredValueEnvelope(stored)) {
1245
- return stored;
1246
- }
1247
- if (stored.kind === "empty") {
1248
- return null;
1249
- }
1250
- return stored.value ?? null;
1251
- }
1252
- function remainingStoredTtlSeconds(stored, now = Date.now()) {
1253
- if (!isStoredValueEnvelope(stored)) {
1254
- return void 0;
1255
- }
1256
- const expiry = maxExpiry(stored);
1257
- if (expiry === null) {
1258
- return void 0;
1259
- }
1260
- const remainingMs = expiry - now;
1261
- if (remainingMs <= 0) {
1262
- return 1;
1263
- }
1264
- return Math.max(1, Math.ceil(remainingMs / 1e3));
1265
- }
1266
- function remainingFreshTtlSeconds(stored, now = Date.now()) {
1267
- if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
1268
- return void 0;
1269
- }
1270
- const remainingMs = stored.freshUntil - now;
1271
- if (remainingMs <= 0) {
1272
- return 0;
1273
- }
1274
- return Math.max(1, Math.ceil(remainingMs / 1e3));
1275
- }
1276
- function refreshStoredEnvelope(stored, now = Date.now()) {
1277
- if (!isStoredValueEnvelope(stored)) {
1278
- return stored;
1279
- }
1280
- return createStoredValueEnvelope({
1281
- kind: stored.kind,
1282
- value: stored.value,
1283
- freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
1284
- staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
1285
- staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
1286
- now
1287
- });
1288
- }
1289
- function maxExpiry(stored) {
1290
- const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
1291
- (value) => value !== null
1292
- );
1293
- if (values.length === 0) {
1294
- return null;
1295
- }
1296
- return Math.max(...values);
1297
- }
1298
- function normalizePositiveSeconds(value) {
1299
- if (!value || value <= 0) {
1300
- return void 0;
1301
- }
1302
- return value;
1303
- }
1304
- function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
1305
- if (value == null) {
1306
- return true;
1307
- }
1308
- return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
1309
- }
1310
-
1311
1499
  // src/internal/TtlResolver.ts
1312
1500
  var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
1313
1501
  var TtlResolver = class {
@@ -1841,15 +2029,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
1841
2029
  snapshotSerializer = new JsonSerializer();
1842
2030
  backgroundRefreshes = /* @__PURE__ */ new Map();
1843
2031
  layerDegradedUntil = /* @__PURE__ */ new Map();
1844
- keyEpochs = /* @__PURE__ */ new Map();
2032
+ maintenance = new CacheStackMaintenance();
1845
2033
  ttlResolver;
1846
2034
  circuitBreakerManager;
1847
2035
  currentGeneration;
1848
- writeBehindQueue = [];
1849
- writeBehindTimer;
1850
- writeBehindFlushPromise;
1851
- generationCleanupPromise;
1852
- clearEpoch = 0;
1853
2036
  isDisconnecting = false;
1854
2037
  disconnectPromise;
1855
2038
  /**
@@ -2005,7 +2188,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2005
2188
  }
2006
2189
  async clear() {
2007
2190
  await this.awaitStartup("clear");
2008
- this.beginClearEpoch();
2191
+ this.maintenance.beginClearEpoch();
2009
2192
  await Promise.all(this.layers.map((layer) => layer.clear()));
2010
2193
  await this.tagIndex.clear();
2011
2194
  this.ttlResolver.clearProfiles();
@@ -2263,9 +2446,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
2263
2446
  bumpGeneration(nextGeneration) {
2264
2447
  const current = this.currentGeneration ?? 0;
2265
2448
  const previousGeneration = this.currentGeneration;
2266
- this.currentGeneration = nextGeneration ?? current + 1;
2267
- if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
2268
- this.scheduleGenerationCleanup(previousGeneration);
2449
+ const updatedGeneration = nextGeneration ?? current + 1;
2450
+ const generationToCleanup = resolveGenerationCleanupTarget({
2451
+ previousGeneration,
2452
+ nextGeneration: updatedGeneration,
2453
+ generationCleanup: this.options.generationCleanup
2454
+ });
2455
+ this.currentGeneration = updatedGeneration;
2456
+ if (generationToCleanup !== null) {
2457
+ this.scheduleGenerationCleanup(generationToCleanup);
2269
2458
  }
2270
2459
  return this.currentGeneration;
2271
2460
  }
@@ -2409,12 +2598,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
2409
2598
  await this.startup;
2410
2599
  await this.unsubscribeInvalidation?.();
2411
2600
  await this.flushWriteBehindQueue();
2412
- await this.generationCleanupPromise;
2601
+ await this.maintenance.waitForGenerationCleanup();
2413
2602
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
2414
- if (this.writeBehindTimer) {
2415
- clearInterval(this.writeBehindTimer);
2416
- this.writeBehindTimer = void 0;
2417
- }
2603
+ this.maintenance.disposeWriteBehindTimer();
2418
2604
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
2419
2605
  })();
2420
2606
  }
@@ -2490,13 +2676,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
2490
2676
  if (!this.shouldNegativeCache(options)) {
2491
2677
  return null;
2492
2678
  }
2493
- if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2679
+ if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2494
2680
  this.logger.debug?.("skip-negative-store-after-invalidation", {
2495
2681
  key,
2496
2682
  expectedClearEpoch,
2497
- clearEpoch: this.clearEpoch,
2683
+ clearEpoch: this.maintenance.currentClearEpoch(),
2498
2684
  expectedKeyEpoch,
2499
- keyEpoch: this.currentKeyEpoch(key)
2685
+ keyEpoch: this.maintenance.currentKeyEpoch(key)
2500
2686
  });
2501
2687
  return null;
2502
2688
  }
@@ -2512,13 +2698,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
2512
2698
  this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
2513
2699
  }
2514
2700
  }
2515
- if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2701
+ if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2516
2702
  this.logger.debug?.("skip-store-after-invalidation", {
2517
2703
  key,
2518
2704
  expectedClearEpoch,
2519
- clearEpoch: this.clearEpoch,
2705
+ clearEpoch: this.maintenance.currentClearEpoch(),
2520
2706
  expectedKeyEpoch,
2521
- keyEpoch: this.currentKeyEpoch(key)
2707
+ keyEpoch: this.maintenance.currentKeyEpoch(key)
2522
2708
  });
2523
2709
  return fetched;
2524
2710
  }
@@ -2526,10 +2712,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
2526
2712
  return fetched;
2527
2713
  }
2528
2714
  async storeEntry(key, kind, value, options) {
2529
- const clearEpoch = this.clearEpoch;
2530
- const keyEpoch = this.currentKeyEpoch(key);
2715
+ const clearEpoch = this.maintenance.currentClearEpoch();
2716
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2531
2717
  await this.writeAcrossLayers(key, kind, value, options);
2532
- if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2718
+ if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2533
2719
  return;
2534
2720
  }
2535
2721
  if (options?.tags) {
@@ -2546,8 +2732,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
2546
2732
  }
2547
2733
  async writeBatch(entries) {
2548
2734
  const now = Date.now();
2549
- const clearEpoch = this.clearEpoch;
2550
- const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
2735
+ const clearEpoch = this.maintenance.currentClearEpoch();
2736
+ const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
2551
2737
  const entriesByLayer = /* @__PURE__ */ new Map();
2552
2738
  const immediateOperations = [];
2553
2739
  const deferredOperations = [];
@@ -2564,11 +2750,11 @@ var CacheStack = class extends import_node_events.EventEmitter {
2564
2750
  }
2565
2751
  for (const [layer, layerEntries] of entriesByLayer.entries()) {
2566
2752
  const operation = async () => {
2567
- if (clearEpoch !== this.clearEpoch) {
2753
+ if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2568
2754
  return;
2569
2755
  }
2570
2756
  const activeEntries = layerEntries.filter(
2571
- (entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
2757
+ (entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
2572
2758
  );
2573
2759
  if (activeEntries.length === 0) {
2574
2760
  return;
@@ -2591,11 +2777,11 @@ var CacheStack = class extends import_node_events.EventEmitter {
2591
2777
  }
2592
2778
  await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
2593
2779
  await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
2594
- if (clearEpoch !== this.clearEpoch) {
2780
+ if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2595
2781
  return;
2596
2782
  }
2597
2783
  for (const entry of entries) {
2598
- if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2784
+ if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2599
2785
  continue;
2600
2786
  }
2601
2787
  if (entry.options?.tags) {
@@ -2699,13 +2885,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
2699
2885
  }
2700
2886
  async writeAcrossLayers(key, kind, value, options) {
2701
2887
  const now = Date.now();
2702
- const clearEpoch = this.clearEpoch;
2703
- const keyEpoch = this.currentKeyEpoch(key);
2888
+ const clearEpoch = this.maintenance.currentClearEpoch();
2889
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2704
2890
  const immediateOperations = [];
2705
2891
  const deferredOperations = [];
2706
2892
  for (const layer of this.layers) {
2707
2893
  const operation = async () => {
2708
- if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2894
+ if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2709
2895
  return;
2710
2896
  }
2711
2897
  if (this.shouldSkipLayer(layer)) {
@@ -2768,11 +2954,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
2768
2954
  return options?.negativeCache ?? this.options.negativeCaching ?? false;
2769
2955
  }
2770
2956
  scheduleBackgroundRefresh(key, fetcher, options) {
2771
- if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
2957
+ if (!shouldStartBackgroundRefresh({
2958
+ isDisconnecting: this.isDisconnecting,
2959
+ hasRefreshInFlight: this.backgroundRefreshes.has(key)
2960
+ })) {
2772
2961
  return;
2773
2962
  }
2774
- const clearEpoch = this.clearEpoch;
2775
- const keyEpoch = this.currentKeyEpoch(key);
2963
+ const clearEpoch = this.maintenance.currentClearEpoch();
2964
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2776
2965
  const refresh = (async () => {
2777
2966
  this.metricsCollector.increment("refreshes");
2778
2967
  try {
@@ -2810,7 +2999,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2810
2999
  if (keys.length === 0) {
2811
3000
  return;
2812
3001
  }
2813
- this.bumpKeyEpochs(keys);
3002
+ this.maintenance.bumpKeyEpochs(keys);
2814
3003
  await this.deleteKeysFromLayers(this.layers, keys);
2815
3004
  for (const key of keys) {
2816
3005
  await this.tagIndex.remove(key);
@@ -2834,7 +3023,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2834
3023
  }
2835
3024
  const localLayers = this.layers.filter((layer) => layer.isLocal);
2836
3025
  if (message.scope === "clear") {
2837
- this.beginClearEpoch();
3026
+ this.maintenance.beginClearEpoch();
2838
3027
  await Promise.all(localLayers.map((layer) => layer.clear()));
2839
3028
  await this.tagIndex.clear();
2840
3029
  this.ttlResolver.clearProfiles();
@@ -2842,7 +3031,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2842
3031
  return;
2843
3032
  }
2844
3033
  const keys = message.keys ?? [];
2845
- this.bumpKeyEpochs(keys);
3034
+ this.maintenance.bumpKeyEpochs(keys);
2846
3035
  await this.deleteKeysFromLayers(localLayers, keys);
2847
3036
  if (message.operation !== "write") {
2848
3037
  for (const key of keys) {
@@ -2900,35 +3089,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
2900
3089
  shouldBroadcastL1Invalidation() {
2901
3090
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
2902
3091
  }
2903
- shouldCleanupGenerations() {
2904
- return Boolean(this.options.generationCleanup);
2905
- }
2906
- generationCleanupBatchSize() {
2907
- const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
2908
- return configured ?? 500;
2909
- }
2910
3092
  scheduleGenerationCleanup(generation) {
2911
- const task = (this.generationCleanupPromise ?? Promise.resolve()).then(() => this.cleanupGeneration(generation)).catch((error) => {
2912
- this.logger.warn?.("generation-cleanup-error", {
2913
- generation,
2914
- error: this.formatError(error)
2915
- });
2916
- });
2917
- this.generationCleanupPromise = task.finally(() => {
2918
- if (this.generationCleanupPromise === task) {
2919
- this.generationCleanupPromise = void 0;
3093
+ this.maintenance.scheduleGenerationCleanup(
3094
+ generation,
3095
+ async (generationToClean) => this.cleanupGeneration(generationToClean),
3096
+ (failedGeneration, error) => {
3097
+ this.logger.warn?.("generation-cleanup-error", {
3098
+ generation: failedGeneration,
3099
+ error: this.formatError(error)
3100
+ });
2920
3101
  }
2921
- });
3102
+ );
2922
3103
  }
2923
3104
  async cleanupGeneration(generation) {
2924
3105
  const prefix = `v${generation}:`;
2925
3106
  const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
2926
- if (keys.length === 0) {
2927
- return;
2928
- }
2929
- const batchSize = this.generationCleanupBatchSize();
2930
- for (let index = 0; index < keys.length; index += batchSize) {
2931
- const batch = keys.slice(index, index + batchSize);
3107
+ for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
2932
3108
  await this.deleteKeys(batch);
2933
3109
  await this.publishInvalidation({
2934
3110
  scope: "keys",
@@ -2939,80 +3115,34 @@ var CacheStack = class extends import_node_events.EventEmitter {
2939
3115
  }
2940
3116
  }
2941
3117
  initializeWriteBehind(options) {
2942
- if (this.options.writeStrategy !== "write-behind") {
2943
- return;
2944
- }
2945
- const flushIntervalMs = options?.flushIntervalMs;
2946
- if (!flushIntervalMs || flushIntervalMs <= 0) {
2947
- return;
2948
- }
2949
- this.writeBehindTimer = setInterval(() => {
2950
- void this.flushWriteBehindQueue();
2951
- }, flushIntervalMs);
2952
- this.writeBehindTimer.unref?.();
3118
+ this.maintenance.initializeWriteBehindTimer(
3119
+ this.options.writeStrategy,
3120
+ options,
3121
+ this.flushWriteBehindQueue.bind(this)
3122
+ );
2953
3123
  }
2954
3124
  shouldWriteBehind(layer) {
2955
3125
  return this.options.writeStrategy === "write-behind" && !layer.isLocal;
2956
3126
  }
2957
- beginClearEpoch() {
2958
- this.clearEpoch += 1;
2959
- this.keyEpochs.clear();
2960
- this.writeBehindQueue.length = 0;
2961
- }
2962
- currentKeyEpoch(key) {
2963
- return this.keyEpochs.get(key) ?? 0;
2964
- }
2965
- bumpKeyEpochs(keys) {
2966
- for (const key of keys) {
2967
- this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
2968
- }
2969
- }
2970
- isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
2971
- if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
2972
- return true;
2973
- }
2974
- if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
2975
- return true;
2976
- }
2977
- return false;
2978
- }
2979
3127
  async enqueueWriteBehind(operation) {
2980
- this.writeBehindQueue.push(operation);
2981
- const batchSize = this.options.writeBehind?.batchSize ?? 100;
2982
- const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
2983
- if (this.writeBehindQueue.length >= batchSize) {
2984
- await this.flushWriteBehindQueue();
2985
- return;
2986
- }
2987
- if (this.writeBehindQueue.length >= maxQueueSize) {
2988
- await this.flushWriteBehindQueue();
2989
- }
3128
+ await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
2990
3129
  }
2991
3130
  async flushWriteBehindQueue() {
2992
- if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
2993
- await this.writeBehindFlushPromise;
3131
+ await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
3132
+ }
3133
+ async runWriteBehindBatch(batch) {
3134
+ const results = await Promise.allSettled(batch.map((operation) => operation()));
3135
+ const failures = results.filter((result) => result.status === "rejected");
3136
+ if (failures.length === 0) {
2994
3137
  return;
2995
3138
  }
2996
- const batchSize = this.options.writeBehind?.batchSize ?? 100;
2997
- const batch = this.writeBehindQueue.splice(0, batchSize);
2998
- this.writeBehindFlushPromise = (async () => {
2999
- const results = await Promise.allSettled(batch.map((operation) => operation()));
3000
- const failures = results.filter((result) => result.status === "rejected");
3001
- if (failures.length > 0) {
3002
- this.metricsCollector.increment("writeFailures", failures.length);
3003
- this.logger.error?.("write-behind-flush-failure", {
3004
- failed: failures.length,
3005
- total: batch.length,
3006
- errors: failures.map((failure) => this.formatError(failure.reason))
3007
- });
3008
- this.emitError("write-behind", { failed: failures.length, total: batch.length });
3009
- }
3010
- })();
3011
- await this.writeBehindFlushPromise;
3012
- this.writeBehindFlushPromise = void 0;
3013
- if (this.writeBehindQueue.length > 0) {
3014
- await this.flushWriteBehindQueue();
3015
- }
3139
+ this.metricsCollector.increment("writeFailures", failures.length);
3140
+ this.logger.error?.("write-behind-flush-failure", {
3141
+ failed: failures.length,
3142
+ total: batch.length,
3143
+ errors: failures.map((failure) => this.formatError(failure.reason))
3144
+ });
3145
+ this.emitError("write-behind", { failed: failures.length, total: batch.length });
3016
3146
  }
3017
3147
  buildLayerSetEntry(layer, key, kind, value, options, now) {
3018
3148
  const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
@@ -3042,32 +3172,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
3042
3172
  return [];
3043
3173
  }
3044
3174
  const [firstGroup, ...rest] = groups;
3045
- if (!firstGroup) {
3046
- return [];
3047
- }
3048
3175
  const restSets = rest.map((group) => new Set(group));
3049
3176
  return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
3050
3177
  }
3051
3178
  qualifyKey(key) {
3052
- const prefix = this.generationPrefix();
3053
- return prefix ? `${prefix}${key}` : key;
3179
+ return qualifyGenerationKey(key, this.currentGeneration);
3054
3180
  }
3055
3181
  qualifyPattern(pattern) {
3056
- const prefix = this.generationPrefix();
3057
- return prefix ? `${prefix}${pattern}` : pattern;
3182
+ return qualifyGenerationPattern(pattern, this.currentGeneration);
3058
3183
  }
3059
3184
  stripQualifiedKey(key) {
3060
- const prefix = this.generationPrefix();
3061
- if (!prefix || !key.startsWith(prefix)) {
3062
- return key;
3063
- }
3064
- return key.slice(prefix.length);
3065
- }
3066
- generationPrefix() {
3067
- if (this.currentGeneration === void 0) {
3068
- return "";
3069
- }
3070
- return `v${this.currentGeneration}:`;
3185
+ return stripGenerationPrefix(key, this.currentGeneration);
3071
3186
  }
3072
3187
  async deleteKeysFromLayers(layers, keys) {
3073
3188
  await Promise.all(
@@ -3158,37 +3273,38 @@ var CacheStack = class extends import_node_events.EventEmitter {
3158
3273
  this.assertActive(operation);
3159
3274
  }
3160
3275
  async applyFreshReadPolicies(key, hit, options, fetcher) {
3161
- const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
3162
- const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
3163
- if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
3164
- const refreshed = refreshStoredEnvelope(hit.stored);
3165
- const ttl = remainingStoredTtlSeconds(refreshed);
3276
+ const plan = planFreshReadPolicies({
3277
+ stored: hit.stored,
3278
+ hasFetcher: Boolean(fetcher),
3279
+ slidingTtl: options?.slidingTtl ?? false,
3280
+ refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
3281
+ });
3282
+ if (plan.refreshedStored) {
3166
3283
  for (let index = 0; index <= hit.layerIndex; index += 1) {
3167
3284
  const layer = this.layers[index];
3168
3285
  if (!layer || this.shouldSkipLayer(layer)) {
3169
3286
  continue;
3170
3287
  }
3171
3288
  try {
3172
- await layer.set(key, refreshed, ttl);
3289
+ await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
3173
3290
  } catch (error) {
3174
3291
  await this.handleLayerFailure(layer, "sliding-ttl", error);
3175
3292
  }
3176
3293
  }
3177
3294
  }
3178
- if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
3295
+ if (fetcher && plan.shouldScheduleBackgroundRefresh) {
3179
3296
  this.scheduleBackgroundRefresh(key, fetcher, options);
3180
3297
  }
3181
3298
  }
3182
3299
  shouldSkipLayer(layer) {
3183
- const degradedUntil = this.layerDegradedUntil.get(layer.name);
3184
- return degradedUntil !== void 0 && degradedUntil > Date.now();
3300
+ return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
3185
3301
  }
3186
3302
  async handleLayerFailure(layer, operation, error) {
3187
- if (!this.isGracefulDegradationEnabled()) {
3303
+ const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
3304
+ if (!recovery.degrade) {
3188
3305
  throw error;
3189
3306
  }
3190
- const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
3191
- this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
3307
+ this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
3192
3308
  this.metricsCollector.increment("degradedOperations");
3193
3309
  this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
3194
3310
  this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
@@ -4805,7 +4921,7 @@ var MsgpackSerializer = class {
4805
4921
  return Buffer.from((0, import_msgpack.encode)(value));
4806
4922
  }
4807
4923
  deserialize(payload) {
4808
- const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
4924
+ const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
4809
4925
  return sanitizeMsgpackValue((0, import_msgpack.decode)(normalized), 0, { count: 0 });
4810
4926
  }
4811
4927
  };