layercache 1.3.3 → 1.3.4

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.
package/dist/index.js CHANGED
@@ -871,6 +871,366 @@ function planFreshReadPolicies({
871
871
  };
872
872
  }
873
873
 
874
+ // src/internal/CacheStackReader.ts
875
+ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
876
+ var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
877
+ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
878
+ var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
879
+ var CacheStackReader = class {
880
+ constructor(options) {
881
+ this.options = options;
882
+ }
883
+ options;
884
+ backgroundRefreshes = /* @__PURE__ */ new Map();
885
+ backgroundRefreshAbort = /* @__PURE__ */ new Map();
886
+ get activeRefreshCount() {
887
+ return this.backgroundRefreshes.size;
888
+ }
889
+ async getPrepared(normalizedKey, fetcher, options) {
890
+ const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
891
+ if (hit.found) {
892
+ this.options.ttlResolver.recordAccess(normalizedKey);
893
+ if (this.isNegativeStoredValue(hit.stored)) {
894
+ this.options.metricsCollector.increment("negativeCacheHits");
895
+ }
896
+ if (hit.state === "fresh") {
897
+ this.options.metricsCollector.increment("hits");
898
+ await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
899
+ return hit.value;
900
+ }
901
+ if (hit.state === "stale-while-revalidate") {
902
+ this.options.metricsCollector.increment("hits");
903
+ this.options.metricsCollector.increment("staleHits");
904
+ this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
905
+ if (fetcher) {
906
+ this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
907
+ }
908
+ return hit.value;
909
+ }
910
+ if (!fetcher) {
911
+ this.options.metricsCollector.increment("hits");
912
+ this.options.metricsCollector.increment("staleHits");
913
+ this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
914
+ return hit.value;
915
+ }
916
+ try {
917
+ return await this.fetchWithGuards(normalizedKey, fetcher, options);
918
+ } catch (error) {
919
+ this.options.metricsCollector.increment("staleHits");
920
+ this.options.metricsCollector.increment("refreshErrors");
921
+ this.options.logger.debug?.("stale-if-error", {
922
+ key: normalizedKey,
923
+ error: this.options.formatError(error)
924
+ });
925
+ return hit.value;
926
+ }
927
+ }
928
+ this.options.metricsCollector.increment("misses");
929
+ if (!fetcher) {
930
+ return null;
931
+ }
932
+ return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
933
+ }
934
+ async readLayerEntry(layer, key) {
935
+ if (this.options.shouldSkipLayer(layer)) {
936
+ return null;
937
+ }
938
+ if (layer.getEntry) {
939
+ try {
940
+ return await layer.getEntry(key);
941
+ } catch (error) {
942
+ return this.options.handleLayerFailure(layer, "read", error);
943
+ }
944
+ }
945
+ try {
946
+ return await layer.get(key);
947
+ } catch (error) {
948
+ return this.options.handleLayerFailure(layer, "read", error);
949
+ }
950
+ }
951
+ async backfill(key, stored, upToIndex, options) {
952
+ if (upToIndex < 0) {
953
+ return;
954
+ }
955
+ for (let index = 0; index <= upToIndex; index += 1) {
956
+ const layer = this.options.layers[index];
957
+ if (!layer || this.options.shouldSkipLayer(layer)) {
958
+ continue;
959
+ }
960
+ const ttl = remainingStoredTtlSeconds(stored) ?? this.options.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
961
+ try {
962
+ await layer.set(key, stored, ttl);
963
+ } catch (error) {
964
+ await this.options.handleLayerFailure(layer, "backfill", error);
965
+ continue;
966
+ }
967
+ this.options.metricsCollector.increment("backfills");
968
+ this.options.logger.debug?.("backfill", { key, layer: layer.name });
969
+ this.options.emit("backfill", { key, layer: layer.name });
970
+ }
971
+ }
972
+ abortAllRefreshes() {
973
+ for (const key of this.backgroundRefreshAbort.keys()) {
974
+ this.backgroundRefreshAbort.set(key, true);
975
+ }
976
+ }
977
+ getAllRefreshPromises() {
978
+ return [...this.backgroundRefreshes.values()];
979
+ }
980
+ async readFromLayers(key, options, mode) {
981
+ let sawRetainableValue = false;
982
+ for (let index = 0; index < this.options.layers.length; index += 1) {
983
+ const layer = this.options.layers[index];
984
+ if (!layer) continue;
985
+ const readStart = performance.now();
986
+ const stored = await this.readLayerEntry(layer, key);
987
+ const readDuration = performance.now() - readStart;
988
+ this.options.metricsCollector.recordLatency(layer.name, readDuration);
989
+ if (stored === null) {
990
+ this.options.metricsCollector.incrementLayer("missesByLayer", layer.name);
991
+ continue;
992
+ }
993
+ const resolved = resolveStoredValue(stored);
994
+ if (resolved.state === "expired") {
995
+ await layer.delete(key);
996
+ continue;
997
+ }
998
+ sawRetainableValue = true;
999
+ if (mode === "fresh-only" && resolved.state !== "fresh") {
1000
+ continue;
1001
+ }
1002
+ await this.options.tagIndex.touch(key);
1003
+ await this.backfill(key, stored, index - 1, options);
1004
+ this.options.metricsCollector.incrementLayer("hitsByLayer", layer.name);
1005
+ this.options.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
1006
+ this.options.emit("hit", {
1007
+ key,
1008
+ layer: layer.name,
1009
+ state: resolved.state
1010
+ });
1011
+ return {
1012
+ found: true,
1013
+ value: resolved.value,
1014
+ stored,
1015
+ state: resolved.state,
1016
+ layerIndex: index,
1017
+ layerName: layer.name
1018
+ };
1019
+ }
1020
+ if (!sawRetainableValue) {
1021
+ await this.options.tagIndex.remove(key);
1022
+ }
1023
+ this.options.logger.debug?.("miss", { key, mode });
1024
+ this.options.emit("miss", { key, mode });
1025
+ return { found: false, value: null, stored: null, state: "miss" };
1026
+ }
1027
+ async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
1028
+ const fetchTask = async () => {
1029
+ const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
1030
+ if (shouldRecheckFreshLayers) {
1031
+ const secondHit = await this.readFromLayers(key, options, "fresh-only");
1032
+ if (secondHit.found) {
1033
+ this.options.metricsCollector.increment("hits");
1034
+ return secondHit.value;
1035
+ }
1036
+ }
1037
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
1038
+ };
1039
+ const singleFlightTask = async () => {
1040
+ if (!this.options.singleFlightCoordinator) {
1041
+ return fetchTask();
1042
+ }
1043
+ try {
1044
+ return await this.options.singleFlightCoordinator.execute(
1045
+ key,
1046
+ this.resolveSingleFlightOptions(),
1047
+ fetchTask,
1048
+ () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
1049
+ );
1050
+ } catch (error) {
1051
+ if (!this.options.isGracefulDegradationEnabled()) {
1052
+ throw error;
1053
+ }
1054
+ this.options.metricsCollector.increment("degradedOperations");
1055
+ this.options.logger.warn?.("single-flight-coordinator-degraded", {
1056
+ key,
1057
+ error: this.options.formatError(error)
1058
+ });
1059
+ this.options.emitError("single-flight", {
1060
+ key,
1061
+ degraded: true,
1062
+ error: this.options.formatError(error)
1063
+ });
1064
+ return fetchTask();
1065
+ }
1066
+ };
1067
+ if (this.options.stampedePrevention === false) {
1068
+ return singleFlightTask();
1069
+ }
1070
+ return this.options.stampedeGuard.execute(key, singleFlightTask);
1071
+ }
1072
+ async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
1073
+ const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
1074
+ const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
1075
+ const deadline = Date.now() + timeoutMs;
1076
+ this.options.metricsCollector.increment("singleFlightWaits");
1077
+ this.options.emit("stampede-dedupe", { key });
1078
+ while (Date.now() < deadline) {
1079
+ const hit = await this.readFromLayers(key, options, "fresh-only");
1080
+ if (hit.found) {
1081
+ this.options.metricsCollector.increment("hits");
1082
+ return hit.value;
1083
+ }
1084
+ await this.options.sleep(pollIntervalMs);
1085
+ }
1086
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
1087
+ }
1088
+ async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
1089
+ this.options.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
1090
+ this.options.metricsCollector.increment("fetches");
1091
+ const fetchStart = Date.now();
1092
+ let fetched;
1093
+ try {
1094
+ fetched = await this.options.fetchRateLimiter.schedule(
1095
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1096
+ { key, fetcher },
1097
+ fetcher
1098
+ );
1099
+ this.options.circuitBreakerManager.recordSuccess(key);
1100
+ this.options.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1101
+ } catch (error) {
1102
+ this.options.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
1103
+ throw error;
1104
+ }
1105
+ if (fetched === null || fetched === void 0) {
1106
+ if (!this.shouldNegativeCache(options)) {
1107
+ return null;
1108
+ }
1109
+ if (this.options.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
1110
+ this.options.logger.debug?.("skip-negative-store-after-invalidation", {
1111
+ key,
1112
+ expectedClearEpoch,
1113
+ clearEpoch: this.options.maintenance.currentClearEpoch(),
1114
+ expectedKeyEpoch,
1115
+ keyEpoch: this.options.maintenance.currentKeyEpoch(key)
1116
+ });
1117
+ return null;
1118
+ }
1119
+ await this.options.storeEntry(key, "empty", null, options);
1120
+ return null;
1121
+ }
1122
+ if (options?.shouldCache) {
1123
+ try {
1124
+ if (!options.shouldCache(fetched)) {
1125
+ return fetched;
1126
+ }
1127
+ } catch (error) {
1128
+ this.options.logger.warn?.("shouldCache-error", {
1129
+ key,
1130
+ error: this.options.formatError(error)
1131
+ });
1132
+ }
1133
+ }
1134
+ if (this.options.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
1135
+ this.options.logger.debug?.("skip-store-after-invalidation", {
1136
+ key,
1137
+ expectedClearEpoch,
1138
+ clearEpoch: this.options.maintenance.currentClearEpoch(),
1139
+ expectedKeyEpoch,
1140
+ keyEpoch: this.options.maintenance.currentKeyEpoch(key)
1141
+ });
1142
+ return fetched;
1143
+ }
1144
+ await this.options.storeEntry(key, "value", fetched, options);
1145
+ return fetched;
1146
+ }
1147
+ runScheduleBackgroundRefresh(key, fetcher, options) {
1148
+ this.scheduleBackgroundRefresh(key, fetcher, options);
1149
+ }
1150
+ scheduleBackgroundRefresh(key, fetcher, options) {
1151
+ if (!shouldStartBackgroundRefresh({
1152
+ isDisconnecting: this.options.isDisconnecting(),
1153
+ hasRefreshInFlight: this.backgroundRefreshes.has(key)
1154
+ })) {
1155
+ return;
1156
+ }
1157
+ const clearEpoch = this.options.maintenance.currentClearEpoch();
1158
+ const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
1159
+ this.backgroundRefreshAbort.set(key, false);
1160
+ const refresh = (async () => {
1161
+ this.options.metricsCollector.increment("refreshes");
1162
+ try {
1163
+ if (this.backgroundRefreshAbort.get(key)) return;
1164
+ await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
1165
+ } catch (error) {
1166
+ if (this.backgroundRefreshAbort.get(key)) return;
1167
+ this.options.metricsCollector.increment("refreshErrors");
1168
+ this.options.logger.warn?.("background-refresh-error", {
1169
+ key,
1170
+ error: this.options.formatError(error)
1171
+ });
1172
+ } finally {
1173
+ this.backgroundRefreshes.delete(key);
1174
+ this.backgroundRefreshAbort.delete(key);
1175
+ }
1176
+ })();
1177
+ this.backgroundRefreshes.set(key, refresh);
1178
+ }
1179
+ async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
1180
+ const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
1181
+ await this.fetchWithGuards(
1182
+ key,
1183
+ () => this.options.withTimeout(fetcher(), timeoutMs, () => {
1184
+ return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
1185
+ }),
1186
+ options,
1187
+ expectedClearEpoch,
1188
+ expectedKeyEpoch
1189
+ );
1190
+ }
1191
+ async runApplyFreshReadPolicies(key, hit, options, fetcher) {
1192
+ return this.applyFreshReadPolicies(key, hit, options, fetcher);
1193
+ }
1194
+ async applyFreshReadPolicies(key, hit, options, fetcher) {
1195
+ const plan = planFreshReadPolicies({
1196
+ stored: hit.stored,
1197
+ hasFetcher: Boolean(fetcher),
1198
+ slidingTtl: options?.slidingTtl ?? false,
1199
+ refreshAheadSeconds: this.options.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
1200
+ });
1201
+ if (plan.refreshedStored) {
1202
+ for (let index = 0; index <= hit.layerIndex; index += 1) {
1203
+ const layer = this.options.layers[index];
1204
+ if (!layer || this.options.shouldSkipLayer(layer)) {
1205
+ continue;
1206
+ }
1207
+ try {
1208
+ await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
1209
+ } catch (error) {
1210
+ await this.options.handleLayerFailure(layer, "sliding-ttl", error);
1211
+ }
1212
+ }
1213
+ }
1214
+ if (fetcher && plan.shouldScheduleBackgroundRefresh) {
1215
+ this.options.scheduleBackgroundRefreshDispatch(key, fetcher, options);
1216
+ }
1217
+ }
1218
+ resolveSingleFlightOptions() {
1219
+ return {
1220
+ leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
1221
+ waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
1222
+ pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
1223
+ renewIntervalMs: this.options.singleFlightRenewIntervalMs
1224
+ };
1225
+ }
1226
+ shouldNegativeCache(options) {
1227
+ return options?.negativeCache ?? this.options.negativeCaching ?? false;
1228
+ }
1229
+ isNegativeStoredValue(stored) {
1230
+ return isStoredValueEnvelope(stored) && stored.kind === "empty";
1231
+ }
1232
+ };
1233
+
874
1234
  // src/internal/CacheStackSnapshotManager.ts
