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.
@@ -223,6 +223,125 @@ var Mutex = class {
223
223
  }
224
224
  };
225
225
 
226
+ // ../../src/internal/CacheNamespaceMetrics.ts
227
+ function createEmptyNamespaceMetrics(resetAt = Date.now()) {
228
+ return {
229
+ hits: 0,
230
+ misses: 0,
231
+ fetches: 0,
232
+ sets: 0,
233
+ deletes: 0,
234
+ backfills: 0,
235
+ invalidations: 0,
236
+ staleHits: 0,
237
+ refreshes: 0,
238
+ refreshErrors: 0,
239
+ writeFailures: 0,
240
+ singleFlightWaits: 0,
241
+ negativeCacheHits: 0,
242
+ circuitBreakerTrips: 0,
243
+ degradedOperations: 0,
244
+ hitsByLayer: {},
245
+ missesByLayer: {},
246
+ latencyByLayer: {},
247
+ resetAt
248
+ };
249
+ }
250
+ function cloneNamespaceMetrics(metrics) {
251
+ return {
252
+ ...metrics,
253
+ hitsByLayer: { ...metrics.hitsByLayer },
254
+ missesByLayer: { ...metrics.missesByLayer },
255
+ latencyByLayer: Object.fromEntries(
256
+ Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
257
+ )
258
+ };
259
+ }
260
+ function diffNamespaceMetrics(before, after) {
261
+ const latencyByLayer = Object.fromEntries(
262
+ Object.entries(after.latencyByLayer).map(([layer, value]) => [
263
+ layer,
264
+ {
265
+ avgMs: value.avgMs,
266
+ maxMs: value.maxMs,
267
+ count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
268
+ }
269
+ ])
270
+ );
271
+ return {
272
+ hits: after.hits - before.hits,
273
+ misses: after.misses - before.misses,
274
+ fetches: after.fetches - before.fetches,
275
+ sets: after.sets - before.sets,
276
+ deletes: after.deletes - before.deletes,
277
+ backfills: after.backfills - before.backfills,
278
+ invalidations: after.invalidations - before.invalidations,
279
+ staleHits: after.staleHits - before.staleHits,
280
+ refreshes: after.refreshes - before.refreshes,
281
+ refreshErrors: after.refreshErrors - before.refreshErrors,
282
+ writeFailures: after.writeFailures - before.writeFailures,
283
+ singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
284
+ negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
285
+ circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
286
+ degradedOperations: after.degradedOperations - before.degradedOperations,
287
+ hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
288
+ missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
289
+ latencyByLayer,
290
+ resetAt: after.resetAt
291
+ };
292
+ }
293
+ function addNamespaceMetrics(base, delta) {
294
+ return {
295
+ hits: base.hits + delta.hits,
296
+ misses: base.misses + delta.misses,
297
+ fetches: base.fetches + delta.fetches,
298
+ sets: base.sets + delta.sets,
299
+ deletes: base.deletes + delta.deletes,
300
+ backfills: base.backfills + delta.backfills,
301
+ invalidations: base.invalidations + delta.invalidations,
302
+ staleHits: base.staleHits + delta.staleHits,
303
+ refreshes: base.refreshes + delta.refreshes,
304
+ refreshErrors: base.refreshErrors + delta.refreshErrors,
305
+ writeFailures: base.writeFailures + delta.writeFailures,
306
+ singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
307
+ negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
308
+ circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
309
+ degradedOperations: base.degradedOperations + delta.degradedOperations,
310
+ hitsByLayer: addMetricMap(base.hitsByLayer, delta.hitsByLayer),
311
+ missesByLayer: addMetricMap(base.missesByLayer, delta.missesByLayer),
312
+ latencyByLayer: cloneNamespaceMetrics(delta).latencyByLayer,
313
+ resetAt: base.resetAt
314
+ };
315
+ }
316
+ function computeNamespaceHitRate(metrics) {
317
+ const total = metrics.hits + metrics.misses;
318
+ const overall = total === 0 ? 0 : metrics.hits / total;
319
+ const byLayer = {};
320
+ const layers = /* @__PURE__ */ new Set([...Object.keys(metrics.hitsByLayer), ...Object.keys(metrics.missesByLayer)]);
321
+ for (const layer of layers) {
322
+ const hits = metrics.hitsByLayer[layer] ?? 0;
323
+ const misses = metrics.missesByLayer[layer] ?? 0;
324
+ byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
325
+ }
326
+ return { overall, byLayer };
327
+ }
328
+ function diffMetricMap(before, after) {
329
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
330
+ const result = {};
331
+ for (const key of keys) {
332
+ result[key] = (after[key] ?? 0) - (before[key] ?? 0);
333
+ }
334
+ return result;
335
+ }
336
+ function addMetricMap(base, delta) {
337
+ const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
338
+ const result = {};
339
+ for (const key of keys) {
340
+ result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
341
+ }
342
+ return result;
343
+ }
344
+
226
345
  // ../../src/CacheNamespace.ts
