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.cjs CHANGED
@@ -961,6 +961,7 @@ var CacheStackLayerWriter = class {
961
961
  };
962
962
 
963
963
  // src/internal/CacheStackMaintenance.ts
964
+ var MAX_KEY_EPOCHS = 5e4;
964
965
  var CacheStackMaintenance = class {
965
966
  keyEpochs = /* @__PURE__ */ new Map();
966
967
  writeBehindQueue = [];
@@ -1004,6 +1005,7 @@ var CacheStackMaintenance = class {
1004
1005
  for (const key of keys) {
1005
1006
  this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
1006
1007
  }
1008
+ this.pruneKeyEpochsIfNeeded();
1007
1009
  }
1008
1010
  isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
1009
1011
  if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
@@ -1056,6 +1058,16 @@ var CacheStackMaintenance = class {
1056
1058
  async waitForGenerationCleanup() {
1057
1059
  await this.generationCleanupPromise;
1058
1060
  }
1061
+ pruneKeyEpochsIfNeeded() {
1062
+ if (this.keyEpochs.size <= MAX_KEY_EPOCHS) {
1063
+ return;
1064
+ }
1065
+ const sorted = [...this.keyEpochs.entries()].sort((a, b) => a[1] - b[1]);
1066
+ const toDelete = Math.ceil(sorted.length * 0.1);
1067
+ for (let i = 0; i < toDelete; i++) {
1068
+ this.keyEpochs.delete(sorted[i][0]);
1069
+ }
1070
+ }
1059
1071
  };
1060
1072
 
1061
1073
  // src/internal/CacheStackRuntimePolicy.ts
@@ -1094,17 +1106,377 @@ function planFreshReadPolicies({
1094
1106
  };
1095
1107
  }
1096
1108
 
1109
+ // src/internal/CacheStackReader.ts
1110
+ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
1111
+ var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
1112
+ var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
1113
+ var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
1114
+ var CacheStackReader = class {
1115
+ constructor(options) {
1116
+ this.options = options;
1117
+ }
1118
+ options;
1119
+ backgroundRefreshes = /* @__PURE__ */ new Map();
1120
+ backgroundRefreshAbort = /* @__PURE__ */ new Map();
1121
+ get activeRefreshCount() {
1122
+ return this.backgroundRefreshes.size;
1123
+ }
1124
+ async getPrepared(normalizedKey, fetcher, options) {
1125
+ const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
1126
+ if (hit.found) {
1127
+ this.options.ttlResolver.recordAccess(normalizedKey);
1128
+ if (this.isNegativeStoredValue(hit.stored)) {
1129
+ this.options.metricsCollector.increment("negativeCacheHits");
1130
+ }
1131
+ if (hit.state === "fresh") {
1132
+ this.options.metricsCollector.increment("hits");
1133
+ await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
1134
+ return hit.value;
1135
+ }
1136
+ if (hit.state === "stale-while-revalidate") {
1137
+ this.options.metricsCollector.increment("hits");
1138
+ this.options.metricsCollector.increment("staleHits");
1139
+ this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
1140
+ if (fetcher) {
1141
+ this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
1142
+ }
1143
+ return hit.value;
1144
+ }
1145
+ if (!fetcher) {
1146
+ this.options.metricsCollector.increment("hits");
1147
+ this.options.metricsCollector.increment("staleHits");
1148
+ this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
1149
+ return hit.value;
1150
+ }
1151
+ try {
1152
+ return await this.fetchWithGuards(normalizedKey, fetcher, options);
1153
+ } catch (error) {
1154
+ this.options.metricsCollector.increment("staleHits");
1155
+ this.options.metricsCollector.increment("refreshErrors");
1156
+ this.options.logger.debug?.("stale-if-error", {
1157
+ key: normalizedKey,
1158
+ error: this.options.formatError(error)
1159
+ });
1160
+ return hit.value;
1161
+ }
1162
+ }
1163
+ this.options.metricsCollector.increment("misses");
1164
+ if (!fetcher) {
1165
+ return null;
1166
+ }
1167
+ return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
1168
+ }
1169
+ async readLayerEntry(layer, key) {
1170
+ if (this.options.shouldSkipLayer(layer)) {
1171
+ return null;
1172
+ }
1173
+ if (layer.getEntry) {
1174
+ try {
1175
+ return await layer.getEntry(key);
1176
+ } catch (error) {
1177
+ return this.options.handleLayerFailure(layer, "read", error);
1178
+ }
1179
+ }
1180
+ try {
1181
+ return await layer.get(key);
1182
+ } catch (error) {
1183
+ return this.options.handleLayerFailure(layer, "read", error);
1184
+ }
1185
+ }
1186
+ async backfill(key, stored, upToIndex, options) {
1187
+ if (upToIndex < 0) {
1188
+ return;
1189
+ }
1190
+ for (let index = 0; index <= upToIndex; index += 1) {
1191
+ const layer = this.options.layers[index];
1192
+ if (!layer || this.options.shouldSkipLayer(layer)) {
1193
+ continue;
1194
+ }
1195
+ const ttl = remainingStoredTtlSeconds(stored) ?? this.options.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
1196
+ try {
1197
+ await layer.set(key, stored, ttl);
1198
+ } catch (error) {
1199
+ await this.options.handleLayerFailure(layer, "backfill", error);
1200
+ continue;
1201
+ }
1202
+ this.options.metricsCollector.increment("backfills");
1203
+ this.options.logger.debug?.("backfill", { key, layer: layer.name });
1204
+ this.options.emit("backfill", { key, layer: layer.name });
1205
+ }
1206
+ }
1207
+ abortAllRefreshes() {
1208
+ for (const key of this.backgroundRefreshAbort.keys()) {
1209
+ this.backgroundRefreshAbort.set(key, true);
1210
+ }
1211
+ }
1212
+ getAllRefreshPromises() {
1213
+ return [...this.backgroundRefreshes.values()];
1214
+ }
1215
+ async readFromLayers(key, options, mode) {
1216
+ let sawRetainableValue = false;
1217
+ for (let index = 0; index < this.options.layers.length; index += 1) {
1218
+ const layer = this.options.layers[index];
1219
+ if (!layer) continue;
1220
+ const readStart = performance.now();
1221
+ const stored = await this.readLayerEntry(layer, key);
1222
+ const readDuration = performance.now() - readStart;
1223
+ this.options.metricsCollector.recordLatency(layer.name, readDuration);
1224
+ if (stored === null) {
1225
+ this.options.metricsCollector.incrementLayer("missesByLayer", layer.name);
1226
+ continue;
1227
+ }
1228
+ const resolved = resolveStoredValue(stored);
1229
+ if (resolved.state === "expired") {
1230
+ await layer.delete(key);
1231
+ continue;
1232
+ }
1233
+ sawRetainableValue = true;
1234
+ if (mode === "fresh-only" && resolved.state !== "fresh") {
1235
+ continue;
1236
+ }
1237
+ await this.options.tagIndex.touch(key);
1238
+ await this.backfill(key, stored, index - 1, options);
1239
+ this.options.metricsCollector.incrementLayer("hitsByLayer", layer.name);
1240
+ this.options.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
1241
+ this.options.emit("hit", {
1242
+ key,
1243
+ layer: layer.name,
1244
+ state: resolved.state
1245
+ });
1246
+ return {
1247
+ found: true,
1248
+ value: resolved.value,
1249
+ stored,
1250
+ state: resolved.state,
1251
+ layerIndex: index,
1252
+ layerName: layer.name
1253
+ };
1254
+ }
1255
+ if (!sawRetainableValue) {
1256
+ await this.options.tagIndex.remove(key);
1257
+ }
1258
+ this.options.logger.debug?.("miss", { key, mode });
1259
+ this.options.emit("miss", { key, mode });
1260
+ return { found: false, value: null, stored: null, state: "miss" };
1261
+ }
1262
+ async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
1263
+ const fetchTask = async () => {
1264
+ const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
1265
+ if (shouldRecheckFreshLayers) {
1266
+ const secondHit = await this.readFromLayers(key, options, "fresh-only");
1267
+ if (secondHit.found) {
1268
+ this.options.metricsCollector.increment("hits");
1269
+ return secondHit.value;
1270
+ }
1271
+ }
1272
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
1273
+ };
1274
+ const singleFlightTask = async () => {
1275
+ if (!this.options.singleFlightCoordinator) {
1276
+ return fetchTask();
1277
+ }
1278
+ try {
1279
+ return await this.options.singleFlightCoordinator.execute(
1280
+ key,
1281
+ this.resolveSingleFlightOptions(),
1282
+ fetchTask,
1283
+ () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
1284
+ );
1285
+ } catch (error) {
1286
+ if (!this.options.isGracefulDegradationEnabled()) {
1287
+ throw error;
1288
+ }
1289
+ this.options.metricsCollector.increment("degradedOperations");
1290
+ this.options.logger.warn?.("single-flight-coordinator-degraded", {
1291
+ key,
1292
+ error: this.options.formatError(error)
1293
+ });
1294
+ this.options.emitError("single-flight", {
1295
+ key,
1296
+ degraded: true,
1297
+ error: this.options.formatError(error)
1298
+ });
1299
+ return fetchTask();
1300
+ }
1301
+ };
1302
+ if (this.options.stampedePrevention === false) {
1303
+ return singleFlightTask();
1304
+ }
1305
+ return this.options.stampedeGuard.execute(key, singleFlightTask);
1306
+ }
1307
+ async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
1308
+ const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
1309
+ const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
1310
+ const deadline = Date.now() + timeoutMs;
1311
+ this.options.metricsCollector.increment("singleFlightWaits");
1312
+ this.options.emit("stampede-dedupe", { key });
1313
+ while (Date.now() < deadline) {
1314
+ const hit = await this.readFromLayers(key, options, "fresh-only");
1315
+ if (hit.found) {
1316
+ this.options.metricsCollector.increment("hits");
1317
+ return hit.value;
1318
+ }
1319
+ await this.options.sleep(pollIntervalMs);
1320
+ }
1321
+ return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
1322
+ }
1323
+ async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
1324
+ this.options.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
1325
+ this.options.metricsCollector.increment("fetches");
1326
+ const fetchStart = Date.now();
1327
+ let fetched;
1328
+ try {
1329
+ fetched = await this.options.fetchRateLimiter.schedule(
1330
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1331
+ { key, fetcher },
1332
+ fetcher
1333
+ );
1334
+ this.options.circuitBreakerManager.recordSuccess(key);
1335
+ this.options.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1336
+ } catch (error) {
1337
+ this.options.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
1338
+ throw error;
1339
+ }
1340
+ if (fetched === null || fetched === void 0) {
1341
+ if (!this.shouldNegativeCache(options)) {
1342
+ return null;
1343
+ }
1344
+ if (this.options.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
1345
+ this.options.logger.debug?.("skip-negative-store-after-invalidation", {
1346
+ key,
1347
+ expectedClearEpoch,
1348
+ clearEpoch: this.options.maintenance.currentClearEpoch(),
1349
+ expectedKeyEpoch,
1350
+ keyEpoch: this.options.maintenance.currentKeyEpoch(key)
1351
+ });
1352
+ return null;
1353
+ }
1354
+ await this.options.storeEntry(key, "empty", null, options);
1355
+ return null;
1356
+ }
1357
+ if (options?.shouldCache) {
1358
+ try {
1359
+ if (!options.shouldCache(fetched)) {
1360
+ return fetched;
1361
+ }
1362
+ } catch (error) {
1363
+ this.options.logger.warn?.("shouldCache-error", {
1364
+ key,
1365
+ error: this.options.formatError(error)
1366
+ });
1367
+ }
1368
+ }
1369
+ if (this.options.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
1370
+ this.options.logger.debug?.("skip-store-after-invalidation", {
1371
+ key,
1372
+ expectedClearEpoch,
1373
+ clearEpoch: this.options.maintenance.currentClearEpoch(),
1374
+ expectedKeyEpoch,
1375
+ keyEpoch: this.options.maintenance.currentKeyEpoch(key)
1376
+ });
1377
+ return fetched;
1378
+ }
1379
+ await this.options.storeEntry(key, "value", fetched, options);
1380
+ return fetched;
1381
+ }
1382
+ runScheduleBackgroundRefresh(key, fetcher, options) {
1383
+ this.scheduleBackgroundRefresh(key, fetcher, options);
1384
+ }
1385
+ scheduleBackgroundRefresh(key, fetcher, options) {
1386
+ if (!shouldStartBackgroundRefresh({
1387
+ isDisconnecting: this.options.isDisconnecting(),
1388
+ hasRefreshInFlight: this.backgroundRefreshes.has(key)
1389
+ })) {
1390
+ return;
1391
+ }
1392
+ const clearEpoch = this.options.maintenance.currentClearEpoch();
1393
+ const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
1394
+ this.backgroundRefreshAbort.set(key, false);
1395
+ const refresh = (async () => {
1396
+ this.options.metricsCollector.increment("refreshes");
1397
+ try {
1398
+ if (this.backgroundRefreshAbort.get(key)) return;
1399
+ await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
1400
+ } catch (error) {
1401
+ if (this.backgroundRefreshAbort.get(key)) return;
1402
+ this.options.metricsCollector.increment("refreshErrors");
1403
+ this.options.logger.warn?.("background-refresh-error", {
1404
+ key,
1405
+ error: this.options.formatError(error)
1406
+ });
1407
+ } finally {
1408
+ this.backgroundRefreshes.delete(key);
1409
+ this.backgroundRefreshAbort.delete(key);
1410
+ }
1411
+ })();
1412
+ this.backgroundRefreshes.set(key, refresh);
1413
+ }
1414
+ async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
1415
+ const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
1416
+ await this.fetchWithGuards(
1417
+ key,
1418
+ () => this.options.withTimeout(fetcher(), timeoutMs, () => {
1419
+ return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
1420
+ }),
1421
+ options,
1422
+ expectedClearEpoch,
1423
+ expectedKeyEpoch
1424
+ );
1425
+ }
1426
+ async runApplyFreshReadPolicies(key, hit, options, fetcher) {
1427
+ return this.applyFreshReadPolicies(key, hit, options, fetcher);
1428
+ }
1429
+ async applyFreshReadPolicies(key, hit, options, fetcher) {
1430
+ const plan = planFreshReadPolicies({
1431
+ stored: hit.stored,
1432
+ hasFetcher: Boolean(fetcher),
1433
+ slidingTtl: options?.slidingTtl ?? false,
1434
+ refreshAheadSeconds: this.options.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
1435
+ });
1436
+ if (plan.refreshedStored) {
1437
+ for (let index = 0; index <= hit.layerIndex; index += 1) {
1438
+ const layer = this.options.layers[index];
1439
+ if (!layer || this.options.shouldSkipLayer(layer)) {
1440
+ continue;
1441
+ }
1442
+ try {
1443
+ await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
1444
+ } catch (error) {
1445
+ await this.options.handleLayerFailure(layer, "sliding-ttl", error);
1446
+ }
1447
+ }
1448
+ }
1449
+ if (fetcher && plan.shouldScheduleBackgroundRefresh) {
1450
+ this.options.scheduleBackgroundRefreshDispatch(key, fetcher, options);
1451
+ }
1452
+ }
1453
+ resolveSingleFlightOptions() {
1454
+ return {
1455
+ leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
1456
+ waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
1457
+ pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
1458
+ renewIntervalMs: this.options.singleFlightRenewIntervalMs
1459
+ };
1460
+ }
1461
+ shouldNegativeCache(options) {
1462
+ return options?.negativeCache ?? this.options.negativeCaching ?? false;
1463
+ }
1464
+ isNegativeStoredValue(stored) {
1465
+ return isStoredValueEnvelope(stored) && stored.kind === "empty";
1466
+ }
1467
+ };
1468
+
1097
1469
  // src/internal/CacheStackSnapshotManager.ts
