layercache 1.3.3 → 1.4.0

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.
Files changed (40) hide show
  1. package/README.md +42 -41
  2. package/dist/{chunk-BORDQ3LA.js → chunk-7KMKQ6QZ.js} +15 -1
  3. package/dist/{chunk-5RCAX2BQ.js → chunk-FFZCC7EQ.js} +3 -3
  4. package/dist/{chunk-4PPBOOXT.js → chunk-KJDFYE5T.js} +38 -26
  5. package/dist/cli.cjs +9 -9
  6. package/dist/cli.js +4 -4
  7. package/dist/{edge-CUHTP9Bc.d.cts → edge-D2FpRlyS.d.cts} +74 -36
  8. package/dist/{edge-CUHTP9Bc.d.ts → edge-D2FpRlyS.d.ts} +74 -36
  9. package/dist/edge.cjs +9 -9
  10. package/dist/edge.d.cts +1 -1
  11. package/dist/edge.d.ts +1 -1
  12. package/dist/edge.js +2 -2
  13. package/dist/index.cjs +787 -466
  14. package/dist/index.d.cts +6 -6
  15. package/dist/index.d.ts +6 -6
  16. package/dist/index.js +682 -383
  17. package/package.json +6 -6
  18. package/benchmarks/direct.ts +0 -221
  19. package/benchmarks/edge-utils.ts +0 -28
  20. package/benchmarks/edge.ts +0 -491
  21. package/benchmarks/http.ts +0 -99
  22. package/benchmarks/latency.ts +0 -45
  23. package/benchmarks/memory-pressure.ts +0 -144
  24. package/benchmarks/multi-process-fanout.ts +0 -231
  25. package/benchmarks/multi-process-worker.ts +0 -151
  26. package/benchmarks/paths.ts +0 -25
  27. package/benchmarks/queue-amplification-utils.ts +0 -48
  28. package/benchmarks/queue-amplification.ts +0 -230
  29. package/benchmarks/redis-latency-proxy.ts +0 -100
  30. package/benchmarks/redis.ts +0 -107
  31. package/benchmarks/scenario-utils.ts +0 -38
  32. package/benchmarks/server.ts +0 -157
  33. package/benchmarks/slow-redis-latency.ts +0 -309
  34. package/benchmarks/slow-redis-utils.ts +0 -29
  35. package/benchmarks/slow-redis.ts +0 -47
  36. package/benchmarks/stampede.ts +0 -26
  37. package/benchmarks/stats.ts +0 -46
  38. package/benchmarks/workload.ts +0 -77
  39. package/examples/express-api/index.ts +0 -31
  40. package/examples/nextjs-api-routes/route.ts +0 -16
package/dist/index.cjs CHANGED
@@ -248,6 +248,9 @@ var CacheNamespace = class _CacheNamespace {
248
248
  async invalidateByTag(tag) {
249
249
  await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
250
250
  }
251
+ async expireByTag(tag) {
252
+ await this.trackMetrics(() => this.cache.expireByTag(this.qualifyTag(tag)));
253
+ }
251
254
  async invalidateByTags(tags, mode = "any") {
252
255
  await this.trackMetrics(
253
256
  () => this.cache.invalidateByTags(
@@ -256,12 +259,26 @@ var CacheNamespace = class _CacheNamespace {
256
259
  )
257
260
  );
258
261
  }
262
+ async expireByTags(tags, mode = "any") {
263
+ await this.trackMetrics(
264
+ () => this.cache.expireByTags(
265
+ tags.map((tag) => this.qualifyTag(tag)),
266
+ mode
267
+ )
268
+ );
269
+ }
259
270
  async invalidateByPattern(pattern) {
260
271
  await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
261
272
  }
273
+ async expireByPattern(pattern) {
274
+ await this.trackMetrics(() => this.cache.expireByPattern(this.qualify(pattern)));
275
+ }
262
276
  async invalidateByPrefix(prefix) {
263
277
  await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
264
278
  }
279
+ async expireByPrefix(prefix) {
280
+ await this.trackMetrics(() => this.cache.expireByPrefix(this.qualify(prefix)));
281
+ }
265
282
  /**
266
283
  * Returns detailed metadata about a single cache key within this namespace.
267
284
  */
@@ -602,68 +619,6 @@ function planGenerationCleanupBatches(keys, generationCleanup) {
602
619
  return batches;
603
620
  }
604
621
 
605
- // src/internal/CacheStackInvalidationSupport.ts
606
- var CacheStackInvalidationSupport = class {
607
- constructor(options) {
608
- this.options = options;
609
- }
610
- options;
611
- async collectKeysForTag(tag, maxKeys) {
612
- const keys = /* @__PURE__ */ new Set();
613
- if (this.options.tagIndex.forEachKeyForTag) {
614
- await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
615
- keys.add(key);
616
- this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
617
- });
618
- return [...keys];
619
- }
620
- for (const key of await this.options.tagIndex.keysForTag(tag)) {
621
- keys.add(key);
622
- this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
623
- }
624
- return [...keys];
625
- }
626
- intersectKeys(groups) {
627
- if (groups.length === 0) {
628
- return [];
629
- }
630
- const [firstGroup, ...rest] = groups;
631
- const restSets = rest.map((group) => new Set(group));
632
- return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
633
- }
634
- async deleteKeysFromLayers(layers, keys) {
635
- await Promise.all(
636
- layers.map(async (layer) => {
637
- if (this.options.shouldSkipLayer(layer)) {
638
- return;
639
- }
640
- if (layer.deleteMany) {
641
- try {
642
- await layer.deleteMany(keys);
643
- } catch (error) {
644
- await this.options.handleLayerFailure(layer, "delete", error);
645
- }
646
- return;
647
- }
648
- await Promise.all(
649
- keys.map(async (key) => {
650
- try {
651
- await layer.delete(key);
652
- } catch (error) {
653
- await this.options.handleLayerFailure(layer, "delete", error);
654
- }
655
- })
656
- );
657
- })
658
- );
659
- }
660
- assertWithinInvalidationKeyLimit(size, maxKeys) {
661
- if (maxKeys !== false && size > maxKeys) {
662
- throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
663
- }
664
- }
665
- };
666
-
667
622
  // src/internal/StoredValue.ts
