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.
@@ -259,6 +259,125 @@ var Mutex = class {
259
259
  }
260
260
  };
261
261
 
262
+ // ../../src/internal/CacheNamespaceMetrics.ts
263
+ function createEmptyNamespaceMetrics(resetAt = Date.now()) {
264
+ return {
265
+ hits: 0,
266
+ misses: 0,
267
+ fetches: 0,
268
+ sets: 0,
269
+ deletes: 0,
270
+ backfills: 0,
271
+ invalidations: 0,
272
+ staleHits: 0,
273
+ refreshes: 0,
274
+ refreshErrors: 0,
275
+ writeFailures: 0,
276
+ singleFlightWaits: 0,
277
+ negativeCacheHits: 0,
278
+ circuitBreakerTrips: 0,
279
+ degradedOperations: 0,
280
+ hitsByLayer: {},
281
+ missesByLayer: {},
282
+ latencyByLayer: {},
283
+ resetAt
284
+ };
285
+ }
286
+ function cloneNamespaceMetrics(metrics) {
287
+ return {
288
+ ...metrics,
289
+ hitsByLayer: { ...metrics.hitsByLayer },
290
+ missesByLayer: { ...metrics.missesByLayer },
291
+ latencyByLayer: Object.fromEntries(
292
+ Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
293
+ )
294
+ };
295
+ }
296
+ function diffNamespaceMetrics(before, after) {
297
+ const latencyByLayer = Object.fromEntries(
298
+ Object.entries(after.latencyByLayer).map(([layer, value]) => [
299
+ layer,
300
+ {
301
+ avgMs: value.avgMs,
302
+ maxMs: value.maxMs,
303
+ count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
304
+ }
305
+ ])
306
+ );
307
+ return {
308
+ hits: after.hits - before.hits,
309
+ misses: after.misses - before.misses,
310
+ fetches: after.fetches - before.fetches,
311
+ sets: after.sets - before.sets,
312
+ deletes: after.deletes - before.deletes,
313
+ backfills: after.backfills - before.backfills,
314
+ invalidations: after.invalidations - before.invalidations,
315
+ staleHits: after.staleHits - before.staleHits,
316
+ refreshes: after.refreshes - before.refreshes,
317
+ refreshErrors: after.refreshErrors - before.refreshErrors,
318
+ writeFailures: after.writeFailures - before.writeFailures,
319
+ singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
320
+ negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
321
+ circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
322
+ degradedOperations: after.degradedOperations - before.degradedOperations,
323
+ hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
324
+ missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
325
+ latencyByLayer,
326
+ resetAt: after.resetAt
327
+ };
328
+ }
329
+ function addNamespaceMetrics(base, delta) {
330
+ return {
331
+ hits: base.hits + delta.hits,
332
+ misses: base.misses + delta.misses,
333
+ fetches: base.fetches + delta.fetches,
334
+ sets: base.sets + delta.sets,
335
+ deletes: base.deletes + delta.deletes,
336
+ backfills: base.backfills + delta.backfills,
337
+ invalidations: base.invalidations + delta.invalidations,
338
+ staleHits: base.staleHits + delta.staleHits,
339
+ refreshes: base.refreshes + delta.refreshes,
340
+ refreshErrors: base.refreshErrors + delta.refreshErrors,
341
+ writeFailures: base.writeFailures + delta.writeFailures,
342
+ singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
343
+ negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
344
+ circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
345
+ degradedOperations: base.degradedOperations + delta.degradedOperations,
346
+ hitsByLayer: addMetricMap(base.hitsByLayer, delta.hitsByLayer),
347
+ missesByLayer: addMetricMap(base.missesByLayer, delta.missesByLayer),
348
+ latencyByLayer: cloneNamespaceMetrics(delta).latencyByLayer,
349
+ resetAt: base.resetAt
350
+ };
351
+ }
352
+ function computeNamespaceHitRate(metrics) {
353
+ const total = metrics.hits + metrics.misses;
354
+ const overall = total === 0 ? 0 : metrics.hits / total;
355
+ const byLayer = {};
356
+ const layers = /* @__PURE__ */ new Set([...Object.keys(metrics.hitsByLayer), ...Object.keys(metrics.missesByLayer)]);
357
+ for (const layer of layers) {
358
+ const hits = metrics.hitsByLayer[layer] ?? 0;
359
+ const misses = metrics.missesByLayer[layer] ?? 0;
360
+ byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
361
+ }
362
+ return { overall, byLayer };
363
+ }
364
+ function diffMetricMap(before, after) {
365
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
366
+ const result = {};
367
+ for (const key of keys) {
368
+ result[key] = (after[key] ?? 0) - (before[key] ?? 0);
369
+ }
370
+ return result;
371
+ }
372
+ function addMetricMap(base, delta) {
373
+ const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
374
+ const result = {};
375
+ for (const key of keys) {
376
+ result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
377
+ }
378
+ return result;
379
+ }
380
+
262
381
  // ../../src/CacheNamespace.ts