1098
- var import_node_crypto = require("crypto");
1099
1470
  var import_node_fs = require("fs");
1100
- var import_node_path = __toESM(require("path"), 1);
1101
1471
 
1102
1472
  // src/internal/CacheSnapshotFile.ts
1103
- function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
1104
- const relative = path2.relative(realBaseDir, candidatePath);
1105
- return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
1473
+ var import_node_crypto = require("crypto");
1474
+ var import_promises = require("fs/promises");
1475
+ function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
1476
+ const relative = path.relative(realBaseDir, candidatePath);
1477
+ return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
1106
1478
  }
1107
- async function findExistingAncestor(directory, fs3, path2) {
1479
+ async function findExistingAncestor(directory, fs3, path) {
1108
1480
  let current = directory;
1109
1481
  while (true) {
1110
1482
  try {
@@ -1115,7 +1487,7 @@ async function findExistingAncestor(directory, fs3, path2) {
1115
1487
  throw error;
1116
1488
  }
1117
1489
  }
1118
- const parent = path2.dirname(current);
1490
+ const parent = path.dirname(current);
1119
1491
  if (parent === current) {
1120
1492
  return current;
1121
1493
  }
@@ -1130,36 +1502,36 @@ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = p
1130
1502
  throw new Error("filePath must not contain null bytes.");
1131
1503
  }
1132
1504
  const { promises: fs3 } = await import("fs");
1133
- const path2 = await import("path");
1134
- const resolved = path2.resolve(filePath);
1135
- const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
1505
+ const path = await import("path");
1506
+ const resolved = path.resolve(filePath);
1507
+ const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
1136
1508
  if (baseDir === false) {
1137
1509
  return resolved;
1138
1510
  }
1139
1511
  await fs3.mkdir(baseDir, { recursive: true });
1140
1512
  const realBaseDir = await fs3.realpath(baseDir);
1141
- if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
1513
+ if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
1142
1514
  throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1143
1515
  }
1144
1516
  if (mode === "read") {
1145
1517
  const realTarget = await fs3.realpath(resolved);
1146
- if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
1518
+ if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
1147
1519
  throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1148
1520
  }
1149
1521
  return realTarget;
1150
1522
  }
1151
- const parentDir = path2.dirname(resolved);
1152
- const existingAncestor = await findExistingAncestor(parentDir, fs3, path2);
1523
+ const parentDir = path.dirname(resolved);
1524
+ const existingAncestor = await findExistingAncestor(parentDir, fs3, path);
1153
1525
  const realExistingAncestor = await fs3.realpath(existingAncestor);
1154
- if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
1526
+ if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
1155
1527
  throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1156
1528
  }
1157
1529
  await fs3.mkdir(parentDir, { recursive: true });
1158
1530
  const realParentDir = await fs3.realpath(parentDir);
1159
- if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
1531
+ if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
1160
1532
  throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1161
1533
  }
1162
- const targetPath = path2.join(realParentDir, path2.basename(resolved));
1534
+ const targetPath = path.join(realParentDir, path.basename(resolved));
1163
1535
  try {
1164
1536
  const existing = await fs3.lstat(targetPath);
1165
1537
  if (existing.isSymbolicLink()) {
@@ -1194,6 +1566,17 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
1194
1566
  }
1195
1567
  return Buffer.concat(chunks).toString("utf8");
1196
1568
  }
1569
+ function atomicWriteTempPath(targetPath) {
1570
+ return `${targetPath}.tmp-${(0, import_node_crypto.randomBytes)(8).toString("hex")}`;
1571
+ }
1572
+ async function commitAtomicWrite(tempPath, targetPath) {
1573
+ try {
1574
+ await (0, import_promises.rename)(tempPath, targetPath);
1575
+ } catch (error) {
1576
+ await (0, import_promises.unlink)(tempPath).catch(() => void 0);
1577
+ throw error;
1578
+ }
1579
+ }
1197
1580
 
1198
1581
  // src/internal/StructuredDataSanitizer.ts
1199
1582
  var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
@@ -1272,10 +1655,7 @@ var CacheStackSnapshotManager = class {
1272
1655
  }
1273
1656
  async persistToFile(filePath, snapshotBaseDir, maxEntries) {
1274
1657
  const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
1275
- const tempPath = import_node_path.default.join(
1276
- import_node_path.default.dirname(targetPath),
1277
- `.layercache-${process.pid}-${Date.now()}-${(0, import_node_crypto.randomBytes)(8).toString("hex")}.tmp`
1278
- );
1658
+ const tempPath = atomicWriteTempPath(targetPath);
1279
1659
  let handle;
1280
1660
  try {
1281
1661
  handle = await import_node_fs.promises.open(tempPath, "wx");
@@ -1290,7 +1670,7 @@ var CacheStackSnapshotManager = class {
1290
1670
  await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
1291
1671
  await openedHandle.close();
1292
1672
  handle = void 0;
1293
- await import_node_fs.promises.rename(tempPath, targetPath);
1673
+ await commitAtomicWrite(tempPath, targetPath);
1294
1674
  } catch (error) {
1295
1675
  await handle?.close().catch(() => void 0);
1296
1676
  await import_node_fs.promises.unlink(tempPath).catch(() => void 0);
@@ -1605,6 +1985,7 @@ var CircuitBreakerManager = class {
1605
1985
 
1606
1986
  // src/internal/FetchRateLimiter.ts
1607
1987
  var MAX_BUCKETS = 1e4;
1988
+ var MAX_QUEUE_PER_BUCKET = 1e4;
1608
1989
  var FetchRateLimiter = class {
1609
1990
  buckets = /* @__PURE__ */ new Map();
1610
1991
  queuesByBucket = /* @__PURE__ */ new Map();
@@ -1613,6 +1994,7 @@ var FetchRateLimiter = class {
1613
1994
  nextFetcherBucketId = 0;
1614
1995
  drainTimer;
1615
1996
  isDisposed = false;
1997
+ rateLimitBypasses = 0;
1616
1998
  async schedule(options, context, task) {
1617
1999
  if (this.isDisposed) {
1618
2000
  throw new Error("FetchRateLimiter has been disposed.");
@@ -1627,6 +2009,11 @@ var FetchRateLimiter = class {
1627
2009
  return new Promise((resolve2, reject) => {
1628
2010
  const bucketKey = this.resolveBucketKey(normalized, context);
1629
2011
  const queue = this.queuesByBucket.get(bucketKey) ?? [];
2012
+ if (queue.length >= MAX_QUEUE_PER_BUCKET) {
2013
+ this.rateLimitBypasses += 1;
2014
+ task().then(resolve2, reject);
2015
+ return;
2016
+ }
1630
2017
  queue.push({
1631
2018
  bucketKey,
1632
2019
  options: normalized,
@@ -1931,7 +2318,13 @@ var MetricsCollector = class {
1931
2318
  };
1932
2319
 
1933
2320
  // src/internal/TtlResolver.ts
2321
+ var import_node_crypto2 = require("crypto");
1934
2322
  var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
2323
+ var secureRandom = {
2324
+ value() {
2325
+ return (0, import_node_crypto2.randomBytes)(4).readUInt32BE(0) / 4294967296;
2326
+ }
2327
+ };
1935
2328
  var TtlResolver = class {
1936
2329
  accessProfiles = /* @__PURE__ */ new Map();
1937
2330
  maxProfileEntries;
@@ -1994,7 +2387,7 @@ var TtlResolver = class {
1994
2387
  if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
1995
2388
  return ttl;
1996
2389
  }
1997
- const delta = (Math.random() * 2 - 1) * jitter;
2390
+ const delta = (secureRandom.value() * 2 - 1) * jitter;
1998
2391
  return Math.max(1, Math.round(ttl + delta));
1999
2392
  }
2000
2393
  resolvePolicyTtl(key, value, policy) {
@@ -2046,7 +2439,7 @@ var MAX_PATTERN_RECURSION_DEPTH = 500;
2046
2439
  var TagIndex = class {
2047
2440
  tagToKeys = /* @__PURE__ */ new Map();
2048
2441
  keyToTags = /* @__PURE__ */ new Map();
2049
- knownKeys = /* @__PURE__ */ new Set();
2442
+ knownKeys = /* @__PURE__ */ new Map();
2050
2443
  maxKnownKeys;
2051
2444
  nextNodeId = 1;
2052
2445
  root = this.createTrieNode();
@@ -2134,10 +2527,11 @@ var TagIndex = class {
2134
2527
  };
2135
2528
  }
2136
2529
  insertKnownKey(key) {
2137
- if (this.knownKeys.has(key)) {
2530
+ const isNew = !this.knownKeys.has(key);
2531
+ this.knownKeys.set(key, Date.now());
2532
+ if (!isNew) {
2138
2533
  return;
2139
2534
  }
2140
- this.knownKeys.add(key);
2141
2535
  let node = this.root;
2142
2536
  for (const character of key) {
2143
2537
  let child = node.children.get(character);
@@ -2232,14 +2626,13 @@ var TagIndex = class {
2232
2626
  if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
2233
2627
  return;
2234
2628
  }
2629
+ const sorted = [...this.knownKeys.entries()].sort((a, b) => a[1] - b[1]);
2235
2630
  const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
2236
- let removed = 0;
2237
- for (const key of this.knownKeys) {
2238
- if (removed >= toRemove) {
2239
- break;
2631
+ for (let i = 0; i < toRemove && i < sorted.length; i += 1) {
2632
+ const entry = sorted[i];
2633
+ if (entry) {
2634
+ this.removeKey(entry[0]);
2240
2635
  }
2241
- this.removeKey(key);
2242
- removed += 1;
2243
2636
  }
2244
2637
  }
2245
2638
  removeKey(key) {
@@ -2264,19 +2657,19 @@ var TagIndex = class {
2264
2657
  if (!this.knownKeys.delete(key)) {
2265
2658
  return;
2266
2659
  }
2267
- const path2 = [];
2660
+ const path = [];
2268
2661
  let node = this.root;
2269
2662
  for (const character of key) {
2270
2663
  const child = node.children.get(character);
2271
2664
  if (!child) {
2272
2665
  return;
2273
2666
  }
2274
- path2.push([node, character]);
2667
+ path.push([node, character]);
2275
2668
  node = child;
2276
2669
  }
2277
2670
  node.terminal = false;
2278
- for (let index = path2.length - 1; index >= 0; index -= 1) {
2279
- const entry = path2[index];
2671
+ for (let index = path.length - 1; index >= 0; index -= 1) {
2672
+ const entry = path[index];
2280
2673
  if (!entry) {
2281
2674
  continue;
2282
2675
  }
@@ -2390,10 +2783,6 @@ var CacheMissError = class extends Error {
2390
2783
  };
2391
2784
 
2392
2785
  // src/CacheStack.ts
2393
- var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
2394
- var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
2395
- var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
2396
- var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
2397
2786
  var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
2398
2787
  var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
2399
2788
  var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
@@ -2503,14 +2892,49 @@ var CacheStack = class extends import_node_events.EventEmitter {
2503
2892
  this.snapshots = new CacheStackSnapshotManager({
2504
2893
  layers: this.layers,
2505
2894
  tagIndex: this.tagIndex,
2506
- snapshotSerializer: this.snapshotSerializer,
2507
- readLayerEntry: this.readLayerEntry.bind(this),
2895
+ snapshotSerializer: this.snapshotSerializer,
2896
+ readLayerEntry: (layer, key) => this.reader.readLayerEntry(layer, key),
2897
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2898
+ handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
2899
+ qualifyKey: this.qualifyKey.bind(this),
2900
+ stripQualifiedKey: this.stripQualifiedKey.bind(this),
2901
+ validateCacheKey,
2902
+ formatError: this.formatError.bind(this)
2903
+ });
2904
+ this.reader = new CacheStackReader({
2905
+ layers: this.layers,
2906
+ metricsCollector: this.metricsCollector,
2907
+ maintenance: this.maintenance,
2908
+ tagIndex: this.tagIndex,
2909
+ circuitBreakerManager: this.circuitBreakerManager,
2910
+ fetchRateLimiter: this.fetchRateLimiter,
2911
+ stampedeGuard: this.stampedeGuard,
2912
+ ttlResolver: this.ttlResolver,
2913
+ logger: this.logger,
2508
2914
  shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2509
2915
  handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
2510
- qualifyKey: this.qualifyKey.bind(this),
2511
- stripQualifiedKey: this.stripQualifiedKey.bind(this),
2512
- validateCacheKey,
2513
- formatError: this.formatError.bind(this)
2916
+ emit: (event, data) => this.emit(event, data),
2917
+ emitError: (operation, context) => this.emitError(operation, context),
2918
+ formatError: (error) => this.formatError(error),
2919
+ storeEntry: (key, kind, value, options2) => this.storeEntry(key, kind, value, options2),
2920
+ recordCircuitFailure: (key, options2, error) => this.recordCircuitFailure(key, options2, error),
2921
+ resolveLayerSeconds: (layerName, override, globalDefault, fallback) => this.resolveLayerSeconds(layerName, override, globalDefault, fallback),
2922
+ sleep: (ms) => this.sleep(ms),
2923
+ withTimeout: (promise, ms, createError) => this.withTimeout(promise, ms, createError),
2924
+ isDisconnecting: () => this.isDisconnecting,
2925
+ isGracefulDegradationEnabled: () => this.isGracefulDegradationEnabled(),
2926
+ scheduleBackgroundRefreshDispatch: (key, fetcher, options2) => this.scheduleBackgroundRefresh(key, fetcher, options2),
2927
+ stampedePrevention: options.stampedePrevention,
2928
+ singleFlightCoordinator: options.singleFlightCoordinator,
2929
+ singleFlightLeaseMs: options.singleFlightLeaseMs,
2930
+ singleFlightTimeoutMs: options.singleFlightTimeoutMs,
2931
+ singleFlightPollMs: options.singleFlightPollMs,
2932
+ singleFlightRenewIntervalMs: options.singleFlightRenewIntervalMs,
2933
+ backgroundRefreshTimeoutMs: options.backgroundRefreshTimeoutMs,
2934
+ negativeCaching: options.negativeCaching,
2935
+ refreshAhead: options.refreshAhead,
2936
+ circuitBreaker: options.circuitBreaker,
2937
+ fetcherRateLimit: options.fetcherRateLimit
2514
2938
  });
2515
2939
  this.initializeWriteBehind(options.writeBehind);
2516
2940
  this.startup = this.initialize();
@@ -2530,8 +2954,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
2530
2954
  invalidation;
2531
2955
  layerWriter;
2532
2956
  snapshots;
2533
- backgroundRefreshes = /* @__PURE__ */ new Map();
2534
- backgroundRefreshAbort = /* @__PURE__ */ new Map();
2535
2957
  layerDegradedUntil = /* @__PURE__ */ new Map();
2536
2958
  maintenance = new CacheStackMaintenance();
2537
2959
  ttlResolver;
@@ -2539,6 +2961,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2539
2961
  nextOperationId = 0;
2540
2962
  currentGeneration;
2541
2963
  isDisconnecting = false;
2964
+ reader;
2542
2965
  disconnectPromise;
2543
2966
  /**
2544
2967
  * Read-through cache get.
@@ -2551,51 +2974,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
2551
2974
  const normalizedKey = this.qualifyKey(validateCacheKey(key));
2552
2975
  this.validateWriteOptions(options);
2553
2976
  await this.awaitStartup("get");
2554
- return this.getPrepared(normalizedKey, fetcher, options);
2977
+ return this.reader.getPrepared(normalizedKey, fetcher, options);
2555
2978
  });
2556
2979
  }
2557
- async getPrepared(normalizedKey, fetcher, options) {
2558
- const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
2559
- if (hit.found) {
2560
- this.ttlResolver.recordAccess(normalizedKey);
2561
- if (this.isNegativeStoredValue(hit.stored)) {
2562
- this.metricsCollector.increment("negativeCacheHits");
2563
- }
2564
- if (hit.state === "fresh") {
2565
- this.metricsCollector.increment("hits");
2566
- await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
2567
- return hit.value;
2568
- }
2569
- if (hit.state === "stale-while-revalidate") {
2570
- this.metricsCollector.increment("hits");
2571
- this.metricsCollector.increment("staleHits");
2572
- this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
2573
- if (fetcher) {
2574
- this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
2575
- }
2576
- return hit.value;
2577
- }
2578
- if (!fetcher) {
2579
- this.metricsCollector.increment("hits");
2580
- this.metricsCollector.increment("staleHits");
2581
- this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
2582
- return hit.value;
2583
- }
2584
- try {
2585
- return await this.fetchWithGuards(normalizedKey, fetcher, options);
2586
- } catch (error) {
2587
- this.metricsCollector.increment("staleHits");
2588
- this.metricsCollector.increment("refreshErrors");
2589
- this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
2590
- return hit.value;
2591
- }
2592
- }
2593
- this.metricsCollector.increment("misses");
2594
- if (!fetcher) {
2595
- return null;
2596
- }
2597
- return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
2598
- }
2599
2980
  /**
2600
2981
  * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
2601
2982
  * Fetches and caches the value if not already present.
@@ -2746,7 +3127,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2746
3127
  const optionsSignature = serializeOptions(entry.options);
2747
3128
  const existing = pendingReads.get(entry.key);
2748
3129
  if (!existing) {
2749
- const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
3130
+ const promise = this.reader.getPrepared(entry.key, entry.fetch, entry.options);
2750
3131
  pendingReads.set(entry.key, {
2751
3132
  promise,
2752
3133
  fetch: entry.fetch,
@@ -2782,7 +3163,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2782
3163
  if (keys.length === 0) {
2783
3164
  break;
2784
3165
  }
2785
- const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
3166
+ const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.reader.readLayerEntry(layer, key)));
2786
3167
  for (let offset = 0; offset < values.length; offset += 1) {
2787
3168
  const key = keys[offset];
2788
3169
  const stored = values[offset];
@@ -2798,7 +3179,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2798
3179
  this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
2799
3180
  }
2800
3181
  await this.tagIndex.touch(key);
2801
- await this.backfill(key, stored, layerIndex - 1);
3182
+ await this.reader.backfill(key, stored, layerIndex - 1);
2802
3183
  resultsByKey.set(key, resolved.value);
2803
3184
  pending.delete(key);
2804
3185
  this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
@@ -2932,7 +3313,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2932
3313
  isLocal: Boolean(layer.isLocal),
2933
3314
  degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
2934
3315
  })),
2935
- backgroundRefreshes: this.backgroundRefreshes.size
3316
+ backgroundRefreshes: this.reader.activeRefreshCount
2936
3317
  };
2937
3318
  }
2938
3319
  resetMetrics() {
@@ -3052,11 +3433,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
3052
3433
  await this.unsubscribeInvalidation?.();
3053
3434
  await this.flushWriteBehindQueue();
3054
3435
  await this.maintenance.waitForGenerationCleanup();
3055
- for (const key of this.backgroundRefreshAbort.keys()) {
3056
- this.backgroundRefreshAbort.set(key, true);
3057
- }
3436
+ this.reader.abortAllRefreshes();
3058
3437
  await Promise.allSettled(
3059
- [...this.backgroundRefreshes.values()].map((promise) => {
3438
+ this.reader.getAllRefreshPromises().map((promise) => {
3060
3439
  let timer;
3061
3440
  return Promise.race([
3062
3441
  promise,
@@ -3069,8 +3448,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3069
3448
  });
3070
3449
  })
3071
3450
  );
3072
- this.backgroundRefreshes.clear();
3073
- this.backgroundRefreshAbort.clear();
3074
3451
  this.maintenance.disposeWriteBehindTimer();
3075
3452
  this.fetchRateLimiter.dispose();
3076
3453
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
@@ -3086,116 +3463,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3086
3463
  await this.handleInvalidationMessage(message);
3087
3464
  });
3088
3465
  }
3089
- async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
3090
- const fetchTask = async () => {
3091
- const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
3092
- if (shouldRecheckFreshLayers) {
3093
- const secondHit = await this.readFromLayers(key, options, "fresh-only");
3094
- if (secondHit.found) {
3095
- this.metricsCollector.increment("hits");
3096
- return secondHit.value;
3097
- }
3098
- }
3099
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
3100
- };
3101
- const singleFlightTask = async () => {
3102
- if (!this.options.singleFlightCoordinator) {
3103
- return fetchTask();
3104
- }
3105
- try {
3106
- return await this.options.singleFlightCoordinator.execute(
3107
- key,
3108
- this.resolveSingleFlightOptions(),
3109
- fetchTask,
3110
- () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
3111
- );
3112
- } catch (error) {
3113
- if (!this.isGracefulDegradationEnabled()) {
3114
- throw error;
3115
- }
3116
- this.metricsCollector.increment("degradedOperations");
3117
- this.logger.warn?.("single-flight-coordinator-degraded", { key, error: this.formatError(error) });
3118
- this.emitError("single-flight", { key, degraded: true, error: this.formatError(error) });
3119
- return fetchTask();
3120
- }
3121
- };
3122
- if (this.options.stampedePrevention === false) {
3123
- return singleFlightTask();
3124
- }
3125
- return this.stampedeGuard.execute(key, singleFlightTask);
3126
- }
3127
- async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
3128
- const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
3129
- const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
3130
- const deadline = Date.now() + timeoutMs;
3131
- this.metricsCollector.increment("singleFlightWaits");
3132
- this.emit("stampede-dedupe", { key });
3133
- while (Date.now() < deadline) {
3134
- const hit = await this.readFromLayers(key, options, "fresh-only");
3135
- if (hit.found) {
3136
- this.metricsCollector.increment("hits");
3137
- return hit.value;
3138
- }
3139
- await this.sleep(pollIntervalMs);
3140
- }
3141
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
3142
- }
3143
- async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
3144
- this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
3145
- this.metricsCollector.increment("fetches");
3146
- const fetchStart = Date.now();
3147
- let fetched;
3148
- try {
3149
- fetched = await this.fetchRateLimiter.schedule(
3150
- options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
3151
- { key, fetcher },
3152
- fetcher
3153
- );
3154
- this.circuitBreakerManager.recordSuccess(key);
3155
- this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
3156
- } catch (error) {
3157
- this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
3158
- throw error;
3159
- }
3160
- if (fetched === null || fetched === void 0) {
3161
- if (!this.shouldNegativeCache(options)) {
3162
- return null;
3163
- }
3164
- if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
3165
- this.logger.debug?.("skip-negative-store-after-invalidation", {
3166
- key,
3167
- expectedClearEpoch,
3168
- clearEpoch: this.maintenance.currentClearEpoch(),
3169
- expectedKeyEpoch,
3170
- keyEpoch: this.maintenance.currentKeyEpoch(key)
3171
- });
3172
- return null;
3173
- }
3174
- await this.storeEntry(key, "empty", null, options);
3175
- return null;
3176
- }
3177
- if (options?.shouldCache) {
3178
- try {
3179
- if (!options.shouldCache(fetched)) {
3180
- return fetched;
3181
- }
3182
- } catch (error) {
3183
- this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
3184
- }
3185
- }
3186
- if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
3187
- this.logger.debug?.("skip-store-after-invalidation", {
3188
- key,
3189
- expectedClearEpoch,
3190
- clearEpoch: this.maintenance.currentClearEpoch(),
3191
- expectedKeyEpoch,
3192
- keyEpoch: this.maintenance.currentKeyEpoch(key)
3193
- });
3194
- return fetched;
3195
- }
3196
- await this.storeEntry(key, "value", fetched, options);
3197
- return fetched;
3198
- }
3199
3466
  async storeEntry(key, kind, value, options) {
3200
3467
  const clearEpoch = this.maintenance.currentClearEpoch();
3201
3468
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
@@ -3242,87 +3509,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3242
3509
  });
3243
3510
  }
3244
3511
  }
3245
- async readFromLayers(key, options, mode) {
3246
- let sawRetainableValue = false;
3247
- for (let index = 0; index < this.layers.length; index += 1) {
3248
- const layer = this.layers[index];
3249
- if (!layer) continue;
3250
- const readStart = performance.now();
3251
- const stored = await this.readLayerEntry(layer, key);
3252
- const readDuration = performance.now() - readStart;
3253
- this.metricsCollector.recordLatency(layer.name, readDuration);
3254
- if (stored === null) {
3255
- this.metricsCollector.incrementLayer("missesByLayer", layer.name);
3256
- continue;
3257
- }
3258
- const resolved = resolveStoredValue(stored);
3259
- if (resolved.state === "expired") {
3260
- await layer.delete(key);
3261
- continue;
3262
- }
3263
- sawRetainableValue = true;
3264
- if (mode === "fresh-only" && resolved.state !== "fresh") {
3265
- continue;
3266
- }
3267
- await this.tagIndex.touch(key);
3268
- await this.backfill(key, stored, index - 1, options);
3269
- this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
3270
- this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
3271
- this.emit("hit", { key, layer: layer.name, state: resolved.state });
3272
- return {
3273
- found: true,
3274
- value: resolved.value,
3275
- stored,
3276
- state: resolved.state,
3277
- layerIndex: index,
3278
- layerName: layer.name
3279
- };
3280
- }
3281
- if (!sawRetainableValue) {
3282
- await this.tagIndex.remove(key);
3283
- }
3284
- this.logger.debug?.("miss", { key, mode });
3285
- this.emit("miss", { key, mode });
3286
- return { found: false, value: null, stored: null, state: "miss" };
3287
- }
3288
- async readLayerEntry(layer, key) {
3289
- if (this.shouldSkipLayer(layer)) {
3290
- return null;
3291
- }
3292
- if (layer.getEntry) {
3293
- try {
3294
- return await layer.getEntry(key);
3295
- } catch (error) {
3296
- return this.handleLayerFailure(layer, "read", error);
3297
- }
3298
- }
3299
- try {
3300
- return await layer.get(key);
3301
- } catch (error) {
3302
- return this.handleLayerFailure(layer, "read", error);
3303
- }
3304
- }
3305
- async backfill(key, stored, upToIndex, options) {
3306
- if (upToIndex < 0) {
3307
- return;
3308
- }
3309
- for (let index = 0; index <= upToIndex; index += 1) {
3310
- const layer = this.layers[index];
3311
- if (!layer || this.shouldSkipLayer(layer)) {
3312
- continue;
3313
- }
3314
- const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
3315
- try {
3316
- await layer.set(key, stored, ttl);
3317
- } catch (error) {
3318
- await this.handleLayerFailure(layer, "backfill", error);
3319
- continue;
3320
- }
3321
- this.metricsCollector.increment("backfills");
3322
- this.logger.debug?.("backfill", { key, layer: layer.name });
3323
- this.emit("backfill", { key, layer: layer.name });
3324
- }
3325
- }
3326
3512
  resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
3327
3513
  return this.ttlResolver.resolveFreshTtl(
3328
3514
  key,
@@ -3338,55 +3524,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3338
3524
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
3339
3525
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
3340
3526
  }
3341
- shouldNegativeCache(options) {
3342
- return options?.negativeCache ?? this.options.negativeCaching ?? false;
3343
- }
3344
- scheduleBackgroundRefresh(key, fetcher, options) {
3345
- if (!shouldStartBackgroundRefresh({
3346
- isDisconnecting: this.isDisconnecting,
3347
- hasRefreshInFlight: this.backgroundRefreshes.has(key)
3348
- })) {
3349
- return;
3350
- }
3351
- const clearEpoch = this.maintenance.currentClearEpoch();
3352
- const keyEpoch = this.maintenance.currentKeyEpoch(key);
3353
- this.backgroundRefreshAbort.set(key, false);
3354
- const refresh = (async () => {
3355
- this.metricsCollector.increment("refreshes");
3356
- try {
3357
- if (this.backgroundRefreshAbort.get(key)) return;
3358
- await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
3359
- } catch (error) {
3360
- if (this.backgroundRefreshAbort.get(key)) return;
3361
- this.metricsCollector.increment("refreshErrors");
3362
- this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
3363
- } finally {
3364
- this.backgroundRefreshes.delete(key);
3365
- this.backgroundRefreshAbort.delete(key);
3366
- }
3367
- })();
3368
- this.backgroundRefreshes.set(key, refresh);
3369
- }
3370
- async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
3371
- const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
3372
- await this.fetchWithGuards(
3373
- key,
3374
- () => this.withTimeout(fetcher(), timeoutMs, () => {
3375
- return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
3376
- }),
3377
- options,
3378
- expectedClearEpoch,
3379
- expectedKeyEpoch
3380
- );
3381
- }
3382
- resolveSingleFlightOptions() {
3383
- return {
3384
- leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
3385
- waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
3386
- pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
3387
- renewIntervalMs: this.options.singleFlightRenewIntervalMs
3388
- };
3389
- }
3390
3527
  async deleteKeys(keys) {
3391
3528
  if (keys.length === 0) {
3392
3529
  return;
@@ -3632,32 +3769,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
3632
3769
  await this.startup;
3633
3770
  this.assertActive(operation);
3634
3771
  }
3772
+ async readLayerEntry(layer, key) {
3773
+ return this.reader.readLayerEntry(layer, key);
3774
+ }
3775
+ scheduleBackgroundRefresh(key, fetcher, options) {
3776
+ this.reader.runScheduleBackgroundRefresh(key, fetcher, options);
3777
+ }
3635
3778
  async applyFreshReadPolicies(key, hit, options, fetcher) {
3636
- const plan = planFreshReadPolicies({
3637
- stored: hit.stored,
3638
- hasFetcher: Boolean(fetcher),
3639
- slidingTtl: options?.slidingTtl ?? false,
3640
- refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
3641
- });
3642
- if (plan.refreshedStored) {
3643
- for (let index = 0; index <= hit.layerIndex; index += 1) {
3644
- const layer = this.layers[index];
3645
- if (!layer || this.shouldSkipLayer(layer)) {
3646
- continue;
3647
- }
3648
- try {
3649
- await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
3650
- } catch (error) {
3651
- await this.handleLayerFailure(layer, "sliding-ttl", error);
3652
- }
3653
- }
3654
- }
3655
- if (fetcher && plan.shouldScheduleBackgroundRefresh) {
3656
- this.scheduleBackgroundRefresh(key, fetcher, options);
3657
- }
3779
+ return this.reader.runApplyFreshReadPolicies(key, hit, options, fetcher);
3658
3780
  }
3659
3781
  shouldSkipLayer(layer) {
3660
- return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
3782
+ const degradedUntil = this.layerDegradedUntil.get(layer.name);
3783
+ const skip = shouldSkipLayer(degradedUntil);
3784
+ if (!skip && degradedUntil !== void 0) {
3785
+ this.layerDegradedUntil.delete(layer.name);
3786
+ }
3787
+ return skip;
3661
3788
  }
3662
3789
  async handleLayerFailure(layer, operation, error) {
3663
3790
  const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
@@ -3691,9 +3818,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3691
3818
  }
3692
3819
  this.emitError("fetch", { key, error: this.formatError(error) });
3693
3820
  }
3694
- isNegativeStoredValue(stored) {
3695
- return isStoredValueEnvelope(stored) && stored.kind === "empty";
3696
- }
3697
3821
  emitError(operation, context) {
3698
3822
  this.logger.error?.(operation, context);
3699
3823
  if (this.listenerCount("error") > 0) {
@@ -4872,12 +4996,12 @@ var RedisLayer = class {
4872
4996
  };
4873
4997
 
4874
4998
  // src/layers/DiskLayer.ts
4875
- var import_node_crypto3 = require("crypto");
4999
+ var import_node_crypto4 = require("crypto");
4876
5000
  var import_node_fs2 = require("fs");
4877
- var import_node_path2 = require("path");
5001
+ var import_node_path = require("path");
4878
5002
 
4879
5003
  // src/internal/PayloadProtection.ts
4880
- var import_node_crypto2 = require("crypto");
5004
+ var import_node_crypto3 = require("crypto");
4881
5005
  var MAGIC_ENCRYPTED = Buffer.from("LCP1:");
4882
5006
  var MAGIC_SIGNED = Buffer.from("LCS1:");
4883
5007
  var ALGORITHM = "aes-256-gcm";
@@ -4890,11 +5014,11 @@ var PayloadProtection = class {
4890
5014
  constructor(options) {
4891
5015
  if (options.encryptionKey) {
4892
5016
  const raw = Buffer.isBuffer(options.encryptionKey) ? options.encryptionKey : Buffer.from(options.encryptionKey, "utf8");
4893
- this.encryptionKey = (0, import_node_crypto2.createHash)("sha256").update(raw).digest();
5017
+ this.encryptionKey = (0, import_node_crypto3.createHash)("sha256").update(raw).digest();
4894
5018
  }
4895
5019
  if (options.signingKey && !options.encryptionKey) {
4896
5020
  const raw = Buffer.isBuffer(options.signingKey) ? options.signingKey : Buffer.from(options.signingKey, "utf8");
4897
- this.signingKey = (0, import_node_crypto2.createHash)("sha256").update(raw).digest();
5021
+ this.signingKey = (0, import_node_crypto3.createHash)("sha256").update(raw).digest();
4898
5022
  }
4899
5023
  }
4900
5024
  /** Returns `true` when any protection (encryption or signing) is configured. */
@@ -4941,8 +5065,8 @@ var PayloadProtection = class {
4941
5065
  }
4942
5066
  // ── Encryption (AES-256-GCM) ──────────────────────────────────────────
4943
5067
  encrypt(plaintext, key) {
4944
- const iv = (0, import_node_crypto2.randomBytes)(IV_LENGTH);
4945
- const cipher = (0, import_node_crypto2.createCipheriv)(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
5068
+ const iv = (0, import_node_crypto3.randomBytes)(IV_LENGTH);
5069
+ const cipher = (0, import_node_crypto3.createCipheriv)(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
4946
5070
  const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4947
5071
  const authTag = cipher.getAuthTag();
4948
5072
  return Buffer.concat([MAGIC_ENCRYPTED, iv, authTag, encrypted]);
@@ -4953,7 +5077,7 @@ var PayloadProtection = class {
4953
5077
  const authTag = payload.subarray(headerEnd + IV_LENGTH, headerEnd + IV_LENGTH + AUTH_TAG_LENGTH);
4954
5078
  const ciphertext = payload.subarray(headerEnd + IV_LENGTH + AUTH_TAG_LENGTH);
4955
5079
  try {
4956
- const decipher = (0, import_node_crypto2.createDecipheriv)(ALGORITHM, key, iv, {
5080
+ const decipher = (0, import_node_crypto3.createDecipheriv)(ALGORITHM, key, iv, {
4957
5081
  authTagLength: AUTH_TAG_LENGTH
4958
5082
  });
4959
5083
  decipher.setAuthTag(authTag);
@@ -4966,15 +5090,15 @@ var PayloadProtection = class {
4966
5090
  }
4967
5091
  // ── Signing (HMAC-SHA256) ─────────────────────────────────────────────
4968
5092
  sign(payload, key) {
4969
- const hmac = (0, import_node_crypto2.createHmac)("sha256", key).update(payload).digest();
5093
+ const hmac = (0, import_node_crypto3.createHmac)("sha256", key).update(payload).digest();
4970
5094
  return Buffer.concat([MAGIC_SIGNED, hmac, payload]);
4971
5095
  }
4972
5096
  verify(payload, key) {
4973
5097
  const headerEnd = MAGIC_SIGNED.length;
4974
5098
  const receivedHmac = payload.subarray(headerEnd, headerEnd + HMAC_LENGTH);
4975
5099
  const data = payload.subarray(headerEnd + HMAC_LENGTH);
4976
- const expectedHmac = (0, import_node_crypto2.createHmac)("sha256", key).update(data).digest();
4977
- if (receivedHmac.length !== HMAC_LENGTH || !(0, import_node_crypto2.timingSafeEqual)(receivedHmac, expectedHmac)) {
5100
+ const expectedHmac = (0, import_node_crypto3.createHmac)("sha256", key).update(data).digest();
5101
+ if (receivedHmac.length !== HMAC_LENGTH || !(0, import_node_crypto3.timingSafeEqual)(receivedHmac, expectedHmac)) {
4978
5102
  throw new PayloadProtectionError(
4979
5103
  "HMAC verification failed. The data may have been tampered with or the signingKey is incorrect."
4980
5104
  );
@@ -5054,7 +5178,7 @@ var DiskLayer = class {
5054
5178
  const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
5055
5179
  const protectedPayload = this.protection.protect(raw);
5056
5180
  const targetPath = this.keyToPath(key);
5057
- const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${(0, import_node_crypto3.randomBytes)(8).toString("hex")}.tmp`;
5181
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${(0, import_node_crypto4.randomBytes)(8).toString("hex")}.tmp`;
5058
5182
  try {
5059
5183
  await import_node_fs2.promises.writeFile(tempPath, protectedPayload);
5060
5184
  await import_node_fs2.promises.rename(tempPath, targetPath);
@@ -5116,7 +5240,7 @@ var DiskLayer = class {
5116
5240
  return;
5117
5241
  }
5118
5242
  await this.deletePathsWithConcurrency(
5119
- entries.filter((name) => name.endsWith(".lc")).map((name) => (0, import_node_path2.join)(this.directory, name))
5243
+ entries.filter((name) => name.endsWith(".lc")).map((name) => (0, import_node_path.join)(this.directory, name))
5120
5244
  );
5121
5245
  });
5122
5246
  }
@@ -5154,8 +5278,8 @@ var DiskLayer = class {
5154
5278
  async dispose() {
5155
5279
  }
5156
5280
  keyToPath(key) {
5157
- const hash = (0, import_node_crypto3.createHash)("sha256").update(key).digest("hex");
5158
- return (0, import_node_path2.join)(this.directory, `${hash}.lc`);
5281
+ const hash = (0, import_node_crypto4.createHash)("sha256").update(key).digest("hex");
5282
+ return (0, import_node_path.join)(this.directory, `${hash}.lc`);
5159
5283
  }
5160
5284
  resolveDirectory(directory) {
5161
5285
  if (typeof directory !== "string" || directory.trim().length === 0) {
@@ -5164,7 +5288,7 @@ var DiskLayer = class {
5164
5288
  if (directory.includes("\0")) {
5165
5289
  throw new Error("DiskLayer.directory must not contain null bytes.");
5166
5290
  }
5167
- return (0, import_node_path2.resolve)(directory);
5291
+ return (0, import_node_path.resolve)(directory);
5168
5292
  }
5169
5293
  normalizeMaxFiles(maxFiles) {
5170
5294
  if (maxFiles === void 0) {
@@ -5247,7 +5371,7 @@ var DiskLayer = class {
5247
5371
  if (name === void 0) {
5248
5372
  return;
5249
5373
  }
5250
- const filePath = (0, import_node_path2.join)(this.directory, name);
5374
+ const filePath = (0, import_node_path.join)(this.directory, name);
5251
5375
  const raw = await this.readEntryFile(filePath);
5252
5376
  if (raw === null) {
5253
5377
  continue;
@@ -5323,7 +5447,7 @@ var DiskLayer = class {
5323
5447
  }
5324
5448
  const withStats = await Promise.all(
5325
5449
  lcFiles.map(async (name) => {
5326
- const filePath = (0, import_node_path2.join)(this.directory, name);
5450
+ const filePath = (0, import_node_path.join)(this.directory, name);
5327
5451
  try {
5328
5452
  const stat = await import_node_fs2.promises.stat(filePath);
5329
5453
  return { filePath, mtimeMs: stat.mtimeMs };
@@ -5439,7 +5563,7 @@ var MsgpackSerializer = class {
5439
5563
  };
5440
5564
 
5441
5565
  // src/singleflight/RedisSingleFlightCoordinator.ts
5442
- var import_node_crypto4 = require("crypto");
5566
+ var import_node_crypto5 = require("crypto");
5443
5567
  var RELEASE_SCRIPT = `
5444
5568
  if redis.call("get", KEYS[1]) == ARGV[1] then
5445
5569
  return redis.call("del", KEYS[1])
@@ -5463,7 +5587,7 @@ var RedisSingleFlightCoordinator = class {
5463
5587
  }
5464
5588
  async execute(key, options, worker, waiter) {
5465
5589
  const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
5466
- const token = (0, import_node_crypto4.randomUUID)();
5590
+ const token = (0, import_node_crypto5.randomUUID)();
5467
5591
  const acquired = await this.runCommand(
5468
5592
  `acquire("${key}")`,
5469
5593
  () => this.client.set(lockKey, token, "PX", options.leaseMs, "NX")