layercache 1.3.2 → 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
@@ -1,11 +1,22 @@
1
1
  import {
2
- RedisTagIndex
3
- } from "./chunk-BQLL6IM5.js";
2
+ RedisTagIndex,
3
+ validateAdaptiveTtlOptions,
4
+ validateCacheKey,
5
+ validateCircuitBreakerOptions,
6
+ validateLayerNumberOption,
7
+ validateNonNegativeNumber,
8
+ validatePattern,
9
+ validatePositiveNumber,
10
+ validateRateLimitOptions,
11
+ validateTag,
12
+ validateTags,
13
+ validateTtlPolicy
14
+ } from "./chunk-BORDQ3LA.js";
4
15
  import {
5
16
  MemoryLayer,
6
17
  TagIndex,
7
18
  createHonoCacheMiddleware
8
- } from "./chunk-GJBKCFE6.js";
19
+ } from "./chunk-5RCAX2BQ.js";
9
20
  import {
10
21
  PatternMatcher,
11
22
  createStoredValueEnvelope,
@@ -715,6 +726,7 @@ var CacheStackLayerWriter = class {
715
726
  };
716
727
 
717
728
  // src/internal/CacheStackMaintenance.ts
729
+ var MAX_KEY_EPOCHS = 5e4;
718
730
  var CacheStackMaintenance = class {
719
731
  keyEpochs = /* @__PURE__ */ new Map();
720
732
  writeBehindQueue = [];
@@ -758,6 +770,7 @@ var CacheStackMaintenance = class {
758
770
  for (const key of keys) {
759
771
  this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
760
772
  }
773
+ this.pruneKeyEpochsIfNeeded();
761
774
  }
762
775
  isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
763
776
  if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
@@ -810,6 +823,16 @@ var CacheStackMaintenance = class {
810
823
  async waitForGenerationCleanup() {
811
824
  await this.generationCleanupPromise;
812
825
  }
826
+ pruneKeyEpochsIfNeeded() {
827
+ if (this.keyEpochs.size <= MAX_KEY_EPOCHS) {
828
+ return;
829
+ }
830
+ const sorted = [...this.keyEpochs.entries()].sort((a, b) => a[1] - b[1]);
831
+ const toDelete = Math.ceil(sorted.length * 0.1);
832
+ for (let i = 0; i < toDelete; i++) {
833
+ this.keyEpochs.delete(sorted[i][0]);
834
+ }
835
+ }
813
836
  };
814
837
 
815
838
  // src/internal/CacheStackRuntimePolicy.ts
@@ -848,17 +871,377 @@ function planFreshReadPolicies({
848
871
  };
849
872
  }
850
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
+
851
1234
  // src/internal/CacheStackSnapshotManager.ts
852
- import { randomBytes } from "crypto";
853
1235
  import { constants, promises as fs } from "fs";
854
- import path from "path";
855
1236
 
856
1237
  // src/internal/CacheSnapshotFile.ts
857
- function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
858
- const relative = path2.relative(realBaseDir, candidatePath);
859
- return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
1238
+ import { randomBytes } from "crypto";
1239
+ import { rename, unlink } from "fs/promises";
1240
+ function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
1241
+ const relative = path.relative(realBaseDir, candidatePath);
1242
+ return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
860
1243
  }
861
- async function findExistingAncestor(directory, fs3, path2) {
1244
+ async function findExistingAncestor(directory, fs3, path) {
862
1245
  let current = directory;
863
1246
  while (true) {
864
1247
  try {
@@ -869,7 +1252,7 @@ async function findExistingAncestor(directory, fs3, path2) {
869
1252
  throw error;
870
1253
  }
871
1254
  }
872
- const parent = path2.dirname(current);
1255
+ const parent = path.dirname(current);
873
1256
  if (parent === current) {
874
1257
  return current;
875
1258
  }
@@ -884,36 +1267,36 @@ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = p
884
1267
  throw new Error("filePath must not contain null bytes.");
885
1268
  }
886
1269
  const { promises: fs3 } = await import("fs");
887
- const path2 = await import("path");
888
- const resolved = path2.resolve(filePath);
889
- const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
1270
+ const path = await import("path");
1271
+ const resolved = path.resolve(filePath);
1272
+ const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
890
1273
  if (baseDir === false) {
891
1274
  return resolved;
892
1275
  }
893
1276
  await fs3.mkdir(baseDir, { recursive: true });
894
1277
  const realBaseDir = await fs3.realpath(baseDir);
895
- if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
1278
+ if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
896
1279
  throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
897
1280
  }
898
1281
  if (mode === "read") {
899
1282
  const realTarget = await fs3.realpath(resolved);
900
- if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
1283
+ if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
901
1284
  throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
902
1285
  }
903
1286
  return realTarget;
904
1287
  }
905
- const parentDir = path2.dirname(resolved);
906
- const existingAncestor = await findExistingAncestor(parentDir, fs3, path2);
1288
+ const parentDir = path.dirname(resolved);
1289
+ const existingAncestor = await findExistingAncestor(parentDir, fs3, path);
907
1290
  const realExistingAncestor = await fs3.realpath(existingAncestor);
908
- if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
1291
+ if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
909
1292
  throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
910
1293
  }
911
1294
  await fs3.mkdir(parentDir, { recursive: true });
912
1295
  const realParentDir = await fs3.realpath(parentDir);
913
- if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
1296
+ if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
914
1297
  throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
915
1298
  }
916
- const targetPath = path2.join(realParentDir, path2.basename(resolved));
1299
+ const targetPath = path.join(realParentDir, path.basename(resolved));
917
1300
  try {
918
1301
  const existing = await fs3.lstat(targetPath);
919
1302
  if (existing.isSymbolicLink()) {
@@ -948,6 +1331,17 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
948
1331
  }
949
1332
  return Buffer.concat(chunks).toString("utf8");
950
1333
  }
1334
+ function atomicWriteTempPath(targetPath) {
1335
+ return `${targetPath}.tmp-${randomBytes(8).toString("hex")}`;
1336
+ }
1337
+ async function commitAtomicWrite(tempPath, targetPath) {
1338
+ try {
1339
+ await rename(tempPath, targetPath);
1340
+ } catch (error) {
1341
+ await unlink(tempPath).catch(() => void 0);
1342
+ throw error;
1343
+ }
1344
+ }
951
1345
 
952
1346
  // src/internal/StructuredDataSanitizer.ts
953
1347
  var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
@@ -1026,10 +1420,7 @@ var CacheStackSnapshotManager = class {
1026
1420
  }
1027
1421
  async persistToFile(filePath, snapshotBaseDir, maxEntries) {
1028
1422
  const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
1029
- const tempPath = path.join(
1030
- path.dirname(targetPath),
1031
- `.layercache-${process.pid}-${Date.now()}-${randomBytes(8).toString("hex")}.tmp`
1032
- );
1423
+ const tempPath = atomicWriteTempPath(targetPath);
1033
1424
  let handle;
1034
1425
  try {
1035
1426
  handle = await fs.open(tempPath, "wx");
@@ -1044,7 +1435,7 @@ var CacheStackSnapshotManager = class {
1044
1435
  await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
1045
1436
  await openedHandle.close();
1046
1437
  handle = void 0;
1047
- await fs.rename(tempPath, targetPath);
1438
+ await commitAtomicWrite(tempPath, targetPath);
1048
1439
  } catch (error) {
1049
1440
  await handle?.close().catch(() => void 0);
1050
1441
  await fs.unlink(tempPath).catch(() => void 0);
@@ -1138,130 +1529,6 @@ var CacheStackSnapshotManager = class {
1138
1529
  }
1139
1530
  };
1140
1531
 
1141
- // src/internal/CacheStackValidation.ts
1142
- var MAX_CACHE_KEY_LENGTH = 1024;
1143
- var MAX_PATTERN_LENGTH = 1024;
1144
- var MAX_TAGS_PER_OPERATION = 128;
1145
- function validatePositiveNumber(name, value) {
1146
- if (value === void 0) {
1147
- return;
1148
- }
1149
- if (!Number.isFinite(value) || value <= 0) {
1150
- throw new Error(`${name} must be a positive finite number.`);
1151
- }
1152
- }
1153
- function validateNonNegativeNumber(name, value) {
1154
- if (!Number.isFinite(value) || value < 0) {
1155
- throw new Error(`${name} must be a non-negative finite number.`);
1156
- }
1157
- }
1158
- function validateLayerNumberOption(name, value) {
1159
- if (value === void 0) {
1160
- return;
1161
- }
1162
- if (typeof value === "number") {
1163
- validateNonNegativeNumber(name, value);
1164
- return;
1165
- }
1166
- for (const [layerName, layerValue] of Object.entries(value)) {
1167
- if (layerValue === void 0) {
1168
- continue;
1169
- }
1170
- validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
1171
- }
1172
- }
1173
- function validateRateLimitOptions(name, options) {
1174
- if (!options) {
1175
- return;
1176
- }
1177
- validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
1178
- validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
1179
- validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
1180
- if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
1181
- throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
1182
- }
1183
- if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
1184
- throw new Error(`${name}.bucketKey must not be empty.`);
1185
- }
1186
- }
1187
- function validateCacheKey(key) {
1188
- if (key.length === 0) {
1189
- throw new Error("Cache key must not be empty.");
1190
- }
1191
- if (key.length > MAX_CACHE_KEY_LENGTH) {
1192
- throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
1193
- }
1194
- if (/[\u0000-\u001F\u007F]/.test(key)) {
1195
- throw new Error("Cache key contains unsupported control characters.");
1196
- }
1197
- if (/[\uD800-\uDFFF]/.test(key)) {
1198
- throw new Error("Cache key contains unsupported surrogate code points.");
1199
- }
1200
- return key;
1201
- }
1202
- function validateTag(tag) {
1203
- if (tag.length === 0) {
1204
- throw new Error("Cache tag must not be empty.");
1205
- }
1206
- if (tag.length > MAX_CACHE_KEY_LENGTH) {
1207
- throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
1208
- }
1209
- if (/[\u0000-\u001F\u007F]/.test(tag)) {
1210
- throw new Error("Cache tag contains unsupported control characters.");
1211
- }
1212
- if (/[\uD800-\uDFFF]/.test(tag)) {
1213
- throw new Error("Cache tag contains unsupported surrogate code points.");
1214
- }
1215
- return tag;
1216
- }
1217
- function validateTags(tags) {
1218
- if (!tags) {
1219
- return;
1220
- }
1221
- if (tags.length > MAX_TAGS_PER_OPERATION) {
1222
- throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
1223
- }
1224
- for (const tag of tags) {
1225
- validateTag(tag);
1226
- }
1227
- }
1228
- function validatePattern(pattern) {
1229
- if (pattern.length === 0) {
1230
- throw new Error("Pattern must not be empty.");
1231
- }
1232
- if (pattern.length > MAX_PATTERN_LENGTH) {
1233
- throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
1234
- }
1235
- if (/[\u0000-\u001F\u007F]/.test(pattern)) {
1236
- throw new Error("Pattern contains unsupported control characters.");
1237
- }
1238
- }
1239
- function validateTtlPolicy(name, policy) {
1240
- if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
1241
- return;
1242
- }
1243
- if ("alignTo" in policy) {
1244
- validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
1245
- return;
1246
- }
1247
- throw new Error(`${name} is invalid.`);
1248
- }
1249
- function validateAdaptiveTtlOptions(options) {
1250
- if (!options || options === true) {
1251
- return;
1252
- }
1253
- validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
1254
- validateLayerNumberOption("adaptiveTtl.step", options.step);
1255
- validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
1256
- }
1257
- function validateCircuitBreakerOptions(options) {
1258
- if (!options) {
1259
- return;
1260
- }
1261
- validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
1262
- validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
1263
- }
1264
-
1265
1532
  // src/internal/CircuitBreakerManager.ts
1266
1533
  var CircuitBreakerManager = class {
1267
1534
  breakers = /* @__PURE__ */ new Map();
@@ -1359,6 +1626,7 @@ var CircuitBreakerManager = class {
1359
1626
 
1360
1627
  // src/internal/FetchRateLimiter.ts
1361
1628
  var MAX_BUCKETS = 1e4;
1629
+ var MAX_QUEUE_PER_BUCKET = 1e4;
1362
1630
  var FetchRateLimiter = class {
1363
1631
  buckets = /* @__PURE__ */ new Map();
1364
1632
  queuesByBucket = /* @__PURE__ */ new Map();
@@ -1367,6 +1635,7 @@ var FetchRateLimiter = class {
1367
1635
  nextFetcherBucketId = 0;
1368
1636
  drainTimer;
1369
1637
  isDisposed = false;
1638
+ rateLimitBypasses = 0;
1370
1639
  async schedule(options, context, task) {
1371
1640
  if (this.isDisposed) {
1372
1641
  throw new Error("FetchRateLimiter has been disposed.");
@@ -1381,6 +1650,11 @@ var FetchRateLimiter = class {
1381
1650
  return new Promise((resolve2, reject) => {
1382
1651
  const bucketKey = this.resolveBucketKey(normalized, context);
1383
1652
  const queue = this.queuesByBucket.get(bucketKey) ?? [];
1653
+ if (queue.length >= MAX_QUEUE_PER_BUCKET) {
1654
+ this.rateLimitBypasses += 1;
1655
+ task().then(resolve2, reject);
1656
+ return;
1657
+ }
1384
1658
  queue.push({
1385
1659
  bucketKey,
1386
1660
  options: normalized,
@@ -1685,7 +1959,13 @@ var MetricsCollector = class {
1685
1959
  };
1686
1960
 
1687
1961
  // src/internal/TtlResolver.ts
1962
+ import { randomBytes as randomBytes2 } from "crypto";
1688
1963
  var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
1964
+ var secureRandom = {
1965
+ value() {
1966
+ return randomBytes2(4).readUInt32BE(0) / 4294967296;
1967
+ }
1968
+ };
1689
1969
  var TtlResolver = class {
1690
1970
  accessProfiles = /* @__PURE__ */ new Map();
1691
1971
  maxProfileEntries;
@@ -1748,7 +2028,7 @@ var TtlResolver = class {
1748
2028
  if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
1749
2029
  return ttl;
1750
2030
  }
1751
- const delta = (Math.random() * 2 - 1) * jitter;
2031
+ const delta = (secureRandom.value() * 2 - 1) * jitter;
1752
2032
  return Math.max(1, Math.round(ttl + delta));
1753
2033
  }
1754
2034
  resolvePolicyTtl(key, value, policy) {
@@ -1895,10 +2175,6 @@ var CacheMissError = class extends Error {
1895
2175
  };
1896
2176
 
1897
2177
  // src/CacheStack.ts
1898
- var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
1899
- var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
1900
- var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1901
- var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1902
2178
  var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
1903
2179
  var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
1904
2180
  var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
@@ -2009,7 +2285,7 @@ var CacheStack = class extends EventEmitter {
2009
2285
  layers: this.layers,
2010
2286
  tagIndex: this.tagIndex,
2011
2287
  snapshotSerializer: this.snapshotSerializer,
2012
- readLayerEntry: this.readLayerEntry.bind(this),
2288
+ readLayerEntry: (layer, key) => this.reader.readLayerEntry(layer, key),
2013
2289
  shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2014
2290
  handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
2015
2291
  qualifyKey: this.qualifyKey.bind(this),
@@ -2017,6 +2293,41 @@ var CacheStack = class extends EventEmitter {
2017
2293
  validateCacheKey,
2018
2294
  formatError: this.formatError.bind(this)
2019
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
+ });
2020
2331
  this.initializeWriteBehind(options.writeBehind);
2021
2332
  this.startup = this.initialize();
2022
2333
  }
@@ -2035,8 +2346,6 @@ var CacheStack = class extends EventEmitter {
2035
2346
  invalidation;
2036
2347
  layerWriter;
2037
2348
  snapshots;
2038
- backgroundRefreshes = /* @__PURE__ */ new Map();
2039
- backgroundRefreshAbort = /* @__PURE__ */ new Map();
2040
2349
  layerDegradedUntil = /* @__PURE__ */ new Map();
2041
2350
  maintenance = new CacheStackMaintenance();
2042
2351
  ttlResolver;
@@ -2044,6 +2353,7 @@ var CacheStack = class extends EventEmitter {
2044
2353
  nextOperationId = 0;
2045
2354
  currentGeneration;
2046
2355
  isDisconnecting = false;
2356
+ reader;
2047
2357
  disconnectPromise;
2048
2358
  /**
2049
2359
  * Read-through cache get.
@@ -2056,51 +2366,9 @@ var CacheStack = class extends EventEmitter {
2056
2366
  const normalizedKey = this.qualifyKey(validateCacheKey(key));
2057
2367
  this.validateWriteOptions(options);
2058
2368
  await this.awaitStartup("get");
2059
- return this.getPrepared(normalizedKey, fetcher, options);
2369
+ return this.reader.getPrepared(normalizedKey, fetcher, options);
2060
2370
  });
2061
2371
  }
2062
- async getPrepared(normalizedKey, fetcher, options) {
2063
- const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
2064
- if (hit.found) {
2065
- this.ttlResolver.recordAccess(normalizedKey);
2066
- if (this.isNegativeStoredValue(hit.stored)) {
2067
- this.metricsCollector.increment("negativeCacheHits");
2068
- }
2069
- if (hit.state === "fresh") {
2070
- this.metricsCollector.increment("hits");
2071
- await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
2072
- return hit.value;
2073
- }
2074
- if (hit.state === "stale-while-revalidate") {
2075
- this.metricsCollector.increment("hits");
2076
- this.metricsCollector.increment("staleHits");
2077
- this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
2078
- if (fetcher) {
2079
- this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
2080
- }
2081
- return hit.value;
2082
- }
2083
- if (!fetcher) {
2084
- this.metricsCollector.increment("hits");
2085
- this.metricsCollector.increment("staleHits");
2086
- this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
2087
- return hit.value;
2088
- }
2089
- try {
2090
- return await this.fetchWithGuards(normalizedKey, fetcher, options);
2091
- } catch (error) {
2092
- this.metricsCollector.increment("staleHits");
2093
- this.metricsCollector.increment("refreshErrors");
2094
- this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
2095
- return hit.value;
2096
- }
2097
- }
2098
- this.metricsCollector.increment("misses");
2099
- if (!fetcher) {
2100
- return null;
2101
- }
2102
- return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
2103
- }
2104
2372
  /**
2105
2373
  * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
2106
2374
  * Fetches and caches the value if not already present.
@@ -2251,7 +2519,7 @@ var CacheStack = class extends EventEmitter {
2251
2519
  const optionsSignature = serializeOptions(entry.options);
2252
2520
  const existing = pendingReads.get(entry.key);
2253
2521
  if (!existing) {
2254
- const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
2522
+ const promise = this.reader.getPrepared(entry.key, entry.fetch, entry.options);
2255
2523
  pendingReads.set(entry.key, {
2256
2524
  promise,
2257
2525
  fetch: entry.fetch,
@@ -2287,7 +2555,7 @@ var CacheStack = class extends EventEmitter {
2287
2555
  if (keys.length === 0) {
2288
2556
  break;
2289
2557
  }
2290
- 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)));
2291
2559
  for (let offset = 0; offset < values.length; offset += 1) {
2292
2560
  const key = keys[offset];
2293
2561
  const stored = values[offset];
@@ -2303,7 +2571,7 @@ var CacheStack = class extends EventEmitter {
2303
2571
  this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
2304
2572
  }
2305
2573
  await this.tagIndex.touch(key);
2306
- await this.backfill(key, stored, layerIndex - 1);
2574
+ await this.reader.backfill(key, stored, layerIndex - 1);
2307
2575
  resultsByKey.set(key, resolved.value);
2308
2576
  pending.delete(key);
2309
2577
  this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
@@ -2437,7 +2705,7 @@ var CacheStack = class extends EventEmitter {
2437
2705
  isLocal: Boolean(layer.isLocal),
2438
2706
  degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
2439
2707
  })),
2440
- backgroundRefreshes: this.backgroundRefreshes.size
2708
+ backgroundRefreshes: this.reader.activeRefreshCount
2441
2709
  };
2442
2710
  }
2443
2711
  resetMetrics() {
@@ -2557,11 +2825,9 @@ var CacheStack = class extends EventEmitter {
2557
2825
  await this.unsubscribeInvalidation?.();
2558
2826
  await this.flushWriteBehindQueue();
2559
2827
  await this.maintenance.waitForGenerationCleanup();
2560
- for (const key of this.backgroundRefreshAbort.keys()) {
2561
- this.backgroundRefreshAbort.set(key, true);
2562
- }
2828
+ this.reader.abortAllRefreshes();
2563
2829
  await Promise.allSettled(
2564
- [...this.backgroundRefreshes.values()].map((promise) => {
2830
+ this.reader.getAllRefreshPromises().map((promise) => {
2565
2831
  let timer;
2566
2832
  return Promise.race([
2567
2833
  promise,
@@ -2574,8 +2840,6 @@ var CacheStack = class extends EventEmitter {
2574
2840
  });
2575
2841
  })
2576
2842
  );
2577
- this.backgroundRefreshes.clear();
2578
- this.backgroundRefreshAbort.clear();
2579
2843
  this.maintenance.disposeWriteBehindTimer();
2580
2844
  this.fetchRateLimiter.dispose();
2581
2845
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
@@ -2591,116 +2855,6 @@ var CacheStack = class extends EventEmitter {
2591
2855
  await this.handleInvalidationMessage(message);
2592
2856
  });
2593
2857
  }
2594
- async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
2595
- const fetchTask = async () => {
2596
- const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
2597
- if (shouldRecheckFreshLayers) {
2598
- const secondHit = await this.readFromLayers(key, options, "fresh-only");
2599
- if (secondHit.found) {
2600
- this.metricsCollector.increment("hits");
2601
- return secondHit.value;
2602
- }
2603
- }
2604
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
2605
- };
2606
- const singleFlightTask = async () => {
2607
- if (!this.options.singleFlightCoordinator) {
2608
- return fetchTask();
2609
- }
2610
- try {
2611
- return await this.options.singleFlightCoordinator.execute(
2612
- key,
2613
- this.resolveSingleFlightOptions(),
2614
- fetchTask,
2615
- () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
2616
- );
2617
- } catch (error) {
2618
- if (!this.isGracefulDegradationEnabled()) {
2619
- throw error;
2620
- }
2621
- this.metricsCollector.increment("degradedOperations");
2622
- this.logger.warn?.("single-flight-coordinator-degraded", { key, error: this.formatError(error) });
2623
- this.emitError("single-flight", { key, degraded: true, error: this.formatError(error) });
2624
- return fetchTask();
2625
- }
2626
- };
2627
- if (this.options.stampedePrevention === false) {
2628
- return singleFlightTask();
2629
- }
2630
- return this.stampedeGuard.execute(key, singleFlightTask);
2631
- }
2632
- async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2633
- const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
2634
- const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
2635
- const deadline = Date.now() + timeoutMs;
2636
- this.metricsCollector.increment("singleFlightWaits");
2637
- this.emit("stampede-dedupe", { key });
2638
- while (Date.now() < deadline) {
2639
- const hit = await this.readFromLayers(key, options, "fresh-only");
2640
- if (hit.found) {
2641
- this.metricsCollector.increment("hits");
2642
- return hit.value;
2643
- }
2644
- await this.sleep(pollIntervalMs);
2645
- }
2646
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
2647
- }
2648
- async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2649
- this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
2650
- this.metricsCollector.increment("fetches");
2651
- const fetchStart = Date.now();
2652
- let fetched;
2653
- try {
2654
- fetched = await this.fetchRateLimiter.schedule(
2655
- options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
2656
- { key, fetcher },
2657
- fetcher
2658
- );
2659
- this.circuitBreakerManager.recordSuccess(key);
2660
- this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
2661
- } catch (error) {
2662
- this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
2663
- throw error;
2664
- }
2665
- if (fetched === null || fetched === void 0) {
2666
- if (!this.shouldNegativeCache(options)) {
2667
- return null;
2668
- }
2669
- if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2670
- this.logger.debug?.("skip-negative-store-after-invalidation", {
2671
- key,
2672
- expectedClearEpoch,
2673
- clearEpoch: this.maintenance.currentClearEpoch(),
2674
- expectedKeyEpoch,
2675
- keyEpoch: this.maintenance.currentKeyEpoch(key)
2676
- });
2677
- return null;
2678
- }
2679
- await this.storeEntry(key, "empty", null, options);
2680
- return null;
2681
- }
2682
- if (options?.shouldCache) {
2683
- try {
2684
- if (!options.shouldCache(fetched)) {
2685
- return fetched;
2686
- }
2687
- } catch (error) {
2688
- this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
2689
- }
2690
- }
2691
- if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
2692
- this.logger.debug?.("skip-store-after-invalidation", {
2693
- key,
2694
- expectedClearEpoch,
2695
- clearEpoch: this.maintenance.currentClearEpoch(),
2696
- expectedKeyEpoch,
2697
- keyEpoch: this.maintenance.currentKeyEpoch(key)
2698
- });
2699
- return fetched;
2700
- }
2701
- await this.storeEntry(key, "value", fetched, options);
2702
- return fetched;
2703
- }
2704
2858
  async storeEntry(key, kind, value, options) {
2705
2859
  const clearEpoch = this.maintenance.currentClearEpoch();
2706
2860
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
@@ -2747,87 +2901,6 @@ var CacheStack = class extends EventEmitter {
2747
2901
  });
2748
2902
  }
2749
2903
  }
2750
- async readFromLayers(key, options, mode) {
2751
- let sawRetainableValue = false;
2752
- for (let index = 0; index < this.layers.length; index += 1) {
2753
- const layer = this.layers[index];
2754
- if (!layer) continue;
2755
- const readStart = performance.now();
2756
- const stored = await this.readLayerEntry(layer, key);
2757
- const readDuration = performance.now() - readStart;
2758
- this.metricsCollector.recordLatency(layer.name, readDuration);
2759
- if (stored === null) {
2760
- this.metricsCollector.incrementLayer("missesByLayer", layer.name);
2761
- continue;
2762
- }
2763
- const resolved = resolveStoredValue(stored);
2764
- if (resolved.state === "expired") {
2765
- await layer.delete(key);
2766
- continue;
2767
- }
2768
- sawRetainableValue = true;
2769
- if (mode === "fresh-only" && resolved.state !== "fresh") {
2770
- continue;
2771
- }
2772
- await this.tagIndex.touch(key);
2773
- await this.backfill(key, stored, index - 1, options);
2774
- this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
2775
- this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
2776
- this.emit("hit", { key, layer: layer.name, state: resolved.state });
2777
- return {
2778
- found: true,
2779
- value: resolved.value,
2780
- stored,
2781
- state: resolved.state,
2782
- layerIndex: index,
2783
- layerName: layer.name
2784
- };
2785
- }
2786
- if (!sawRetainableValue) {
2787
- await this.tagIndex.remove(key);
2788
- }
2789
- this.logger.debug?.("miss", { key, mode });
2790
- this.emit("miss", { key, mode });
2791
- return { found: false, value: null, stored: null, state: "miss" };
2792
- }
2793
- async readLayerEntry(layer, key) {
2794
- if (this.shouldSkipLayer(layer)) {
2795
- return null;
2796
- }
2797
- if (layer.getEntry) {
2798
- try {
2799
- return await layer.getEntry(key);
2800
- } catch (error) {
2801
- return this.handleLayerFailure(layer, "read", error);
2802
- }
2803
- }
2804
- try {
2805
- return await layer.get(key);
2806
- } catch (error) {
2807
- return this.handleLayerFailure(layer, "read", error);
2808
- }
2809
- }
2810
- async backfill(key, stored, upToIndex, options) {
2811
- if (upToIndex < 0) {
2812
- return;
2813
- }
2814
- for (let index = 0; index <= upToIndex; index += 1) {
2815
- const layer = this.layers[index];
2816
- if (!layer || this.shouldSkipLayer(layer)) {
2817
- continue;
2818
- }
2819
- const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
2820
- try {
2821
- await layer.set(key, stored, ttl);
2822
- } catch (error) {
2823
- await this.handleLayerFailure(layer, "backfill", error);
2824
- continue;
2825
- }
2826
- this.metricsCollector.increment("backfills");
2827
- this.logger.debug?.("backfill", { key, layer: layer.name });
2828
- this.emit("backfill", { key, layer: layer.name });
2829
- }
2830
- }
2831
2904
  resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
2832
2905
  return this.ttlResolver.resolveFreshTtl(
2833
2906
  key,
@@ -2843,55 +2916,6 @@ var CacheStack = class extends EventEmitter {
2843
2916
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
2844
2917
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
2845
2918
  }
2846
- shouldNegativeCache(options) {
2847
- return options?.negativeCache ?? this.options.negativeCaching ?? false;
2848
- }
2849
- scheduleBackgroundRefresh(key, fetcher, options) {
2850
- if (!shouldStartBackgroundRefresh({
2851
- isDisconnecting: this.isDisconnecting,
2852
- hasRefreshInFlight: this.backgroundRefreshes.has(key)
2853
- })) {
2854
- return;
2855
- }
2856
- const clearEpoch = this.maintenance.currentClearEpoch();
2857
- const keyEpoch = this.maintenance.currentKeyEpoch(key);
2858
- this.backgroundRefreshAbort.set(key, false);
2859
- const refresh = (async () => {
2860
- this.metricsCollector.increment("refreshes");
2861
- try {
2862
- if (this.backgroundRefreshAbort.get(key)) return;
2863
- await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
2864
- } catch (error) {
2865
- if (this.backgroundRefreshAbort.get(key)) return;
2866
- this.metricsCollector.increment("refreshErrors");
2867
- this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
2868
- } finally {
2869
- this.backgroundRefreshes.delete(key);
2870
- this.backgroundRefreshAbort.delete(key);
2871
- }
2872
- })();
2873
- this.backgroundRefreshes.set(key, refresh);
2874
- }
2875
- async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
2876
- const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
2877
- await this.fetchWithGuards(
2878
- key,
2879
- () => this.withTimeout(fetcher(), timeoutMs, () => {
2880
- return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
2881
- }),
2882
- options,
2883
- expectedClearEpoch,
2884
- expectedKeyEpoch
2885
- );
2886
- }
2887
- resolveSingleFlightOptions() {
2888
- return {
2889
- leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
2890
- waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
2891
- pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
2892
- renewIntervalMs: this.options.singleFlightRenewIntervalMs
2893
- };
2894
- }
2895
2919
  async deleteKeys(keys) {
2896
2920
  if (keys.length === 0) {
2897
2921
  return;
@@ -3137,32 +3161,22 @@ var CacheStack = class extends EventEmitter {
3137
3161
  await this.startup;
3138
3162
  this.assertActive(operation);
3139
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
+ }
3140
3170
  async applyFreshReadPolicies(key, hit, options, fetcher) {
3141
- const plan = planFreshReadPolicies({
3142
- stored: hit.stored,
3143
- hasFetcher: Boolean(fetcher),
3144
- slidingTtl: options?.slidingTtl ?? false,
3145
- refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
3146
- });
3147
- if (plan.refreshedStored) {
3148
- for (let index = 0; index <= hit.layerIndex; index += 1) {
3149
- const layer = this.layers[index];
3150
- if (!layer || this.shouldSkipLayer(layer)) {
3151
- continue;
3152
- }
3153
- try {
3154
- await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
3155
- } catch (error) {
3156
- await this.handleLayerFailure(layer, "sliding-ttl", error);
3157
- }
3158
- }
3159
- }
3160
- if (fetcher && plan.shouldScheduleBackgroundRefresh) {
3161
- this.scheduleBackgroundRefresh(key, fetcher, options);
3162
- }
3171
+ return this.reader.runApplyFreshReadPolicies(key, hit, options, fetcher);
3163
3172
  }
3164
3173
  shouldSkipLayer(layer) {
3165
- return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
3174
+ const degradedUntil = this.layerDegradedUntil.get(layer.name);
3175
+ const skip = shouldSkipLayer(degradedUntil);
3176
+ if (!skip && degradedUntil !== void 0) {
3177
+ this.layerDegradedUntil.delete(layer.name);
3178
+ }
3179
+ return skip;
3166
3180
  }
3167
3181
  async handleLayerFailure(layer, operation, error) {
3168
3182
  const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
@@ -3196,9 +3210,6 @@ var CacheStack = class extends EventEmitter {
3196
3210
  }
3197
3211
  this.emitError("fetch", { key, error: this.formatError(error) });
3198
3212
  }
3199
- isNegativeStoredValue(stored) {
3200
- return isStoredValueEnvelope(stored) && stored.kind === "empty";
3201
- }
3202
3213
  emitError(operation, context) {
3203
3214
  this.logger.error?.(operation, context);
3204
3215
  if (this.listenerCount("error") > 0) {
@@ -3964,12 +3975,12 @@ var RedisLayer = class {
3964
3975
  };
3965
3976
 
3966
3977
  // src/layers/DiskLayer.ts
3967
- import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
3978
+ import { createHash as createHash2, randomBytes as randomBytes4 } from "crypto";
3968
3979
  import { promises as fs2 } from "fs";
3969
3980
  import { join, resolve } from "path";
3970
3981
 
3971
3982
  // src/internal/PayloadProtection.ts
3972
- import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
3983
+ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
3973
3984
  var MAGIC_ENCRYPTED = Buffer.from("LCP1:");
3974
3985
  var MAGIC_SIGNED = Buffer.from("LCS1:");
3975
3986
  var ALGORITHM = "aes-256-gcm";
@@ -4033,7 +4044,7 @@ var PayloadProtection = class {
4033
4044
  }
4034
4045
  // ── Encryption (AES-256-GCM) ──────────────────────────────────────────
4035
4046
  encrypt(plaintext, key) {
4036
- const iv = randomBytes2(IV_LENGTH);
4047
+ const iv = randomBytes3(IV_LENGTH);
4037
4048
  const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
4038
4049
  const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4039
4050
  const authTag = cipher.getAuthTag();
@@ -4146,7 +4157,7 @@ var DiskLayer = class {
4146
4157
  const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
4147
4158
  const protectedPayload = this.protection.protect(raw);
4148
4159
  const targetPath = this.keyToPath(key);
4149
- const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${randomBytes3(8).toString("hex")}.tmp`;
4160
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${randomBytes4(8).toString("hex")}.tmp`;
4150
4161
  try {
4151
4162
  await fs2.writeFile(tempPath, protectedPayload);
4152
4163
  await fs2.rename(tempPath, targetPath);