668
623
  function isStoredValueEnvelope(value) {
669
624
  if (typeof value !== "object" || value === null) {
@@ -704,29 +659,29 @@ function isStoredValueEnvelope(value) {
704
659
  if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
705
660
  return false;
706
661
  }
707
- const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
708
- if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
662
+ const maxTtlMs = 10 * 365 * 24 * 60 * 60 * 1e3;
663
+ if (!isValidEnvelopeTtlMs(v.freshTtlMs, maxTtlMs)) {
709
664
  return false;
710
665
  }
711
- if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
666
+ if (!isValidEnvelopeTtlMs(v.staleWhileRevalidateMs, maxTtlMs)) {
712
667
  return false;
713
668
  }
714
- if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
669
+ if (!isValidEnvelopeTtlMs(v.staleIfErrorMs, maxTtlMs)) {
715
670
  return false;
716
671
  }
717
- if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
672
+ if (v.freshTtlMs == null && (v.staleWhileRevalidateMs != null || v.staleIfErrorMs != null)) {
718
673
  return false;
719
674
  }
720
675
  return true;
721
676
  }
722
677
  function createStoredValueEnvelope(options) {
723
678
  const now = options.now ?? Date.now();
724
- const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
725
- const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
726
- const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
727
- const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
728
- const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
729
- const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
679
+ const freshTtlMs = normalizePositiveMs(options.freshTtlMs);
680
+ const staleWhileRevalidateMs = normalizePositiveMs(options.staleWhileRevalidateMs);
681
+ const staleIfErrorMs = normalizePositiveMs(options.staleIfErrorMs);
682
+ const freshUntil = freshTtlMs ? now + freshTtlMs : null;
683
+ const staleUntil = freshUntil && staleWhileRevalidateMs ? freshUntil + staleWhileRevalidateMs : null;
684
+ const errorUntil = freshUntil && staleIfErrorMs ? freshUntil + staleIfErrorMs : null;
730
685
  return {
731
686
  __layercache: 1,
732
687
  kind: options.kind,
@@ -734,9 +689,9 @@ function createStoredValueEnvelope(options) {
734
689
  freshUntil,
735
690
  staleUntil,
736
691
  errorUntil,
737
- freshTtlSeconds: freshTtlSeconds ?? null,
738
- staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
739
- staleIfErrorSeconds: staleIfErrorSeconds ?? null
692
+ freshTtlMs: freshTtlMs ?? null,
693
+ staleWhileRevalidateMs: staleWhileRevalidateMs ?? null,
694
+ staleIfErrorMs: staleIfErrorMs ?? null
740
695
  };
741
696
  }
742
697
  function resolveStoredValue(stored, now = Date.now()) {
@@ -763,7 +718,7 @@ function unwrapStoredValue(stored) {
763
718
  }
764
719
  return stored.value ?? null;
765
720
  }
766
- function remainingStoredTtlSeconds(stored, now = Date.now()) {
721
+ function remainingStoredTtlMs(stored, now = Date.now()) {
767
722
  if (!isStoredValueEnvelope(stored)) {
768
723
  return void 0;
769
724
  }
@@ -775,9 +730,9 @@ function remainingStoredTtlSeconds(stored, now = Date.now()) {
775
730
  if (remainingMs <= 0) {
776
731
  return 1;
777
732
  }
778
- return Math.max(1, Math.ceil(remainingMs / 1e3));
733
+ return Math.max(1, Math.ceil(remainingMs));
779
734
  }
780
- function remainingFreshTtlSeconds(stored, now = Date.now()) {
735
+ function remainingFreshTtlMs(stored, now = Date.now()) {
781
736
  if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
782
737
  return void 0;
783
738
  }
@@ -785,7 +740,7 @@ function remainingFreshTtlSeconds(stored, now = Date.now()) {
785
740
  if (remainingMs <= 0) {
786
741
  return 0;
787
742
  }
788
- return Math.max(1, Math.ceil(remainingMs / 1e3));
743
+ return Math.max(1, Math.ceil(remainingMs));
789
744
  }
790
745
  function refreshStoredEnvelope(stored, now = Date.now()) {
791
746
  if (!isStoredValueEnvelope(stored)) {
@@ -794,12 +749,23 @@ function refreshStoredEnvelope(stored, now = Date.now()) {
794
749
  return createStoredValueEnvelope({
795
750
  kind: stored.kind,
796
751
  value: stored.value,
797
- freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
798
- staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
799
- staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
752
+ freshTtlMs: stored.freshTtlMs ?? void 0,
753
+ staleWhileRevalidateMs: stored.staleWhileRevalidateMs ?? void 0,
754
+ staleIfErrorMs: stored.staleIfErrorMs ?? void 0,
800
755
  now
801
756
  });
802
757
  }
758
+ function expireStoredEnvelope(stored, now = Date.now()) {
759
+ if (!isStoredValueEnvelope(stored)) {
760
+ return stored;
761
+ }
762
+ const futureDeadlines = [stored.staleUntil, stored.errorUntil].filter((value) => value !== null);
763
+ const freshUntil = futureDeadlines.length > 0 ? Math.min(now, ...futureDeadlines) : now;
764
+ return {
765
+ ...stored,
766
+ freshUntil
767
+ };
768
+ }
803
769
  function maxExpiry(stored) {
804
770
  const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
805
771
  (value) => value !== null
@@ -809,19 +775,110 @@ function maxExpiry(stored) {
809
775
  }
810
776
  return Math.max(...values);
811
777
  }
812
- function normalizePositiveSeconds(value) {
778
+ function normalizePositiveMs(value) {
813
779
  if (!value || value <= 0) {
814
780
  return void 0;
815
781
  }
816
782
  return value;
817
783
  }
818
- function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
784
+ function isValidEnvelopeTtlMs(value, maxTtlMs) {
819
785
  if (value == null) {
820
786
  return true;
821
787
  }
822
- return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
788
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlMs;
823
789
  }
824
790
 
791
+ // src/internal/CacheStackInvalidationSupport.ts
792
+ var CacheStackInvalidationSupport = class {
793
+ constructor(options) {
794
+ this.options = options;
795
+ }
796
+ options;
797
+ async collectKeysForTag(tag, maxKeys) {
798
+ const keys = /* @__PURE__ */ new Set();
799
+ if (this.options.tagIndex.forEachKeyForTag) {
800
+ await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
801
+ keys.add(key);
802
+ this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
803
+ });
804
+ return [...keys];
805
+ }
806
+ for (const key of await this.options.tagIndex.keysForTag(tag)) {
807
+ keys.add(key);
808
+ this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
809
+ }
810
+ return [...keys];
811
+ }
812
+ intersectKeys(groups) {
813
+ if (groups.length === 0) {
814
+ return [];
815
+ }
816
+ const [firstGroup, ...rest] = groups;
817
+ const restSets = rest.map((group) => new Set(group));
818
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
819
+ }
820
+ async deleteKeysFromLayers(layers, keys) {
821
+ await Promise.all(
822
+ layers.map(async (layer) => {
823
+ if (this.options.shouldSkipLayer(layer)) {
824
+ return;
825
+ }
826
+ if (layer.deleteMany) {
827
+ try {
828
+ await layer.deleteMany(keys);
829
+ } catch (error) {
830
+ await this.options.handleLayerFailure(layer, "delete", error);
831
+ }
832
+ return;
833
+ }
834
+ await Promise.all(
835
+ keys.map(async (key) => {
836
+ try {
837
+ await layer.delete(key);
838
+ } catch (error) {
839
+ await this.options.handleLayerFailure(layer, "delete", error);
840
+ }
841
+ })
842
+ );
843
+ })
844
+ );
845
+ }
846
+ async expireKeysInLayers(layers, keys) {
847
+ const foundKeys = /* @__PURE__ */ new Set();
848
+ await Promise.all(
849
+ layers.map(async (layer) => {
850
+ if (this.options.shouldSkipLayer(layer)) {
851
+ return;
852
+ }
853
+ await Promise.all(
854
+ keys.map(async (key) => {
855
+ try {
856
+ const stored = layer.getEntry ? await layer.getEntry(key) : await layer.get(key);
857
+ if (stored === null) {
858
+ return;
859
+ }
860
+ foundKeys.add(key);
861
+ const expired = expireStoredEnvelope(stored);
862
+ if (expired === stored) {
863
+ return;
864
+ }
865
+ await layer.set(key, expired, remainingStoredTtlMs(expired));
866
+ } catch (error) {
867
+ await this.options.handleLayerFailure(layer, "expire", error);
868
+ }
869
+ })
870
+ );
871
+ })
872
+ );
873
+ return foundKeys;
874
+ }
875
+ assertWithinInvalidationKeyLimit(size, maxKeys) {
876
+ if (maxKeys !== false && size > maxKeys) {
877
+ throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
878
+ }
879
+ }
880
+ };
881
+
825
882
  // src/internal/CacheStackLayerWriter.ts
826
883
  var CacheStackLayerWriter = class {
827
884
  constructor(options) {
@@ -933,12 +990,12 @@ var CacheStackLayerWriter = class {
933
990
  }
934
991
  buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
935
992
  const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
936
- const staleWhileRevalidate = this.options.resolveLayerSeconds(
993
+ const staleWhileRevalidate = this.options.resolveLayerMs(
937
994
  layer.name,
938
995
  writeOptions?.staleWhileRevalidate,
939
996
  this.options.globalStaleWhileRevalidate
940
997
  );
941
- const staleIfError = this.options.resolveLayerSeconds(
998
+ const staleIfError = this.options.resolveLayerMs(
942
999
  layer.name,
943
1000
  writeOptions?.staleIfError,
944
1001
  this.options.globalStaleIfError
@@ -946,12 +1003,12 @@ var CacheStackLayerWriter = class {
946
1003
  const payload = createStoredValueEnvelope({
947
1004
  kind,
948
1005
  value,
949
- freshTtlSeconds: freshTtl,
950
- staleWhileRevalidateSeconds: staleWhileRevalidate,
951
- staleIfErrorSeconds: staleIfError,
1006
+ freshTtlMs: freshTtl,
1007
+ staleWhileRevalidateMs: staleWhileRevalidate,
1008
+ staleIfErrorMs: staleIfError,
952
1009
  now
953
1010
  });
954
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1011
+ const ttl = remainingStoredTtlMs(payload, now) ?? freshTtl;
955
1012
  return {
956
1013
  key,
957
1014
  value: payload,
@@ -1094,18 +1151,420 @@ function planFreshReadPolicies({
1094
1151
  stored,
1095
1152
  hasFetcher,
1096
1153
  slidingTtl,
1097
- refreshAheadSeconds
1154
+ refreshAheadMs
1098
1155
  }) {
1099
1156
  const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
1100
- const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
1101
- const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
1157
+ const refreshedStoredTtl = refreshedStored ? remainingStoredTtlMs(refreshedStored) ?? void 0 : void 0;
1158
+ const remainingFreshTtl = remainingFreshTtlMs(stored) ?? 0;
1102
1159
  return {
1103
1160
  refreshedStored,
1104
1161
  refreshedStoredTtl,
1105
- shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
1162
+ shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadMs > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadMs
1106
1163
  };
1107
1164
  }
1108
1165
 
1166
+ // src/internal/CacheStackReader.ts
1167
+ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
1168
+ var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
1169
+ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1170
+ var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1171
+ var CacheStackReader = class {
1172
+ constructor(options) {
1173
+ this.options = options;
1174
+ }
1175
+ options;
1176
+ backgroundRefreshes = /* @__PURE__ */ new Map();
1177
+ backgroundRefreshAbort = /* @__PURE__ */ new Map();
1178
+ get activeRefreshCount() {
1179
+ return this.backgroundRefreshes.size;
1180
+ }
1181
+ async getPrepared(normalizedKey, fetcher, options) {
1182
+ const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
1183
+ if (hit.found) {
1184
+ this.options.ttlResolver.recordAccess(normalizedKey);
1185
+ if (this.isNegativeStoredValue(hit.stored)) {
1186
+ this.options.metricsCollector.increment("negativeCacheHits");
1187
+ }
1188
+ if (hit.state === "fresh") {
1189
+ this.options.metricsCollector.increment("hits");
1190
+ await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
1191
+ return hit.value;
1192
+ }
1193
+ if (hit.state === "stale-while-revalidate") {
1194
+ this.options.metricsCollector.increment("hits");
1195
+ this.options.metricsCollector.increment("staleHits");
1196
+ this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
1197
+ if (fetcher) {
1198
+ this.scheduleBackgroundRefresh(normalizedKey, fetcher, options, this.createFetcherContext(normalizedKey, hit));
1199
+ }
1200
+ return hit.value;
1201
+ }
1202
+ if (!fetcher) {
1203
+ this.options.metricsCollector.increment("hits");
1204
+ this.options.metricsCollector.increment("staleHits");
1205
+ this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
1206
+ return hit.value;
1207
+ }
1208
+ try {
1209
+ return await this.fetchWithGuards(
1210
+ normalizedKey,
1211
+ fetcher,
1212
+ options,
1213
+ void 0,
1214
+ void 0,
1215
+ false,
1216
+ this.createFetcherContext(normalizedKey, hit)
1217
+ );
1218
+ } catch (error) {
1219
+ this.options.metricsCollector.increment("staleHits");
1220
+ this.options.metricsCollector.increment("refreshErrors");
1221
+ this.options.logger.debug?.("stale-if-error", {
1222
+ key: normalizedKey,
1223
+ error: this.options.formatError(error)
1224
+ });
1225
+ return hit.value;
1226
+ }
1227
+ }
1228
+ this.options.metricsCollector.increment("misses");
1229
+ if (!fetcher) {
1230
+ return null;
1231
+ }
1232
+ return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true, {
1233
+ key: normalizedKey,
1234
+ currentValue: void 0,
1235
+ state: "miss"
1236
+ });
1237
+ }
1238
+ async readLayerEntry(layer, key) {
1239
+ if (this.options.shouldSkipLayer(layer)) {
1240
+ return null;
1241
+ }
1242
+ if (layer.getEntry) {
1243
+ try {
1244
+ return await layer.getEntry(key);
1245
+ } catch (error) {
1246
+ return this.options.handleLayerFailure(layer, "read", error);
1247
+ }
1248
+ }
1249
+ try {
1250
+ return await layer.get(key);
1251
+ } catch (error) {
1252
+ return this.options.handleLayerFailure(layer, "read", error);
1253
+ }
1254
+ }
1255
+ async backfill(key, stored, upToIndex, options) {
1256
+ if (upToIndex < 0) {
1257
+ return;
1258
+ }
1259
+ for (let index = 0; index <= upToIndex; index += 1) {
1260
+ const layer = this.options.layers[index];
1261
+ if (!layer || this.options.shouldSkipLayer(layer)) {
1262
+ continue;
1263
+ }
1264
+ const ttl = remainingStoredTtlMs(stored) ?? this.options.resolveLayerMs(layer.name, options?.ttl, void 0, layer.defaultTtl);
1265
+ try {
1266
+ await layer.set(key, stored, ttl);
1267
+ } catch (error) {
1268
+ await this.options.handleLayerFailure(layer, "backfill", error);
1269
+ continue;
1270
+ }
1271
+ this.options.metricsCollector.increment("backfills");
1272
+ this.options.logger.debug?.("backfill", { key, layer: layer.name });
1273
+ this.options.emit("backfill", { key, layer: layer.name });
1274
+ }
1275
+ }
1276
+ abortAllRefreshes() {
1277
+ for (const key of this.backgroundRefreshAbort.keys()) {
1278
+ this.backgroundRefreshAbort.set(key, true);
1279
+ }
1280
+ }
1281
+ getAllRefreshPromises() {
1282
+ return [...this.backgroundRefreshes.values()];
1283
+ }
1284
+ async readFromLayers(key, options, mode) {
1285
+ let sawRetainableValue = false;
1286
+ for (let index = 0; index < this.options.layers.length; index += 1) {
1287
+ const layer = this.options.layers[index];
1288
+ if (!layer) continue;
1289
+ const readStart = performance.now();
1290
+ const stored = await this.readLayerEntry(layer, key);
1291
+ const readDuration = performance.now() - readStart;
1292
+ this.options.metricsCollector.recordLatency(layer.name, readDuration);
1293
+ if (stored === null) {
1294
+ this.options.metricsCollector.incrementLayer("missesByLayer", layer.name);
1295
+ continue;
1296
+ }
1297
+ const resolved = resolveStoredValue(stored);
1298
+ if (resolved.state === "expired") {
1299
+ await layer.delete(key);
1300
+ continue;
1301
+ }
1302
+ sawRetainableValue = true;
1303
+ if (mode === "fresh-only" && resolved.state !== "fresh") {
1304
+ continue;
1305
+ }
1306
+ await this.options.tagIndex.touch(key);
1307
+ await this.backfill(key, stored, index - 1, options);
1308
+ this.options.metricsCollector.incrementLayer("hitsByLayer", layer.name);
1309
+ this.options.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
1310
+ this.options.emit("hit", {
1311
+ key,
1312
+ layer: layer.name,
1313
+ state: resolved.state
1314
+ });
1315
+ return {
1316
+ found: true,
1317
+ value: resolved.value,
1318
+ stored,
1319
+ state: resolved.state,
1320
+ layerIndex: index,
1321
+ layerName: layer.name
1322
+ };
1323
+ }
1324
+ if (!sawRetainableValue) {
1325
+ await this.options.tagIndex.remove(key);
1326
+ }
1327
+ this.options.logger.debug?.("miss", { key, mode });
1328
+ this.options.emit("miss", { key, mode });
1329
+ return { found: false, value: null, stored: null, state: "miss" };
1330
+ }
1331
+ async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false, fetcherContext = {
1332
+ key,
1333
+ currentValue: void 0,
1334
+ state: "miss"
1335
+ }) {
1336
+ const fetchTask = async () => {
1337
+ const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
1338
+ if (shouldRecheckFreshLayers) {
1339
+ const secondHit = await this.readFromLayers(key, options, "fresh-only");
1340
+ if (secondHit.found) {
1341
+ this.options.metricsCollector.increment("hits");
1342
+ return secondHit.value;
1343
+ }
1344
+ }
1345
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
1346
+ };
1347
+ const singleFlightTask = async () => {
1348
+ if (!this.options.singleFlightCoordinator) {
1349
+ return fetchTask();
1350
+ }
1351
+ try {
1352
+ return await this.options.singleFlightCoordinator.execute(
1353
+ key,
1354
+ this.resolveSingleFlightOptions(),
1355
+ fetchTask,
1356
+ () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext)
1357
+ );
1358
+ } catch (error) {
1359
+ if (!this.options.isGracefulDegradationEnabled()) {
1360
+ throw error;
1361
+ }
1362
+ this.options.metricsCollector.increment("degradedOperations");
1363
+ this.options.logger.warn?.("single-flight-coordinator-degraded", {
1364
+ key,
1365
+ error: this.options.formatError(error)
1366
+ });
1367
+ this.options.emitError("single-flight", {
1368
+ key,
1369
+ degraded: true,
1370
+ error: this.options.formatError(error)
1371
+ });
1372
+ return fetchTask();
1373
+ }
1374
+ };
1375
+ if (this.options.stampedePrevention === false) {
1376
+ return singleFlightTask();
1377
+ }
1378
+ return this.options.stampedeGuard.execute(key, singleFlightTask);
1379
+ }
1380
+ async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
1381
+ key,
1382
+ currentValue: void 0,
1383
+ state: "miss"
1384
+ }) {
1385
+ const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
1386
+ const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
1387
+ const deadline = Date.now() + timeoutMs;
1388
+ this.options.metricsCollector.increment("singleFlightWaits");
1389
+ this.options.emit("stampede-dedupe", { key });
1390
+ while (Date.now() < deadline) {
1391
+ const hit = await this.readFromLayers(key, options, "fresh-only");
1392
+ if (hit.found) {
1393
+ this.options.metricsCollector.increment("hits");
1394
+ return hit.value;
1395
+ }
1396
+ await this.options.sleep(pollIntervalMs);
1397
+ }
1398
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
1399
+ }
1400
+ async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
1401
+ key,
1402
+ currentValue: void 0,
1403
+ state: "miss"
1404
+ }) {
1405
+ this.options.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
1406
+ this.options.metricsCollector.increment("fetches");
1407
+ const fetchStart = Date.now();
1408
+ let fetched;
1409
+ try {
1410
+ fetched = await this.options.fetchRateLimiter.schedule(
1411
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1412
+ { key, fetcher },
1413
+ () => fetcher(fetcherContext)
1414
+ );
1415
+ this.options.circuitBreakerManager.recordSuccess(key);
1416
+ this.options.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1417
+ } catch (error) {
1418
+ this.options.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
1419
+ throw error;
1420
+ }
1421
+ if (fetched === null || fetched === void 0) {
1422
+ if (!this.shouldNegativeCache(options)) {
1423
+ return null;
1424
+ }
1425
+ if (this.options.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
1426
+ this.options.logger.debug?.("skip-negative-store-after-invalidation", {
1427
+ key,
1428
+ expectedClearEpoch,
1429
+ clearEpoch: this.options.maintenance.currentClearEpoch(),
1430
+ expectedKeyEpoch,
1431
+ keyEpoch: this.options.maintenance.currentKeyEpoch(key)
1432
+ });
1433
+ return null;
1434
+ }
1435
+ await this.options.storeEntry(key, "empty", null, options);
1436
+ return null;
1437
+ }
1438
+ if (options?.shouldCache) {
1439
+ try {
1440
+ if (!options.shouldCache(fetched)) {
1441
+ return fetched;
1442
+ }
1443
+ } catch (error) {
1444
+ this.options.logger.warn?.("shouldCache-error", {
1445
+ key,
1446
+ error: this.options.formatError(error)
1447
+ });
1448
+ }
1449
+ }
1450
+ if (this.options.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
1451
+ this.options.logger.debug?.("skip-store-after-invalidation", {
1452
+ key,
1453
+ expectedClearEpoch,
1454
+ clearEpoch: this.options.maintenance.currentClearEpoch(),
1455
+ expectedKeyEpoch,
1456
+ keyEpoch: this.options.maintenance.currentKeyEpoch(key)
1457
+ });
1458
+ return fetched;
1459
+ }
1460
+ await this.options.storeEntry(key, "value", fetched, options);
1461
+ return fetched;
1462
+ }
1463
+ runScheduleBackgroundRefresh(key, fetcher, options, fetcherContext) {
1464
+ this.scheduleBackgroundRefresh(key, fetcher, options, fetcherContext);
1465
+ }
1466
+ scheduleBackgroundRefresh(key, fetcher, options, fetcherContext = {
1467
+ key,
1468
+ currentValue: void 0,
1469
+ state: "miss"
1470
+ }) {
1471
+ if (!shouldStartBackgroundRefresh({
1472
+ isDisconnecting: this.options.isDisconnecting(),
1473
+ hasRefreshInFlight: this.backgroundRefreshes.has(key)
1474
+ })) {
1475
+ return;
1476
+ }
1477
+ const clearEpoch = this.options.maintenance.currentClearEpoch();
1478
+ const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
1479
+ this.backgroundRefreshAbort.set(key, false);
1480
+ const refresh = (async () => {
1481
+ this.options.metricsCollector.increment("refreshes");
1482
+ try {
1483
+ if (this.backgroundRefreshAbort.get(key)) return;
1484
+ await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch, fetcherContext);
1485
+ } catch (error) {
1486
+ if (this.backgroundRefreshAbort.get(key)) return;
1487
+ this.options.metricsCollector.increment("refreshErrors");
1488
+ this.options.logger.warn?.("background-refresh-error", {
1489
+ key,
1490
+ error: this.options.formatError(error)
1491
+ });
1492
+ } finally {
1493
+ this.backgroundRefreshes.delete(key);
1494
+ this.backgroundRefreshAbort.delete(key);
1495
+ }
1496
+ })();
1497
+ this.backgroundRefreshes.set(key, refresh);
1498
+ }
1499
+ async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
1500
+ key,
1501
+ currentValue: void 0,
1502
+ state: "miss"
1503
+ }) {
1504
+ const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
1505
+ await this.fetchWithGuards(
1506
+ key,
1507
+ (context) => this.options.withTimeout(fetcher(context), timeoutMs, () => {
1508
+ return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
1509
+ }),
1510
+ options,
1511
+ expectedClearEpoch,
1512
+ expectedKeyEpoch,
1513
+ false,
1514
+ fetcherContext
1515
+ );
1516
+ }
1517
+ async runApplyFreshReadPolicies(key, hit, options, fetcher) {
1518
+ return this.applyFreshReadPolicies(key, hit, options, fetcher);
1519
+ }
1520
+ async applyFreshReadPolicies(key, hit, options, fetcher) {
1521
+ const plan = planFreshReadPolicies({
1522
+ stored: hit.stored,
1523
+ hasFetcher: Boolean(fetcher),
1524
+ slidingTtl: options?.slidingTtl ?? false,
1525
+ refreshAheadMs: this.options.resolveLayerMs(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
1526
+ });
1527
+ if (plan.refreshedStored) {
1528
+ for (let index = 0; index <= hit.layerIndex; index += 1) {
1529
+ const layer = this.options.layers[index];
1530
+ if (!layer || this.options.shouldSkipLayer(layer)) {
1531
+ continue;
1532
+ }
1533
+ try {
1534
+ await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
1535
+ } catch (error) {
1536
+ await this.options.handleLayerFailure(layer, "sliding-ttl", error);
1537
+ }
1538
+ }
1539
+ }
1540
+ if (fetcher && plan.shouldScheduleBackgroundRefresh) {
1541
+ this.options.scheduleBackgroundRefreshDispatch(key, fetcher, options, this.createFetcherContext(key, hit));
1542
+ }
1543
+ }
1544
+ createFetcherContext(key, hit) {
1545
+ return {
1546
+ key,
1547
+ currentValue: hit.value === null ? void 0 : hit.value,
1548
+ state: hit.state,
1549
+ layer: hit.layerName
1550
+ };
1551
+ }
1552
+ resolveSingleFlightOptions() {
1553
+ return {
1554
+ leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
1555
+ waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
1556
+ pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
1557
+ renewIntervalMs: this.options.singleFlightRenewIntervalMs
1558
+ };
1559
+ }
1560
+ shouldNegativeCache(options) {
1561
+ return options?.negativeCache ?? this.options.negativeCaching ?? false;
1562
+ }
1563
+ isNegativeStoredValue(stored) {
1564
+ return isStoredValueEnvelope(stored) && stored.kind === "empty";
1565
+ }
1566
+ };
1567
+
1109
1568
  // src/internal/CacheStackSnapshotManager.ts
1110
1569
  var import_node_fs = require("fs");
1111
1570
 
@@ -1371,7 +1830,7 @@ var CacheStackSnapshotManager = class {
1371
1830
  await visitor({
1372
1831
  key: exportedKey,
1373
1832
  value: stored,
1374
- ttl: remainingStoredTtlSeconds(stored)
1833
+ ttl: remainingStoredTtlMs(stored)
1375
1834
  });
1376
1835
  };
1377
1836
  if (layer.forEachKey) {
@@ -1527,6 +1986,19 @@ function validateCircuitBreakerOptions(options) {
1527
1986
  validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
1528
1987
  validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
1529
1988
  }
1989
+ function validateContextEntryOptions(name, options) {
1990
+ if (!options) {
1991
+ return;
1992
+ }
1993
+ validateLayerNumberOption(`${name}.ttl`, options.ttl);
1994
+ validateLayerNumberOption(`${name}.negativeTtl`, options.negativeTtl);
1995
+ validateLayerNumberOption(`${name}.staleWhileRevalidate`, options.staleWhileRevalidate);
1996
+ validateLayerNumberOption(`${name}.staleIfError`, options.staleIfError);
1997
+ validateLayerNumberOption(`${name}.ttlJitter`, options.ttlJitter);
1998
+ validateTtlPolicy(`${name}.ttlPolicy`, options.ttlPolicy);
1999
+ validateAdaptiveTtlOptions(options.adaptiveTtl);
2000
+ validateTags(options.tags);
2001
+ }
1530
2002
 
1531
2003
  // src/internal/CircuitBreakerManager.ts
1532
2004
  var CircuitBreakerManager = class {
@@ -1959,7 +2431,7 @@ var MetricsCollector = class {
1959
2431
 
1960
2432
  // src/internal/TtlResolver.ts
1961
2433
  var import_node_crypto2 = require("crypto");
1962
- var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
2434
+ var DEFAULT_NEGATIVE_TTL_MS = 6e4;
1963
2435
  var secureRandom = {
1964
2436
  value() {
1965
2437
  return (0, import_node_crypto2.randomBytes)(4).readUInt32BE(0) / 4294967296;
@@ -1986,17 +2458,17 @@ var TtlResolver = class {
1986
2458
  }
1987
2459
  resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
1988
2460
  const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
1989
- const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
2461
+ const baseTtl = kind === "empty" ? this.resolveLayerMs(
1990
2462
  layerName,
1991
2463
  options?.negativeTtl,
1992
2464
  globalNegativeTtl,
1993
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
1994
- ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
2465
+ this.resolveLayerMs(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_MS
2466
+ ) : this.resolveLayerMs(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
1995
2467
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
1996
- const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
2468
+ const jitter = this.resolveLayerMs(layerName, options?.ttlJitter, void 0);
1997
2469
  return this.applyJitter(adaptiveTtl, jitter);
1998
2470
  }
1999
- resolveLayerSeconds(layerName, override, globalDefault, fallback) {
2471
+ resolveLayerMs(layerName, override, globalDefault, fallback) {
2000
2472
  if (override !== void 0) {
2001
2473
  return this.readLayerNumber(layerName, override) ?? fallback;
2002
2474
  }
@@ -2018,8 +2490,8 @@ var TtlResolver = class {
2018
2490
  if (profile.hits < hotAfter) {
2019
2491
  return ttl;
2020
2492
  }
2021
- const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
2022
- const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
2493
+ const step = this.resolveLayerMs(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
2494
+ const maxTtl = this.resolveLayerMs(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
2023
2495
  const multiplier = Math.floor(profile.hits / hotAfter);
2024
2496
  return Math.min(maxTtl, ttl + step * multiplier);
2025
2497
  }
@@ -2041,17 +2513,17 @@ var TtlResolver = class {
2041
2513
  if (policy === "until-midnight") {
2042
2514
  const nextMidnight = new Date(now);
2043
2515
  nextMidnight.setHours(24, 0, 0, 0);
2044
- return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
2516
+ return Math.max(1, Math.ceil(nextMidnight.getTime() - now.getTime()));
2045
2517
  }
2046
2518
  if (policy === "next-hour") {
2047
2519
  const nextHour = new Date(now);
2048
2520
  nextHour.setMinutes(60, 0, 0);
2049
- return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
2521
+ return Math.max(1, Math.ceil(nextHour.getTime() - now.getTime()));
2050
2522
  }
2051
- const alignToSeconds = policy.alignTo;
2052
- const currentSeconds = Math.floor(Date.now() / 1e3);
2053
- const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
2054
- return Math.max(1, nextBoundary - currentSeconds);
2523
+ const alignToMs = policy.alignTo;
2524
+ const currentMs = Date.now();
2525
+ const nextBoundary = Math.ceil((currentMs + 1) / alignToMs) * alignToMs;
2526
+ return Math.max(1, nextBoundary - currentMs);
2055
2527
  }
2056
2528
  readLayerNumber(layerName, value) {
2057
2529
  if (typeof value === "number") {
@@ -2423,10 +2895,6 @@ var CacheMissError = class extends Error {
2423
2895
  };
2424
2896
 
2425
2897
  // src/CacheStack.ts
2426
- var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
2427
- var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
2428
- var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
2429
- var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
2430
2898
  var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
2431
2899
  var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
2432
2900
  var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
@@ -2506,7 +2974,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2506
2974
  },
2507
2975
  enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
2508
2976
  resolveFreshTtl: this.resolveFreshTtl.bind(this),
2509
- resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
2977
+ resolveLayerMs: this.resolveLayerMs.bind(this),
2510
2978
  globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
2511
2979
  globalStaleIfError: this.options.staleIfError,
2512
2980
  writePolicy: this.options.writePolicy,
@@ -2537,7 +3005,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2537
3005
  layers: this.layers,
2538
3006
  tagIndex: this.tagIndex,
2539
3007
  snapshotSerializer: this.snapshotSerializer,
2540
- readLayerEntry: this.readLayerEntry.bind(this),
3008
+ readLayerEntry: (layer, key) => this.reader.readLayerEntry(layer, key),
2541
3009
  shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2542
3010
  handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
2543
3011
  qualifyKey: this.qualifyKey.bind(this),
@@ -2545,6 +3013,41 @@ var CacheStack = class extends import_node_events.EventEmitter {
2545
3013
  validateCacheKey,
2546
3014
  formatError: this.formatError.bind(this)
2547
3015
  });
3016
+ this.reader = new CacheStackReader({
3017
+ layers: this.layers,
3018
+ metricsCollector: this.metricsCollector,
3019
+ maintenance: this.maintenance,
3020
+ tagIndex: this.tagIndex,
3021
+ circuitBreakerManager: this.circuitBreakerManager,
3022
+ fetchRateLimiter: this.fetchRateLimiter,
3023
+ stampedeGuard: this.stampedeGuard,
3024
+ ttlResolver: this.ttlResolver,
3025
+ logger: this.logger,
3026
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
3027
+ handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
3028
+ emit: (event, data) => this.emit(event, data),
3029
+ emitError: (operation, context) => this.emitError(operation, context),
3030
+ formatError: (error) => this.formatError(error),
3031
+ storeEntry: (key, kind, value, options2) => this.storeEntry(key, kind, value, options2),
3032
+ recordCircuitFailure: (key, options2, error) => this.recordCircuitFailure(key, options2, error),
3033
+ resolveLayerMs: (layerName, override, globalDefault, fallback) => this.resolveLayerMs(layerName, override, globalDefault, fallback),
3034
+ sleep: (ms) => this.sleep(ms),
3035
+ withTimeout: (promise, ms, createError) => this.withTimeout(promise, ms, createError),
3036
+ isDisconnecting: () => this.isDisconnecting,
3037
+ isGracefulDegradationEnabled: () => this.isGracefulDegradationEnabled(),
3038
+ scheduleBackgroundRefreshDispatch: (key, fetcher, options2, fetcherContext) => this.scheduleBackgroundRefresh(key, fetcher, options2, fetcherContext),
3039
+ stampedePrevention: options.stampedePrevention,
3040
+ singleFlightCoordinator: options.singleFlightCoordinator,
3041
+ singleFlightLeaseMs: options.singleFlightLeaseMs,
3042
+ singleFlightTimeoutMs: options.singleFlightTimeoutMs,
3043
+ singleFlightPollMs: options.singleFlightPollMs,
3044
+ singleFlightRenewIntervalMs: options.singleFlightRenewIntervalMs,
3045
+ backgroundRefreshTimeoutMs: options.backgroundRefreshTimeoutMs,
3046
+ negativeCaching: options.negativeCaching,
3047
+ refreshAhead: options.refreshAhead,
3048
+ circuitBreaker: options.circuitBreaker,
3049
+ fetcherRateLimit: options.fetcherRateLimit
3050
+ });
2548
3051
  this.initializeWriteBehind(options.writeBehind);
2549
3052
  this.startup = this.initialize();
2550
3053
  }
@@ -2563,8 +3066,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
2563
3066
  invalidation;
2564
3067
  layerWriter;
2565
3068
  snapshots;
2566
- backgroundRefreshes = /* @__PURE__ */ new Map();
2567
- backgroundRefreshAbort = /* @__PURE__ */ new Map();
2568
3069
  layerDegradedUntil = /* @__PURE__ */ new Map();
2569
3070
  maintenance = new CacheStackMaintenance();
2570
3071
  ttlResolver;
@@ -2572,6 +3073,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2572
3073
  nextOperationId = 0;
2573
3074
  currentGeneration;
2574
3075
  isDisconnecting = false;
3076
+ reader;
2575
3077
  disconnectPromise;
2576
3078
  /**
2577
3079
  * Read-through cache get.
@@ -2584,51 +3086,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
2584
3086
  const normalizedKey = this.qualifyKey(validateCacheKey(key));
2585
3087
  this.validateWriteOptions(options);
2586
3088
  await this.awaitStartup("get");
2587
- return this.getPrepared(normalizedKey, fetcher, options);
3089
+ return this.reader.getPrepared(normalizedKey, fetcher, options);
2588
3090
  });
2589
3091
  }
2590
- async getPrepared(normalizedKey, fetcher, options) {
2591
- const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
2592
- if (hit.found) {
2593
- this.ttlResolver.recordAccess(normalizedKey);
2594
- if (this.isNegativeStoredValue(hit.stored)) {
2595
- this.metricsCollector.increment("negativeCacheHits");
2596
- }
2597
- if (hit.state === "fresh") {
2598
- this.metricsCollector.increment("hits");
2599
- await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
2600
- return hit.value;
2601
- }
2602
- if (hit.state === "stale-while-revalidate") {
2603
- this.metricsCollector.increment("hits");
2604
- this.metricsCollector.increment("staleHits");
2605
- this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
2606
- if (fetcher) {
2607
- this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
2608
- }
2609
- return hit.value;
2610
- }
2611
- if (!fetcher) {
2612
- this.metricsCollector.increment("hits");
2613
- this.metricsCollector.increment("staleHits");
2614
- this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
2615
- return hit.value;
2616
- }
2617
- try {
2618
- return await this.fetchWithGuards(normalizedKey, fetcher, options);
2619
- } catch (error) {
2620
- this.metricsCollector.increment("staleHits");
2621
- this.metricsCollector.increment("refreshErrors");
2622
- this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
2623
- return hit.value;
2624
- }
2625
- }
2626
- this.metricsCollector.increment("misses");
2627
- if (!fetcher) {
2628
- return null;
2629
- }
2630
- return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
2631
- }
2632
3092
  /**
2633
3093
  * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
2634
3094
  * Fetches and caches the value if not already present.
@@ -2681,7 +3141,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2681
3141
  return false;
2682
3142
  }
2683
3143
  /**
2684
- * Returns the remaining TTL in seconds for the key in the fastest layer
3144
+ * Returns the remaining TTL in milliseconds for the key in the fastest layer
2685
3145
  * that has it, or null if the key is not found / has no TTL.
2686
3146
  */
2687
3147
  async ttl(key) {
@@ -2779,7 +3239,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2779
3239
  const optionsSignature = serializeOptions(entry.options);
2780
3240
  const existing = pendingReads.get(entry.key);
2781
3241
  if (!existing) {
2782
- const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
3242
+ const promise = this.reader.getPrepared(entry.key, entry.fetch, entry.options);
2783
3243
  pendingReads.set(entry.key, {
2784
3244
  promise,
2785
3245
  fetch: entry.fetch,
@@ -2815,7 +3275,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2815
3275
  if (keys.length === 0) {
2816
3276
  break;
2817
3277
  }
2818
- const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
3278
+ const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.reader.readLayerEntry(layer, key)));
2819
3279
  for (let offset = 0; offset < values.length; offset += 1) {
2820
3280
  const key = keys[offset];
2821
3281
  const stored = values[offset];
@@ -2831,7 +3291,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2831
3291
  this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
2832
3292
  }
2833
3293
  await this.tagIndex.touch(key);
2834
- await this.backfill(key, stored, layerIndex - 1);
3294
+ await this.reader.backfill(key, stored, layerIndex - 1);
2835
3295
  resultsByKey.set(key, resolved.value);
2836
3296
  pending.delete(key);
2837
3297
  this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
@@ -2917,20 +3377,45 @@ var CacheStack = class extends import_node_events.EventEmitter {
2917
3377
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2918
3378
  });
2919
3379
  }
2920
- async invalidateByTags(tags, mode = "any") {
2921
- await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
3380
+ async expireByTag(tag) {
3381
+ await this.observeOperation("layercache.expire_by_tag", void 0, async () => {
3382
+ validateTag(tag);
3383
+ await this.awaitStartup("expireByTag");
3384
+ const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
3385
+ await this.expireKeys(keys);
3386
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
3387
+ });
3388
+ }
3389
+ async invalidateByTags(tags, mode = "any") {
3390
+ await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
3391
+ if (tags.length === 0) {
3392
+ return;
3393
+ }
3394
+ validateTags(tags);
3395
+ await this.awaitStartup("invalidateByTags");
3396
+ const keysByTag = await Promise.all(
3397
+ tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
3398
+ );
3399
+ const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
3400
+ this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
3401
+ await this.deleteKeys(keys);
3402
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3403
+ });
3404
+ }
3405
+ async expireByTags(tags, mode = "any") {
3406
+ await this.observeOperation("layercache.expire_by_tags", void 0, async () => {
2922
3407
  if (tags.length === 0) {
2923
3408
  return;
2924
3409
  }
2925
3410
  validateTags(tags);
2926
- await this.awaitStartup("invalidateByTags");
3411
+ await this.awaitStartup("expireByTags");
2927
3412
  const keysByTag = await Promise.all(
2928
3413
  tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
2929
3414
  );
2930
3415
  const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
2931
3416
  this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
2932
- await this.deleteKeys(keys);
2933
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3417
+ await this.expireKeys(keys);
3418
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
2934
3419
  });
2935
3420
  }
2936
3421
  async invalidateByPattern(pattern) {
@@ -2945,6 +3430,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
2945
3430
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2946
3431
  });
2947
3432
  }
3433
+ async expireByPattern(pattern) {
3434
+ await this.observeOperation("layercache.expire_by_pattern", void 0, async () => {
3435
+ validatePattern(pattern);
3436
+ await this.awaitStartup("expireByPattern");
3437
+ const keys = await this.keyDiscovery.collectKeysMatchingPattern(
3438
+ this.qualifyPattern(pattern),
3439
+ this.invalidationMaxKeys()
3440
+ );
3441
+ await this.expireKeys(keys);
3442
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
3443
+ });
3444
+ }
2948
3445
  async invalidateByPrefix(prefix) {
2949
3446
  await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
2950
3447
  await this.awaitStartup("invalidateByPrefix");
@@ -2954,6 +3451,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
2954
3451
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2955
3452
  });
2956
3453
  }
3454
+ async expireByPrefix(prefix) {
3455
+ await this.observeOperation("layercache.expire_by_prefix", void 0, async () => {
3456
+ await this.awaitStartup("expireByPrefix");
3457
+ const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
3458
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
3459
+ await this.expireKeys(keys);
3460
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
3461
+ });
3462
+ }
2957
3463
  getMetrics() {
2958
3464
  return this.metricsCollector.snapshot;
2959
3465
  }
@@ -2965,7 +3471,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2965
3471
  isLocal: Boolean(layer.isLocal),
2966
3472
  degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
2967
3473
  })),
2968
- backgroundRefreshes: this.backgroundRefreshes.size
3474
+ backgroundRefreshes: this.reader.activeRefreshCount
2969
3475
  };
2970
3476
  }
2971
3477
  resetMetrics() {
@@ -3030,9 +3536,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
3030
3536
  const normalizedKey = this.qualifyKey(userKey);
3031
3537
  await this.awaitStartup("inspect");
3032
3538
  const foundInLayers = [];
3033
- let freshTtlSeconds = null;
3034
- let staleTtlSeconds = null;
3035
- let errorTtlSeconds = null;
3539
+ let freshTtlMs = null;
3540
+ let staleTtlMs = null;
3541
+ let errorTtlMs = null;
3036
3542
  let isStale = false;
3037
3543
  for (const layer of this.layers) {
3038
3544
  if (this.shouldSkipLayer(layer)) {
@@ -3049,9 +3555,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
3049
3555
  foundInLayers.push(layer.name);
3050
3556
  if (foundInLayers.length === 1 && resolved.envelope) {
3051
3557
  const now = Date.now();
3052
- freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
3053
- staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
3054
- errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
3558
+ freshTtlMs = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil(resolved.envelope.freshUntil - now)) : null;
3559
+ staleTtlMs = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil(resolved.envelope.staleUntil - now)) : null;
3560
+ errorTtlMs = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil(resolved.envelope.errorUntil - now)) : null;
3055
3561
  isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
3056
3562
  }
3057
3563
  }
@@ -3059,7 +3565,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3059
3565
  return null;
3060
3566
  }
3061
3567
  const tags = await this.getTagsForKey(normalizedKey);
3062
- return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
3568
+ return { key: userKey, foundInLayers, freshTtlMs, staleTtlMs, errorTtlMs, isStale, tags };
3063
3569
  }
3064
3570
  async exportState() {
3065
3571
  await this.awaitStartup("exportState");
@@ -3085,11 +3591,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
3085
3591
  await this.unsubscribeInvalidation?.();
3086
3592
  await this.flushWriteBehindQueue();
3087
3593
  await this.maintenance.waitForGenerationCleanup();
3088
- for (const key of this.backgroundRefreshAbort.keys()) {
3089
- this.backgroundRefreshAbort.set(key, true);
3090
- }
3594
+ this.reader.abortAllRefreshes();
3091
3595
  await Promise.allSettled(
3092
- [...this.backgroundRefreshes.values()].map((promise) => {
3596
+ this.reader.getAllRefreshPromises().map((promise) => {
3093
3597
  let timer;
3094
3598
  return Promise.race([
3095
3599
  promise,
@@ -3102,8 +3606,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3102
3606
  });
3103
3607
  })
3104
3608
  );
3105
- this.backgroundRefreshes.clear();
3106
- this.backgroundRefreshAbort.clear();
3107
3609
  this.maintenance.disposeWriteBehindTimer();
3108
3610
  this.fetchRateLimiter.dispose();
3109
3611
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
@@ -3119,141 +3621,36 @@ var CacheStack = class extends import_node_events.EventEmitter {
3119
3621
  await this.handleInvalidationMessage(message);
3120
3622
  });
3121
3623
  }
3122
- async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
3123
- const fetchTask = async () => {
3124
- const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
3125
- if (shouldRecheckFreshLayers) {
3126
- const secondHit = await this.readFromLayers(key, options, "fresh-only");
3127
- if (secondHit.found) {
3128
- this.metricsCollector.increment("hits");
3129
- return secondHit.value;
3130
- }
3131
- }
3132
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
3133
- };
3134
- const singleFlightTask = async () => {
3135
- if (!this.options.singleFlightCoordinator) {
3136
- return fetchTask();
3137
- }
3138
- try {
3139
- return await this.options.singleFlightCoordinator.execute(
3140
- key,
3141
- this.resolveSingleFlightOptions(),
3142
- fetchTask,
3143
- () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
3144
- );
3145
- } catch (error) {
3146
- if (!this.isGracefulDegradationEnabled()) {
3147
- throw error;
3148
- }
3149
- this.metricsCollector.increment("degradedOperations");
3150
- this.logger.warn?.("single-flight-coordinator-degraded", { key, error: this.formatError(error) });
3151
- this.emitError("single-flight", { key, degraded: true, error: this.formatError(error) });
3152
- return fetchTask();
3153
- }
3154
- };
3155
- if (this.options.stampedePrevention === false) {
3156
- return singleFlightTask();
3157
- }
3158
- return this.stampedeGuard.execute(key, singleFlightTask);
3159
- }
3160
- async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
3161
- const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
3162
- const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
3163
- const deadline = Date.now() + timeoutMs;
3164
- this.metricsCollector.increment("singleFlightWaits");
3165
- this.emit("stampede-dedupe", { key });
3166
- while (Date.now() < deadline) {
3167
- const hit = await this.readFromLayers(key, options, "fresh-only");
3168
- if (hit.found) {
3169
- this.metricsCollector.increment("hits");
3170
- return hit.value;
3171
- }
3172
- await this.sleep(pollIntervalMs);
3173
- }
3174
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
3175
- }
3176
- async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
3177
- this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
3178
- this.metricsCollector.increment("fetches");
3179
- const fetchStart = Date.now();
3180
- let fetched;
3181
- try {
3182
- fetched = await this.fetchRateLimiter.schedule(
3183
- options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
3184
- { key, fetcher },
3185
- fetcher
3186
- );
3187
- this.circuitBreakerManager.recordSuccess(key);
3188
- this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
3189
- } catch (error) {
3190
- this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
3191
- throw error;
3192
- }
3193
- if (fetched === null || fetched === void 0) {
3194
- if (!this.shouldNegativeCache(options)) {
3195
- return null;
3196
- }
3197
- if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
3198
- this.logger.debug?.("skip-negative-store-after-invalidation", {
3199
- key,
3200
- expectedClearEpoch,
3201
- clearEpoch: this.maintenance.currentClearEpoch(),
3202
- expectedKeyEpoch,
3203
- keyEpoch: this.maintenance.currentKeyEpoch(key)
3204
- });
3205
- return null;
3206
- }
3207
- await this.storeEntry(key, "empty", null, options);
3208
- return null;
3209
- }
3210
- if (options?.shouldCache) {
3211
- try {
3212
- if (!options.shouldCache(fetched)) {
3213
- return fetched;
3214
- }
3215
- } catch (error) {
3216
- this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
3217
- }
3218
- }
3219
- if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
3220
- this.logger.debug?.("skip-store-after-invalidation", {
3221
- key,
3222
- expectedClearEpoch,
3223
- clearEpoch: this.maintenance.currentClearEpoch(),
3224
- expectedKeyEpoch,
3225
- keyEpoch: this.maintenance.currentKeyEpoch(key)
3226
- });
3227
- return fetched;
3228
- }
3229
- await this.storeEntry(key, "value", fetched, options);
3230
- return fetched;
3231
- }
3232
3624
  async storeEntry(key, kind, value, options) {
3625
+ const resolvedOptions = this.resolveContextOptions(key, kind, value, options);
3233
3626
  const clearEpoch = this.maintenance.currentClearEpoch();
3234
3627
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
3235
- await this.layerWriter.writeAcrossLayers(key, kind, value, options);
3628
+ await this.layerWriter.writeAcrossLayers(key, kind, value, resolvedOptions);
3236
3629
  if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
3237
3630
  return;
3238
3631
  }
3239
- if (options?.tags) {
3240
- await this.tagIndex.track(key, options.tags);
3632
+ if (resolvedOptions?.tags) {
3633
+ await this.tagIndex.track(key, resolvedOptions.tags);
3241
3634
  } else {
3242
3635
  await this.tagIndex.touch(key);
3243
3636
  }
3244
3637
  this.metricsCollector.increment("sets");
3245
- this.logger.debug?.("set", { key, kind, tags: options?.tags });
3246
- this.emit("set", { key, kind, tags: options?.tags });
3638
+ this.logger.debug?.("set", { key, kind, tags: resolvedOptions?.tags });
3639
+ this.emit("set", { key, kind, tags: resolvedOptions?.tags });
3247
3640
  if (this.shouldBroadcastL1Invalidation()) {
3248
3641
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
3249
3642
  }
3250
3643
  }
3251
3644
  async writeBatch(entries) {
3252
- const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
3645
+ const resolvedEntries = entries.map((entry) => ({
3646
+ ...entry,
3647
+ options: this.resolveContextOptions(entry.key, "value", entry.value, entry.options)
3648
+ }));
3649
+ const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(resolvedEntries);
3253
3650
  if (clearEpoch !== this.maintenance.currentClearEpoch()) {
3254
3651
  return;
3255
3652
  }
3256
- for (const entry of entries) {
3653
+ for (const entry of resolvedEntries) {
3257
3654
  if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
3258
3655
  continue;
3259
3656
  }
@@ -3275,87 +3672,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3275
3672
  });
3276
3673
  }
3277
3674
  }
3278
- async readFromLayers(key, options, mode) {
3279
- let sawRetainableValue = false;
3280
- for (let index = 0; index < this.layers.length; index += 1) {
3281
- const layer = this.layers[index];
3282
- if (!layer) continue;
3283
- const readStart = performance.now();
3284
- const stored = await this.readLayerEntry(layer, key);
3285
- const readDuration = performance.now() - readStart;
3286
- this.metricsCollector.recordLatency(layer.name, readDuration);
3287
- if (stored === null) {
3288
- this.metricsCollector.incrementLayer("missesByLayer", layer.name);
3289
- continue;
3290
- }
3291
- const resolved = resolveStoredValue(stored);
3292
- if (resolved.state === "expired") {
3293
- await layer.delete(key);
3294
- continue;
3295
- }
3296
- sawRetainableValue = true;
3297
- if (mode === "fresh-only" && resolved.state !== "fresh") {
3298
- continue;
3299
- }
3300
- await this.tagIndex.touch(key);
3301
- await this.backfill(key, stored, index - 1, options);
3302
- this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
3303
- this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
3304
- this.emit("hit", { key, layer: layer.name, state: resolved.state });
3305
- return {
3306
- found: true,
3307
- value: resolved.value,
3308
- stored,
3309
- state: resolved.state,
3310
- layerIndex: index,
3311
- layerName: layer.name
3312
- };
3313
- }
3314
- if (!sawRetainableValue) {
3315
- await this.tagIndex.remove(key);
3316
- }
3317
- this.logger.debug?.("miss", { key, mode });
3318
- this.emit("miss", { key, mode });
3319
- return { found: false, value: null, stored: null, state: "miss" };
3320
- }
3321
- async readLayerEntry(layer, key) {
3322
- if (this.shouldSkipLayer(layer)) {
3323
- return null;
3324
- }
3325
- if (layer.getEntry) {
3326
- try {
3327
- return await layer.getEntry(key);
3328
- } catch (error) {
3329
- return this.handleLayerFailure(layer, "read", error);
3330
- }
3331
- }
3332
- try {
3333
- return await layer.get(key);
3334
- } catch (error) {
3335
- return this.handleLayerFailure(layer, "read", error);
3336
- }
3337
- }
3338
- async backfill(key, stored, upToIndex, options) {
3339
- if (upToIndex < 0) {
3340
- return;
3341
- }
3342
- for (let index = 0; index <= upToIndex; index += 1) {
3343
- const layer = this.layers[index];
3344
- if (!layer || this.shouldSkipLayer(layer)) {
3345
- continue;
3346
- }
3347
- const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
3348
- try {
3349
- await layer.set(key, stored, ttl);
3350
- } catch (error) {
3351
- await this.handleLayerFailure(layer, "backfill", error);
3352
- continue;
3353
- }
3354
- this.metricsCollector.increment("backfills");
3355
- this.logger.debug?.("backfill", { key, layer: layer.name });
3356
- this.emit("backfill", { key, layer: layer.name });
3357
- }
3358
- }
3359
3675
  resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
3360
3676
  return this.ttlResolver.resolveFreshTtl(
3361
3677
  key,
@@ -3368,58 +3684,47 @@ var CacheStack = class extends import_node_events.EventEmitter {
3368
3684
  value
3369
3685
  );
3370
3686
  }
3371
- resolveLayerSeconds(layerName, override, globalDefault, fallback) {
3372
- return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
3373
- }
3374
- shouldNegativeCache(options) {
3375
- return options?.negativeCache ?? this.options.negativeCaching ?? false;
3687
+ resolveLayerMs(layerName, override, globalDefault, fallback) {
3688
+ return this.ttlResolver.resolveLayerMs(layerName, override, globalDefault, fallback);
3376
3689
  }
3377
- scheduleBackgroundRefresh(key, fetcher, options) {
3378
- if (!shouldStartBackgroundRefresh({
3379
- isDisconnecting: this.isDisconnecting,
3380
- hasRefreshInFlight: this.backgroundRefreshes.has(key)
3381
- })) {
3382
- return;
3690
+ resolveContextOptions(key, kind, value, options) {
3691
+ if (!options?.contextOptions) {
3692
+ return options;
3693
+ }
3694
+ const { contextOptions, ...baseOptions } = options;
3695
+ let overrides;
3696
+ try {
3697
+ overrides = contextOptions({ key, value, kind });
3698
+ } catch (error) {
3699
+ throw new Error(`options.contextOptions() failed for key "${key}": ${this.formatError(error)}`);
3700
+ }
3701
+ if (!overrides) {
3702
+ return baseOptions;
3703
+ }
3704
+ if (!this.isPlainObject(overrides)) {
3705
+ throw new Error(
3706
+ `options.contextOptions() must return a plain object or undefined for key "${key}". Async resolvers are not supported.`
3707
+ );
3708
+ }
3709
+ try {
3710
+ validateContextEntryOptions("options.contextOptions()", overrides);
3711
+ } catch (error) {
3712
+ throw new Error(
3713
+ `options.contextOptions() returned invalid entry options for key "${key}": ${this.formatError(error)}`
3714
+ );
3383
3715
  }
3384
- const clearEpoch = this.maintenance.currentClearEpoch();
3385
- const keyEpoch = this.maintenance.currentKeyEpoch(key);
3386
- this.backgroundRefreshAbort.set(key, false);
3387
- const refresh = (async () => {
3388
- this.metricsCollector.increment("refreshes");
3389
- try {
3390
- if (this.backgroundRefreshAbort.get(key)) return;
3391
- await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
3392
- } catch (error) {
3393
- if (this.backgroundRefreshAbort.get(key)) return;
3394
- this.metricsCollector.increment("refreshErrors");
3395
- this.logger.warn?.("background-refresh-error", { key, error: this.formatError(error) });
3396
- } finally {
3397
- this.backgroundRefreshes.delete(key);
3398
- this.backgroundRefreshAbort.delete(key);
3399
- }
3400
- })();
3401
- this.backgroundRefreshes.set(key, refresh);
3402
- }
3403
- async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
3404
- const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
3405
- await this.fetchWithGuards(
3406
- key,
3407
- () => this.withTimeout(fetcher(), timeoutMs, () => {
3408
- return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
3409
- }),
3410
- options,
3411
- expectedClearEpoch,
3412
- expectedKeyEpoch
3413
- );
3414
- }
3415
- resolveSingleFlightOptions() {
3416
3716
  return {
3417
- leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
3418
- waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
3419
- pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
3420
- renewIntervalMs: this.options.singleFlightRenewIntervalMs
3717
+ ...baseOptions,
3718
+ ...overrides
3421
3719
  };
3422
3720
  }
3721
+ isPlainObject(value) {
3722
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3723
+ return false;
3724
+ }
3725
+ const prototype = Object.getPrototypeOf(value);
3726
+ return prototype === Object.prototype || prototype === null;
3727
+ }
3423
3728
  async deleteKeys(keys) {
3424
3729
  if (keys.length === 0) {
3425
3730
  return;
@@ -3436,6 +3741,30 @@ var CacheStack = class extends import_node_events.EventEmitter {
3436
3741
  this.logger.debug?.("delete", { keys });
3437
3742
  this.emit("delete", { keys });
3438
3743
  }
3744
+ async expireKeys(keys) {
3745
+ if (keys.length === 0) {
3746
+ return;
3747
+ }
3748
+ this.maintenance.bumpKeyEpochs(keys);
3749
+ const foundKeys = await this.expireKeysInLayers(keys, this.layers);
3750
+ for (const key of keys) {
3751
+ if (foundKeys.has(key)) {
3752
+ continue;
3753
+ }
3754
+ await this.tagIndex.remove(key);
3755
+ this.ttlResolver.deleteProfile(key);
3756
+ this.circuitBreakerManager.delete(key);
3757
+ }
3758
+ this.metricsCollector.increment("invalidations");
3759
+ this.logger.debug?.("expire", { keys });
3760
+ this.emit("expire", { keys });
3761
+ }
3762
+ async expireKeysInLayers(keys, layers) {
3763
+ if (keys.length === 0) {
3764
+ return /* @__PURE__ */ new Set();
3765
+ }
3766
+ return this.invalidation.expireKeysInLayers(layers, keys);
3767
+ }
3439
3768
  async publishInvalidation(message) {
3440
3769
  if (!this.options.invalidationBus) {
3441
3770
  return;
@@ -3457,6 +3786,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
3457
3786
  }
3458
3787
  const keys = message.keys ?? [];
3459
3788
  this.maintenance.bumpKeyEpochs(keys);
3789
+ if (message.operation === "expire") {
3790
+ await this.expireKeysInLayers(keys, localLayers);
3791
+ return;
3792
+ }
3460
3793
  await this.invalidation.deleteKeysFromLayers(localLayers, keys);
3461
3794
  if (message.operation !== "write") {
3462
3795
  for (const key of keys) {
@@ -3654,6 +3987,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
3654
3987
  validateCircuitBreakerOptions(options.circuitBreaker);
3655
3988
  validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
3656
3989
  validateTags(options.tags);
3990
+ if (options.contextOptions && typeof options.contextOptions !== "function") {
3991
+ throw new Error("options.contextOptions must be a function.");
3992
+ }
3657
3993
  }
3658
3994
  assertActive(operation) {
3659
3995
  if (this.isDisconnecting) {
@@ -3665,29 +4001,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
3665
4001
  await this.startup;
3666
4002
  this.assertActive(operation);
3667
4003
  }
4004
+ async readLayerEntry(layer, key) {
4005
+ return this.reader.readLayerEntry(layer, key);
4006
+ }
4007
+ scheduleBackgroundRefresh(key, fetcher, options, fetcherContext) {
4008
+ this.reader.runScheduleBackgroundRefresh(key, fetcher, options, fetcherContext);
4009
+ }
3668
4010
  async applyFreshReadPolicies(key, hit, options, fetcher) {
3669
- const plan = planFreshReadPolicies({
3670
- stored: hit.stored,
3671
- hasFetcher: Boolean(fetcher),
3672
- slidingTtl: options?.slidingTtl ?? false,
3673
- refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
3674
- });
3675
- if (plan.refreshedStored) {
3676
- for (let index = 0; index <= hit.layerIndex; index += 1) {
3677
- const layer = this.layers[index];
3678
- if (!layer || this.shouldSkipLayer(layer)) {
3679
- continue;
3680
- }
3681
- try {
3682
- await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
3683
- } catch (error) {
3684
- await this.handleLayerFailure(layer, "sliding-ttl", error);
3685
- }
3686
- }
3687
- }
3688
- if (fetcher && plan.shouldScheduleBackgroundRefresh) {
3689
- this.scheduleBackgroundRefresh(key, fetcher, options);
3690
- }
4011
+ return this.reader.runApplyFreshReadPolicies(key, hit, options, fetcher);
3691
4012
  }
3692
4013
  shouldSkipLayer(layer) {
3693
4014
  const degradedUntil = this.layerDegradedUntil.get(layer.name);
@@ -3729,9 +4050,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3729
4050
  }
3730
4051
  this.emitError("fetch", { key, error: this.formatError(error) });
3731
4052
  }
3732
- isNegativeStoredValue(stored) {
3733
- return isStoredValueEnvelope(stored) && stored.kind === "empty";
3734
- }
3735
4053
  emitError(operation, context) {
3736
4054
  this.logger.error?.(operation, context);
3737
4055
  if (this.listenerCount("error") > 0) {
@@ -3831,7 +4149,7 @@ var RedisInvalidationBus = class {
3831
4149
  }
3832
4150
  const candidate = value;
3833
4151
  const validScope = candidate.scope === "key" || candidate.scope === "keys" || candidate.scope === "clear";
3834
- const validOperation = candidate.operation === void 0 || candidate.operation === "write" || candidate.operation === "delete" || candidate.operation === "invalidate" || candidate.operation === "clear";
4152
+ const validOperation = candidate.operation === void 0 || candidate.operation === "write" || candidate.operation === "delete" || candidate.operation === "invalidate" || candidate.operation === "expire" || candidate.operation === "clear";
3835
4153
  const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
3836
4154
  return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
3837
4155
  }
@@ -4354,7 +4672,7 @@ var MemoryLayer = class {
4354
4672
  this.entries.delete(key);
4355
4673
  this.entries.set(key, {
4356
4674
  value,
4357
- expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null,
4675
+ expiresAt: ttl && ttl > 0 ? Date.now() + ttl : null,
4358
4676
  accessCount: 0,
4359
4677
  insertedAt: Date.now()
4360
4678
  });
@@ -4385,7 +4703,7 @@ var MemoryLayer = class {
4385
4703
  if (entry.expiresAt === null) {
4386
4704
  return null;
4387
4705
  }
4388
- return Math.max(0, Math.ceil((entry.expiresAt - Date.now()) / 1e3));
4706
+ return Math.max(0, Math.ceil(entry.expiresAt - Date.now()));
4389
4707
  }
4390
4708
  async size() {
4391
4709
  this.pruneExpired();
@@ -4575,7 +4893,7 @@ var RedisLayer = class {
4575
4893
  const payload = await this.encodePayload(serialized);
4576
4894
  const normalizedKey = this.withPrefix(entry.key);
4577
4895
  if (entry.ttl && entry.ttl > 0) {
4578
- pipeline.set(normalizedKey, payload, "EX", entry.ttl);
4896
+ pipeline.set(normalizedKey, payload, "PX", entry.ttl);
4579
4897
  } else {
4580
4898
  pipeline.set(normalizedKey, payload);
4581
4899
  }
@@ -4590,7 +4908,7 @@ var RedisLayer = class {
4590
4908
  if (ttl && ttl > 0) {
4591
4909
  await this.runCommand(
4592
4910
  `set(${this.displayKey(key)})`,
4593
- () => this.client.set(normalizedKey, payload, "EX", ttl)
4911
+ () => this.client.set(normalizedKey, payload, "PX", ttl)
4594
4912
  );
4595
4913
  return;
4596
4914
  }
@@ -4619,7 +4937,10 @@ var RedisLayer = class {
4619
4937
  }
4620
4938
  async ttl(key) {
4621
4939
  this.validateKey(key);
4622
- const remaining = await this.runCommand(`ttl(${this.displayKey(key)})`, () => this.client.ttl(this.withPrefix(key)));
4940
+ const remaining = await this.runCommand(
4941
+ `ttl(${this.displayKey(key)})`,
4942
+ () => this.client.pttl(this.withPrefix(key))
4943
+ );
4623
4944
  if (remaining < 0) {
4624
4945
  return null;
4625
4946
  }
@@ -4770,12 +5091,12 @@ var RedisLayer = class {
4770
5091
  const payload = await this.encodePayload(serialized);
4771
5092
  const ttl = await this.runCommand(
4772
5093
  `rewrite-ttl(${this.displayKey(key)})`,
4773
- () => this.client.ttl(this.withPrefix(key))
5094
+ () => this.client.pttl(this.withPrefix(key))
4774
5095
  );
4775
5096
  if (ttl > 0) {
4776
5097
  await this.runCommand(
4777
5098
  `rewrite-set(${this.displayKey(key)})`,
4778
- () => this.client.set(this.withPrefix(key), payload, "EX", ttl)
5099
+ () => this.client.set(this.withPrefix(key), payload, "PX", ttl)
4779
5100
  );
4780
5101
  return;
4781
5102
  }
@@ -5086,7 +5407,7 @@ var DiskLayer = class {
5086
5407
  const entry = {
5087
5408
  key,
5088
5409
  value,
5089
- expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
5410
+ expiresAt: ttl && ttl > 0 ? Date.now() + ttl : null
5090
5411
  };
5091
5412
  const payload = this.serializer.serialize(entry);
5092
5413
  const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
@@ -5131,7 +5452,7 @@ var DiskLayer = class {
5131
5452
  if (entry.expiresAt === null) {
5132
5453
  return null;
5133
5454
  }
5134
- const remaining = Math.ceil((entry.expiresAt - Date.now()) / 1e3);
5455
+ const remaining = Math.ceil(entry.expiresAt - Date.now());
5135
5456
  if (remaining <= 0) {
5136
5457
  return null;
5137
5458
  }
@@ -5421,7 +5742,7 @@ var MemcachedLayer = class {
5421
5742
  this.validateKey(key);
5422
5743
  const payload = this.serializer.serialize(value);
5423
5744
  await this.client.set(this.withPrefix(key), payload, {
5424
- expires: ttl && ttl > 0 ? ttl : void 0
5745
+ expires: ttl && ttl > 0 ? Math.ceil(ttl / 1e3) : void 0
5425
5746
  });
5426
5747
  }
5427
5748
  async has(key) {