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.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  validateAdaptiveTtlOptions,
4
4
  validateCacheKey,
5
5
  validateCircuitBreakerOptions,
6
+ validateContextEntryOptions,
6
7
  validateLayerNumberOption,
7
8
  validateNonNegativeNumber,
8
9
  validatePattern,
@@ -11,22 +12,23 @@ import {
11
12
  validateTag,
12
13
  validateTags,
13
14
  validateTtlPolicy
14
- } from "./chunk-BORDQ3LA.js";
15
+ } from "./chunk-7KMKQ6QZ.js";
15
16
  import {
16
17
  MemoryLayer,
17
18
  TagIndex,
18
19
  createHonoCacheMiddleware
19
- } from "./chunk-5RCAX2BQ.js";
20
+ } from "./chunk-FFZCC7EQ.js";
20
21
  import {
21
22
  PatternMatcher,
22
23
  createStoredValueEnvelope,
24
+ expireStoredEnvelope,
23
25
  isStoredValueEnvelope,
24
26
  refreshStoredEnvelope,
25
- remainingFreshTtlSeconds,
26
- remainingStoredTtlSeconds,
27
+ remainingFreshTtlMs,
28
+ remainingStoredTtlMs,
27
29
  resolveStoredValue,
28
30
  unwrapStoredValue
29
- } from "./chunk-4PPBOOXT.js";
31
+ } from "./chunk-KJDFYE5T.js";
30
32
 
31
33
  // src/CacheStack.ts
32
34
  import { EventEmitter } from "events";
@@ -219,6 +221,9 @@ var CacheNamespace = class _CacheNamespace {
219
221
  async invalidateByTag(tag) {
220
222
  await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
221
223
  }
224
+ async expireByTag(tag) {
225
+ await this.trackMetrics(() => this.cache.expireByTag(this.qualifyTag(tag)));
226
+ }
222
227
  async invalidateByTags(tags, mode = "any") {
223
228
  await this.trackMetrics(
224
229
  () => this.cache.invalidateByTags(
@@ -227,12 +232,26 @@ var CacheNamespace = class _CacheNamespace {
227
232
  )
228
233
  );
229
234
  }
235
+ async expireByTags(tags, mode = "any") {
236
+ await this.trackMetrics(
237
+ () => this.cache.expireByTags(
238
+ tags.map((tag) => this.qualifyTag(tag)),
239
+ mode
240
+ )
241
+ );
242
+ }
230
243
  async invalidateByPattern(pattern) {
231
244
  await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
232
245
  }
246
+ async expireByPattern(pattern) {
247
+ await this.trackMetrics(() => this.cache.expireByPattern(this.qualify(pattern)));
248
+ }
233
249
  async invalidateByPrefix(prefix) {
234
250
  await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
235
251
  }
252
+ async expireByPrefix(prefix) {
253
+ await this.trackMetrics(() => this.cache.expireByPrefix(this.qualify(prefix)));
254
+ }
236
255
  /**
237
256
  * Returns detailed metadata about a single cache key within this namespace.
238
257
  */
@@ -580,6 +599,35 @@ var CacheStackInvalidationSupport = class {
580
599
  })
581
600
  );
582
601
  }
602
+ async expireKeysInLayers(layers, keys) {
603
+ const foundKeys = /* @__PURE__ */ new Set();
604
+ await Promise.all(
605
+ layers.map(async (layer) => {
606
+ if (this.options.shouldSkipLayer(layer)) {
607
+ return;
608
+ }
609
+ await Promise.all(
610
+ keys.map(async (key) => {
611
+ try {
612
+ const stored = layer.getEntry ? await layer.getEntry(key) : await layer.get(key);
613
+ if (stored === null) {
614
+ return;
615
+ }
616
+ foundKeys.add(key);
617
+ const expired = expireStoredEnvelope(stored);
618
+ if (expired === stored) {
619
+ return;
620
+ }
621
+ await layer.set(key, expired, remainingStoredTtlMs(expired));
622
+ } catch (error) {
623
+ await this.options.handleLayerFailure(layer, "expire", error);
624
+ }
625
+ })
626
+ );
627
+ })
628
+ );
629
+ return foundKeys;
630
+ }
583
631
  assertWithinInvalidationKeyLimit(size, maxKeys) {
584
632
  if (maxKeys !== false && size > maxKeys) {
585
633
  throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
@@ -698,12 +746,12 @@ var CacheStackLayerWriter = class {
698
746
  }
699
747
  buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
700
748
  const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
701
- const staleWhileRevalidate = this.options.resolveLayerSeconds(
749
+ const staleWhileRevalidate = this.options.resolveLayerMs(
702
750
  layer.name,
703
751
  writeOptions?.staleWhileRevalidate,
704
752
  this.options.globalStaleWhileRevalidate
705
753
  );
706
- const staleIfError = this.options.resolveLayerSeconds(
754
+ const staleIfError = this.options.resolveLayerMs(
707
755
  layer.name,
708
756
  writeOptions?.staleIfError,
709
757
  this.options.globalStaleIfError
@@ -711,12 +759,12 @@ var CacheStackLayerWriter = class {
711
759
  const payload = createStoredValueEnvelope({
712
760
  kind,
713
761
  value,
714
- freshTtlSeconds: freshTtl,
715
- staleWhileRevalidateSeconds: staleWhileRevalidate,
716
- staleIfErrorSeconds: staleIfError,
762
+ freshTtlMs: freshTtl,
763
+ staleWhileRevalidateMs: staleWhileRevalidate,
764
+ staleIfErrorMs: staleIfError,
717
765
  now
718
766
  });
719
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
767
+ const ttl = remainingStoredTtlMs(payload, now) ?? freshTtl;
720
768
  return {
721
769
  key,
722
770
  value: payload,
@@ -859,18 +907,420 @@ function planFreshReadPolicies({
859
907
  stored,
860
908
  hasFetcher,
861
909
  slidingTtl,
862
- refreshAheadSeconds
910
+ refreshAheadMs
863
911
  }) {
864
912
  const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
865
- const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
866
- const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
913
+ const refreshedStoredTtl = refreshedStored ? remainingStoredTtlMs(refreshedStored) ?? void 0 : void 0;
914
+ const remainingFreshTtl = remainingFreshTtlMs(stored) ?? 0;
867
915
  return {
868
916
  refreshedStored,
869
917
  refreshedStoredTtl,
870
- shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
918
+ shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadMs > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadMs
871
919
  };
872
920
  }
873
921
 
922
+ // src/internal/CacheStackReader.ts
923
+ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
924
+ var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
925
+ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
926
+ var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
927
+ var CacheStackReader = class {
928
+ constructor(options) {
929
+ this.options = options;
930
+ }
931
+ options;
932
+ backgroundRefreshes = /* @__PURE__ */ new Map();
933
+ backgroundRefreshAbort = /* @__PURE__ */ new Map();
934
+ get activeRefreshCount() {
935
+ return this.backgroundRefreshes.size;
936
+ }
937
+ async getPrepared(normalizedKey, fetcher, options) {
938
+ const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
939
+ if (hit.found) {
940
+ this.options.ttlResolver.recordAccess(normalizedKey);
941
+ if (this.isNegativeStoredValue(hit.stored)) {
942
+ this.options.metricsCollector.increment("negativeCacheHits");
943
+ }
944
+ if (hit.state === "fresh") {
945
+ this.options.metricsCollector.increment("hits");
946
+ await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
947
+ return hit.value;
948
+ }
949
+ if (hit.state === "stale-while-revalidate") {
950
+ this.options.metricsCollector.increment("hits");
951
+ this.options.metricsCollector.increment("staleHits");
952
+ this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
953
+ if (fetcher) {
954
+ this.scheduleBackgroundRefresh(normalizedKey, fetcher, options, this.createFetcherContext(normalizedKey, hit));
955
+ }
956
+ return hit.value;
957
+ }
958
+ if (!fetcher) {
959
+ this.options.metricsCollector.increment("hits");
960
+ this.options.metricsCollector.increment("staleHits");
961
+ this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
962
+ return hit.value;
963
+ }
964
+ try {
965
+ return await this.fetchWithGuards(
966
+ normalizedKey,
967
+ fetcher,
968
+ options,
969
+ void 0,
970
+ void 0,
971
+ false,
972
+ this.createFetcherContext(normalizedKey, hit)
973
+ );
974
+ } catch (error) {
975
+ this.options.metricsCollector.increment("staleHits");
976
+ this.options.metricsCollector.increment("refreshErrors");
977
+ this.options.logger.debug?.("stale-if-error", {
978
+ key: normalizedKey,
979
+ error: this.options.formatError(error)
980
+ });
981
+ return hit.value;
982
+ }
983
+ }
984
+ this.options.metricsCollector.increment("misses");
985
+ if (!fetcher) {
986
+ return null;
987
+ }
988
+ return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true, {
989
+ key: normalizedKey,
990
+ currentValue: void 0,
991
+ state: "miss"
992
+ });
993
+ }
994
+ async readLayerEntry(layer, key) {
995
+ if (this.options.shouldSkipLayer(layer)) {
996
+ return null;
997
+ }
998
+ if (layer.getEntry) {
999
+ try {
1000
+ return await layer.getEntry(key);
1001
+ } catch (error) {
1002
+ return this.options.handleLayerFailure(layer, "read", error);
1003
+ }
1004
+ }
1005
+ try {
1006
+ return await layer.get(key);
1007
+ } catch (error) {
1008
+ return this.options.handleLayerFailure(layer, "read", error);
1009
+ }
1010
+ }
1011
+ async backfill(key, stored, upToIndex, options) {
1012
+ if (upToIndex < 0) {
1013
+ return;
1014
+ }
1015
+ for (let index = 0; index <= upToIndex; index += 1) {
1016
+ const layer = this.options.layers[index];
1017
+ if (!layer || this.options.shouldSkipLayer(layer)) {
1018
+ continue;
1019
+ }
1020
+ const ttl = remainingStoredTtlMs(stored) ?? this.options.resolveLayerMs(layer.name, options?.ttl, void 0, layer.defaultTtl);
1021
+ try {
1022
+ await layer.set(key, stored, ttl);
1023
+ } catch (error) {
1024
+ await this.options.handleLayerFailure(layer, "backfill", error);
1025
+ continue;
1026
+ }
1027
+ this.options.metricsCollector.increment("backfills");
1028
+ this.options.logger.debug?.("backfill", { key, layer: layer.name });
1029
+ this.options.emit("backfill", { key, layer: layer.name });
1030
+ }
1031
+ }
1032
+ abortAllRefreshes() {
1033
+ for (const key of this.backgroundRefreshAbort.keys()) {
1034
+ this.backgroundRefreshAbort.set(key, true);
1035
+ }
1036
+ }
1037
+ getAllRefreshPromises() {
1038
+ return [...this.backgroundRefreshes.values()];
1039
+ }
1040
+ async readFromLayers(key, options, mode) {
1041
+ let sawRetainableValue = false;
1042
+ for (let index = 0; index < this.options.layers.length; index += 1) {
1043
+ const layer = this.options.layers[index];
1044
+ if (!layer) continue;
1045
+ const readStart = performance.now();
1046
+ const stored = await this.readLayerEntry(layer, key);
1047
+ const readDuration = performance.now() - readStart;
1048
+ this.options.metricsCollector.recordLatency(layer.name, readDuration);
1049
+ if (stored === null) {
1050
+ this.options.metricsCollector.incrementLayer("missesByLayer", layer.name);
1051
+ continue;
1052
+ }
1053
+ const resolved = resolveStoredValue(stored);
1054
+ if (resolved.state === "expired") {
1055
+ await layer.delete(key);
1056
+ continue;
1057
+ }
1058
+ sawRetainableValue = true;
1059
+ if (mode === "fresh-only" && resolved.state !== "fresh") {
1060
+ continue;
1061
+ }
1062
+ await this.options.tagIndex.touch(key);
1063
+ await this.backfill(key, stored, index - 1, options);
1064
+ this.options.metricsCollector.incrementLayer("hitsByLayer", layer.name);
1065
+ this.options.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
1066
+ this.options.emit("hit", {
1067
+ key,
1068
+ layer: layer.name,
1069
+ state: resolved.state
1070
+ });
1071
+ return {
1072
+ found: true,
1073
+ value: resolved.value,
1074
+ stored,
1075
+ state: resolved.state,
1076
+ layerIndex: index,
1077
+ layerName: layer.name
1078
+ };
1079
+ }
1080
+ if (!sawRetainableValue) {
1081
+ await this.options.tagIndex.remove(key);
1082
+ }
1083
+ this.options.logger.debug?.("miss", { key, mode });
1084
+ this.options.emit("miss", { key, mode });
1085
+ return { found: false, value: null, stored: null, state: "miss" };
1086
+ }
1087
+ async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false, fetcherContext = {
1088
+ key,
1089
+ currentValue: void 0,
1090
+ state: "miss"
1091
+ }) {
1092
+ const fetchTask = async () => {
1093
+ const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
1094
+ if (shouldRecheckFreshLayers) {
1095
+ const secondHit = await this.readFromLayers(key, options, "fresh-only");
1096
+ if (secondHit.found) {
1097
+ this.options.metricsCollector.increment("hits");
1098
+ return secondHit.value;
1099
+ }
1100
+ }
1101
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
1102
+ };
1103
+ const singleFlightTask = async () => {
1104
+ if (!this.options.singleFlightCoordinator) {
1105
+ return fetchTask();
1106
+ }
1107
+ try {
1108
+ return await this.options.singleFlightCoordinator.execute(
1109
+ key,
1110
+ this.resolveSingleFlightOptions(),
1111
+ fetchTask,
1112
+ () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext)
1113
+ );
1114
+ } catch (error) {
1115
+ if (!this.options.isGracefulDegradationEnabled()) {
1116
+ throw error;
1117
+ }
1118
+ this.options.metricsCollector.increment("degradedOperations");
1119
+ this.options.logger.warn?.("single-flight-coordinator-degraded", {
1120
+ key,
1121
+ error: this.options.formatError(error)
1122
+ });
1123
+ this.options.emitError("single-flight", {
1124
+ key,
1125
+ degraded: true,
1126
+ error: this.options.formatError(error)
1127
+ });
1128
+ return fetchTask();
1129
+ }
1130
+ };
1131
+ if (this.options.stampedePrevention === false) {
1132
+ return singleFlightTask();
1133
+ }
1134
+ return this.options.stampedeGuard.execute(key, singleFlightTask);
1135
+ }
1136
+ async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
1137
+ key,
1138
+ currentValue: void 0,
1139
+ state: "miss"
1140
+ }) {
1141
+ const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
1142
+ const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
1143
+ const deadline = Date.now() + timeoutMs;
1144
+ this.options.metricsCollector.increment("singleFlightWaits");
1145
+ this.options.emit("stampede-dedupe", { key });
1146
+ while (Date.now() < deadline) {
1147
+ const hit = await this.readFromLayers(key, options, "fresh-only");
1148
+ if (hit.found) {
1149
+ this.options.metricsCollector.increment("hits");
1150
+ return hit.value;
1151
+ }
1152
+ await this.options.sleep(pollIntervalMs);
1153
+ }
1154
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
1155
+ }
1156
+ async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
1157
+ key,
1158
+ currentValue: void 0,
1159
+ state: "miss"
1160
+ }) {
1161
+ this.options.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
1162
+ this.options.metricsCollector.increment("fetches");
1163
+ const fetchStart = Date.now();
1164
+ let fetched;
1165
+ try {
1166
+ fetched = await this.options.fetchRateLimiter.schedule(
1167
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1168
+ { key, fetcher },
1169
+ () => fetcher(fetcherContext)
1170
+ );
1171
+ this.options.circuitBreakerManager.recordSuccess(key);
1172
+ this.options.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1173
+ } catch (error) {
1174
+ this.options.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
1175
+ throw error;
1176
+ }
1177
+ if (fetched === null || fetched === void 0) {
1178
+ if (!this.shouldNegativeCache(options)) {
1179
+ return null;
1180
+ }
1181
+ if (this.options.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
1182
+ this.options.logger.debug?.("skip-negative-store-after-invalidation", {
1183
+ key,
1184
+ expectedClearEpoch,
1185
+ clearEpoch: this.options.maintenance.currentClearEpoch(),
1186
+ expectedKeyEpoch,
1187
+ keyEpoch: this.options.maintenance.currentKeyEpoch(key)
1188
+ });
1189
+ return null;
1190
+ }
1191
+ await this.options.storeEntry(key, "empty", null, options);
1192
+ return null;
1193
+ }
1194
+ if (options?.shouldCache) {
1195
+ try {
1196
+ if (!options.shouldCache(fetched)) {
1197
+ return fetched;
1198
+ }
1199
+ } catch (error) {
1200
+ this.options.logger.warn?.("shouldCache-error", {
1201
+ key,
1202
+ error: this.options.formatError(error)
1203
+ });
1204
+ }
1205
+ }
1206
+ if (this.options.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
1207
+ this.options.logger.debug?.("skip-store-after-invalidation", {
1208
+ key,
1209
+ expectedClearEpoch,
1210
+ clearEpoch: this.options.maintenance.currentClearEpoch(),
1211
+ expectedKeyEpoch,
1212
+ keyEpoch: this.options.maintenance.currentKeyEpoch(key)
1213
+ });
1214
+ return fetched;
1215
+ }
1216
+ await this.options.storeEntry(key, "value", fetched, options);
1217
+ return fetched;
1218
+ }
1219
+ runScheduleBackgroundRefresh(key, fetcher, options, fetcherContext) {
1220
+ this.scheduleBackgroundRefresh(key, fetcher, options, fetcherContext);
1221
+ }
1222
+ scheduleBackgroundRefresh(key, fetcher, options, fetcherContext = {
1223
+ key,
1224
+ currentValue: void 0,
1225
+ state: "miss"
1226
+ }) {
1227
+ if (!shouldStartBackgroundRefresh({
1228
+ isDisconnecting: this.options.isDisconnecting(),
1229
+ hasRefreshInFlight: this.backgroundRefreshes.has(key)
1230
+ })) {
1231
+ return;
1232
+ }
1233
+ const clearEpoch = this.options.maintenance.currentClearEpoch();
1234
+ const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
1235
+ this.backgroundRefreshAbort.set(key, false);
1236
+ const refresh = (async () => {
1237
+ this.options.metricsCollector.increment("refreshes");
1238
+ try {
1239
+ if (this.backgroundRefreshAbort.get(key)) return;
1240
+ await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch, fetcherContext);
1241
+ } catch (error) {
1242
+ if (this.backgroundRefreshAbort.get(key)) return;
1243
+ this.options.metricsCollector.increment("refreshErrors");
1244
+ this.options.logger.warn?.("background-refresh-error", {
1245
+ key,
1246
+ error: this.options.formatError(error)
1247
+ });
1248
+ } finally {
1249
+ this.backgroundRefreshes.delete(key);
1250
+ this.backgroundRefreshAbort.delete(key);
1251
+ }
1252
+ })();
1253
+ this.backgroundRefreshes.set(key, refresh);
1254
+ }
1255
+ async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
1256
+ key,
1257
+ currentValue: void 0,
1258
+ state: "miss"
1259
+ }) {
1260
+ const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
1261
+ await this.fetchWithGuards(
1262
+ key,
1263
+ (context) => this.options.withTimeout(fetcher(context), timeoutMs, () => {
1264
+ return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
1265
+ }),
1266
+ options,
1267
+ expectedClearEpoch,
1268
+ expectedKeyEpoch,
1269
+ false,
1270
+ fetcherContext
1271
+ );
1272
+ }
1273
+ async runApplyFreshReadPolicies(key, hit, options, fetcher) {
1274
+ return this.applyFreshReadPolicies(key, hit, options, fetcher);
1275
+ }
1276
+ async applyFreshReadPolicies(key, hit, options, fetcher) {
1277
+ const plan = planFreshReadPolicies({
1278
+ stored: hit.stored,
1279
+ hasFetcher: Boolean(fetcher),
1280
+ slidingTtl: options?.slidingTtl ?? false,
1281
+ refreshAheadMs: this.options.resolveLayerMs(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
1282
+ });
1283
+ if (plan.refreshedStored) {
1284
+ for (let index = 0; index <= hit.layerIndex; index += 1) {
1285
+ const layer = this.options.layers[index];
1286
+ if (!layer || this.options.shouldSkipLayer(layer)) {
1287
+ continue;
1288
+ }
1289
+ try {
1290
+ await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
1291
+ } catch (error) {
1292
+ await this.options.handleLayerFailure(layer, "sliding-ttl", error);
1293
+ }
1294
+ }
1295
+ }
1296
+ if (fetcher && plan.shouldScheduleBackgroundRefresh) {
1297
+ this.options.scheduleBackgroundRefreshDispatch(key, fetcher, options, this.createFetcherContext(key, hit));
1298
+ }
1299
+ }
1300
+ createFetcherContext(key, hit) {
1301
+ return {
1302
+ key,
1303
+ currentValue: hit.value === null ? void 0 : hit.value,
1304
+ state: hit.state,
1305
+ layer: hit.layerName
1306
+ };
1307
+ }
1308
+ resolveSingleFlightOptions() {
1309
+ return {
1310
+ leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
1311
+ waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
1312
+ pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
1313
+ renewIntervalMs: this.options.singleFlightRenewIntervalMs
1314
+ };
1315
+ }
1316
+ shouldNegativeCache(options) {
1317
+ return options?.negativeCache ?? this.options.negativeCaching ?? false;
1318
+ }
1319
+ isNegativeStoredValue(stored) {
1320
+ return isStoredValueEnvelope(stored) && stored.kind === "empty";
1321
+ }
1322
+ };
1323
+
874
1324
  // src/internal/CacheStackSnapshotManager.ts
875
1325
  import { constants, promises as fs } from "fs";
876
1326
 
@@ -1136,7 +1586,7 @@ var CacheStackSnapshotManager = class {
1136
1586
  await visitor({
1137
1587
  key: exportedKey,
1138
1588
  value: stored,
1139
- ttl: remainingStoredTtlSeconds(stored)
1589
+ ttl: remainingStoredTtlMs(stored)
1140
1590
  });
1141
1591
  };
1142
1592
  if (layer.forEachKey) {
@@ -1600,7 +2050,7 @@ var MetricsCollector = class {
1600
2050
 
1601
2051
  // src/internal/TtlResolver.ts
1602
2052
  import { randomBytes as randomBytes2 } from "crypto";
1603
- var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
2053
+ var DEFAULT_NEGATIVE_TTL_MS = 6e4;
1604
2054
  var secureRandom = {
1605
2055
  value() {
1606
2056
  return randomBytes2(4).readUInt32BE(0) / 4294967296;
@@ -1627,17 +2077,17 @@ var TtlResolver = class {
1627
2077
  }
1628
2078
  resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
1629
2079
  const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
1630
- const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
2080
+ const baseTtl = kind === "empty" ? this.resolveLayerMs(
1631
2081
  layerName,
1632
2082
  options?.negativeTtl,
1633
2083
  globalNegativeTtl,
1634
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
1635
- ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
2084
+ this.resolveLayerMs(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_MS
2085
+ ) : this.resolveLayerMs(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
1636
2086
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
1637
- const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
2087
+ const jitter = this.resolveLayerMs(layerName, options?.ttlJitter, void 0);
1638
2088
  return this.applyJitter(adaptiveTtl, jitter);
1639
2089
  }
1640
- resolveLayerSeconds(layerName, override, globalDefault, fallback) {
2090
+ resolveLayerMs(layerName, override, globalDefault, fallback) {
1641
2091
  if (override !== void 0) {
1642
2092
  return this.readLayerNumber(layerName, override) ?? fallback;
1643
2093
  }
@@ -1659,8 +2109,8 @@ var TtlResolver = class {
1659
2109
  if (profile.hits < hotAfter) {
1660
2110
  return ttl;
1661
2111
  }
1662
- const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
1663
- const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
2112
+ const step = this.resolveLayerMs(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
2113
+ const maxTtl = this.resolveLayerMs(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
1664
2114
  const multiplier = Math.floor(profile.hits / hotAfter);
1665
2115
  return Math.min(maxTtl, ttl + step * multiplier);
1666
2116
  }
@@ -1682,17 +2132,17 @@ var TtlResolver = class {
1682
2132
  if (policy === "until-midnight") {
1683
2133
  const nextMidnight = new Date(now);
1684
2134
  nextMidnight.setHours(24, 0, 0, 0);
1685
- return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
2135
+ return Math.max(1, Math.ceil(nextMidnight.getTime() - now.getTime()));
1686
2136
  }
1687
2137
  if (policy === "next-hour") {
1688
2138
  const nextHour = new Date(now);
1689
2139
  nextHour.setMinutes(60, 0, 0);
1690
- return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
2140
+ return Math.max(1, Math.ceil(nextHour.getTime() - now.getTime()));
1691
2141
  }
1692
- const alignToSeconds = policy.alignTo;
1693
- const currentSeconds = Math.floor(Date.now() / 1e3);
1694
- const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
1695
- return Math.max(1, nextBoundary - currentSeconds);
2142
+ const alignToMs = policy.alignTo;
2143
+ const currentMs = Date.now();
2144
+ const nextBoundary = Math.ceil((currentMs + 1) / alignToMs) * alignToMs;
2145
+ return Math.max(1, nextBoundary - currentMs);
1696
2146
  }
1697
2147
  readLayerNumber(layerName, value) {
1698
2148
  if (typeof value === "number") {
@@ -1815,10 +2265,6 @@ var CacheMissError = class extends Error {
1815
2265
  };
1816
2266
 
1817
2267
  // src/CacheStack.ts
1818
- var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
1819
- var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
1820
- var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1821
- var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1822
2268
  var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
1823
2269
  var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
1824
2270
  var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
@@ -1898,7 +2344,7 @@ var CacheStack = class extends EventEmitter {
1898
2344
  },
1899
2345
  enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
1900
2346
  resolveFreshTtl: this.resolveFreshTtl.bind(this),
1901
- resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
2347
+ resolveLayerMs: this.resolveLayerMs.bind(this),
1902
2348
  globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
1903
2349
  globalStaleIfError: this.options.staleIfError,
1904
2350
  writePolicy: this.options.writePolicy,
@@ -1929,7 +2375,7 @@ var CacheStack = class extends EventEmitter {
1929
2375
  layers: this.layers,
1930
2376
  tagIndex: this.tagIndex,
1931
2377
  snapshotSerializer: this.snapshotSerializer,
1932
- readLayerEntry: this.readLayerEntry.bind(this),
2378
+ readLayerEntry: (layer, key) => this.reader.readLayerEntry(layer, key),
1933
2379
  shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
1934
2380
  handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
1935
2381
  qualifyKey: this.qualifyKey.bind(this),
@@ -1937,6 +2383,41 @@ var CacheStack = class extends EventEmitter {
1937
2383
  validateCacheKey,
1938
2384
  formatError: this.formatError.bind(this)
1939
2385
  });
2386
+ this.reader = new CacheStackReader({
2387
+ layers: this.layers,
2388
+ metricsCollector: this.metricsCollector,
2389
+ maintenance: this.maintenance,
2390
+ tagIndex: this.tagIndex,
2391
+ circuitBreakerManager: this.circuitBreakerManager,
2392
+ fetchRateLimiter: this.fetchRateLimiter,
2393
+ stampedeGuard: this.stampedeGuard,
2394
+ ttlResolver: this.ttlResolver,
2395
+ logger: this.logger,
2396
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2397
+ handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
2398
+ emit: (event, data) => this.emit(event, data),
2399
+ emitError: (operation, context) => this.emitError(operation, context),
2400
+ formatError: (error) => this.formatError(error),
2401
+ storeEntry: (key, kind, value, options2) => this.storeEntry(key, kind, value, options2),
2402
+ recordCircuitFailure: (key, options2, error) => this.recordCircuitFailure(key, options2, error),
2403
+ resolveLayerMs: (layerName, override, globalDefault, fallback) => this.resolveLayerMs(layerName, override, globalDefault, fallback),
2404
+ sleep: (ms) => this.sleep(ms),
2405
+ withTimeout: (promise, ms, createError) => this.withTimeout(promise, ms, createError),
2406
+ isDisconnecting: () => this.isDisconnecting,
2407
+ isGracefulDegradationEnabled: () => this.isGracefulDegradationEnabled(),
2408
+ scheduleBackgroundRefreshDispatch: (key, fetcher, options2, fetcherContext) => this.scheduleBackgroundRefresh(key, fetcher, options2, fetcherContext),
2409
+ stampedePrevention: options.stampedePrevention,
2410
+ singleFlightCoordinator: options.singleFlightCoordinator,
2411
+ singleFlightLeaseMs: options.singleFlightLeaseMs,
2412
+ singleFlightTimeoutMs: options.singleFlightTimeoutMs,
2413
+ singleFlightPollMs: options.singleFlightPollMs,
2414
+ singleFlightRenewIntervalMs: options.singleFlightRenewIntervalMs,
2415
+ backgroundRefreshTimeoutMs: options.backgroundRefreshTimeoutMs,
2416
+ negativeCaching: options.negativeCaching,
2417
+ refreshAhead: options.refreshAhead,
2418
+ circuitBreaker: options.circuitBreaker,
2419
+ fetcherRateLimit: options.fetcherRateLimit
2420
+ });
1940
2421
  this.initializeWriteBehind(options.writeBehind);
1941
2422
  this.startup = this.initialize();
1942
2423
  }
@@ -1955,8 +2436,6 @@ var CacheStack = class extends EventEmitter {
1955
2436
  invalidation;
1956
2437
  layerWriter;
1957
2438
  snapshots;
1958
- backgroundRefreshes = /* @__PURE__ */ new Map();
1959
- backgroundRefreshAbort = /* @__PURE__ */ new Map();
1960
2439
  layerDegradedUntil = /* @__PURE__ */ new Map();
1961
2440
  maintenance = new CacheStackMaintenance();
1962
2441
  ttlResolver;
@@ -1964,6 +2443,7 @@ var CacheStack = class extends EventEmitter {
1964
2443
  nextOperationId = 0;
1965
2444
  currentGeneration;
1966
2445
  isDisconnecting = false;
2446
+ reader;
1967
2447
  disconnectPromise;
1968
2448
  /**
1969
2449
  * Read-through cache get.
@@ -1976,51 +2456,9 @@ var CacheStack = class extends EventEmitter {
1976
2456
  const normalizedKey = this.qualifyKey(validateCacheKey(key));
1977
2457
  this.validateWriteOptions(options);
1978
2458
  await this.awaitStartup("get");
1979
- return this.getPrepared(normalizedKey, fetcher, options);
2459
+ return this.reader.getPrepared(normalizedKey, fetcher, options);
1980
2460
  });
1981
2461
  }
1982
- async getPrepared(normalizedKey, fetcher, options) {
1983
- const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
1984
- if (hit.found) {
1985
- this.ttlResolver.recordAccess(normalizedKey);
1986
- if (this.isNegativeStoredValue(hit.stored)) {
1987
- this.metricsCollector.increment("negativeCacheHits");
1988
- }
1989
- if (hit.state === "fresh") {
1990
- this.metricsCollector.increment("hits");
1991
- await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
1992
- return hit.value;
1993
- }
1994
- if (hit.state === "stale-while-revalidate") {
1995
- this.metricsCollector.increment("hits");
1996
- this.metricsCollector.increment("staleHits");
1997
- this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
1998
- if (fetcher) {
1999
- this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
2000
- }
2001
- return hit.value;
2002
- }
2003
- if (!fetcher) {
2004
- this.metricsCollector.increment("hits");
2005
- this.metricsCollector.increment("staleHits");
2006
- this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
2007
- return hit.value;
2008
- }
2009
- try {
2010
- return await this.fetchWithGuards(normalizedKey, fetcher, options);
2011
- } catch (error) {
2012
- this.metricsCollector.increment("staleHits");
2013
- this.metricsCollector.increment("refreshErrors");
2014
- this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
2015
- return hit.value;
2016
- }
2017
- }
2018
- this.metricsCollector.increment("misses");
2019
- if (!fetcher) {
2020
- return null;
2021
- }
2022
- return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
2023
- }
2024
2462
  /**
2025
2463
  * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
2026
2464
  * Fetches and caches the value if not already present.
@@ -2073,7 +2511,7 @@ var CacheStack = class extends EventEmitter {
2073
2511
  return false;
2074
2512
  }
2075
2513
  /**
2076
- * Returns the remaining TTL in seconds for the key in the fastest layer
2514
+ * Returns the remaining TTL in milliseconds for the key in the fastest layer
2077
2515
  * that has it, or null if the key is not found / has no TTL.
2078
2516
  */
2079
2517
  async ttl(key) {
@@ -2171,7 +2609,7 @@ var CacheStack = class extends EventEmitter {
2171
2609
  const optionsSignature = serializeOptions(entry.options);
2172
2610
  const existing = pendingReads.get(entry.key);
2173
2611
  if (!existing) {
2174
- const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
2612
+ const promise = this.reader.getPrepared(entry.key, entry.fetch, entry.options);
2175
2613
  pendingReads.set(entry.key, {
2176
2614
  promise,
2177
2615
  fetch: entry.fetch,
@@ -2207,7 +2645,7 @@ var CacheStack = class extends EventEmitter {
2207
2645
  if (keys.length === 0) {
2208
2646
  break;
2209
2647
  }
2210
- const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
2648
+ const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.reader.readLayerEntry(layer, key)));
2211
2649
  for (let offset = 0; offset < values.length; offset += 1) {
2212
2650
  const key = keys[offset];
2213
2651
  const stored = values[offset];
@@ -2223,7 +2661,7 @@ var CacheStack = class extends EventEmitter {
2223
2661
  this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
2224
2662
  }
2225
2663
  await this.tagIndex.touch(key);
2226
- await this.backfill(key, stored, layerIndex - 1);
2664
+ await this.reader.backfill(key, stored, layerIndex - 1);
2227
2665
  resultsByKey.set(key, resolved.value);
2228
2666
  pending.delete(key);
2229
2667
  this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
@@ -2309,20 +2747,45 @@ var CacheStack = class extends EventEmitter {
2309
2747
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2310
2748
  });
2311
2749
  }
2312
- async invalidateByTags(tags, mode = "any") {
2313
- await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
2750
+ async expireByTag(tag) {
2751
+ await this.observeOperation("layercache.expire_by_tag", void 0, async () => {
2752
+ validateTag(tag);
2753
+ await this.awaitStartup("expireByTag");
2754
+ const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
2755
+ await this.expireKeys(keys);
2756
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
2757
+ });
2758
+ }
2759
+ async invalidateByTags(tags, mode = "any") {
2760
+ await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
2761
+ if (tags.length === 0) {
2762
+ return;
2763
+ }
2764
+ validateTags(tags);
2765
+ await this.awaitStartup("invalidateByTags");
2766
+ const keysByTag = await Promise.all(
2767
+ tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
2768
+ );
2769
+ const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
2770
+ this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
2771
+ await this.deleteKeys(keys);
2772
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2773
+ });
2774
+ }
2775
+ async expireByTags(tags, mode = "any") {
2776
+ await this.observeOperation("layercache.expire_by_tags", void 0, async () => {
2314
2777
  if (tags.length === 0) {
2315
2778
  return;
2316
2779
  }
2317
2780
  validateTags(tags);
2318
- await this.awaitStartup("invalidateByTags");
2781
+ await this.awaitStartup("expireByTags");
2319
2782
  const keysByTag = await Promise.all(
2320
2783
  tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
2321
2784
  );
2322
2785
  const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
2323
2786
  this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
2324
- await this.deleteKeys(keys);
2325
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2787
+ await this.expireKeys(keys);
2788
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
2326
2789
  });
2327
2790
  }
2328
2791
  async invalidateByPattern(pattern) {
@@ -2337,6 +2800,18 @@ var CacheStack = class extends EventEmitter {
2337
2800
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2338
2801
  });
2339
2802
  }
2803
+ async expireByPattern(pattern) {
2804
+ await this.observeOperation("layercache.expire_by_pattern", void 0, async () => {
2805
+ validatePattern(pattern);
2806
+ await this.awaitStartup("expireByPattern");
2807
+ const keys = await this.keyDiscovery.collectKeysMatchingPattern(
2808
+ this.qualifyPattern(pattern),
2809
+ this.invalidationMaxKeys()
2810
+ );
2811
+ await this.expireKeys(keys);
2812
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
2813
+ });
2814
+ }
2340
2815
  async invalidateByPrefix(prefix) {
2341
2816
  await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
2342
2817
  await this.awaitStartup("invalidateByPrefix");
@@ -2346,6 +2821,15 @@ var CacheStack = class extends EventEmitter {
2346
2821
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
2347
2822
  });
2348
2823
  }
2824
+ async expireByPrefix(prefix) {
2825
+ await this.observeOperation("layercache.expire_by_prefix", void 0, async () => {
2826
+ await this.awaitStartup("expireByPrefix");
2827
+ const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
2828
+ const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
2829
+ await this.expireKeys(keys);
2830
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
2831
+ });
2832
+ }
2349
2833
  getMetrics() {
2350
2834
  return this.metricsCollector.snapshot;
2351
2835
  }
@@ -2357,7 +2841,7 @@ var CacheStack = class extends EventEmitter {
2357
2841
  isLocal: Boolean(layer.isLocal),
2358
2842
  degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
2359
2843
  })),
2360
- backgroundRefreshes: this.backgroundRefreshes.size
2844
+ backgroundRefreshes: this.reader.activeRefreshCount
2361
2845
  };
2362
2846
  }
2363
2847
  resetMetrics() {
@@ -2422,9 +2906,9 @@ var CacheStack = class extends EventEmitter {
2422
2906
  const normalizedKey = this.qualifyKey(userKey);
2423
2907
  await this.awaitStartup("inspect");
2424
2908
  const foundInLayers = [];
2425
- let freshTtlSeconds = null;
2426
- let staleTtlSeconds = null;
2427
- let errorTtlSeconds = null;
2909
+ let freshTtlMs = null;
2910
+ let staleTtlMs = null;
2911
+ let errorTtlMs = null;
2428
2912
  let isStale = false;
2429
2913
  for (const layer of this.layers) {
2430
2914
  if (this.shouldSkipLayer(layer)) {
@@ -2441,9 +2925,9 @@ var CacheStack = class extends EventEmitter {
2441
2925
  foundInLayers.push(layer.name);
2442
2926
  if (foundInLayers.length === 1 && resolved.envelope) {
2443
2927
  const now = Date.now();
2444
- freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
2445
- staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
2446
- errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
2928
+ freshTtlMs = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil(resolved.envelope.freshUntil - now)) : null;
2929
+ staleTtlMs = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil(resolved.envelope.staleUntil - now)) : null;
2930
+ errorTtlMs = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil(resolved.envelope.errorUntil - now)) : null;
2447
2931
  isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
2448
2932
  }
2449
2933
  }
@@ -2451,7 +2935,7 @@ var CacheStack = class extends EventEmitter {
2451
2935
  return null;
2452
2936
  }
2453
2937
  const tags = await this.getTagsForKey(normalizedKey);
2454
- return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
2938
+ return { key: userKey, foundInLayers, freshTtlMs, staleTtlMs, errorTtlMs, isStale, tags };
2455
2939
  }
2456
2940
  async exportState() {
2457
2941
  await this.awaitStartup("exportState");
@@ -2477,11 +2961,9 @@ var CacheStack = class extends EventEmitter {
2477
2961
  await this.unsubscribeInvalidation?.();
2478
2962
  await this.flushWriteBehindQueue();
2479
2963
  await this.maintenance.waitForGenerationCleanup();
2480
- for (const key of this.backgroundRefreshAbort.keys()) {
2481
- this.backgroundRefreshAbort.set(key, true);
2482
- }
2964
+ this.reader.abortAllRefreshes();
2483
2965
  await Promise.allSettled(
2484
- [...this.backgroundRefreshes.values()].map((promise) => {
2966
+ this.reader.getAllRefreshPromises().map((promise) => {
2485
2967
  let timer;
2486
2968
  return Promise.race([
2487
2969
  promise,
@@ -2494,8 +2976,6 @@ var CacheStack = class extends EventEmitter {
2494
2976
  });
2495
2977
  })
2496
2978
  );
2497
- this.backgroundRefreshes.clear();
2498
- this.backgroundRefreshAbort.clear();
2499
2979
  this.maintenance.disposeWriteBehindTimer();
2500
2980
  this.fetchRateLimiter.dispose();
2501
2981
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
@@ -2511,141 +2991,36 @@ var CacheStack = class extends EventEmitter {
2511
2991
  await this.handleInvalidationMessage(message);
2512
2992
  });
2513
2993
  }
2514
- async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
2515
- const fetchTask = async () => {
2516
- const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
2517
- if (shouldRecheckFreshLayers) {
2518
- const secondHit = await this.readFromLayers(key, options, "fresh-only");
2519
- if (secondHit.found) {
2520
- this.metricsCollector.increment("hits");
2521
- return secondHit.value;
2522
- }
2523
- }
2524
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
2525
- };
2526
- const singleFlightTask = async () => {
2527
- if (!this.options.singleFlightCoordinator) {
2528
- return fetchTask();
2529
- }
2530
- try {
2531
- return await this.options.singleFlightCoordinator.execute(
2532
- key,
2533
- this.resolveSingleFlightOptions(),
2534
- fetchTask,
2535
- () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
2536
- );
2537
- } catch (error) {
2538
- if (!this.isGracefulDegradationEnabled()) {
2539
- throw error;
2540
- }
2541
- this.metricsCollector.increment("degradedOperations");
2542
- this.logger.warn?.("single-flight-coordinator-degraded", { key, error: this.formatError(error) });
2543
- this.emitError("single-flight", { key, degraded: true, error: this.formatError(error) });
2544
- return fetchTask();
2545
- }
2546
- };
2547
- if (this.options.stampedePrevention === false) {
2548
- return singleFlightTask();
2549
- }
2550
- return this.stampedeGuard.execute(key, singleFlightTask);
2551
- }
2552
- async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2553
- const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
2554
- const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
2555
- const deadline = Date.now() + timeoutMs;
2556
- this.metricsCollector.increment("singleFlightWaits");
2557
- this.emit("stampede-dedupe", { key });
2558
- while (Date.now() < deadline) {
2559
- const hit = await this.readFromLayers(key, options, "fresh-only");
2560
- if (hit.found) {
2561
- this.metricsCollector.increment("hits");
2562
- return hit.value;
2563
- }
2564
- await this.sleep(pollIntervalMs);
2565
- }
2566
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
2567
- }
2568
- async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2569
- this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
2570
- this.metricsCollector.increment("fetches");
2571
- const fetchStart = Date.now();
2572
- let fetched;
2573
- try {
2574
- fetched = await this.fetchRateLimiter.schedule(
2575
- options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
2576
- { key, fetcher },
2577
- fetcher
2578
- );
2579
- this.circuitBreakerManager.recordSuccess(key);
2580
- this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
2581
- } catch (error) {
2582
- this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
2583
- throw error;
2584
- }
2585
- if (fetched === null || fetched === void 0) {
2586
- if (!this.shouldNegativeCache(options)) {
2587
- return null;
2588
- }
2589
- if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2590
- this.logger.debug?.("skip-negative-store-after-invalidation", {
2591
- key,
2592
- expectedClearEpoch,
2593
- clearEpoch: this.maintenance.currentClearEpoch(),
2594
- expectedKeyEpoch,
2595
- keyEpoch: this.maintenance.currentKeyEpoch(key)
2596
- });
2597
- return null;
2598
- }
2599
- await this.storeEntry(key, "empty", null, options);
2600
- return null;
2601
- }
2602
- if (options?.shouldCache) {
2603
- try {
2604
- if (!options.shouldCache(fetched)) {
2605
- return fetched;
2606
- }
2607
- } catch (error) {
2608
- this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
2609
- }
2610
- }
2611
- if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2612
- this.logger.debug?.("skip-store-after-invalidation", {
2613
- key,
2614
- expectedClearEpoch,
2615
- clearEpoch: this.maintenance.currentClearEpoch(),
2616
- expectedKeyEpoch,
2617
- keyEpoch: this.maintenance.currentKeyEpoch(key)
2618
- });
2619
- return fetched;
2620
- }
2621
- await this.storeEntry(key, "value", fetched, options);
2622
- return fetched;
2623
- }
2624
2994
  async storeEntry(key, kind, value, options) {
2995
+ const resolvedOptions = this.resolveContextOptions(key, kind, value, options);
2625
2996
  const clearEpoch = this.maintenance.currentClearEpoch();
2626
2997
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
2627
- await this.layerWriter.writeAcrossLayers(key, kind, value, options);
2998
+ await this.layerWriter.writeAcrossLayers(key, kind, value, resolvedOptions);
2628
2999
  if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
2629
3000
  return;
2630
3001
  }
2631
- if (options?.tags) {
2632
- await this.tagIndex.track(key, options.tags);
3002
+ if (resolvedOptions?.tags) {
3003
+ await this.tagIndex.track(key, resolvedOptions.tags);
2633
3004
  } else {
2634
3005
  await this.tagIndex.touch(key);
2635
3006
  }
2636
3007
  this.metricsCollector.increment("sets");
2637
- this.logger.debug?.("set", { key, kind, tags: options?.tags });
2638
- this.emit("set", { key, kind, tags: options?.tags });
3008
+ this.logger.debug?.("set", { key, kind, tags: resolvedOptions?.tags });
3009
+ this.emit("set", { key, kind, tags: resolvedOptions?.tags });
2639
3010
  if (this.shouldBroadcastL1Invalidation()) {
2640
3011
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
2641
3012
  }
2642
3013
  }
2643
3014
  async writeBatch(entries) {
2644
- const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
3015
+ const resolvedEntries = entries.map((entry) => ({
3016
+ ...entry,
3017
+ options: this.resolveContextOptions(entry.key, "value", entry.value, entry.options)
3018
+ }));
3019
+ const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(resolvedEntries);
2645
3020
  if (clearEpoch !== this.maintenance.currentClearEpoch()) {
2646
3021
  return;
2647
3022
  }
2648
- for (const entry of entries) {
3023
+ for (const entry of resolvedEntries) {
2649
3024
  if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
2650
3025
  continue;
2651
3026
  }
@@ -2667,87 +3042,6 @@ var CacheStack = class extends EventEmitter {
2667
3042
  });
2668
3043
  }
2669
3044
  }
2670
- async readFromLayers(key, options, mode) {
2671
- let sawRetainableValue = false;
2672
- for (let index = 0; index < this.layers.length; index += 1) {
2673
- const layer = this.layers[index];
2674
- if (!layer) continue;
2675
- const readStart = performance.now();
2676
- const stored = await this.readLayerEntry(layer, key);
2677
- const readDuration = performance.now() - readStart;
2678
- this.metricsCollector.recordLatency(layer.name, readDuration);
2679
- if (stored === null) {
2680
- this.metricsCollector.incrementLayer("missesByLayer", layer.name);
2681
- continue;
2682
- }
2683
- const resolved = resolveStoredValue(stored);
2684
- if (resolved.state === "expired") {
2685
- await layer.delete(key);
2686
- continue;
2687
- }
2688
- sawRetainableValue = true;
2689
- if (mode === "fresh-only" && resolved.state !== "fresh") {
2690
- continue;
2691
- }
2692
- await this.tagIndex.touch(key);
2693
- await this.backfill(key, stored, index - 1, options);
2694
- this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
2695
- this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
2696
- this.emit("hit", { key, layer: layer.name, state: resolved.state });
2697
- return {
2698
- found: true,
2699
- value: resolved.value,
2700
- stored,
2701
- state: resolved.state,
2702
- layerIndex: index,
2703
- layerName: layer.name
2704
- };
2705
- }
2706
- if (!sawRetainableValue) {
2707
- await this.tagIndex.remove(key);
2708
- }
2709
- this.logger.debug?.("miss", { key, mode });
2710
- this.emit("miss", { key, mode });
2711
- return { found: false, value: null, stored: null, state: "miss" };
2712
- }
2713
- async readLayerEntry(layer, key) {
2714
- if (this.shouldSkipLayer(layer)) {
2715
- return null;
2716
- }
2717
- if (layer.getEntry) {
2718
- try {
2719
- return await layer.getEntry(key);
2720
- } catch (error) {
2721
- return this.handleLayerFailure(layer, "read", error);
2722
- }
2723
- }
2724
- try {
2725
- return await layer.get(key);
2726
- } catch (error) {
2727
- return this.handleLayerFailure(layer, "read", error);
2728
- }
2729
- }
2730
- async backfill(key, stored, upToIndex, options) {
2731
- if (upToIndex < 0) {
2732
- return;
2733
- }
2734
- for (let index = 0; index <= upToIndex; index += 1) {
2735
- const layer = this.layers[index];
2736
- if (!layer || this.shouldSkipLayer(layer)) {
2737
- continue;
2738
- }
2739
- const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
2740
- try {
2741
- await layer.set(key, stored, ttl);
2742
- } catch (error) {
2743
- await this.handleLayerFailure(layer, "backfill", error);
2744
- continue;
2745
- }
2746
- this.metricsCollector.increment("backfills");
2747
- this.logger.debug?.("backfill", { key, layer: layer.name });
2748
- this.emit("backfill", { key, layer: layer.name });
2749
- }
2750
- }
2751
3045
  resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
2752
3046
  return this.ttlResolver.resolveFreshTtl(
2753
3047
  key,
@@ -2760,58 +3054,47 @@ var CacheStack = class extends EventEmitter {
2760
3054
  value
2761
3055
  );
2762
3056
  }
2763
- resolveLayerSeconds(layerName, override, globalDefault, fallback) {
2764
- return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
2765
- }
2766
- shouldNegativeCache(options) {
2767
- return options?.negativeCache ?? this.options.negativeCaching ?? false;
3057
+ resolveLayerMs(layerName, override, globalDefault, fallback) {
3058
+ return this.ttlResolver.resolveLayerMs(layerName, override, globalDefault, fallback);
2768
3059
  }
2769
- scheduleBackgroundRefresh(key, fetcher, options) {
2770
- if (!shouldStartBackgroundRefresh({
2771
- isDisconnecting: this.isDisconnecting,
2772
- hasRefreshInFlight: this.backgroundRefreshes.has(key)
2773
- })) {
2774
- return;
3060
+ resolveContextOptions(key, kind, value, options) {
3061
+ if (!options?.contextOptions) {
3062
+ return options;
3063
+ }
3064
+ const { contextOptions, ...baseOptions } = options;
3065
+ let overrides;
3066
+ try {
3067
+ overrides = contextOptions({ key, value, kind });
3068
+ } catch (error) {
3069
+ throw new Error(`options.contextOptions() failed for key "${key}": ${this.formatError(error)}`);
3070
+ }
3071
+ if (!overrides) {
3072
+ return baseOptions;
3073
+ }
3074
+ if (!this.isPlainObject(overrides)) {
3075
+ throw new Error(
3076
+ `options.contextOptions() must return a plain object or undefined for key "${key}". Async resolvers are not supported.`
3077
+ );
3078
+ }
3079
+ try {
3080
+ validateContextEntryOptions("options.contextOptions()", overrides);
3081
+ } catch (error) {
3082
+ throw new Error(
3083
+ `options.contextOptions() returned invalid entry options for key "${key}": ${this.formatError(error)}`
3084
+ );
2775
3085
  }
2776
- const clearEpoch = this.maintenance.currentClearEpoch();
2777
- const keyEpoch = this.maintenance.currentKeyEpoch(key);
2778
- this.backgroundRefreshAbort.set(key, false);
2779
- const refresh = (async () => {
2780
- this.metricsCollector.increment("refreshes");
2781
- try {
2782
- if (this.backgroundRefreshAbort.get(key)) return;
2783
- await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
2784
- } catch (error) {
2785
- if (this.backgroundRefreshAbort.get(key)) return;
2786
- this.metricsCollector.increment("refreshErrors");
2787
- this.logger.warn?.("background-refresh-error", { key, error: this.formatError(error) });
2788
- } finally {
2789
- this.backgroundRefreshes.delete(key);
2790
- this.backgroundRefreshAbort.delete(key);
2791
- }
2792
- })();
2793
- this.backgroundRefreshes.set(key, refresh);
2794
- }
2795
- async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2796
- const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
2797
- await this.fetchWithGuards(
2798
- key,
2799
- () => this.withTimeout(fetcher(), timeoutMs, () => {
2800
- return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
2801
- }),
2802
- options,
2803
- expectedClearEpoch,
2804
- expectedKeyEpoch
2805
- );
2806
- }
2807
- resolveSingleFlightOptions() {
2808
3086
  return {
2809
- leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
2810
- waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
2811
- pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
2812
- renewIntervalMs: this.options.singleFlightRenewIntervalMs
3087
+ ...baseOptions,
3088
+ ...overrides
2813
3089
  };
2814
3090
  }
3091
+ isPlainObject(value) {
3092
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3093
+ return false;
3094
+ }
3095
+ const prototype = Object.getPrototypeOf(value);
3096
+ return prototype === Object.prototype || prototype === null;
3097
+ }
2815
3098
  async deleteKeys(keys) {
2816
3099
  if (keys.length === 0) {
2817
3100
  return;
@@ -2828,6 +3111,30 @@ var CacheStack = class extends EventEmitter {
2828
3111
  this.logger.debug?.("delete", { keys });
2829
3112
  this.emit("delete", { keys });
2830
3113
  }
3114
+ async expireKeys(keys) {
3115
+ if (keys.length === 0) {
3116
+ return;
3117
+ }
3118
+ this.maintenance.bumpKeyEpochs(keys);
3119
+ const foundKeys = await this.expireKeysInLayers(keys, this.layers);
3120
+ for (const key of keys) {
3121
+ if (foundKeys.has(key)) {
3122
+ continue;
3123
+ }
3124
+ await this.tagIndex.remove(key);
3125
+ this.ttlResolver.deleteProfile(key);
3126
+ this.circuitBreakerManager.delete(key);
3127
+ }
3128
+ this.metricsCollector.increment("invalidations");
3129
+ this.logger.debug?.("expire", { keys });
3130
+ this.emit("expire", { keys });
3131
+ }
3132
+ async expireKeysInLayers(keys, layers) {
3133
+ if (keys.length === 0) {
3134
+ return /* @__PURE__ */ new Set();
3135
+ }
3136
+ return this.invalidation.expireKeysInLayers(layers, keys);
3137
+ }
2831
3138
  async publishInvalidation(message) {
2832
3139
  if (!this.options.invalidationBus) {
2833
3140
  return;
@@ -2849,6 +3156,10 @@ var CacheStack = class extends EventEmitter {
2849
3156
  }
2850
3157
  const keys = message.keys ?? [];
2851
3158
  this.maintenance.bumpKeyEpochs(keys);
3159
+ if (message.operation === "expire") {
3160
+ await this.expireKeysInLayers(keys, localLayers);
3161
+ return;
3162
+ }
2852
3163
  await this.invalidation.deleteKeysFromLayers(localLayers, keys);
2853
3164
  if (message.operation !== "write") {
2854
3165
  for (const key of keys) {
@@ -3046,6 +3357,9 @@ var CacheStack = class extends EventEmitter {
3046
3357
  validateCircuitBreakerOptions(options.circuitBreaker);
3047
3358
  validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
3048
3359
  validateTags(options.tags);
3360
+ if (options.contextOptions && typeof options.contextOptions !== "function") {
3361
+ throw new Error("options.contextOptions must be a function.");
3362
+ }
3049
3363
  }
3050
3364
  assertActive(operation) {
3051
3365
  if (this.isDisconnecting) {
@@ -3057,29 +3371,14 @@ var CacheStack = class extends EventEmitter {
3057
3371
  await this.startup;
3058
3372
  this.assertActive(operation);
3059
3373
  }
3374
+ async readLayerEntry(layer, key) {
3375
+ return this.reader.readLayerEntry(layer, key);
3376
+ }
3377
+ scheduleBackgroundRefresh(key, fetcher, options, fetcherContext) {
3378
+ this.reader.runScheduleBackgroundRefresh(key, fetcher, options, fetcherContext);
3379
+ }
3060
3380
  async applyFreshReadPolicies(key, hit, options, fetcher) {
3061
- const plan = planFreshReadPolicies({
3062
- stored: hit.stored,
3063
- hasFetcher: Boolean(fetcher),
3064
- slidingTtl: options?.slidingTtl ?? false,
3065
- refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
3066
- });
3067
- if (plan.refreshedStored) {
3068
- for (let index = 0; index <= hit.layerIndex; index += 1) {
3069
- const layer = this.layers[index];
3070
- if (!layer || this.shouldSkipLayer(layer)) {
3071
- continue;
3072
- }
3073
- try {
3074
- await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
3075
- } catch (error) {
3076
- await this.handleLayerFailure(layer, "sliding-ttl", error);
3077
- }
3078
- }
3079
- }
3080
- if (fetcher && plan.shouldScheduleBackgroundRefresh) {
3081
- this.scheduleBackgroundRefresh(key, fetcher, options);
3082
- }
3381
+ return this.reader.runApplyFreshReadPolicies(key, hit, options, fetcher);
3083
3382
  }
3084
3383
  shouldSkipLayer(layer) {
3085
3384
  const degradedUntil = this.layerDegradedUntil.get(layer.name);
@@ -3121,9 +3420,6 @@ var CacheStack = class extends EventEmitter {
3121
3420
  }
3122
3421
  this.emitError("fetch", { key, error: this.formatError(error) });
3123
3422
  }
3124
- isNegativeStoredValue(stored) {
3125
- return isStoredValueEnvelope(stored) && stored.kind === "empty";
3126
- }
3127
3423
  emitError(operation, context) {
3128
3424
  this.logger.error?.(operation, context);
3129
3425
  if (this.listenerCount("error") > 0) {
@@ -3223,7 +3519,7 @@ var RedisInvalidationBus = class {
3223
3519
  }
3224
3520
  const candidate = value;
3225
3521
  const validScope = candidate.scope === "key" || candidate.scope === "keys" || candidate.scope === "clear";
3226
- const validOperation = candidate.operation === void 0 || candidate.operation === "write" || candidate.operation === "delete" || candidate.operation === "invalidate" || candidate.operation === "clear";
3522
+ const validOperation = candidate.operation === void 0 || candidate.operation === "write" || candidate.operation === "delete" || candidate.operation === "invalidate" || candidate.operation === "expire" || candidate.operation === "clear";
3227
3523
  const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
3228
3524
  return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
3229
3525
  }
@@ -3554,7 +3850,7 @@ var RedisLayer = class {
3554
3850
  const payload = await this.encodePayload(serialized);
3555
3851
  const normalizedKey = this.withPrefix(entry.key);
3556
3852
  if (entry.ttl && entry.ttl > 0) {
3557
- pipeline.set(normalizedKey, payload, "EX", entry.ttl);
3853
+ pipeline.set(normalizedKey, payload, "PX", entry.ttl);
3558
3854
  } else {
3559
3855
  pipeline.set(normalizedKey, payload);
3560
3856
  }
@@ -3569,7 +3865,7 @@ var RedisLayer = class {
3569
3865
  if (ttl && ttl > 0) {
3570
3866
  await this.runCommand(
3571
3867
  `set(${this.displayKey(key)})`,
3572
- () => this.client.set(normalizedKey, payload, "EX", ttl)
3868
+ () => this.client.set(normalizedKey, payload, "PX", ttl)
3573
3869
  );
3574
3870
  return;
3575
3871
  }
@@ -3598,7 +3894,10 @@ var RedisLayer = class {
3598
3894
  }
3599
3895
  async ttl(key) {
3600
3896
  this.validateKey(key);
3601
- const remaining = await this.runCommand(`ttl(${this.displayKey(key)})`, () => this.client.ttl(this.withPrefix(key)));
3897
+ const remaining = await this.runCommand(
3898
+ `ttl(${this.displayKey(key)})`,
3899
+ () => this.client.pttl(this.withPrefix(key))
3900
+ );
3602
3901
  if (remaining < 0) {
3603
3902
  return null;
3604
3903
  }
@@ -3749,12 +4048,12 @@ var RedisLayer = class {
3749
4048
  const payload = await this.encodePayload(serialized);
3750
4049
  const ttl = await this.runCommand(
3751
4050
  `rewrite-ttl(${this.displayKey(key)})`,
3752
- () => this.client.ttl(this.withPrefix(key))
4051
+ () => this.client.pttl(this.withPrefix(key))
3753
4052
  );
3754
4053
  if (ttl > 0) {
3755
4054
  await this.runCommand(
3756
4055
  `rewrite-set(${this.displayKey(key)})`,
3757
- () => this.client.set(this.withPrefix(key), payload, "EX", ttl)
4056
+ () => this.client.set(this.withPrefix(key), payload, "PX", ttl)
3758
4057
  );
3759
4058
  return;
3760
4059
  }
@@ -4065,7 +4364,7 @@ var DiskLayer = class {
4065
4364
  const entry = {
4066
4365
  key,
4067
4366
  value,
4068
- expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
4367
+ expiresAt: ttl && ttl > 0 ? Date.now() + ttl : null
4069
4368
  };
4070
4369
  const payload = this.serializer.serialize(entry);
4071
4370
  const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
@@ -4110,7 +4409,7 @@ var DiskLayer = class {
4110
4409
  if (entry.expiresAt === null) {
4111
4410
  return null;
4112
4411
  }
4113
- const remaining = Math.ceil((entry.expiresAt - Date.now()) / 1e3);
4412
+ const remaining = Math.ceil(entry.expiresAt - Date.now());
4114
4413
  if (remaining <= 0) {
4115
4414
  return null;
4116
4415
  }
@@ -4400,7 +4699,7 @@ var MemcachedLayer = class {
4400
4699
  this.validateKey(key);
4401
4700
  const payload = this.serializer.serialize(value);
4402
4701
  await this.client.set(this.withPrefix(key), payload, {
4403
- expires: ttl && ttl > 0 ? ttl : void 0
4702
+ expires: ttl && ttl > 0 ? Math.ceil(ttl / 1e3) : void 0
4404
4703
  });
4405
4704
  }
4406
4705
  async has(key) {