227
346
  var CacheNamespace = class _CacheNamespace {
228
347
  constructor(cache, prefix) {
@@ -233,7 +352,7 @@ var CacheNamespace = class _CacheNamespace {
233
352
  cache;
234
353
  prefix;
235
354
  static metricsMutexes = /* @__PURE__ */ new WeakMap();
236
- metrics = emptyMetrics();
355
+ metrics = createEmptyNamespaceMetrics();
237
356
  async get(key, fetcher, options) {
238
357
  return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
239
358
  }
@@ -330,19 +449,10 @@ var CacheNamespace = class _CacheNamespace {
330
449
  );
331
450
  }
332
451
  getMetrics() {
333
- return cloneMetrics(this.metrics);
452
+ return cloneNamespaceMetrics(this.metrics);
334
453
  }
335
454
  getHitRate() {
336
- const total = this.metrics.hits + this.metrics.misses;
337
- const overall = total === 0 ? 0 : this.metrics.hits / total;
338
- const byLayer = {};
339
- const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
340
- for (const layer of layers) {
341
- const hits = this.metrics.hitsByLayer[layer] ?? 0;
342
- const misses = this.metrics.missesByLayer[layer] ?? 0;
343
- byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
344
- }
345
- return { overall, byLayer };
455
+ return computeNamespaceHitRate(this.metrics);
346
456
  }
347
457
  /**
348
458
  * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
@@ -383,7 +493,7 @@ var CacheNamespace = class _CacheNamespace {
383
493
  const before = this.cache.getMetrics();
384
494
  const result = await operation();
385
495
  const after = this.cache.getMetrics();
386
- this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
496
+ this.metrics = addNamespaceMetrics(this.metrics, diffNamespaceMetrics(before, after));
387
497
  return result;
388
498
  });
389
499
  }
@@ -397,111 +507,6 @@ var CacheNamespace = class _CacheNamespace {
397
507
  return mutex;
398
508
  }
399
509
  };
400
- function emptyMetrics() {
401
- return {
402
- hits: 0,
403
- misses: 0,
404
- fetches: 0,
405
- sets: 0,
406
- deletes: 0,
407
- backfills: 0,
408
- invalidations: 0,
409
- staleHits: 0,
410
- refreshes: 0,
411
- refreshErrors: 0,
412
- writeFailures: 0,
413
- singleFlightWaits: 0,
414
- negativeCacheHits: 0,
415
- circuitBreakerTrips: 0,
416
- degradedOperations: 0,
417
- hitsByLayer: {},
418
- missesByLayer: {},
419
- latencyByLayer: {},
420
- resetAt: Date.now()
421
- };
422
- }
423
- function cloneMetrics(metrics) {
424
- return {
425
- ...metrics,
426
- hitsByLayer: { ...metrics.hitsByLayer },
427
- missesByLayer: { ...metrics.missesByLayer },
428
- latencyByLayer: Object.fromEntries(
429
- Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
430
- )
431
- };
432
- }
433
- function diffMetrics(before, after) {
434
- const latencyByLayer = Object.fromEntries(
435
- Object.entries(after.latencyByLayer).map(([layer, value]) => [
436
- layer,
437
- {
438
- avgMs: value.avgMs,
439
- maxMs: value.maxMs,
440
- count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
441
- }
442
- ])
443
- );
444
- return {
445
- hits: after.hits - before.hits,
446
- misses: after.misses - before.misses,
447
- fetches: after.fetches - before.fetches,
448
- sets: after.sets - before.sets,
449
- deletes: after.deletes - before.deletes,
450
- backfills: after.backfills - before.backfills,
451
- invalidations: after.invalidations - before.invalidations,
452
- staleHits: after.staleHits - before.staleHits,
453
- refreshes: after.refreshes - before.refreshes,
454
- refreshErrors: after.refreshErrors - before.refreshErrors,
455
- writeFailures: after.writeFailures - before.writeFailures,
456
- singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
457
- negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
458
- circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
459
- degradedOperations: after.degradedOperations - before.degradedOperations,
460
- hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
461
- missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
462
- latencyByLayer,
463
- resetAt: after.resetAt
464
- };
465
- }
466
- function addMetrics(base, delta) {
467
- return {
468
- hits: base.hits + delta.hits,
469
- misses: base.misses + delta.misses,
470
- fetches: base.fetches + delta.fetches,
471
- sets: base.sets + delta.sets,
472
- deletes: base.deletes + delta.deletes,
473
- backfills: base.backfills + delta.backfills,
474
- invalidations: base.invalidations + delta.invalidations,
475
- staleHits: base.staleHits + delta.staleHits,
476
- refreshes: base.refreshes + delta.refreshes,
477
- refreshErrors: base.refreshErrors + delta.refreshErrors,
478
- writeFailures: base.writeFailures + delta.writeFailures,
479
- singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
480
- negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
481
- circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
482
- degradedOperations: base.degradedOperations + delta.degradedOperations,
483
- hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
484
- missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
485
- latencyByLayer: cloneMetrics(delta).latencyByLayer,
486
- resetAt: base.resetAt
487
- };
488
- }
489
- function diffMap(before, after) {
490
- const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
491
- const result = {};
492
- for (const key of keys) {
493
- result[key] = (after[key] ?? 0) - (before[key] ?? 0);
494
- }
495
- return result;
496
- }
497
- function addMap(base, delta) {
498
- const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
499
- const result = {};
500
- for (const key of keys) {
501
- result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
502
- }
503
- return result;
504
- }
505
510
  function validateNamespaceKey(key) {
506
511
  if (key.length === 0) {
507
512
  throw new Error("Namespace prefix must not be empty.");
@@ -804,7 +809,346 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
804
809
  chunks.push(buffer.subarray(0, bytesRead));
805
810
  position += bytesRead;
806
811
  }
807
- return Buffer.concat(chunks).toString("utf8");
812
+ return Buffer.concat(chunks).toString("utf8");
813
+ }
814
+
815
+ // ../../src/internal/CacheStackGeneration.ts
816
+ var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
817
+ function generationPrefix(generation) {
818
+ return generation === void 0 ? "" : `v${generation}:`;
819
+ }
820
+ function qualifyGenerationKey(key, generation) {
821
+ const prefix = generationPrefix(generation);
822
+ return prefix ? `${prefix}${key}` : key;
823
+ }
824
+ function qualifyGenerationPattern(pattern, generation) {
825
+ return qualifyGenerationKey(pattern, generation);
826
+ }
827
+ function stripGenerationPrefix(key, generation) {
828
+ const prefix = generationPrefix(generation);
829
+ if (!prefix || !key.startsWith(prefix)) {
830
+ return key;
831
+ }
832
+ return key.slice(prefix.length);
833
+ }
834
+ function resolveGenerationCleanupTarget({
835
+ previousGeneration,
836
+ nextGeneration,
837
+ generationCleanup
838
+ }) {
839
+ if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
840
+ return null;
841
+ }
842
+ return previousGeneration;
843
+ }
844
+ function resolveGenerationCleanupBatchSize(generationCleanup) {
845
+ if (typeof generationCleanup !== "object" || generationCleanup === null) {
846
+ return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
847
+ }
848
+ return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
849
+ }
850
+ function planGenerationCleanupBatches(keys, generationCleanup) {
851
+ if (keys.length === 0) {
852
+ return [];
853
+ }
854
+ const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
855
+ const batches = [];
856
+ for (let index = 0; index < keys.length; index += batchSize) {
857
+ batches.push(keys.slice(index, index + batchSize));
858
+ }
859
+ return batches;
860
+ }
861
+
862
+ // ../../src/internal/CacheStackMaintenance.ts
863
+ var CacheStackMaintenance = class {
864
+ keyEpochs = /* @__PURE__ */ new Map();
865
+ writeBehindQueue = [];
866
+ writeBehindTimer;
867
+ writeBehindFlushPromise;
868
+ generationCleanupPromise;
869
+ clearEpoch = 0;
870
+ initializeWriteBehindTimer(writeStrategy, options, flush) {
871
+ if (writeStrategy !== "write-behind") {
872
+ return;
873
+ }
874
+ const flushIntervalMs = options?.flushIntervalMs;
875
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
876
+ return;
877
+ }
878
+ this.disposeWriteBehindTimer();
879
+ this.writeBehindTimer = setInterval(() => {
880
+ void flush();
881
+ }, flushIntervalMs);
882
+ this.writeBehindTimer.unref?.();
883
+ }
884
+ disposeWriteBehindTimer() {
885
+ if (!this.writeBehindTimer) {
886
+ return;
887
+ }
888
+ clearInterval(this.writeBehindTimer);
889
+ this.writeBehindTimer = void 0;
890
+ }
891
+ beginClearEpoch() {
892
+ this.clearEpoch += 1;
893
+ this.keyEpochs.clear();
894
+ this.writeBehindQueue.length = 0;
895
+ }
896
+ currentClearEpoch() {
897
+ return this.clearEpoch;
898
+ }
899
+ currentKeyEpoch(key) {
900
+ return this.keyEpochs.get(key) ?? 0;
901
+ }
902
+ bumpKeyEpochs(keys) {
903
+ for (const key of keys) {
904
+ this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
905
+ }
906
+ }
907
+ isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
908
+ if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
909
+ return true;
910
+ }
911
+ if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
912
+ return true;
913
+ }
914
+ return false;
915
+ }
916
+ async enqueueWriteBehind(operation, options, flushBatch) {
917
+ this.writeBehindQueue.push(operation);
918
+ const batchSize = options?.batchSize ?? 100;
919
+ const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
920
+ if (this.writeBehindQueue.length >= batchSize) {
921
+ await this.flushWriteBehindQueue(options, flushBatch);
922
+ return;
923
+ }
924
+ if (this.writeBehindQueue.length >= maxQueueSize) {
925
+ await this.flushWriteBehindQueue(options, flushBatch);
926
+ }
927
+ }
928
+ async flushWriteBehindQueue(options, flushBatch) {
929
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
930
+ await this.writeBehindFlushPromise;
931
+ return;
932
+ }
933
+ const batchSize = options?.batchSize ?? 100;
934
+ const batch = this.writeBehindQueue.splice(0, batchSize);
935
+ this.writeBehindFlushPromise = flushBatch(batch);
936
+ try {
937
+ await this.writeBehindFlushPromise;
938
+ } finally {
939
+ this.writeBehindFlushPromise = void 0;
940
+ }
941
+ if (this.writeBehindQueue.length > 0) {
942
+ await this.flushWriteBehindQueue(options, flushBatch);
943
+ }
944
+ }
945
+ scheduleGenerationCleanup(generation, task, onError) {
946
+ const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
947
+ onError(generation, error);
948
+ });
949
+ this.generationCleanupPromise = scheduledTask.finally(() => {
950
+ if (this.generationCleanupPromise === scheduledTask) {
951
+ this.generationCleanupPromise = void 0;
952
+ }
953
+ });
954
+ }
955
+ async waitForGenerationCleanup() {
956
+ await this.generationCleanupPromise;
957
+ }
958
+ };
959
+
960
+ // ../../src/internal/StoredValue.ts
961
+ function isStoredValueEnvelope(value) {
962
+ if (typeof value !== "object" || value === null) {
963
+ return false;
964
+ }
965
+ const v = value;
966
+ if (v.__layercache !== 1) {
967
+ return false;
968
+ }
969
+ if (v.kind !== "value" && v.kind !== "empty") {
970
+ return false;
971
+ }
972
+ if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
973
+ return false;
974
+ }
975
+ if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
976
+ return false;
977
+ }
978
+ if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
979
+ return false;
980
+ }
981
+ const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
982
+ if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
983
+ return false;
984
+ }
985
+ if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
986
+ return false;
987
+ }
988
+ if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
989
+ return false;
990
+ }
991
+ if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
992
+ return false;
993
+ }
994
+ if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
995
+ return false;
996
+ }
997
+ if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
998
+ return false;
999
+ }
1000
+ const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
1001
+ if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
1002
+ return false;
1003
+ }
1004
+ if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
1005
+ return false;
1006
+ }
1007
+ if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
1008
+ return false;
1009
+ }
1010
+ if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
1011
+ return false;
1012
+ }
1013
+ return true;
1014
+ }
1015
+ function createStoredValueEnvelope(options) {
1016
+ const now = options.now ?? Date.now();
1017
+ const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
1018
+ const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
1019
+ const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
1020
+ const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
1021
+ const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
1022
+ const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
1023
+ return {
1024
+ __layercache: 1,
1025
+ kind: options.kind,
1026
+ value: options.value,
1027
+ freshUntil,
1028
+ staleUntil,
1029
+ errorUntil,
1030
+ freshTtlSeconds: freshTtlSeconds ?? null,
1031
+ staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
1032
+ staleIfErrorSeconds: staleIfErrorSeconds ?? null
1033
+ };
1034
+ }
1035
+ function resolveStoredValue(stored, now = Date.now()) {
1036
+ if (!isStoredValueEnvelope(stored)) {
1037
+ return { state: "fresh", value: stored, stored };
1038
+ }
1039
+ if (stored.freshUntil === null || stored.freshUntil > now) {
1040
+ return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
1041
+ }
1042
+ if (stored.staleUntil !== null && stored.staleUntil > now) {
1043
+ return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
1044
+ }
1045
+ if (stored.errorUntil !== null && stored.errorUntil > now) {
1046
+ return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
1047
+ }
1048
+ return { state: "expired", value: null, stored, envelope: stored };
1049
+ }
1050
+ function unwrapStoredValue(stored) {
1051
+ if (!isStoredValueEnvelope(stored)) {
1052
+ return stored;
1053
+ }
1054
+ if (stored.kind === "empty") {
1055
+ return null;
1056
+ }
1057
+ return stored.value ?? null;
1058
+ }
1059
+ function remainingStoredTtlSeconds(stored, now = Date.now()) {
1060
+ if (!isStoredValueEnvelope(stored)) {
1061
+ return void 0;
1062
+ }
1063
+ const expiry = maxExpiry(stored);
1064
+ if (expiry === null) {
1065
+ return void 0;
1066
+ }
1067
+ const remainingMs = expiry - now;
1068
+ if (remainingMs <= 0) {
1069
+ return 1;
1070
+ }
1071
+ return Math.max(1, Math.ceil(remainingMs / 1e3));
1072
+ }
1073
+ function remainingFreshTtlSeconds(stored, now = Date.now()) {
1074
+ if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
1075
+ return void 0;
1076
+ }
1077
+ const remainingMs = stored.freshUntil - now;
1078
+ if (remainingMs <= 0) {
1079
+ return 0;
1080
+ }
1081
+ return Math.max(1, Math.ceil(remainingMs / 1e3));
1082
+ }
1083
+ function refreshStoredEnvelope(stored, now = Date.now()) {
1084
+ if (!isStoredValueEnvelope(stored)) {
1085
+ return stored;
1086
+ }
1087
+ return createStoredValueEnvelope({
1088
+ kind: stored.kind,
1089
+ value: stored.value,
1090
+ freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
1091
+ staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
1092
+ staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
1093
+ now
1094
+ });
1095
+ }
1096
+ function maxExpiry(stored) {
1097
+ const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
1098
+ (value) => value !== null
1099
+ );
1100
+ if (values.length === 0) {
1101
+ return null;
1102
+ }
1103
+ return Math.max(...values);
1104
+ }
1105
+ function normalizePositiveSeconds(value) {
1106
+ if (!value || value <= 0) {
1107
+ return void 0;
1108
+ }
1109
+ return value;
1110
+ }
1111
+ function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
1112
+ if (value == null) {
1113
+ return true;
1114
+ }
1115
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
1116
+ }
1117
+
1118
+ // ../../src/internal/CacheStackRuntimePolicy.ts
1119
+ function shouldSkipLayer(degradedUntil, now = Date.now()) {
1120
+ return degradedUntil !== void 0 && degradedUntil > now;
1121
+ }
1122
+ function shouldStartBackgroundRefresh({
1123
+ isDisconnecting,
1124
+ hasRefreshInFlight
1125
+ }) {
1126
+ return !isDisconnecting && !hasRefreshInFlight;
1127
+ }
1128
+ function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
1129
+ if (!gracefulDegradation) {
1130
+ return { degrade: false };
1131
+ }
1132
+ const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
1133
+ return {
1134
+ degrade: true,
1135
+ degradedUntil: now + retryAfterMs
1136
+ };
1137
+ }
1138
+ function planFreshReadPolicies({
1139
+ stored,
1140
+ hasFetcher,
1141
+ slidingTtl,
1142
+ refreshAheadSeconds
1143
+ }) {
1144
+ const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
1145
+ const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
1146
+ const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
1147
+ return {
1148
+ refreshedStored,
1149
+ refreshedStoredTtl,
1150
+ shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
1151
+ };
808
1152
  }