875
1235
  import { constants, promises as fs } from "fs";
876
1236
 
@@ -1815,10 +2175,6 @@ var CacheMissError = class extends Error {
1815
2175
  };
1816
2176
 
1817
2177
  // 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
2178
  var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
1823
2179
  var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
1824
2180
  var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
@@ -1929,7 +2285,7 @@ var CacheStack = class extends EventEmitter {
1929
2285
  layers: this.layers,
1930
2286
  tagIndex: this.tagIndex,
1931
2287
  snapshotSerializer: this.snapshotSerializer,
1932
- readLayerEntry: this.readLayerEntry.bind(this),
2288
+ readLayerEntry: (layer, key) => this.reader.readLayerEntry(layer, key),
1933
2289
  shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
1934
2290
  handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
1935
2291
  qualifyKey: this.qualifyKey.bind(this),
@@ -1937,6 +2293,41 @@ var CacheStack = class extends EventEmitter {
1937
2293
  validateCacheKey,
1938
2294
  formatError: this.formatError.bind(this)
1939
2295
  });
2296
+ this.reader = new CacheStackReader({
2297
+ layers: this.layers,
2298
+ metricsCollector: this.metricsCollector,
2299
+ maintenance: this.maintenance,
2300
+ tagIndex: this.tagIndex,
2301
+ circuitBreakerManager: this.circuitBreakerManager,
2302
+ fetchRateLimiter: this.fetchRateLimiter,
2303
+ stampedeGuard: this.stampedeGuard,
2304
+ ttlResolver: this.ttlResolver,
2305
+ logger: this.logger,
2306
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2307
+ handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
2308
+ emit: (event, data) => this.emit(event, data),
2309
+ emitError: (operation, context) => this.emitError(operation, context),
2310
+ formatError: (error) => this.formatError(error),
2311
+ storeEntry: (key, kind, value, options2) => this.storeEntry(key, kind, value, options2),
2312
+ recordCircuitFailure: (key, options2, error) => this.recordCircuitFailure(key, options2, error),
2313
+ resolveLayerSeconds: (layerName, override, globalDefault, fallback) => this.resolveLayerSeconds(layerName, override, globalDefault, fallback),
2314
+ sleep: (ms) => this.sleep(ms),
2315
+ withTimeout: (promise, ms, createError) => this.withTimeout(promise, ms, createError),
2316
+ isDisconnecting: () => this.isDisconnecting,
2317
+ isGracefulDegradationEnabled: () => this.isGracefulDegradationEnabled(),
2318
+ scheduleBackgroundRefreshDispatch: (key, fetcher, options2) => this.scheduleBackgroundRefresh(key, fetcher, options2),
2319
+ stampedePrevention: options.stampedePrevention,
2320
+ singleFlightCoordinator: options.singleFlightCoordinator,
2321
+ singleFlightLeaseMs: options.singleFlightLeaseMs,
2322
+ singleFlightTimeoutMs: options.singleFlightTimeoutMs,
2323
+ singleFlightPollMs: options.singleFlightPollMs,
2324
+ singleFlightRenewIntervalMs: options.singleFlightRenewIntervalMs,
2325
+ backgroundRefreshTimeoutMs: options.backgroundRefreshTimeoutMs,
2326
+ negativeCaching: options.negativeCaching,
2327
+ refreshAhead: options.refreshAhead,
2328
+ circuitBreaker: options.circuitBreaker,
2329
+ fetcherRateLimit: options.fetcherRateLimit
2330
+ });
1940
2331
  this.initializeWriteBehind(options.writeBehind);
1941
2332
  this.startup = this.initialize();
1942
2333
  }
@@ -1955,8 +2346,6 @@ var CacheStack = class extends EventEmitter {
1955
2346
  invalidation;
1956
2347
  layerWriter;
1957
2348
  snapshots;
1958
- backgroundRefreshes = /* @__PURE__ */ new Map();
1959
- backgroundRefreshAbort = /* @__PURE__ */ new Map();
1960
2349
  layerDegradedUntil = /* @__PURE__ */ new Map();
1961
2350
  maintenance = new CacheStackMaintenance();
1962
2351
  ttlResolver;
@@ -1964,6 +2353,7 @@ var CacheStack = class extends EventEmitter {
1964
2353
  nextOperationId = 0;
1965
2354
  currentGeneration;
1966
2355
  isDisconnecting = false;
2356
+ reader;
1967
2357
  disconnectPromise;
1968
2358
  /**
1969
2359
  * Read-through cache get.
@@ -1976,51 +2366,9 @@ var CacheStack = class extends EventEmitter {
1976
2366
  const normalizedKey = this.qualifyKey(validateCacheKey(key));
1977
2367
  this.validateWriteOptions(options);
1978
2368
  await this.awaitStartup("get");
1979
- return this.getPrepared(normalizedKey, fetcher, options);
2369
+ return this.reader.getPrepared(normalizedKey, fetcher, options);
1980
2370
  });
1981
2371
  }
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
2372
  /**
2025
2373
  * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
2026
2374
  * Fetches and caches the value if not already present.
@@ -2171,7 +2519,7 @@ var CacheStack = class extends EventEmitter {
2171
2519
  const optionsSignature = serializeOptions(entry.options);
2172
2520
  const existing = pendingReads.get(entry.key);
2173
2521
  if (!existing) {
2174
- const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
2522
+ const promise = this.reader.getPrepared(entry.key, entry.fetch, entry.options);
2175
2523
  pendingReads.set(entry.key, {
2176
2524
  promise,
2177
2525
  fetch: entry.fetch,
@@ -2207,7 +2555,7 @@ var CacheStack = class extends EventEmitter {
2207
2555
  if (keys.length === 0) {
2208
2556
  break;
2209
2557
  }
2210
- const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
2558
+ const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.reader.readLayerEntry(layer, key)));
2211
2559
  for (let offset = 0; offset < values.length; offset += 1) {
2212
2560
  const key = keys[offset];
2213
2561
  const stored = values[offset];
@@ -2223,7 +2571,7 @@ var CacheStack = class extends EventEmitter {
2223
2571
  this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
2224
2572
  }
2225
2573
  await this.tagIndex.touch(key);
2226
- await this.backfill(key, stored, layerIndex - 1);
2574
+ await this.reader.backfill(key, stored, layerIndex - 1);
2227
2575
  resultsByKey.set(key, resolved.value);
2228
2576
  pending.delete(key);
2229
2577
  this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
@@ -2357,7 +2705,7 @@ var CacheStack = class extends EventEmitter {
2357
2705
  isLocal: Boolean(layer.isLocal),
2358
2706
  degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
2359
2707
  })),
2360
- backgroundRefreshes: this.backgroundRefreshes.size
2708
+ backgroundRefreshes: this.reader.activeRefreshCount
2361
2709
  };
2362
2710
  }
2363
2711
  resetMetrics() {
@@ -2477,11 +2825,9 @@ var CacheStack = class extends EventEmitter {
2477
2825
  await this.unsubscribeInvalidation?.();
2478
2826
  await this.flushWriteBehindQueue();
2479
2827
  await this.maintenance.waitForGenerationCleanup();
2480
- for (const key of this.backgroundRefreshAbort.keys()) {
2481
- this.backgroundRefreshAbort.set(key, true);
2482
- }
2828
+ this.reader.abortAllRefreshes();
2483
2829
  await Promise.allSettled(
2484
- [...this.backgroundRefreshes.values()].map((promise) => {
2830
+ this.reader.getAllRefreshPromises().map((promise) => {
2485
2831
  let timer;
2486
2832
  return Promise.race([
2487
2833
  promise,
@@ -2494,8 +2840,6 @@ var CacheStack = class extends EventEmitter {
2494
2840
  });
2495
2841
  })
2496
2842
  );
2497
- this.backgroundRefreshes.clear();
2498
- this.backgroundRefreshAbort.clear();
2499
2843
  this.maintenance.disposeWriteBehindTimer();
2500
2844
  this.fetchRateLimiter.dispose();
2501
2845
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
@@ -2511,116 +2855,6 @@ var CacheStack = class extends EventEmitter {
2511
2855
  await this.handleInvalidationMessage(message);
2512
2856
  });
2513
2857
  }
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
2858
  async storeEntry(key, kind, value, options) {
2625
2859
  const clearEpoch = this.maintenance.currentClearEpoch();
2626
2860
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
@@ -2667,87 +2901,6 @@ var CacheStack = class extends EventEmitter {
2667
2901
  });
2668
2902
  }
2669
2903
  }
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
2904
  resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
2752
2905
  return this.ttlResolver.resolveFreshTtl(
2753
2906
  key,
@@ -2763,55 +2916,6 @@ var CacheStack = class extends EventEmitter {
2763
2916
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
2764
2917
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
2765
2918
  }
2766
- shouldNegativeCache(options) {
2767
- return options?.negativeCache ?? this.options.negativeCaching ?? false;
2768
- }
2769
- scheduleBackgroundRefresh(key, fetcher, options) {
2770
- if (!shouldStartBackgroundRefresh({
2771
- isDisconnecting: this.isDisconnecting,
2772
- hasRefreshInFlight: this.backgroundRefreshes.has(key)
2773
- })) {
2774
- return;
2775
- }
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
- 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
2813
- };
2814
- }
2815
2919
  async deleteKeys(keys) {
2816
2920
  if (keys.length === 0) {
2817
2921
  return;
@@ -3057,29 +3161,14 @@ var CacheStack = class extends EventEmitter {
3057
3161
  await this.startup;
3058
3162
  this.assertActive(operation);
3059
3163
  }
3164
+ async readLayerEntry(layer, key) {
3165
+ return this.reader.readLayerEntry(layer, key);
3166
+ }
3167
+ scheduleBackgroundRefresh(key, fetcher, options) {
3168
+ this.reader.runScheduleBackgroundRefresh(key, fetcher, options);
3169
+ }
3060
3170
  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
- }
3171
+ return this.reader.runApplyFreshReadPolicies(key, hit, options, fetcher);
3083
3172
  }
3084
3173
  shouldSkipLayer(layer) {
3085
3174
  const degradedUntil = this.layerDegradedUntil.get(layer.name);
@@ -3121,9 +3210,6 @@ var CacheStack = class extends EventEmitter {
3121
3210
  }
3122
3211
  this.emitError("fetch", { key, error: this.formatError(error) });
3123
3212
  }
3124
- isNegativeStoredValue(stored) {
3125
- return isStoredValueEnvelope(stored) && stored.kind === "empty";
3126
- }
3127
3213
  emitError(operation, context) {
3128
3214
  this.logger.error?.(operation, context);
3129
3215
  if (this.listenerCount("error") > 0) {