263
382
  var CacheNamespace = class _CacheNamespace {
264
383
  constructor(cache, prefix) {
@@ -269,7 +388,7 @@ var CacheNamespace = class _CacheNamespace {
269
388
  cache;
270
389
  prefix;
271
390
  static metricsMutexes = /* @__PURE__ */ new WeakMap();
272
- metrics = emptyMetrics();
391
+ metrics = createEmptyNamespaceMetrics();
273
392
  async get(key, fetcher, options) {
274
393
  return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
275
394
  }
@@ -366,19 +485,10 @@ var CacheNamespace = class _CacheNamespace {
366
485
  );
367
486
  }
368
487
  getMetrics() {
369
- return cloneMetrics(this.metrics);
488
+ return cloneNamespaceMetrics(this.metrics);
370
489
  }
371
490
  getHitRate() {
372
- const total = this.metrics.hits + this.metrics.misses;
373
- const overall = total === 0 ? 0 : this.metrics.hits / total;
374
- const byLayer = {};
375
- const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
376
- for (const layer of layers) {
377
- const hits = this.metrics.hitsByLayer[layer] ?? 0;
378
- const misses = this.metrics.missesByLayer[layer] ?? 0;
379
- byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
380
- }
381
- return { overall, byLayer };
491
+ return computeNamespaceHitRate(this.metrics);
382
492
  }
383
493
  /**
384
494
  * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
@@ -419,7 +529,7 @@ var CacheNamespace = class _CacheNamespace {
419
529
  const before = this.cache.getMetrics();
420
530
  const result = await operation();
421
531
  const after = this.cache.getMetrics();
422
- this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
532
+ this.metrics = addNamespaceMetrics(this.metrics, diffNamespaceMetrics(before, after));
423
533
  return result;
424
534
  });
425
535
  }
@@ -433,111 +543,6 @@ var CacheNamespace = class _CacheNamespace {
433
543
  return mutex;
434
544
  }
435
545
  };
436
- function emptyMetrics() {
437
- return {
438
- hits: 0,
439
- misses: 0,
440
- fetches: 0,
441
- sets: 0,
442
- deletes: 0,
443
- backfills: 0,
444
- invalidations: 0,
445
- staleHits: 0,
446
- refreshes: 0,
447
- refreshErrors: 0,
448
- writeFailures: 0,
449
- singleFlightWaits: 0,
450
- negativeCacheHits: 0,
451
- circuitBreakerTrips: 0,
452
- degradedOperations: 0,
453
- hitsByLayer: {},
454
- missesByLayer: {},
455
- latencyByLayer: {},
456
- resetAt: Date.now()
457
- };
458
- }
459
- function cloneMetrics(metrics) {
460
- return {
461
- ...metrics,
462
- hitsByLayer: { ...metrics.hitsByLayer },
463
- missesByLayer: { ...metrics.missesByLayer },
464
- latencyByLayer: Object.fromEntries(
465
- Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
466
- )
467
- };
468
- }
469
- function diffMetrics(before, after) {
470
- const latencyByLayer = Object.fromEntries(
471
- Object.entries(after.latencyByLayer).map(([layer, value]) => [
472
- layer,
473
- {
474
- avgMs: value.avgMs,
475
- maxMs: value.maxMs,
476
- count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
477
- }
478
- ])
479
- );
480
- return {
481
- hits: after.hits - before.hits,
482
- misses: after.misses - before.misses,
483
- fetches: after.fetches - before.fetches,
484
- sets: after.sets - before.sets,
485
- deletes: after.deletes - before.deletes,
486
- backfills: after.backfills - before.backfills,
487
- invalidations: after.invalidations - before.invalidations,
488
- staleHits: after.staleHits - before.staleHits,
489
- refreshes: after.refreshes - before.refreshes,
490
- refreshErrors: after.refreshErrors - before.refreshErrors,
491
- writeFailures: after.writeFailures - before.writeFailures,
492
- singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
493
- negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
494
- circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
495
- degradedOperations: after.degradedOperations - before.degradedOperations,
496
- hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
497
- missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
498
- latencyByLayer,
499
- resetAt: after.resetAt
500
- };
501
- }
502
- function addMetrics(base, delta) {
503
- return {
504
- hits: base.hits + delta.hits,
505
- misses: base.misses + delta.misses,
506
- fetches: base.fetches + delta.fetches,
507
- sets: base.sets + delta.sets,
508
- deletes: base.deletes + delta.deletes,
509
- backfills: base.backfills + delta.backfills,
510
- invalidations: base.invalidations + delta.invalidations,
511
- staleHits: base.staleHits + delta.staleHits,
512
- refreshes: base.refreshes + delta.refreshes,
513
- refreshErrors: base.refreshErrors + delta.refreshErrors,
514
- writeFailures: base.writeFailures + delta.writeFailures,
515
- singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
516
- negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
517
- circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
518
- degradedOperations: base.degradedOperations + delta.degradedOperations,
519
- hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
520
- missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
521
- latencyByLayer: cloneMetrics(delta).latencyByLayer,
522
- resetAt: base.resetAt
523
- };
524
- }
525
- function diffMap(before, after) {
526
- const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
527
- const result = {};
528
- for (const key of keys) {
529
- result[key] = (after[key] ?? 0) - (before[key] ?? 0);
530
- }
531
- return result;
532
- }
533
- function addMap(base, delta) {
534
- const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
535
- const result = {};
536
- for (const key of keys) {
537
- result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
538
- }
539
- return result;
540
- }
541
546
  function validateNamespaceKey(key) {
542
547
  if (key.length === 0) {
543
548
  throw new Error("Namespace prefix must not be empty.");
@@ -840,7 +845,346 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
840
845
  chunks.push(buffer.subarray(0, bytesRead));
841
846
  position += bytesRead;
842
847
  }
843
- return Buffer.concat(chunks).toString("utf8");
848
+ return Buffer.concat(chunks).toString("utf8");
849
+ }
850
+
851
+ // ../../src/internal/CacheStackGeneration.ts
852
+ var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
853
+ function generationPrefix(generation) {
854
+ return generation === void 0 ? "" : `v${generation}:`;
855
+ }
856
+ function qualifyGenerationKey(key, generation) {
857
+ const prefix = generationPrefix(generation);
858
+ return prefix ? `${prefix}${key}` : key;
859
+ }
860
+ function qualifyGenerationPattern(pattern, generation) {
861
+ return qualifyGenerationKey(pattern, generation);
862
+ }
863
+ function stripGenerationPrefix(key, generation) {
864
+ const prefix = generationPrefix(generation);
865
+ if (!prefix || !key.startsWith(prefix)) {
866
+ return key;
867
+ }
868
+ return key.slice(prefix.length);
869
+ }
870
+ function resolveGenerationCleanupTarget({
871
+ previousGeneration,
872
+ nextGeneration,
873
+ generationCleanup
874
+ }) {
875
+ if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
876
+ return null;
877
+ }
878
+ return previousGeneration;
879
+ }
880
+ function resolveGenerationCleanupBatchSize(generationCleanup) {
881
+ if (typeof generationCleanup !== "object" || generationCleanup === null) {
882
+ return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
883
+ }
884
+ return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
885
+ }
886
+ function planGenerationCleanupBatches(keys, generationCleanup) {
887
+ if (keys.length === 0) {
888
+ return [];
889
+ }
890
+ const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
891
+ const batches = [];
892
+ for (let index = 0; index < keys.length; index += batchSize) {
893
+ batches.push(keys.slice(index, index + batchSize));
894
+ }
895
+ return batches;
896
+ }
897
+
898
+ // ../../src/internal/CacheStackMaintenance.ts
899
+ var CacheStackMaintenance = class {
900
+ keyEpochs = /* @__PURE__ */ new Map();
901
+ writeBehindQueue = [];
902
+ writeBehindTimer;
903
+ writeBehindFlushPromise;
904
+ generationCleanupPromise;
905
+ clearEpoch = 0;
906
+ initializeWriteBehindTimer(writeStrategy, options, flush) {
907
+ if (writeStrategy !== "write-behind") {
908
+ return;
909
+ }
910
+ const flushIntervalMs = options?.flushIntervalMs;
911
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
912
+ return;
913
+ }
914
+ this.disposeWriteBehindTimer();
915
+ this.writeBehindTimer = setInterval(() => {
916
+ void flush();
917
+ }, flushIntervalMs);
918
+ this.writeBehindTimer.unref?.();
919
+ }
920
+ disposeWriteBehindTimer() {
921
+ if (!this.writeBehindTimer) {
922
+ return;
923
+ }
924
+ clearInterval(this.writeBehindTimer);
925
+ this.writeBehindTimer = void 0;
926
+ }
927
+ beginClearEpoch() {
928
+ this.clearEpoch += 1;
929
+ this.keyEpochs.clear();
930
+ this.writeBehindQueue.length = 0;
931
+ }
932
+ currentClearEpoch() {
933
+ return this.clearEpoch;
934
+ }
935
+ currentKeyEpoch(key) {
936
+ return this.keyEpochs.get(key) ?? 0;
937
+ }
938
+ bumpKeyEpochs(keys) {
939
+ for (const key of keys) {
940
+ this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
941
+ }
942
+ }
943
+ isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
944
+ if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
945
+ return true;
946
+ }
947
+ if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
948
+ return true;
949
+ }
950
+ return false;
951
+ }
952
+ async enqueueWriteBehind(operation, options, flushBatch) {
953
+ this.writeBehindQueue.push(operation);
954
+ const batchSize = options?.batchSize ?? 100;
955
+ const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
956
+ if (this.writeBehindQueue.length >= batchSize) {
957
+ await this.flushWriteBehindQueue(options, flushBatch);
958
+ return;
959
+ }
960
+ if (this.writeBehindQueue.length >= maxQueueSize) {
961
+ await this.flushWriteBehindQueue(options, flushBatch);
962
+ }
963
+ }
964
+ async flushWriteBehindQueue(options, flushBatch) {
965
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
966
+ await this.writeBehindFlushPromise;
967
+ return;
968
+ }
969
+ const batchSize = options?.batchSize ?? 100;
970
+ const batch = this.writeBehindQueue.splice(0, batchSize);
971
+ this.writeBehindFlushPromise = flushBatch(batch);
972
+ try {
973
+ await this.writeBehindFlushPromise;
974
+ } finally {
975
+ this.writeBehindFlushPromise = void 0;
976
+ }
977
+ if (this.writeBehindQueue.length > 0) {
978
+ await this.flushWriteBehindQueue(options, flushBatch);
979
+ }
980
+ }
981
+ scheduleGenerationCleanup(generation, task, onError) {
982
+ const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
983
+ onError(generation, error);
984
+ });
985
+ this.generationCleanupPromise = scheduledTask.finally(() => {
986
+ if (this.generationCleanupPromise === scheduledTask) {
987
+ this.generationCleanupPromise = void 0;
988
+ }
989
+ });
990
+ }
991
+ async waitForGenerationCleanup() {
992
+ await this.generationCleanupPromise;
993
+ }
994
+ };
995
+
996
+ // ../../src/internal/StoredValue.ts
997
+ function isStoredValueEnvelope(value) {
998
+ if (typeof value !== "object" || value === null) {
999
+ return false;
1000
+ }
1001
+ const v = value;
1002
+ if (v.__layercache !== 1) {
1003
+ return false;
1004
+ }
1005
+ if (v.kind !== "value" && v.kind !== "empty") {
1006
+ return false;
1007
+ }
1008
+ if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
1009
+ return false;
1010
+ }
1011
+ if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
1012
+ return false;
1013
+ }
1014
+ if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
1015
+ return false;
1016
+ }
1017
+ const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
1018
+ if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
1019
+ return false;
1020
+ }
1021
+ if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
1022
+ return false;
1023
+ }
1024
+ if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
1025
+ return false;
1026
+ }
1027
+ if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
1028
+ return false;
1029
+ }
1030
+ if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
1031
+ return false;
1032
+ }
1033
+ if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
1034
+ return false;
1035
+ }
1036
+ const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
1037
+ if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
1038
+ return false;
1039
+ }
1040
+ if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
1041
+ return false;
1042
+ }
1043
+ if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
1044
+ return false;
1045
+ }
1046
+ if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
1047
+ return false;
1048
+ }
1049
+ return true;
1050
+ }
1051
+ function createStoredValueEnvelope(options) {
1052
+ const now = options.now ?? Date.now();
1053
+ const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
1054
+ const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
1055
+ const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
1056
+ const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
1057
+ const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
1058
+ const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
1059
+ return {
1060
+ __layercache: 1,
1061
+ kind: options.kind,
1062
+ value: options.value,
1063
+ freshUntil,
1064
+ staleUntil,
1065
+ errorUntil,
1066
+ freshTtlSeconds: freshTtlSeconds ?? null,
1067
+ staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
1068
+ staleIfErrorSeconds: staleIfErrorSeconds ?? null
1069
+ };
1070
+ }
1071
+ function resolveStoredValue(stored, now = Date.now()) {
1072
+ if (!isStoredValueEnvelope(stored)) {
1073
+ return { state: "fresh", value: stored, stored };
1074
+ }
1075
+ if (stored.freshUntil === null || stored.freshUntil > now) {
1076
+ return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
1077
+ }
1078
+ if (stored.staleUntil !== null && stored.staleUntil > now) {
1079
+ return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
1080
+ }
1081
+ if (stored.errorUntil !== null && stored.errorUntil > now) {
1082
+ return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
1083
+ }
1084
+ return { state: "expired", value: null, stored, envelope: stored };
1085
+ }
1086
+ function unwrapStoredValue(stored) {
1087
+ if (!isStoredValueEnvelope(stored)) {
1088
+ return stored;
1089
+ }
1090
+ if (stored.kind === "empty") {
1091
+ return null;
1092
+ }
1093
+ return stored.value ?? null;
1094
+ }
1095
+ function remainingStoredTtlSeconds(stored, now = Date.now()) {
1096
+ if (!isStoredValueEnvelope(stored)) {
1097
+ return void 0;
1098
+ }
1099
+ const expiry = maxExpiry(stored);
1100
+ if (expiry === null) {
1101
+ return void 0;
1102
+ }
1103
+ const remainingMs = expiry - now;
1104
+ if (remainingMs <= 0) {
1105
+ return 1;
1106
+ }
1107
+ return Math.max(1, Math.ceil(remainingMs / 1e3));
1108
+ }
1109
+ function remainingFreshTtlSeconds(stored, now = Date.now()) {
1110
+ if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
1111
+ return void 0;
1112
+ }
1113
+ const remainingMs = stored.freshUntil - now;
1114
+ if (remainingMs <= 0) {
1115
+ return 0;
1116
+ }
1117
+ return Math.max(1, Math.ceil(remainingMs / 1e3));
1118
+ }
1119
+ function refreshStoredEnvelope(stored, now = Date.now()) {
1120
+ if (!isStoredValueEnvelope(stored)) {
1121
+ return stored;
1122
+ }
1123
+ return createStoredValueEnvelope({
1124
+ kind: stored.kind,
1125
+ value: stored.value,
1126
+ freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
1127
+ staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
1128
+ staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
1129
+ now
1130
+ });
1131
+ }
1132
+ function maxExpiry(stored) {
1133
+ const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
1134
+ (value) => value !== null
1135
+ );
1136
+ if (values.length === 0) {
1137
+ return null;
1138
+ }
1139
+ return Math.max(...values);
1140
+ }
1141
+ function normalizePositiveSeconds(value) {
1142
+ if (!value || value <= 0) {
1143
+ return void 0;
1144
+ }
1145
+ return value;
1146
+ }
1147
+ function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
1148
+ if (value == null) {
1149
+ return true;
1150
+ }
1151
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
1152
+ }
1153
+
1154
+ // ../../src/internal/CacheStackRuntimePolicy.ts
1155
+ function shouldSkipLayer(degradedUntil, now = Date.now()) {
1156
+ return degradedUntil !== void 0 && degradedUntil > now;
1157
+ }
1158
+ function shouldStartBackgroundRefresh({
1159
+ isDisconnecting,
1160
+ hasRefreshInFlight
1161
+ }) {
1162
+ return !isDisconnecting && !hasRefreshInFlight;
1163
+ }
1164
+ function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
1165
+ if (!gracefulDegradation) {
1166
+ return { degrade: false };
1167
+ }
1168
+ const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
1169
+ return {
1170
+ degrade: true,
1171
+ degradedUntil: now + retryAfterMs
1172
+ };
1173
+ }
1174
+ function planFreshReadPolicies({
1175
+ stored,
1176
+ hasFetcher,
1177
+ slidingTtl,
1178
+ refreshAheadSeconds
1179
+ }) {
1180
+ const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
1181
+ const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
1182
+ const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
1183
+ return {
1184
+ refreshedStored,
1185
+ refreshedStoredTtl,
1186
+ shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
1187
+ };
844
1188
  }