809
1153
 
810
1154
  // ../../src/internal/CacheStackValidation.ts
@@ -960,7 +1304,6 @@ var CircuitBreakerManager = class {
960
1304
  if (!options) {
961
1305
  return;
962
1306
  }
963
- this.pruneIfNeeded();
964
1307
  const failureThreshold = options.failureThreshold ?? 3;
965
1308
  const cooldownMs = options.cooldownMs ?? 3e4;
966
1309
  const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
@@ -969,6 +1312,7 @@ var CircuitBreakerManager = class {
969
1312
  state.openUntil = Date.now() + cooldownMs;
970
1313
  }
971
1314
  this.breakers.set(key, state);
1315
+ this.pruneIfNeeded();
972
1316
  }
973
1317
  recordSuccess(key) {
974
1318
  this.breakers.delete(key);
@@ -1312,164 +1656,6 @@ var MetricsCollector = class {
1312
1656
  }
1313
1657
  };
1314
1658
 
1315
- // ../../src/internal/StoredValue.ts
1316
- function isStoredValueEnvelope(value) {
1317
- if (typeof value !== "object" || value === null) {
1318
- return false;
1319
- }
1320
- const v = value;
1321
- if (v.__layercache !== 1) {
1322
- return false;
1323
- }
1324
- if (v.kind !== "value" && v.kind !== "empty") {
1325
- return false;
1326
- }
1327
- if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
1328
- return false;
1329
- }
1330
- if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
1331
- return false;
1332
- }
1333
- if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
1334
- return false;
1335
- }
1336
- const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
1337
- if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
1338
- return false;
1339
- }
1340
- if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
1341
- return false;
1342
- }
1343
- if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
1344
- return false;
1345
- }
1346
- if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
1347
- return false;
1348
- }
1349
- if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
1350
- return false;
1351
- }
1352
- if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
1353
- return false;
1354
- }
1355
- const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
1356
- if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
1357
- return false;
1358
- }
1359
- if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
1360
- return false;
1361
- }
1362
- if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
1363
- return false;
1364
- }
1365
- if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
1366
- return false;
1367
- }
1368
- return true;
1369
- }
1370
- function createStoredValueEnvelope(options) {
1371
- const now = options.now ?? Date.now();
1372
- const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
1373
- const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
1374
- const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
1375
- const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
1376
- const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
1377
- const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
1378
- return {
1379
- __layercache: 1,
1380
- kind: options.kind,
1381
- value: options.value,
1382
- freshUntil,
1383
- staleUntil,
1384
- errorUntil,
1385
- freshTtlSeconds: freshTtlSeconds ?? null,
1386
- staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
1387
- staleIfErrorSeconds: staleIfErrorSeconds ?? null
1388
- };
1389
- }
1390
- function resolveStoredValue(stored, now = Date.now()) {
1391
- if (!isStoredValueEnvelope(stored)) {
1392
- return { state: "fresh", value: stored, stored };
1393
- }
1394
- if (stored.freshUntil === null || stored.freshUntil > now) {
1395
- return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
1396
- }
1397
- if (stored.staleUntil !== null && stored.staleUntil > now) {
1398
- return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
1399
- }
1400
- if (stored.errorUntil !== null && stored.errorUntil > now) {
1401
- return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
1402
- }
1403
- return { state: "expired", value: null, stored, envelope: stored };
1404
- }
1405
- function unwrapStoredValue(stored) {
1406
- if (!isStoredValueEnvelope(stored)) {
1407
- return stored;
1408
- }
1409
- if (stored.kind === "empty") {
1410
- return null;
1411
- }
1412
- return stored.value ?? null;
1413
- }
1414
- function remainingStoredTtlSeconds(stored, now = Date.now()) {
1415
- if (!isStoredValueEnvelope(stored)) {
1416
- return void 0;
1417
- }
1418
- const expiry = maxExpiry(stored);
1419
- if (expiry === null) {
1420
- return void 0;
1421
- }
1422
- const remainingMs = expiry - now;
1423
- if (remainingMs <= 0) {
1424
- return 1;
1425
- }
1426
- return Math.max(1, Math.ceil(remainingMs / 1e3));
1427
- }
1428
- function remainingFreshTtlSeconds(stored, now = Date.now()) {
1429
- if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
1430
- return void 0;
1431
- }
1432
- const remainingMs = stored.freshUntil - now;
1433
- if (remainingMs <= 0) {
1434
- return 0;
1435
- }
1436
- return Math.max(1, Math.ceil(remainingMs / 1e3));
1437
- }
1438
- function refreshStoredEnvelope(stored, now = Date.now()) {
1439
- if (!isStoredValueEnvelope(stored)) {
1440
- return stored;
1441
- }
1442
- return createStoredValueEnvelope({
1443
- kind: stored.kind,
1444
- value: stored.value,
1445
- freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
1446
- staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
1447
- staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
1448
- now
1449
- });
1450
- }
1451
- function maxExpiry(stored) {
1452
- const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
1453
- (value) => value !== null
1454
- );
1455
- if (values.length === 0) {
1456
- return null;
1457
- }
1458
- return Math.max(...values);
1459
- }
1460
- function normalizePositiveSeconds(value) {
1461
- if (!value || value <= 0) {
1462
- return void 0;
1463
- }
1464
- return value;
1465
- }
1466
- function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
1467
- if (value == null) {
1468
- return true;
1469
- }
1470
- return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
1471
- }
1472
-
1473
1659
  // ../../src/internal/TtlResolver.ts
1474
1660
  var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
1475
1661
  var TtlResolver = class {
@@ -2002,15 +2188,10 @@ var CacheStack = class extends EventEmitter {
2002
2188
  snapshotSerializer = new JsonSerializer();
2003
2189
  backgroundRefreshes = /* @__PURE__ */ new Map();
2004
2190
  layerDegradedUntil = /* @__PURE__ */ new Map();
2005
- keyEpochs = /* @__PURE__ */ new Map();
2191
+ maintenance = new CacheStackMaintenance();
2006
2192
  ttlResolver;
2007
2193
  circuitBreakerManager;
2008
2194
  currentGeneration;
2009
- writeBehindQueue = [];
2010
- writeBehindTimer;
2011
- writeBehindFlushPromise;
2012
- generationCleanupPromise;
2013
- clearEpoch = 0;
2014
2195
  isDisconnecting = false;
2015
2196
  disconnectPromise;
2016
2197
  /**
@@ -2166,7 +2347,7 @@ var CacheStack = class extends EventEmitter {
2166
2347
  }
2167
2348
  async clear() {
2168
2349
  await this.awaitStartup("clear");
2169
- this.beginClearEpoch();
2350
+ this.maintenance.beginClearEpoch();
2170
2351
  await Promise.all(this.layers.map((layer) => layer.clear()));
2171
2352
  await this.tagIndex.clear();
2172
2353
  this.ttlResolver.clearProfiles();
@@ -2424,9 +2605,15 @@ var CacheStack = class extends EventEmitter {
2424
2605
  bumpGeneration(nextGeneration) {
2425
2606
  const current = this.currentGeneration ?? 0;
2426
2607
  const previousGeneration = this.currentGeneration;
2427
- this.currentGeneration = nextGeneration ?? current + 1;
2428
- if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
2429
- this.scheduleGenerationCleanup(previousGeneration);
2608
+ const updatedGeneration = nextGeneration ?? current + 1;
2609
+ const generationToCleanup = resolveGenerationCleanupTarget({
2610
+ previousGeneration,
2611
+ nextGeneration: updatedGeneration,
2612
+ generationCleanup: this.options.generationCleanup
2613
+ });
2614
+ this.currentGeneration = updatedGeneration;
2615
+ if (generationToCleanup !== null) {
2616
+ this.scheduleGenerationCleanup(generationToCleanup);
2430
2617
  }
2431
2618
  return this.currentGeneration;
2432
2619
  }
@@ -2570,12 +2757,9 @@ var CacheStack = class extends EventEmitter {
2570
2757
  await this.startup;
2571
2758
  await this.unsubscribeInvalidation?.();
2572
2759
  await this.flushWriteBehindQueue();
2573
- await this.generationCleanupPromise;
2760
+ await this.maintenance.waitForGenerationCleanup();
2574
2761
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
2575
- if (this.writeBehindTimer) {
2576
- clearInterval(this.writeBehindTimer);
2577
- this.writeBehindTimer = void 0;
2578
- }
2762
+ this.maintenance.disposeWriteBehindTimer();
2579
2763
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
2580
2764
  })();
2581
2765
  }
@@ -2651,13 +2835,13 @@ var CacheStack = class extends EventEmitter {
2651
2835
  if (!this.shouldNegativeCache(options)) {
2652
2836
  return null;
2653
2837
  }
2654
- if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2838
+ if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2655
2839
  this.logger.debug?.("skip-negative-store-after-invalidation", {
2656
2840
  key,
2657
2841
  expectedClearEpoch,
2658
- clearEpoch: this.clearEpoch,
2842
+ clearEpoch: this.maintenance.currentClearEpoch(),
2659
2843
  expectedKeyEpoch,
2660
- keyEpoch: this.currentKeyEpoch(key)
2844
+ keyEpoch: this.maintenance.currentKeyEpoch(key)
2661
2845
  });
2662
2846
  return null;
2663
2847
  }
@@ -2673,13 +2857,13 @@ var CacheStack = class extends EventEmitter {
2673
2857
  this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
2674
2858
  }
2675
2859
  }
2676
- if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2860
+ if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2677
2861
  this.logger.debug?.("skip-store-after-invalidation", {
2678
2862
  key,
2679
2863
  expectedClearEpoch,
2680
- clearEpoch: this.clearEpoch,
2864
+ clearEpoch: this.maintenance.currentClearEpoch(),
2681
2865
  expectedKeyEpoch,
2682
- keyEpoch: this.currentKeyEpoch(key)
2866
+ keyEpoch: this.maintenance.currentKeyEpoch(key)
2683
2867
  });
2684
2868
  return fetched;
2685
2869
  }
@@ -2687,10 +2871,10 @@ var CacheStack = class extends EventEmitter {
2687
2871
  return fetched;
2688
2872
  }
2689
2873
  async storeEntry(key, kind, value, options) {
2690
- const clearEpoch = this.clearEpoch;
2691
- const keyEpoch = this.currentKeyEpoch(key);
2874
+ const clearEpoch = this.maintenance.currentClearEpoch();
2875
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2692
2876
  await this.writeAcrossLayers(key, kind, value, options);
2693
- if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2877
+ if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2694
2878
  return;
2695
2879
  }
2696
2880
  if (options?.tags) {
@@ -2707,8 +2891,8 @@ var CacheStack = class extends EventEmitter {
2707
2891
  }
2708
2892
  async writeBatch(entries) {
2709
2893
  const now = Date.now();
2710
- const clearEpoch = this.clearEpoch;
2711
- const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
2894
+ const clearEpoch = this.maintenance.currentClearEpoch();
2895
+ const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
2712
2896
  const entriesByLayer = /* @__PURE__ */ new Map();
2713
2897
  const immediateOperations = [];
2714
2898
  const deferredOperations = [];
@@ -2725,11 +2909,11 @@ var CacheStack = class extends EventEmitter {
2725
2909
  }
2726
2910
  for (const [layer, layerEntries] of entriesByLayer.entries()) {
2727
2911
  const operation = async () => {
2728
- if (clearEpoch !== this.clearEpoch) {
2912
+ if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2729
2913
  return;
2730
2914
  }
2731
2915
  const activeEntries = layerEntries.filter(
2732
- (entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
2916
+ (entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
2733
2917
  );
2734
2918
  if (activeEntries.length === 0) {
2735
2919
  return;
@@ -2752,11 +2936,11 @@ var CacheStack = class extends EventEmitter {
2752
2936
  }
2753
2937
  await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
2754
2938
  await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
2755
- if (clearEpoch !== this.clearEpoch) {
2939
+ if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2756
2940
  return;
2757
2941
  }
2758
2942
  for (const entry of entries) {
2759
- if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2943
+ if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2760
2944
  continue;
2761
2945
  }
2762
2946
  if (entry.options?.tags) {
@@ -2860,13 +3044,13 @@ var CacheStack = class extends EventEmitter {
2860
3044
  }
2861
3045
  async writeAcrossLayers(key, kind, value, options) {
2862
3046
  const now = Date.now();
2863
- const clearEpoch = this.clearEpoch;
2864
- const keyEpoch = this.currentKeyEpoch(key);
3047
+ const clearEpoch = this.maintenance.currentClearEpoch();
3048
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2865
3049
  const immediateOperations = [];
2866
3050
  const deferredOperations = [];
2867
3051
  for (const layer of this.layers) {
2868
3052
  const operation = async () => {
2869
- if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
3053
+ if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2870
3054
  return;
2871
3055
  }
2872
3056
  if (this.shouldSkipLayer(layer)) {
@@ -2929,11 +3113,14 @@ var CacheStack = class extends EventEmitter {
2929
3113
  return options?.negativeCache ?? this.options.negativeCaching ?? false;
2930
3114
  }
2931
3115
  scheduleBackgroundRefresh(key, fetcher, options) {
2932
- if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
3116
+ if (!shouldStartBackgroundRefresh({
3117
+ isDisconnecting: this.isDisconnecting,
3118
+ hasRefreshInFlight: this.backgroundRefreshes.has(key)
3119
+ })) {
2933
3120
  return;
2934
3121
  }
2935
- const clearEpoch = this.clearEpoch;
2936
- const keyEpoch = this.currentKeyEpoch(key);
3122
+ const clearEpoch = this.maintenance.currentClearEpoch();
3123
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2937
3124
  const refresh = (async () => {
2938
3125
  this.metricsCollector.increment("refreshes");
2939
3126
  try {
@@ -2971,7 +3158,7 @@ var CacheStack = class extends EventEmitter {
2971
3158
  if (keys.length === 0) {
2972
3159
  return;
2973
3160
  }
2974
- this.bumpKeyEpochs(keys);
3161
+ this.maintenance.bumpKeyEpochs(keys);
2975
3162
  await this.deleteKeysFromLayers(this.layers, keys);
2976
3163
  for (const key of keys) {
2977
3164
  await this.tagIndex.remove(key);
@@ -2995,7 +3182,7 @@ var CacheStack = class extends EventEmitter {
2995
3182
  }
2996
3183
  const localLayers = this.layers.filter((layer) => layer.isLocal);
2997
3184
  if (message.scope === "clear") {
2998
- this.beginClearEpoch();
3185
+ this.maintenance.beginClearEpoch();
2999
3186
  await Promise.all(localLayers.map((layer) => layer.clear()));
3000
3187
  await this.tagIndex.clear();
3001
3188
  this.ttlResolver.clearProfiles();
@@ -3003,7 +3190,7 @@ var CacheStack = class extends EventEmitter {
3003
3190
  return;
3004
3191
  }
3005
3192
  const keys = message.keys ?? [];
3006
- this.bumpKeyEpochs(keys);
3193
+ this.maintenance.bumpKeyEpochs(keys);
3007
3194
  await this.deleteKeysFromLayers(localLayers, keys);
3008
3195
  if (message.operation !== "write") {
3009
3196
  for (const key of keys) {
@@ -3061,35 +3248,22 @@ var CacheStack = class extends EventEmitter {
3061
3248
  shouldBroadcastL1Invalidation() {
3062
3249
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
3063
3250
  }
3064
- shouldCleanupGenerations() {
3065
- return Boolean(this.options.generationCleanup);
3066
- }
3067
- generationCleanupBatchSize() {
3068
- const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
3069
- return configured ?? 500;
3070
- }
3071
3251
  scheduleGenerationCleanup(generation) {
3072
- const task = (this.generationCleanupPromise ?? Promise.resolve()).then(() => this.cleanupGeneration(generation)).catch((error) => {
3073
- this.logger.warn?.("generation-cleanup-error", {
3074
- generation,
3075
- error: this.formatError(error)
3076
- });
3077
- });
3078
- this.generationCleanupPromise = task.finally(() => {
3079
- if (this.generationCleanupPromise === task) {
3080
- this.generationCleanupPromise = void 0;
3252
+ this.maintenance.scheduleGenerationCleanup(
3253
+ generation,
3254
+ async (generationToClean) => this.cleanupGeneration(generationToClean),
3255
+ (failedGeneration, error) => {
3256
+ this.logger.warn?.("generation-cleanup-error", {
3257
+ generation: failedGeneration,
3258
+ error: this.formatError(error)
3259
+ });
3081
3260
  }
3082
- });
3261
+ );
3083
3262
  }
3084
3263
  async cleanupGeneration(generation) {
3085
3264
  const prefix = `v${generation}:`;
3086
3265
  const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
3087
- if (keys.length === 0) {
3088
- return;
3089
- }
3090
- const batchSize = this.generationCleanupBatchSize();
3091
- for (let index = 0; index < keys.length; index += batchSize) {
3092
- const batch = keys.slice(index, index + batchSize);
3266
+ for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
3093
3267
  await this.deleteKeys(batch);
3094
3268
  await this.publishInvalidation({
3095
3269
  scope: "keys",
@@ -3100,80 +3274,34 @@ var CacheStack = class extends EventEmitter {
3100
3274
  }
3101
3275
  }
3102
3276
  initializeWriteBehind(options) {
3103
- if (this.options.writeStrategy !== "write-behind") {
3104
- return;
3105
- }
3106
- const flushIntervalMs = options?.flushIntervalMs;
3107
- if (!flushIntervalMs || flushIntervalMs <= 0) {
3108
- return;
3109
- }
3110
- this.writeBehindTimer = setInterval(() => {
3111
- void this.flushWriteBehindQueue();
3112
- }, flushIntervalMs);
3113
- this.writeBehindTimer.unref?.();
3277
+ this.maintenance.initializeWriteBehindTimer(
3278
+ this.options.writeStrategy,
3279
+ options,
3280
+ this.flushWriteBehindQueue.bind(this)
3281
+ );
3114
3282
  }
3115
3283
  shouldWriteBehind(layer) {
3116
3284
  return this.options.writeStrategy === "write-behind" && !layer.isLocal;
3117
3285
  }
3118
- beginClearEpoch() {
3119
- this.clearEpoch += 1;
3120
- this.keyEpochs.clear();
3121
- this.writeBehindQueue.length = 0;
3122
- }
3123
- currentKeyEpoch(key) {
3124
- return this.keyEpochs.get(key) ?? 0;
3125
- }
3126
- bumpKeyEpochs(keys) {
3127
- for (const key of keys) {
3128
- this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
3129
- }
3130
- }
3131
- isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
3132
- if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
3133
- return true;
3134
- }
3135
- if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
3136
- return true;
3137
- }
3138
- return false;
3139
- }
3140
3286
  async enqueueWriteBehind(operation) {
3141
- this.writeBehindQueue.push(operation);
3142
- const batchSize = this.options.writeBehind?.batchSize ?? 100;
3143
- const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
3144
- if (this.writeBehindQueue.length >= batchSize) {
3145
- await this.flushWriteBehindQueue();
3146
- return;
3147
- }
3148
- if (this.writeBehindQueue.length >= maxQueueSize) {
3149
- await this.flushWriteBehindQueue();
3150
- }
3287
+ await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
3151
3288
  }
3152
3289
  async flushWriteBehindQueue() {
3153
- if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
3154
- await this.writeBehindFlushPromise;
3290
+ await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
3291
+ }
3292
+ async runWriteBehindBatch(batch) {
3293
+ const results = await Promise.allSettled(batch.map((operation) => operation()));
3294
+ const failures = results.filter((result) => result.status === "rejected");
3295
+ if (failures.length === 0) {
3155
3296
  return;
3156
3297
  }
3157
- const batchSize = this.options.writeBehind?.batchSize ?? 100;
3158
- const batch = this.writeBehindQueue.splice(0, batchSize);
3159
- this.writeBehindFlushPromise = (async () => {
3160
- const results = await Promise.allSettled(batch.map((operation) => operation()));
3161
- const failures = results.filter((result) => result.status === "rejected");
3162
- if (failures.length > 0) {
3163
- this.metricsCollector.increment("writeFailures", failures.length);
3164
- this.logger.error?.("write-behind-flush-failure", {
3165
- failed: failures.length,
3166
- total: batch.length,
3167
- errors: failures.map((failure) => this.formatError(failure.reason))
3168
- });
3169
- this.emitError("write-behind", { failed: failures.length, total: batch.length });
3170
- }
3171
- })();
3172
- await this.writeBehindFlushPromise;
3173
- this.writeBehindFlushPromise = void 0;
3174
- if (this.writeBehindQueue.length > 0) {
3175
- await this.flushWriteBehindQueue();
3176
- }
3298
+ this.metricsCollector.increment("writeFailures", failures.length);
3299
+ this.logger.error?.("write-behind-flush-failure", {
3300
+ failed: failures.length,
3301
+ total: batch.length,
3302
+ errors: failures.map((failure) => this.formatError(failure.reason))
3303
+ });
3304
+ this.emitError("write-behind", { failed: failures.length, total: batch.length });
3177
3305
  }
3178
3306
  buildLayerSetEntry(layer, key, kind, value, options, now) {
3179
3307
  const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
@@ -3203,32 +3331,17 @@ var CacheStack = class extends EventEmitter {
3203
3331
  return [];
3204
3332
  }
3205
3333
  const [firstGroup, ...rest] = groups;
3206
- if (!firstGroup) {
3207
- return [];
3208
- }
3209
3334
  const restSets = rest.map((group) => new Set(group));
3210
3335
  return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
3211
3336
  }
3212
3337
  qualifyKey(key) {
3213
- const prefix = this.generationPrefix();
3214
- return prefix ? `${prefix}${key}` : key;
3338
+ return qualifyGenerationKey(key, this.currentGeneration);
3215
3339
  }
3216
3340
  qualifyPattern(pattern) {
3217
- const prefix = this.generationPrefix();
3218
- return prefix ? `${prefix}${pattern}` : pattern;
3341
+ return qualifyGenerationPattern(pattern, this.currentGeneration);
3219
3342
  }
3220
3343
  stripQualifiedKey(key) {
3221
- const prefix = this.generationPrefix();
3222
- if (!prefix || !key.startsWith(prefix)) {
3223
- return key;
3224
- }
3225
- return key.slice(prefix.length);
3226
- }
3227
- generationPrefix() {
3228
- if (this.currentGeneration === void 0) {
3229
- return "";
3230
- }
3231
- return `v${this.currentGeneration}:`;
3344
+ return stripGenerationPrefix(key, this.currentGeneration);
3232
3345
  }
3233
3346
  async deleteKeysFromLayers(layers, keys) {
3234
3347
  await Promise.all(
@@ -3319,37 +3432,38 @@ var CacheStack = class extends EventEmitter {
3319
3432
  this.assertActive(operation);
3320
3433
  }
3321
3434
  async applyFreshReadPolicies(key, hit, options, fetcher) {
3322
- const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
3323
- const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
3324
- if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
3325
- const refreshed = refreshStoredEnvelope(hit.stored);
3326
- const ttl = remainingStoredTtlSeconds(refreshed);
3435
+ const plan = planFreshReadPolicies({
3436
+ stored: hit.stored,
3437
+ hasFetcher: Boolean(fetcher),
3438
+ slidingTtl: options?.slidingTtl ?? false,
3439
+ refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
3440
+ });
3441
+ if (plan.refreshedStored) {
3327
3442
  for (let index = 0; index <= hit.layerIndex; index += 1) {
3328
3443
  const layer = this.layers[index];
3329
3444
  if (!layer || this.shouldSkipLayer(layer)) {
3330
3445
  continue;
3331
3446
  }
3332
3447
  try {
3333
- await layer.set(key, refreshed, ttl);
3448
+ await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
3334
3449
  } catch (error) {
3335
3450
  await this.handleLayerFailure(layer, "sliding-ttl", error);
3336
3451
  }
3337
3452
  }
3338
3453
  }
3339
- if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
3454
+ if (fetcher && plan.shouldScheduleBackgroundRefresh) {
3340
3455
  this.scheduleBackgroundRefresh(key, fetcher, options);
3341
3456
  }
3342
3457
  }
3343
3458
  shouldSkipLayer(layer) {
3344
- const degradedUntil = this.layerDegradedUntil.get(layer.name);
3345
- return degradedUntil !== void 0 && degradedUntil > Date.now();
3459
+ return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
3346
3460
  }
3347
3461
  async handleLayerFailure(layer, operation, error) {
3348
- if (!this.isGracefulDegradationEnabled()) {
3462
+ const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
3463
+ if (!recovery.degrade) {
3349
3464
  throw error;
3350
3465
  }
3351
- const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
3352
- this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
3466
+ this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
3353
3467
  this.metricsCollector.increment("degradedOperations");
3354
3468
  this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
3355
3469
  this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });