layercache 1.3.4 → 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-DKkrQ_Ky.d.cts → edge-D2FpRlyS.d.cts} +71 -22
  8. package/dist/{edge-DKkrQ_Ky.d.ts → edge-D2FpRlyS.d.ts} +71 -22
  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 +399 -164
  14. package/dist/index.d.cts +6 -6
  15. package/dist/index.d.ts +6 -6
  16. package/dist/index.js +294 -81
  17. package/package.json +5 -5
  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,15 +1151,15 @@ 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
 
@@ -1138,7 +1195,7 @@ var CacheStackReader = class {
1138
1195
  this.options.metricsCollector.increment("staleHits");
1139
1196
  this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
1140
1197
  if (fetcher) {
1141
- this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
1198
+ this.scheduleBackgroundRefresh(normalizedKey, fetcher, options, this.createFetcherContext(normalizedKey, hit));
1142
1199
  }
1143
1200
  return hit.value;
1144
1201
  }
@@ -1149,7 +1206,15 @@ var CacheStackReader = class {
1149
1206
  return hit.value;
1150
1207
  }
1151
1208
  try {
1152
- return await this.fetchWithGuards(normalizedKey, fetcher, options);
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
+ );
1153
1218
  } catch (error) {
1154
1219
  this.options.metricsCollector.increment("staleHits");
1155
1220
  this.options.metricsCollector.increment("refreshErrors");
@@ -1164,7 +1229,11 @@ var CacheStackReader = class {
1164
1229
  if (!fetcher) {
1165
1230
  return null;
1166
1231
  }
1167
- return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
1232
+ return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true, {
1233
+ key: normalizedKey,
1234
+ currentValue: void 0,
1235
+ state: "miss"
1236
+ });
1168
1237
  }
1169
1238
  async readLayerEntry(layer, key) {
1170
1239
  if (this.options.shouldSkipLayer(layer)) {
@@ -1192,7 +1261,7 @@ var CacheStackReader = class {
1192
1261
  if (!layer || this.options.shouldSkipLayer(layer)) {
1193
1262
  continue;
1194
1263
  }
1195
- const ttl = remainingStoredTtlSeconds(stored) ?? this.options.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
1264
+ const ttl = remainingStoredTtlMs(stored) ?? this.options.resolveLayerMs(layer.name, options?.ttl, void 0, layer.defaultTtl);
1196
1265
  try {
1197
1266
  await layer.set(key, stored, ttl);
1198
1267
  } catch (error) {
@@ -1259,7 +1328,11 @@ var CacheStackReader = class {
1259
1328
  this.options.emit("miss", { key, mode });
1260
1329
  return { found: false, value: null, stored: null, state: "miss" };
1261
1330
  }
1262
- async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
1331
+ async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false, fetcherContext = {
1332
+ key,
1333
+ currentValue: void 0,
1334
+ state: "miss"
1335
+ }) {
1263
1336
  const fetchTask = async () => {
1264
1337
  const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
1265
1338
  if (shouldRecheckFreshLayers) {
@@ -1269,7 +1342,7 @@ var CacheStackReader = class {
1269
1342
  return secondHit.value;
1270
1343
  }
1271
1344
  }
1272
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
1345
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
1273
1346
  };
1274
1347
  const singleFlightTask = async () => {
1275
1348
  if (!this.options.singleFlightCoordinator) {
@@ -1280,7 +1353,7 @@ var CacheStackReader = class {
1280
1353
  key,
1281
1354
  this.resolveSingleFlightOptions(),
1282
1355
  fetchTask,
1283
- () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
1356
+ () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext)
1284
1357
  );
1285
1358
  } catch (error) {
1286
1359
  if (!this.options.isGracefulDegradationEnabled()) {
@@ -1304,7 +1377,11 @@ var CacheStackReader = class {
1304
1377
  }
1305
1378
  return this.options.stampedeGuard.execute(key, singleFlightTask);
1306
1379
  }
1307
- async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
1380
+ async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
1381
+ key,
1382
+ currentValue: void 0,
1383
+ state: "miss"
1384
+ }) {
1308
1385
  const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
1309
1386
  const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
1310
1387
  const deadline = Date.now() + timeoutMs;
@@ -1318,9 +1395,13 @@ var CacheStackReader = class {
1318
1395
  }
1319
1396
  await this.options.sleep(pollIntervalMs);
1320
1397
  }
1321
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
1398
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
1322
1399
  }
1323
- async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
1400
+ async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
1401
+ key,
1402
+ currentValue: void 0,
1403
+ state: "miss"
1404
+ }) {
1324
1405
  this.options.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
1325
1406
  this.options.metricsCollector.increment("fetches");
1326
1407
  const fetchStart = Date.now();
@@ -1329,7 +1410,7 @@ var CacheStackReader = class {
1329
1410
  fetched = await this.options.fetchRateLimiter.schedule(
1330
1411
  options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1331
1412
  { key, fetcher },
1332
- fetcher
1413
+ () => fetcher(fetcherContext)
1333
1414
  );
1334
1415
  this.options.circuitBreakerManager.recordSuccess(key);
1335
1416
  this.options.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
@@ -1379,10 +1460,14 @@ var CacheStackReader = class {
1379
1460
  await this.options.storeEntry(key, "value", fetched, options);
1380
1461
  return fetched;
1381
1462
  }
1382
- runScheduleBackgroundRefresh(key, fetcher, options) {
1383
- this.scheduleBackgroundRefresh(key, fetcher, options);
1463
+ runScheduleBackgroundRefresh(key, fetcher, options, fetcherContext) {
1464
+ this.scheduleBackgroundRefresh(key, fetcher, options, fetcherContext);
1384
1465
  }
1385
- scheduleBackgroundRefresh(key, fetcher, options) {
1466
+ scheduleBackgroundRefresh(key, fetcher, options, fetcherContext = {
1467
+ key,
1468
+ currentValue: void 0,
1469
+ state: "miss"
1470
+ }) {
1386
1471
  if (!shouldStartBackgroundRefresh({
1387
1472
  isDisconnecting: this.options.isDisconnecting(),
1388
1473
  hasRefreshInFlight: this.backgroundRefreshes.has(key)
@@ -1396,7 +1481,7 @@ var CacheStackReader = class {
1396
1481
  this.options.metricsCollector.increment("refreshes");
1397
1482
  try {
1398
1483
  if (this.backgroundRefreshAbort.get(key)) return;
1399
- await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
1484
+ await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch, fetcherContext);
1400
1485
  } catch (error) {
1401
1486
  if (this.backgroundRefreshAbort.get(key)) return;
1402
1487
  this.options.metricsCollector.increment("refreshErrors");
@@ -1411,16 +1496,22 @@ var CacheStackReader = class {
1411
1496
  })();
1412
1497
  this.backgroundRefreshes.set(key, refresh);
1413
1498
  }
1414
- async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
1499
+ async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
1500
+ key,
1501
+ currentValue: void 0,
1502
+ state: "miss"
1503
+ }) {
1415
1504
  const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
1416
1505
  await this.fetchWithGuards(
1417
1506
  key,
1418
- () => this.options.withTimeout(fetcher(), timeoutMs, () => {
1507
+ (context) => this.options.withTimeout(fetcher(context), timeoutMs, () => {
1419
1508
  return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
1420
1509
  }),
1421
1510
  options,
1422
1511
  expectedClearEpoch,
1423
- expectedKeyEpoch
1512
+ expectedKeyEpoch,
1513
+ false,
1514
+ fetcherContext
1424
1515
  );
1425
1516
  }
1426
1517
  async runApplyFreshReadPolicies(key, hit, options, fetcher) {
@@ -1431,7 +1522,7 @@ var CacheStackReader = class {
1431
1522
  stored: hit.stored,
1432
1523
  hasFetcher: Boolean(fetcher),
1433
1524
  slidingTtl: options?.slidingTtl ?? false,
1434
- refreshAheadSeconds: this.options.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
1525
+ refreshAheadMs: this.options.resolveLayerMs(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
1435
1526
  });
1436
1527
  if (plan.refreshedStored) {
1437
1528
  for (let index = 0; index <= hit.layerIndex; index += 1) {
@@ -1447,9 +1538,17 @@ var CacheStackReader = class {
1447
1538
  }
1448
1539
  }
1449
1540
  if (fetcher && plan.shouldScheduleBackgroundRefresh) {
1450
- this.options.scheduleBackgroundRefreshDispatch(key, fetcher, options);
1541
+ this.options.scheduleBackgroundRefreshDispatch(key, fetcher, options, this.createFetcherContext(key, hit));
1451
1542
  }
1452
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
+ }
1453
1552
  resolveSingleFlightOptions() {
1454
1553
  return {
1455
1554
  leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
@@ -1731,7 +1830,7 @@ var CacheStackSnapshotManager = class {
1731
1830
  await visitor({
1732
1831
  key: exportedKey,
1733
1832
  value: stored,
1734
- ttl: remainingStoredTtlSeconds(stored)
1833
+ ttl: remainingStoredTtlMs(stored)
1735
1834
  });
1736
1835
  };
1737
1836
  if (layer.forEachKey) {
@@ -1887,6 +1986,19 @@ function validateCircuitBreakerOptions(options) {
1887
1986
  validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
1888
1987
  validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
1889
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
+ }
1890
2002
 
1891
2003
  // src/internal/CircuitBreakerManager.ts
1892
2004
  var CircuitBreakerManager = class {
@@ -2319,7 +2431,7 @@ var MetricsCollector = class {
2319
2431
 
2320
2432
  // src/internal/TtlResolver.ts
2321
2433
  var import_node_crypto2 = require("crypto");
2322
- var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
2434
+ var DEFAULT_NEGATIVE_TTL_MS = 6e4;
2323
2435
  var secureRandom = {
2324
2436
  value() {
2325
2437
  return (0, import_node_crypto2.randomBytes)(4).readUInt32BE(0) / 4294967296;
@@ -2346,17 +2458,17 @@ var TtlResolver = class {
2346
2458
  }
2347
2459
  resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
2348
2460
  const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
2349
- const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
2461
+ const baseTtl = kind === "empty" ? this.resolveLayerMs(
2350
2462
  layerName,
2351
2463
  options?.negativeTtl,
2352
2464
  globalNegativeTtl,
2353
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
2354
- ) : 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);
2355
2467
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
2356
- const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
2468
+ const jitter = this.resolveLayerMs(layerName, options?.ttlJitter, void 0);
2357
2469
  return this.applyJitter(adaptiveTtl, jitter);
2358
2470
  }
2359
- resolveLayerSeconds(layerName, override, globalDefault, fallback) {
2471
+ resolveLayerMs(layerName, override, globalDefault, fallback) {
2360
2472
  if (override !== void 0) {
2361
2473
  return this.readLayerNumber(layerName, override) ?? fallback;
2362
2474
  }
@@ -2378,8 +2490,8 @@ var TtlResolver = class {
2378
2490
  if (profile.hits < hotAfter) {
2379
2491
  return ttl;
2380
2492
  }
2381
- const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
2382
- 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;
2383
2495
  const multiplier = Math.floor(profile.hits / hotAfter);
2384
2496
  return Math.min(maxTtl, ttl + step * multiplier);
2385
2497
  }
@@ -2401,17 +2513,17 @@ var TtlResolver = class {
2401
2513
  if (policy === "until-midnight") {
2402
2514
  const nextMidnight = new Date(now);
2403
2515
  nextMidnight.setHours(24, 0, 0, 0);
2404
- return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
2516
+ return Math.max(1, Math.ceil(nextMidnight.getTime() - now.getTime()));
2405
2517
  }
2406
2518
  if (policy === "next-hour") {
2407
2519
  const nextHour = new Date(now);
2408
2520
  nextHour.setMinutes(60, 0, 0);
2409
- return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
2521
+ return Math.max(1, Math.ceil(nextHour.getTime() - now.getTime()));
2410
2522
  }
2411
- const alignToSeconds = policy.alignTo;
2412
- const currentSeconds = Math.floor(Date.now() / 1e3);
2413
- const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
2414
- 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);
2415
2527
  }
2416
2528
  readLayerNumber(layerName, value) {
2417
2529
  if (typeof value === "number") {
@@ -2862,7 +2974,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2862
2974
  },
2863
2975
  enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
2864
2976
  resolveFreshTtl: this.resolveFreshTtl.bind(this),
2865
- resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
2977
+ resolveLayerMs: this.resolveLayerMs.bind(this),
2866
2978
  globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
2867
2979
  globalStaleIfError: this.options.staleIfError,
2868
2980
  writePolicy: this.options.writePolicy,
@@ -2918,12 +3030,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
2918
3030
  formatError: (error) => this.formatError(error),
2919
3031
  storeEntry: (key, kind, value, options2) => this.storeEntry(key, kind, value, options2),
2920
3032
  recordCircuitFailure: (key, options2, error) => this.recordCircuitFailure(key, options2, error),
2921
- resolveLayerSeconds: (layerName, override, globalDefault, fallback) => this.resolveLayerSeconds(layerName, override, globalDefault, fallback),
3033
+ resolveLayerMs: (layerName, override, globalDefault, fallback) => this.resolveLayerMs(layerName, override, globalDefault, fallback),
2922
3034
  sleep: (ms) => this.sleep(ms),
2923
3035
  withTimeout: (promise, ms, createError) => this.withTimeout(promise, ms, createError),
2924
3036
  isDisconnecting: () => this.isDisconnecting,
2925
3037
  isGracefulDegradationEnabled: () => this.isGracefulDegradationEnabled(),
2926
- scheduleBackgroundRefreshDispatch: (key, fetcher, options2) => this.scheduleBackgroundRefresh(key, fetcher, options2),
3038
+ scheduleBackgroundRefreshDispatch: (key, fetcher, options2, fetcherContext) => this.scheduleBackgroundRefresh(key, fetcher, options2, fetcherContext),
2927
3039
  stampedePrevention: options.stampedePrevention,
2928
3040
  singleFlightCoordinator: options.singleFlightCoordinator,
2929
3041
  singleFlightLeaseMs: options.singleFlightLeaseMs,
@@ -3029,7 +3141,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3029
3141
  return false;
3030
3142
  }
3031
3143
  /**
3032
- * 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
3033
3145
  * that has it, or null if the key is not found / has no TTL.
3034
3146
  */
3035
3147
  async ttl(key) {
@@ -3265,6 +3377,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
3265
3377
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3266
3378
  });
3267
3379
  }
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
+ }
3268
3389
  async invalidateByTags(tags, mode = "any") {
3269
3390
  await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
3270
3391
  if (tags.length === 0) {
@@ -3281,6 +3402,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
3281
3402
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3282
3403
  });
3283
3404
  }
3405
+ async expireByTags(tags, mode = "any") {
3406
+ await this.observeOperation("layercache.expire_by_tags", void 0, async () => {
3407
+ if (tags.length === 0) {
3408
+ return;
3409
+ }
3410
+ validateTags(tags);
3411
+ await this.awaitStartup("expireByTags");
3412
+ const keysByTag = await Promise.all(
3413
+ tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
3414
+ );
3415
+ const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
3416
+ this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
3417
+ await this.expireKeys(keys);
3418
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
3419
+ });
3420
+ }
3284
3421
  async invalidateByPattern(pattern) {
3285
3422
  await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
3286
3423
  validatePattern(pattern);
@@ -3293,6 +3430,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
3293
3430
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3294
3431
  });
3295
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
+ }
3296
3445
  async invalidateByPrefix(prefix) {
3297
3446
  await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
3298
3447
  await this.awaitStartup("invalidateByPrefix");
@@ -3302,6 +3451,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
3302
3451
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3303
3452
  });
3304
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
+ }
3305
3463
  getMetrics() {
3306
3464
  return this.metricsCollector.snapshot;
3307
3465
  }
@@ -3378,9 +3536,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
3378
3536
  const normalizedKey = this.qualifyKey(userKey);
3379
3537
  await this.awaitStartup("inspect");
3380
3538
  const foundInLayers = [];
3381
- let freshTtlSeconds = null;
3382
- let staleTtlSeconds = null;
3383
- let errorTtlSeconds = null;
3539
+ let freshTtlMs = null;
3540
+ let staleTtlMs = null;
3541
+ let errorTtlMs = null;
3384
3542
  let isStale = false;
3385
3543
  for (const layer of this.layers) {
3386
3544
  if (this.shouldSkipLayer(layer)) {
@@ -3397,9 +3555,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
3397
3555
  foundInLayers.push(layer.name);
3398
3556
  if (foundInLayers.length === 1 && resolved.envelope) {
3399
3557
  const now = Date.now();
3400
- freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
3401
- staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
3402
- 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;
3403
3561
  isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
3404
3562
  }
3405
3563
  }
@@ -3407,7 +3565,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
3407
3565
  return null;
3408
3566
  }
3409
3567
  const tags = await this.getTagsForKey(normalizedKey);
3410
- return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
3568
+ return { key: userKey, foundInLayers, freshTtlMs, staleTtlMs, errorTtlMs, isStale, tags };
3411
3569
  }
3412
3570
  async exportState() {
3413
3571
  await this.awaitStartup("exportState");
@@ -3464,30 +3622,35 @@ var CacheStack = class extends import_node_events.EventEmitter {
3464
3622
  });
3465
3623
  }
3466
3624
  async storeEntry(key, kind, value, options) {
3625
+ const resolvedOptions = this.resolveContextOptions(key, kind, value, options);
3467
3626
  const clearEpoch = this.maintenance.currentClearEpoch();
3468
3627
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
3469
- await this.layerWriter.writeAcrossLayers(key, kind, value, options);
3628
+ await this.layerWriter.writeAcrossLayers(key, kind, value, resolvedOptions);
3470
3629
  if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
3471
3630
  return;
3472
3631
  }
3473
- if (options?.tags) {
3474
- await this.tagIndex.track(key, options.tags);
3632
+ if (resolvedOptions?.tags) {
3633
+ await this.tagIndex.track(key, resolvedOptions.tags);
3475
3634
  } else {
3476
3635
  await this.tagIndex.touch(key);
3477
3636
  }
3478
3637
  this.metricsCollector.increment("sets");
3479
- this.logger.debug?.("set", { key, kind, tags: options?.tags });
3480
- 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 });
3481
3640
  if (this.shouldBroadcastL1Invalidation()) {
3482
3641
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
3483
3642
  }
3484
3643
  }
3485
3644
  async writeBatch(entries) {
3486
- 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);
3487
3650
  if (clearEpoch !== this.maintenance.currentClearEpoch()) {
3488
3651
  return;
3489
3652
  }
3490
- for (const entry of entries) {
3653
+ for (const entry of resolvedEntries) {
3491
3654
  if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
3492
3655
  continue;
3493
3656
  }
@@ -3521,8 +3684,46 @@ var CacheStack = class extends import_node_events.EventEmitter {
3521
3684
  value
3522
3685
  );
3523
3686
  }
3524
- resolveLayerSeconds(layerName, override, globalDefault, fallback) {
3525
- return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
3687
+ resolveLayerMs(layerName, override, globalDefault, fallback) {
3688
+ return this.ttlResolver.resolveLayerMs(layerName, override, globalDefault, fallback);
3689
+ }
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
+ );
3715
+ }
3716
+ return {
3717
+ ...baseOptions,
3718
+ ...overrides
3719
+ };
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;
3526
3727
  }
3527
3728
  async deleteKeys(keys) {
3528
3729
  if (keys.length === 0) {
@@ -3540,6 +3741,30 @@ var CacheStack = class extends import_node_events.EventEmitter {
3540
3741
  this.logger.debug?.("delete", { keys });
3541
3742
  this.emit("delete", { keys });
3542
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
+ }
3543
3768
  async publishInvalidation(message) {
3544
3769
  if (!this.options.invalidationBus) {
3545
3770
  return;
@@ -3561,6 +3786,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
3561
3786
  }
3562
3787
  const keys = message.keys ?? [];
3563
3788
  this.maintenance.bumpKeyEpochs(keys);
3789
+ if (message.operation === "expire") {
3790
+ await this.expireKeysInLayers(keys, localLayers);
3791
+ return;
3792
+ }
3564
3793
  await this.invalidation.deleteKeysFromLayers(localLayers, keys);
3565
3794
  if (message.operation !== "write") {
3566
3795
  for (const key of keys) {
@@ -3758,6 +3987,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
3758
3987
  validateCircuitBreakerOptions(options.circuitBreaker);
3759
3988
  validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
3760
3989
  validateTags(options.tags);
3990
+ if (options.contextOptions && typeof options.contextOptions !== "function") {
3991
+ throw new Error("options.contextOptions must be a function.");
3992
+ }
3761
3993
  }
3762
3994
  assertActive(operation) {
3763
3995
  if (this.isDisconnecting) {
@@ -3772,8 +4004,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
3772
4004
  async readLayerEntry(layer, key) {
3773
4005
  return this.reader.readLayerEntry(layer, key);
3774
4006
  }
3775
- scheduleBackgroundRefresh(key, fetcher, options) {
3776
- this.reader.runScheduleBackgroundRefresh(key, fetcher, options);
4007
+ scheduleBackgroundRefresh(key, fetcher, options, fetcherContext) {
4008
+ this.reader.runScheduleBackgroundRefresh(key, fetcher, options, fetcherContext);
3777
4009
  }
3778
4010
  async applyFreshReadPolicies(key, hit, options, fetcher) {
3779
4011
  return this.reader.runApplyFreshReadPolicies(key, hit, options, fetcher);
@@ -3917,7 +4149,7 @@ var RedisInvalidationBus = class {
3917
4149
  }
3918
4150
  const candidate = value;
3919
4151
  const validScope = candidate.scope === "key" || candidate.scope === "keys" || candidate.scope === "clear";
3920
- 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";
3921
4153
  const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
3922
4154
  return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
3923
4155
  }
@@ -4440,7 +4672,7 @@ var MemoryLayer = class {
4440
4672
  this.entries.delete(key);
4441
4673
  this.entries.set(key, {
4442
4674
  value,
4443
- expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null,
4675
+ expiresAt: ttl && ttl > 0 ? Date.now() + ttl : null,
4444
4676
  accessCount: 0,
4445
4677
  insertedAt: Date.now()
4446
4678
  });
@@ -4471,7 +4703,7 @@ var MemoryLayer = class {
4471
4703
  if (entry.expiresAt === null) {
4472
4704
  return null;
4473
4705
  }
4474
- return Math.max(0, Math.ceil((entry.expiresAt - Date.now()) / 1e3));
4706
+ return Math.max(0, Math.ceil(entry.expiresAt - Date.now()));
4475
4707
  }
4476
4708
  async size() {
4477
4709
  this.pruneExpired();
@@ -4661,7 +4893,7 @@ var RedisLayer = class {
4661
4893
  const payload = await this.encodePayload(serialized);
4662
4894
  const normalizedKey = this.withPrefix(entry.key);
4663
4895
  if (entry.ttl && entry.ttl > 0) {
4664
- pipeline.set(normalizedKey, payload, "EX", entry.ttl);
4896
+ pipeline.set(normalizedKey, payload, "PX", entry.ttl);
4665
4897
  } else {
4666
4898
  pipeline.set(normalizedKey, payload);
4667
4899
  }
@@ -4676,7 +4908,7 @@ var RedisLayer = class {
4676
4908
  if (ttl && ttl > 0) {
4677
4909
  await this.runCommand(
4678
4910
  `set(${this.displayKey(key)})`,
4679
- () => this.client.set(normalizedKey, payload, "EX", ttl)
4911
+ () => this.client.set(normalizedKey, payload, "PX", ttl)
4680
4912
  );
4681
4913
  return;
4682
4914
  }
@@ -4705,7 +4937,10 @@ var RedisLayer = class {
4705
4937
  }
4706
4938
  async ttl(key) {
4707
4939
  this.validateKey(key);
4708
- 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
+ );
4709
4944
  if (remaining < 0) {
4710
4945
  return null;
4711
4946
  }
@@ -4856,12 +5091,12 @@ var RedisLayer = class {
4856
5091
  const payload = await this.encodePayload(serialized);
4857
5092
  const ttl = await this.runCommand(
4858
5093
  `rewrite-ttl(${this.displayKey(key)})`,
4859
- () => this.client.ttl(this.withPrefix(key))
5094
+ () => this.client.pttl(this.withPrefix(key))
4860
5095
  );
4861
5096
  if (ttl > 0) {
4862
5097
  await this.runCommand(
4863
5098
  `rewrite-set(${this.displayKey(key)})`,
4864
- () => this.client.set(this.withPrefix(key), payload, "EX", ttl)
5099
+ () => this.client.set(this.withPrefix(key), payload, "PX", ttl)
4865
5100
  );
4866
5101
  return;
4867
5102
  }
@@ -5172,7 +5407,7 @@ var DiskLayer = class {
5172
5407
  const entry = {
5173
5408
  key,
5174
5409
  value,
5175
- expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
5410
+ expiresAt: ttl && ttl > 0 ? Date.now() + ttl : null
5176
5411
  };
5177
5412
  const payload = this.serializer.serialize(entry);
5178
5413
  const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
@@ -5217,7 +5452,7 @@ var DiskLayer = class {
5217
5452
  if (entry.expiresAt === null) {
5218
5453
  return null;
5219
5454
  }
5220
- const remaining = Math.ceil((entry.expiresAt - Date.now()) / 1e3);
5455
+ const remaining = Math.ceil(entry.expiresAt - Date.now());
5221
5456
  if (remaining <= 0) {
5222
5457
  return null;
5223
5458
  }
@@ -5507,7 +5742,7 @@ var MemcachedLayer = class {
5507
5742
  this.validateKey(key);
5508
5743
  const payload = this.serializer.serialize(value);
5509
5744
  await this.client.set(this.withPrefix(key), payload, {
5510
- expires: ttl && ttl > 0 ? ttl : void 0
5745
+ expires: ttl && ttl > 0 ? Math.ceil(ttl / 1e3) : void 0
5511
5746
  });
5512
5747
  }
5513
5748
  async has(key) {