845
1189
 
846
1190
  // ../../src/internal/CacheStackValidation.ts
@@ -996,7 +1340,6 @@ var CircuitBreakerManager = class {
996
1340
  if (!options) {
997
1341
  return;
998
1342
  }
999
- this.pruneIfNeeded();
1000
1343
  const failureThreshold = options.failureThreshold ?? 3;
1001
1344
  const cooldownMs = options.cooldownMs ?? 3e4;
1002
1345
  const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
@@ -1005,6 +1348,7 @@ var CircuitBreakerManager = class {
1005
1348
  state.openUntil = Date.now() + cooldownMs;
1006
1349
  }
1007
1350
  this.breakers.set(key, state);
1351
+ this.pruneIfNeeded();
1008
1352
  }
1009
1353
  recordSuccess(key) {
1010
1354
  this.breakers.delete(key);
@@ -1348,164 +1692,6 @@ var MetricsCollector = class {
1348
1692
  }
1349
1693
  };
1350
1694
 
1351
- // ../../src/internal/StoredValue.ts
1352
- function isStoredValueEnvelope(value) {
1353
- if (typeof value !== "object" || value === null) {
1354
- return false;
1355
- }
1356
- const v = value;
1357
- if (v.__layercache !== 1) {
1358
- return false;
1359
- }
1360
- if (v.kind !== "value" && v.kind !== "empty") {
1361
- return false;
1362
- }
1363
- if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
1364
- return false;
1365
- }
1366
- if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
1367
- return false;
1368
- }
1369
- if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
1370
- return false;
1371
- }
1372
- const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
1373
- if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
1374
- return false;
1375
- }
1376
- if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
1377
- return false;
1378
- }
1379
- if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
1380
- return false;
1381
- }
1382
- if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
1383
- return false;
1384
- }
1385
- if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
1386
- return false;
1387
- }
1388
- if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
1389
- return false;
1390
- }
1391
- const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
1392
- if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
1393
- return false;
1394
- }
1395
- if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
1396
- return false;
1397
- }
1398
- if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
1399
- return false;
1400
- }
1401
- if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
1402
- return false;
1403
- }
1404
- return true;
1405
- }
1406
- function createStoredValueEnvelope(options) {
1407
- const now = options.now ?? Date.now();
1408
- const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
1409
- const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
1410
- const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
1411
- const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
1412
- const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
1413
- const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
1414
- return {
1415
- __layercache: 1,
1416
- kind: options.kind,
1417
- value: options.value,
1418
- freshUntil,
1419
- staleUntil,
1420
- errorUntil,
1421
- freshTtlSeconds: freshTtlSeconds ?? null,
1422
- staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
1423
- staleIfErrorSeconds: staleIfErrorSeconds ?? null
1424
- };
1425
- }
1426
- function resolveStoredValue(stored, now = Date.now()) {
1427
- if (!isStoredValueEnvelope(stored)) {
1428
- return { state: "fresh", value: stored, stored };
1429
- }
1430
- if (stored.freshUntil === null || stored.freshUntil > now) {
1431
- return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
1432
- }
1433
- if (stored.staleUntil !== null && stored.staleUntil > now) {
1434
- return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
1435
- }
1436
- if (stored.errorUntil !== null && stored.errorUntil > now) {
1437
- return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
1438
- }
1439
- return { state: "expired", value: null, stored, envelope: stored };
1440
- }
1441
- function unwrapStoredValue(stored) {
1442
- if (!isStoredValueEnvelope(stored)) {
1443
- return stored;
1444
- }
1445
- if (stored.kind === "empty") {
1446
- return null;
1447
- }
1448
- return stored.value ?? null;
1449
- }
1450
- function remainingStoredTtlSeconds(stored, now = Date.now()) {
1451
- if (!isStoredValueEnvelope(stored)) {
1452
- return void 0;
1453
- }
1454
- const expiry = maxExpiry(stored);
1455
- if (expiry === null) {
1456
- return void 0;
1457
- }
1458
- const remainingMs = expiry - now;
1459
- if (remainingMs <= 0) {
1460
- return 1;
1461
- }
1462
- return Math.max(1, Math.ceil(remainingMs / 1e3));
1463
- }
1464
- function remainingFreshTtlSeconds(stored, now = Date.now()) {
1465
- if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
1466
- return void 0;
1467
- }
1468
- const remainingMs = stored.freshUntil - now;
1469
- if (remainingMs <= 0) {
1470
- return 0;
1471
- }
1472
- return Math.max(1, Math.ceil(remainingMs / 1e3));
1473
- }
1474
- function refreshStoredEnvelope(stored, now = Date.now()) {
1475
- if (!isStoredValueEnvelope(stored)) {
1476
- return stored;
1477
- }
1478
- return createStoredValueEnvelope({
1479
- kind: stored.kind,
1480
- value: stored.value,
1481
- freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
1482
- staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
1483
- staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
1484
- now
1485
- });
1486
- }
1487
- function maxExpiry(stored) {
1488
- const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
1489
- (value) => value !== null
1490
- );
1491
- if (values.length === 0) {
1492
- return null;
1493
- }
1494
- return Math.max(...values);
1495
- }
1496
- function normalizePositiveSeconds(value) {
1497
- if (!value || value <= 0) {
1498
- return void 0;
1499
- }
1500
- return value;
1501
- }
1502
- function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
1503
- if (value == null) {
1504
- return true;
1505
- }
1506
- return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
1507
- }
1508
-
1509
1695
  // ../../src/internal/TtlResolver.ts
1510
1696
  var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
1511
1697
  var TtlResolver = class {
@@ -2038,15 +2224,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
2038
2224
  snapshotSerializer = new JsonSerializer();
2039
2225
  backgroundRefreshes = /* @__PURE__ */ new Map();
2040
2226
  layerDegradedUntil = /* @__PURE__ */ new Map();
2041
- keyEpochs = /* @__PURE__ */ new Map();
2227
+ maintenance = new CacheStackMaintenance();
2042
2228
  ttlResolver;
2043
2229
  circuitBreakerManager;
2044
2230
  currentGeneration;
2045
- writeBehindQueue = [];
2046
- writeBehindTimer;
2047
- writeBehindFlushPromise;
2048
- generationCleanupPromise;
2049
- clearEpoch = 0;
2050
2231
  isDisconnecting = false;
2051
2232
  disconnectPromise;
2052
2233
  /**
@@ -2202,7 +2383,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2202
2383
  }
2203
2384
  async clear() {
2204
2385
  await this.awaitStartup("clear");
2205
- this.beginClearEpoch();
2386
+ this.maintenance.beginClearEpoch();
2206
2387
  await Promise.all(this.layers.map((layer) => layer.clear()));
2207
2388
  await this.tagIndex.clear();
2208
2389
  this.ttlResolver.clearProfiles();
@@ -2460,9 +2641,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
2460
2641
  bumpGeneration(nextGeneration) {
2461
2642
  const current = this.currentGeneration ?? 0;
2462
2643
  const previousGeneration = this.currentGeneration;
2463
- this.currentGeneration = nextGeneration ?? current + 1;
2464
- if (previousGeneration !== void 0 && previousGeneration !== this.currentGeneration && this.shouldCleanupGenerations()) {
2465
- this.scheduleGenerationCleanup(previousGeneration);
2644
+ const updatedGeneration = nextGeneration ?? current + 1;
2645
+ const generationToCleanup = resolveGenerationCleanupTarget({
2646
+ previousGeneration,
2647
+ nextGeneration: updatedGeneration,
2648
+ generationCleanup: this.options.generationCleanup
2649
+ });
2650
+ this.currentGeneration = updatedGeneration;
2651
+ if (generationToCleanup !== null) {
2652
+ this.scheduleGenerationCleanup(generationToCleanup);
2466
2653
  }
2467
2654
  return this.currentGeneration;
2468
2655
  }
@@ -2606,12 +2793,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
2606
2793
  await this.startup;
2607
2794
  await this.unsubscribeInvalidation?.();
2608
2795
  await this.flushWriteBehindQueue();
2609
- await this.generationCleanupPromise;
2796
+ await this.maintenance.waitForGenerationCleanup();
2610
2797
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
2611
- if (this.writeBehindTimer) {
2612
- clearInterval(this.writeBehindTimer);
2613
- this.writeBehindTimer = void 0;
2614
- }
2798
+ this.maintenance.disposeWriteBehindTimer();
2615
2799
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
2616
2800
  })();
2617
2801
  }
@@ -2687,13 +2871,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
2687
2871
  if (!this.shouldNegativeCache(options)) {
2688
2872
  return null;
2689
2873
  }
2690
- if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2874
+ if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2691
2875
  this.logger.debug?.("skip-negative-store-after-invalidation", {
2692
2876
  key,
2693
2877
  expectedClearEpoch,
2694
- clearEpoch: this.clearEpoch,
2878
+ clearEpoch: this.maintenance.currentClearEpoch(),
2695
2879
  expectedKeyEpoch,
2696
- keyEpoch: this.currentKeyEpoch(key)
2880
+ keyEpoch: this.maintenance.currentKeyEpoch(key)
2697
2881
  });
2698
2882
  return null;
2699
2883
  }
@@ -2709,13 +2893,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
2709
2893
  this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
2710
2894
  }
2711
2895
  }
2712
- if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2896
+ if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2713
2897
  this.logger.debug?.("skip-store-after-invalidation", {
2714
2898
  key,
2715
2899
  expectedClearEpoch,
2716
- clearEpoch: this.clearEpoch,
2900
+ clearEpoch: this.maintenance.currentClearEpoch(),
2717
2901
  expectedKeyEpoch,
2718
- keyEpoch: this.currentKeyEpoch(key)
2902
+ keyEpoch: this.maintenance.currentKeyEpoch(key)
2719
2903
  });
2720
2904
  return fetched;
2721
2905
  }
@@ -2723,10 +2907,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
2723
2907
  return fetched;
2724
2908
  }
2725
2909
  async storeEntry(key, kind, value, options) {
2726
- const clearEpoch = this.clearEpoch;
2727
- const keyEpoch = this.currentKeyEpoch(key);
2910
+ const clearEpoch = this.maintenance.currentClearEpoch();
2911
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2728
2912
  await this.writeAcrossLayers(key, kind, value, options);
2729
- if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2913
+ if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2730
2914
  return;
2731
2915
  }
2732
2916
  if (options?.tags) {
@@ -2743,8 +2927,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
2743
2927
  }
2744
2928
  async writeBatch(entries) {
2745
2929
  const now = Date.now();
2746
- const clearEpoch = this.clearEpoch;
2747
- const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
2930
+ const clearEpoch = this.maintenance.currentClearEpoch();
2931
+ const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
2748
2932
  const entriesByLayer = /* @__PURE__ */ new Map();
2749
2933
  const immediateOperations = [];
2750
2934
  const deferredOperations = [];
@@ -2761,11 +2945,11 @@ var CacheStack = class extends import_node_events.EventEmitter {
2761
2945
  }
2762
2946
  for (const [layer, layerEntries] of entriesByLayer.entries()) {
2763
2947
  const operation = async () => {
2764
- if (clearEpoch !== this.clearEpoch) {
2948
+ if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2765
2949
  return;
2766
2950
  }
2767
2951
  const activeEntries = layerEntries.filter(
2768
- (entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
2952
+ (entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
2769
2953
  );
2770
2954
  if (activeEntries.length === 0) {
2771
2955
  return;
@@ -2788,11 +2972,11 @@ var CacheStack = class extends import_node_events.EventEmitter {
2788
2972
  }
2789
2973
  await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
2790
2974
  await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
2791
- if (clearEpoch !== this.clearEpoch) {
2975
+ if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2792
2976
  return;
2793
2977
  }
2794
2978
  for (const entry of entries) {
2795
- if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2979
+ if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2796
2980
  continue;
2797
2981
  }
2798
2982
  if (entry.options?.tags) {
@@ -2896,13 +3080,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
2896
3080
  }
2897
3081
  async writeAcrossLayers(key, kind, value, options) {
2898
3082
  const now = Date.now();
2899
- const clearEpoch = this.clearEpoch;
2900
- const keyEpoch = this.currentKeyEpoch(key);
3083
+ const clearEpoch = this.maintenance.currentClearEpoch();
3084
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2901
3085
  const immediateOperations = [];
2902
3086
  const deferredOperations = [];
2903
3087
  for (const layer of this.layers) {
2904
3088
  const operation = async () => {
2905
- if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
3089
+ if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2906
3090
  return;
2907
3091
  }
2908
3092
  if (this.shouldSkipLayer(layer)) {
@@ -2965,11 +3149,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
2965
3149
  return options?.negativeCache ?? this.options.negativeCaching ?? false;
2966
3150
  }
2967
3151
  scheduleBackgroundRefresh(key, fetcher, options) {
2968
- if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
3152
+ if (!shouldStartBackgroundRefresh({
3153
+ isDisconnecting: this.isDisconnecting,
3154
+ hasRefreshInFlight: this.backgroundRefreshes.has(key)
3155
+ })) {
2969
3156
  return;
2970
3157
  }
2971
- const clearEpoch = this.clearEpoch;
2972
- const keyEpoch = this.currentKeyEpoch(key);
3158
+ const clearEpoch = this.maintenance.currentClearEpoch();
3159
+ const keyEpoch = this.maintenance.currentKeyEpoch(key);
2973
3160
  const refresh = (async () => {
2974
3161
  this.metricsCollector.increment("refreshes");
2975
3162
  try {
@@ -3007,7 +3194,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3007
3194
  if (keys.length === 0) {
3008
3195
  return;
3009
3196
  }
3010
- this.bumpKeyEpochs(keys);
3197
+ this.maintenance.bumpKeyEpochs(keys);
3011
3198
  await this.deleteKeysFromLayers(this.layers, keys);
3012
3199
  for (const key of keys) {
3013
3200
  await this.tagIndex.remove(key);
@@ -3031,7 +3218,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3031
3218
  }
3032
3219
  const localLayers = this.layers.filter((layer) => layer.isLocal);
3033
3220
  if (message.scope === "clear") {
3034
- this.beginClearEpoch();
3221
+ this.maintenance.beginClearEpoch();
3035
3222
  await Promise.all(localLayers.map((layer) => layer.clear()));
3036
3223
  await this.tagIndex.clear();
3037
3224
  this.ttlResolver.clearProfiles();
@@ -3039,7 +3226,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3039
3226
  return;
3040
3227
  }
3041
3228
  const keys = message.keys ?? [];
3042
- this.bumpKeyEpochs(keys);
3229
+ this.maintenance.bumpKeyEpochs(keys);
3043
3230
  await this.deleteKeysFromLayers(localLayers, keys);
3044
3231
  if (message.operation !== "write") {
3045
3232
  for (const key of keys) {
@@ -3097,35 +3284,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
3097
3284
  shouldBroadcastL1Invalidation() {
3098
3285
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
3099
3286
  }
3100
- shouldCleanupGenerations() {
3101
- return Boolean(this.options.generationCleanup);
3102
- }
3103
- generationCleanupBatchSize() {
3104
- const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
3105
- return configured ?? 500;
3106
- }
3107
3287
  scheduleGenerationCleanup(generation) {
3108
- const task = (this.generationCleanupPromise ?? Promise.resolve()).then(() => this.cleanupGeneration(generation)).catch((error) => {
3109
- this.logger.warn?.("generation-cleanup-error", {
3110
- generation,
3111
- error: this.formatError(error)
3112
- });
3113
- });
3114
- this.generationCleanupPromise = task.finally(() => {
3115
- if (this.generationCleanupPromise === task) {
3116
- this.generationCleanupPromise = void 0;
3288
+ this.maintenance.scheduleGenerationCleanup(
3289
+ generation,
3290
+ async (generationToClean) => this.cleanupGeneration(generationToClean),
3291
+ (failedGeneration, error) => {
3292
+ this.logger.warn?.("generation-cleanup-error", {
3293
+ generation: failedGeneration,
3294
+ error: this.formatError(error)
3295
+ });
3117
3296
  }
3118
- });
3297
+ );
3119
3298
  }
3120
3299
  async cleanupGeneration(generation) {
3121
3300
  const prefix = `v${generation}:`;
3122
3301
  const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
3123
- if (keys.length === 0) {
3124
- return;
3125
- }
3126
- const batchSize = this.generationCleanupBatchSize();
3127
- for (let index = 0; index < keys.length; index += batchSize) {
3128
- const batch = keys.slice(index, index + batchSize);
3302
+ for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
3129
3303
  await this.deleteKeys(batch);
3130
3304
  await this.publishInvalidation({
3131
3305
  scope: "keys",
@@ -3136,80 +3310,34 @@ var CacheStack = class extends import_node_events.EventEmitter {
3136
3310
  }
3137
3311
  }
3138
3312
  initializeWriteBehind(options) {
3139
- if (this.options.writeStrategy !== "write-behind") {
3140
- return;
3141
- }
3142
- const flushIntervalMs = options?.flushIntervalMs;
3143
- if (!flushIntervalMs || flushIntervalMs <= 0) {
3144
- return;
3145
- }
3146
- this.writeBehindTimer = setInterval(() => {
3147
- void this.flushWriteBehindQueue();
3148
- }, flushIntervalMs);
3149
- this.writeBehindTimer.unref?.();
3313
+ this.maintenance.initializeWriteBehindTimer(
3314
+ this.options.writeStrategy,
3315
+ options,
3316
+ this.flushWriteBehindQueue.bind(this)
3317
+ );
3150
3318
  }
3151
3319
  shouldWriteBehind(layer) {
3152
3320
  return this.options.writeStrategy === "write-behind" && !layer.isLocal;
3153
3321
  }
3154
- beginClearEpoch() {
3155
- this.clearEpoch += 1;
3156
- this.keyEpochs.clear();
3157
- this.writeBehindQueue.length = 0;
3158
- }
3159
- currentKeyEpoch(key) {
3160
- return this.keyEpochs.get(key) ?? 0;
3161
- }
3162
- bumpKeyEpochs(keys) {
3163
- for (const key of keys) {
3164
- this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
3165
- }
3166
- }
3167
- isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
3168
- if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
3169
- return true;
3170
- }
3171
- if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
3172
- return true;
3173
- }
3174
- return false;
3175
- }
3176
3322
  async enqueueWriteBehind(operation) {
3177
- this.writeBehindQueue.push(operation);
3178
- const batchSize = this.options.writeBehind?.batchSize ?? 100;
3179
- const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
3180
- if (this.writeBehindQueue.length >= batchSize) {
3181
- await this.flushWriteBehindQueue();
3182
- return;
3183
- }
3184
- if (this.writeBehindQueue.length >= maxQueueSize) {
3185
- await this.flushWriteBehindQueue();
3186
- }
3323
+ await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
3187
3324
  }
3188
3325
  async flushWriteBehindQueue() {
3189
- if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
3190
- await this.writeBehindFlushPromise;
3326
+ await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
3327
+ }
3328
+ async runWriteBehindBatch(batch) {
3329
+ const results = await Promise.allSettled(batch.map((operation) => operation()));
3330
+ const failures = results.filter((result) => result.status === "rejected");
3331
+ if (failures.length === 0) {
3191
3332
  return;
3192
3333
  }
3193
- const batchSize = this.options.writeBehind?.batchSize ?? 100;
3194
- const batch = this.writeBehindQueue.splice(0, batchSize);
3195
- this.writeBehindFlushPromise = (async () => {
3196
- const results = await Promise.allSettled(batch.map((operation) => operation()));
3197
- const failures = results.filter((result) => result.status === "rejected");
3198
- if (failures.length > 0) {
3199
- this.metricsCollector.increment("writeFailures", failures.length);
3200
- this.logger.error?.("write-behind-flush-failure", {
3201
- failed: failures.length,
3202
- total: batch.length,
3203
- errors: failures.map((failure) => this.formatError(failure.reason))
3204
- });
3205
- this.emitError("write-behind", { failed: failures.length, total: batch.length });
3206
- }
3207
- })();
3208
- await this.writeBehindFlushPromise;
3209
- this.writeBehindFlushPromise = void 0;
3210
- if (this.writeBehindQueue.length > 0) {
3211
- await this.flushWriteBehindQueue();
3212
- }
3334
+ this.metricsCollector.increment("writeFailures", failures.length);
3335
+ this.logger.error?.("write-behind-flush-failure", {
3336
+ failed: failures.length,
3337
+ total: batch.length,
3338
+ errors: failures.map((failure) => this.formatError(failure.reason))
3339
+ });
3340
+ this.emitError("write-behind", { failed: failures.length, total: batch.length });
3213
3341
  }
3214
3342
  buildLayerSetEntry(layer, key, kind, value, options, now) {
3215
3343
  const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
@@ -3239,32 +3367,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
3239
3367
  return [];
3240
3368
  }
3241
3369
  const [firstGroup, ...rest] = groups;
3242
- if (!firstGroup) {
3243
- return [];
3244
- }
3245
3370
  const restSets = rest.map((group) => new Set(group));
3246
3371
  return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
3247
3372
  }
3248
3373
  qualifyKey(key) {
3249
- const prefix = this.generationPrefix();
3250
- return prefix ? `${prefix}${key}` : key;
3374
+ return qualifyGenerationKey(key, this.currentGeneration);
3251
3375
  }
3252
3376
  qualifyPattern(pattern) {
3253
- const prefix = this.generationPrefix();
3254
- return prefix ? `${prefix}${pattern}` : pattern;
3377
+ return qualifyGenerationPattern(pattern, this.currentGeneration);
3255
3378
  }
3256
3379
  stripQualifiedKey(key) {
3257
- const prefix = this.generationPrefix();
3258
- if (!prefix || !key.startsWith(prefix)) {
3259
- return key;
3260
- }
3261
- return key.slice(prefix.length);
3262
- }
3263
- generationPrefix() {
3264
- if (this.currentGeneration === void 0) {
3265
- return "";
3266
- }
3267
- return `v${this.currentGeneration}:`;
3380
+ return stripGenerationPrefix(key, this.currentGeneration);
3268
3381
  }
3269
3382
  async deleteKeysFromLayers(layers, keys) {
3270
3383
  await Promise.all(
@@ -3355,37 +3468,38 @@ var CacheStack = class extends import_node_events.EventEmitter {
3355
3468
  this.assertActive(operation);
3356
3469
  }
3357
3470
  async applyFreshReadPolicies(key, hit, options, fetcher) {
3358
- const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
3359
- const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
3360
- if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
3361
- const refreshed = refreshStoredEnvelope(hit.stored);
3362
- const ttl = remainingStoredTtlSeconds(refreshed);
3471
+ const plan = planFreshReadPolicies({
3472
+ stored: hit.stored,
3473
+ hasFetcher: Boolean(fetcher),
3474
+ slidingTtl: options?.slidingTtl ?? false,
3475
+ refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
3476
+ });
3477
+ if (plan.refreshedStored) {
3363
3478
  for (let index = 0; index <= hit.layerIndex; index += 1) {
3364
3479
  const layer = this.layers[index];
3365
3480
  if (!layer || this.shouldSkipLayer(layer)) {
3366
3481
  continue;
3367
3482
  }
3368
3483
  try {
3369
- await layer.set(key, refreshed, ttl);
3484
+ await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
3370
3485
  } catch (error) {
3371
3486
  await this.handleLayerFailure(layer, "sliding-ttl", error);
3372
3487
  }
3373
3488
  }
3374
3489
  }
3375
- if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
3490
+ if (fetcher && plan.shouldScheduleBackgroundRefresh) {
3376
3491
  this.scheduleBackgroundRefresh(key, fetcher, options);
3377
3492
  }
3378
3493
  }
3379
3494
  shouldSkipLayer(layer) {
3380
- const degradedUntil = this.layerDegradedUntil.get(layer.name);
3381
- return degradedUntil !== void 0 && degradedUntil > Date.now();
3495
+ return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
3382
3496
  }
3383
3497
  async handleLayerFailure(layer, operation, error) {
3384
- if (!this.isGracefulDegradationEnabled()) {
3498
+ const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
3499
+ if (!recovery.degrade) {
3385
3500
  throw error;
3386
3501
  }
3387
- const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
3388
- this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
3502
+ this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
3389
3503
  this.metricsCollector.increment("degradedOperations");
3390
3504
  this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
3391
3505
  this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });