layercache 1.3.3 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1106,6 +1106,366 @@ function planFreshReadPolicies({
1106
1106
  };
1107
1107
  }
1108
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
+
1109
1469
  // src/internal/CacheStackSnapshotManager.ts
1110
1470
  var import_node_fs = require("fs");
1111
1471
 
@@ -2423,10 +2783,6 @@ var CacheMissError = class extends Error {
2423
2783
  };
2424
2784
 
2425
2785
  // src/CacheStack.ts
2426
- var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
2427
- var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
2428
- var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
2429
- var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
2430
2786
  var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
2431
2787
  var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
2432
2788
  var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
@@ -2537,7 +2893,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2537
2893
  layers: this.layers,
2538
2894
  tagIndex: this.tagIndex,
2539
2895
  snapshotSerializer: this.snapshotSerializer,
2540
- readLayerEntry: this.readLayerEntry.bind(this),
2896
+ readLayerEntry: (layer, key) => this.reader.readLayerEntry(layer, key),
2541
2897
  shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2542
2898
  handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
2543
2899
  qualifyKey: this.qualifyKey.bind(this),
@@ -2545,6 +2901,41 @@ var CacheStack = class extends import_node_events.EventEmitter {
2545
2901
  validateCacheKey,
2546
2902
  formatError: this.formatError.bind(this)
2547
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,
2914
+ shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2915
+ handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
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
2938
+ });
2548
2939
  this.initializeWriteBehind(options.writeBehind);
2549
2940
  this.startup = this.initialize();
2550
2941
  }
@@ -2563,8 +2954,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
2563
2954
  invalidation;
2564
2955
  layerWriter;
2565
2956
  snapshots;
2566
- backgroundRefreshes = /* @__PURE__ */ new Map();
2567
- backgroundRefreshAbort = /* @__PURE__ */ new Map();
2568
2957
  layerDegradedUntil = /* @__PURE__ */ new Map();
2569
2958
  maintenance = new CacheStackMaintenance();
2570
2959
  ttlResolver;
@@ -2572,6 +2961,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2572
2961
  nextOperationId = 0;
2573
2962
  currentGeneration;
2574
2963
  isDisconnecting = false;
2964
+ reader;
2575
2965
  disconnectPromise;
2576
2966
  /**
2577
2967
  * Read-through cache get.
@@ -2584,51 +2974,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
2584
2974
  const normalizedKey = this.qualifyKey(validateCacheKey(key));
2585
2975
  this.validateWriteOptions(options);
2586
2976
  await this.awaitStartup("get");
2587
- return this.getPrepared(normalizedKey, fetcher, options);
2977
+ return this.reader.getPrepared(normalizedKey, fetcher, options);
2588
2978
  });
2589
2979
  }
2590
- async getPrepared(normalizedKey, fetcher, options) {
2591
- const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
2592
- if (hit.found) {
2593
- this.ttlResolver.recordAccess(normalizedKey);
2594
- if (this.isNegativeStoredValue(hit.stored)) {
2595
- this.metricsCollector.increment("negativeCacheHits");
2596
- }
2597
- if (hit.state === "fresh") {
2598
- this.metricsCollector.increment("hits");
2599
- await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
2600
- return hit.value;
2601
- }
2602
- if (hit.state === "stale-while-revalidate") {
2603
- this.metricsCollector.increment("hits");
2604
- this.metricsCollector.increment("staleHits");
2605
- this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
2606
- if (fetcher) {
2607
- this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
2608
- }
2609
- return hit.value;
2610
- }
2611
- if (!fetcher) {
2612
- this.metricsCollector.increment("hits");
2613
- this.metricsCollector.increment("staleHits");
2614
- this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
2615
- return hit.value;
2616
- }
2617
- try {
2618
- return await this.fetchWithGuards(normalizedKey, fetcher, options);
2619
- } catch (error) {
2620
- this.metricsCollector.increment("staleHits");
2621
- this.metricsCollector.increment("refreshErrors");
2622
- this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
2623
- return hit.value;
2624
- }
2625
- }
2626
- this.metricsCollector.increment("misses");
2627
- if (!fetcher) {
2628
- return null;
2629
- }
2630
- return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
2631
- }
2632
2980
  /**
2633
2981
  * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
2634
2982
  * Fetches and caches the value if not already present.
@@ -2779,7 +3127,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2779
3127
  const optionsSignature = serializeOptions(entry.options);
2780
3128
  const existing = pendingReads.get(entry.key);
2781
3129
  if (!existing) {
2782
- const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
3130
+ const promise = this.reader.getPrepared(entry.key, entry.fetch, entry.options);
2783
3131
  pendingReads.set(entry.key, {
2784
3132
  promise,
2785
3133
  fetch: entry.fetch,
@@ -2815,7 +3163,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2815
3163
  if (keys.length === 0) {
2816
3164
  break;
2817
3165
  }
2818
- 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)));
2819
3167
  for (let offset = 0; offset < values.length; offset += 1) {
2820
3168
  const key = keys[offset];
2821
3169
  const stored = values[offset];
@@ -2831,7 +3179,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2831
3179
  this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
2832
3180
  }
2833
3181
  await this.tagIndex.touch(key);
2834
- await this.backfill(key, stored, layerIndex - 1);
3182
+ await this.reader.backfill(key, stored, layerIndex - 1);
2835
3183
  resultsByKey.set(key, resolved.value);
2836
3184
  pending.delete(key);
2837
3185
  this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
@@ -2965,7 +3313,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
2965
3313
  isLocal: Boolean(layer.isLocal),
2966
3314
  degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
2967
3315
  })),
2968
- backgroundRefreshes: this.backgroundRefreshes.size
3316
+ backgroundRefreshes: this.reader.activeRefreshCount
2969
3317
  };
2970
3318
  }
2971
3319
  resetMetrics() {
@@ -3085,11 +3433,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
3085
3433
  await this.unsubscribeInvalidation?.();
3086
3434
  await this.flushWriteBehindQueue();
3087
3435
  await this.maintenance.waitForGenerationCleanup();
3088
- for (const key of this.backgroundRefreshAbort.keys()) {
3089
- this.backgroundRefreshAbort.set(key, true);
3090
- }
3436
+ this.reader.abortAllRefreshes();
3091
3437
  await Promise.allSettled(
3092
- [...this.backgroundRefreshes.values()].map((promise) => {
3438
+ this.reader.getAllRefreshPromises().map((promise) => {
3093
3439
  let timer;
3094
3440
  return Promise.race([
3095
3441
  promise,
@@ -3102,8 +3448,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3102
3448
  });
3103
3449
  })
3104
3450
  );
3105
- this.backgroundRefreshes.clear();
3106
- this.backgroundRefreshAbort.clear();
3107
3451
  this.maintenance.disposeWriteBehindTimer();
3108
3452
  this.fetchRateLimiter.dispose();
3109
3453
  await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
@@ -3119,116 +3463,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3119
3463
  await this.handleInvalidationMessage(message);
3120
3464
  });
3121
3465
  }
3122
- async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
3123
- const fetchTask = async () => {
3124
- const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
3125
- if (shouldRecheckFreshLayers) {
3126
- const secondHit = await this.readFromLayers(key, options, "fresh-only");
3127
- if (secondHit.found) {
3128
- this.metricsCollector.increment("hits");
3129
- return secondHit.value;
3130
- }
3131
- }
3132
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
3133
- };
3134
- const singleFlightTask = async () => {
3135
- if (!this.options.singleFlightCoordinator) {
3136
- return fetchTask();
3137
- }
3138
- try {
3139
- return await this.options.singleFlightCoordinator.execute(
3140
- key,
3141
- this.resolveSingleFlightOptions(),
3142
- fetchTask,
3143
- () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
3144
- );
3145
- } catch (error) {
3146
- if (!this.isGracefulDegradationEnabled()) {
3147
- throw error;
3148
- }
3149
- this.metricsCollector.increment("degradedOperations");
3150
- this.logger.warn?.("single-flight-coordinator-degraded", { key, error: this.formatError(error) });
3151
- this.emitError("single-flight", { key, degraded: true, error: this.formatError(error) });
3152
- return fetchTask();
3153
- }
3154
- };
3155
- if (this.options.stampedePrevention === false) {
3156
- return singleFlightTask();
3157
- }
3158
- return this.stampedeGuard.execute(key, singleFlightTask);
3159
- }
3160
- async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
3161
- const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
3162
- const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
3163
- const deadline = Date.now() + timeoutMs;
3164
- this.metricsCollector.increment("singleFlightWaits");
3165
- this.emit("stampede-dedupe", { key });
3166
- while (Date.now() < deadline) {
3167
- const hit = await this.readFromLayers(key, options, "fresh-only");
3168
- if (hit.found) {
3169
- this.metricsCollector.increment("hits");
3170
- return hit.value;
3171
- }
3172
- await this.sleep(pollIntervalMs);
3173
- }
3174
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
3175
- }
3176
- async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
3177
- this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
3178
- this.metricsCollector.increment("fetches");
3179
- const fetchStart = Date.now();
3180
- let fetched;
3181
- try {
3182
- fetched = await this.fetchRateLimiter.schedule(
3183
- options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
3184
- { key, fetcher },
3185
- fetcher
3186
- );
3187
- this.circuitBreakerManager.recordSuccess(key);
3188
- this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
3189
- } catch (error) {
3190
- this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
3191
- throw error;
3192
- }
3193
- if (fetched === null || fetched === void 0) {
3194
- if (!this.shouldNegativeCache(options)) {
3195
- return null;
3196
- }
3197
- if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
3198
- this.logger.debug?.("skip-negative-store-after-invalidation", {
3199
- key,
3200
- expectedClearEpoch,
3201
- clearEpoch: this.maintenance.currentClearEpoch(),
3202
- expectedKeyEpoch,
3203
- keyEpoch: this.maintenance.currentKeyEpoch(key)
3204
- });
3205
- return null;
3206
- }
3207
- await this.storeEntry(key, "empty", null, options);
3208
- return null;
3209
- }
3210
- if (options?.shouldCache) {
3211
- try {
3212
- if (!options.shouldCache(fetched)) {
3213
- return fetched;
3214
- }
3215
- } catch (error) {
3216
- this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
3217
- }
3218
- }
3219
- if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
3220
- this.logger.debug?.("skip-store-after-invalidation", {
3221
- key,
3222
- expectedClearEpoch,
3223
- clearEpoch: this.maintenance.currentClearEpoch(),
3224
- expectedKeyEpoch,
3225
- keyEpoch: this.maintenance.currentKeyEpoch(key)
3226
- });
3227
- return fetched;
3228
- }
3229
- await this.storeEntry(key, "value", fetched, options);
3230
- return fetched;
3231
- }
3232
3466
  async storeEntry(key, kind, value, options) {
3233
3467
  const clearEpoch = this.maintenance.currentClearEpoch();
3234
3468
  const keyEpoch = this.maintenance.currentKeyEpoch(key);
@@ -3275,87 +3509,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3275
3509
  });
3276
3510
  }
3277
3511
  }
3278
- async readFromLayers(key, options, mode) {
3279
- let sawRetainableValue = false;
3280
- for (let index = 0; index < this.layers.length; index += 1) {
3281
- const layer = this.layers[index];
3282
- if (!layer) continue;
3283
- const readStart = performance.now();
3284
- const stored = await this.readLayerEntry(layer, key);
3285
- const readDuration = performance.now() - readStart;
3286
- this.metricsCollector.recordLatency(layer.name, readDuration);
3287
- if (stored === null) {
3288
- this.metricsCollector.incrementLayer("missesByLayer", layer.name);
3289
- continue;
3290
- }
3291
- const resolved = resolveStoredValue(stored);
3292
- if (resolved.state === "expired") {
3293
- await layer.delete(key);
3294
- continue;
3295
- }
3296
- sawRetainableValue = true;
3297
- if (mode === "fresh-only" && resolved.state !== "fresh") {
3298
- continue;
3299
- }
3300
- await this.tagIndex.touch(key);
3301
- await this.backfill(key, stored, index - 1, options);
3302
- this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
3303
- this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
3304
- this.emit("hit", { key, layer: layer.name, state: resolved.state });
3305
- return {
3306
- found: true,
3307
- value: resolved.value,
3308
- stored,
3309
- state: resolved.state,
3310
- layerIndex: index,
3311
- layerName: layer.name
3312
- };
3313
- }
3314
- if (!sawRetainableValue) {
3315
- await this.tagIndex.remove(key);
3316
- }
3317
- this.logger.debug?.("miss", { key, mode });
3318
- this.emit("miss", { key, mode });
3319
- return { found: false, value: null, stored: null, state: "miss" };
3320
- }
3321
- async readLayerEntry(layer, key) {
3322
- if (this.shouldSkipLayer(layer)) {
3323
- return null;
3324
- }
3325
- if (layer.getEntry) {
3326
- try {
3327
- return await layer.getEntry(key);
3328
- } catch (error) {
3329
- return this.handleLayerFailure(layer, "read", error);
3330
- }
3331
- }
3332
- try {
3333
- return await layer.get(key);
3334
- } catch (error) {
3335
- return this.handleLayerFailure(layer, "read", error);
3336
- }
3337
- }
3338
- async backfill(key, stored, upToIndex, options) {
3339
- if (upToIndex < 0) {
3340
- return;
3341
- }
3342
- for (let index = 0; index <= upToIndex; index += 1) {
3343
- const layer = this.layers[index];
3344
- if (!layer || this.shouldSkipLayer(layer)) {
3345
- continue;
3346
- }
3347
- const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
3348
- try {
3349
- await layer.set(key, stored, ttl);
3350
- } catch (error) {
3351
- await this.handleLayerFailure(layer, "backfill", error);
3352
- continue;
3353
- }
3354
- this.metricsCollector.increment("backfills");
3355
- this.logger.debug?.("backfill", { key, layer: layer.name });
3356
- this.emit("backfill", { key, layer: layer.name });
3357
- }
3358
- }
3359
3512
  resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
3360
3513
  return this.ttlResolver.resolveFreshTtl(
3361
3514
  key,
@@ -3371,55 +3524,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3371
3524
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
3372
3525
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
3373
3526
  }
3374
- shouldNegativeCache(options) {
3375
- return options?.negativeCache ?? this.options.negativeCaching ?? false;
3376
- }
3377
- scheduleBackgroundRefresh(key, fetcher, options) {
3378
- if (!shouldStartBackgroundRefresh({
3379
- isDisconnecting: this.isDisconnecting,
3380
- hasRefreshInFlight: this.backgroundRefreshes.has(key)
3381
- })) {
3382
- return;
3383
- }
3384
- const clearEpoch = this.maintenance.currentClearEpoch();
3385
- const keyEpoch = this.maintenance.currentKeyEpoch(key);
3386
- this.backgroundRefreshAbort.set(key, false);
3387
- const refresh = (async () => {
3388
- this.metricsCollector.increment("refreshes");
3389
- try {
3390
- if (this.backgroundRefreshAbort.get(key)) return;
3391
- await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
3392
- } catch (error) {
3393
- if (this.backgroundRefreshAbort.get(key)) return;
3394
- this.metricsCollector.increment("refreshErrors");
3395
- this.logger.warn?.("background-refresh-error", { key, error: this.formatError(error) });
3396
- } finally {
3397
- this.backgroundRefreshes.delete(key);
3398
- this.backgroundRefreshAbort.delete(key);
3399
- }
3400
- })();
3401
- this.backgroundRefreshes.set(key, refresh);
3402
- }
3403
- async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
3404
- const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
3405
- await this.fetchWithGuards(
3406
- key,
3407
- () => this.withTimeout(fetcher(), timeoutMs, () => {
3408
- return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
3409
- }),
3410
- options,
3411
- expectedClearEpoch,
3412
- expectedKeyEpoch
3413
- );
3414
- }
3415
- resolveSingleFlightOptions() {
3416
- return {
3417
- leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
3418
- waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
3419
- pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
3420
- renewIntervalMs: this.options.singleFlightRenewIntervalMs
3421
- };
3422
- }
3423
3527
  async deleteKeys(keys) {
3424
3528
  if (keys.length === 0) {
3425
3529
  return;
@@ -3665,29 +3769,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
3665
3769
  await this.startup;
3666
3770
  this.assertActive(operation);
3667
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
+ }
3668
3778
  async applyFreshReadPolicies(key, hit, options, fetcher) {
3669
- const plan = planFreshReadPolicies({
3670
- stored: hit.stored,
3671
- hasFetcher: Boolean(fetcher),
3672
- slidingTtl: options?.slidingTtl ?? false,
3673
- refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
3674
- });
3675
- if (plan.refreshedStored) {
3676
- for (let index = 0; index <= hit.layerIndex; index += 1) {
3677
- const layer = this.layers[index];
3678
- if (!layer || this.shouldSkipLayer(layer)) {
3679
- continue;
3680
- }
3681
- try {
3682
- await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
3683
- } catch (error) {
3684
- await this.handleLayerFailure(layer, "sliding-ttl", error);
3685
- }
3686
- }
3687
- }
3688
- if (fetcher && plan.shouldScheduleBackgroundRefresh) {
3689
- this.scheduleBackgroundRefresh(key, fetcher, options);
3690
- }
3779
+ return this.reader.runApplyFreshReadPolicies(key, hit, options, fetcher);
3691
3780
  }
3692
3781
  shouldSkipLayer(layer) {
3693
3782
  const degradedUntil = this.layerDegradedUntil.get(layer.name);
@@ -3729,9 +3818,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
3729
3818
  }
3730
3819
  this.emitError("fetch", { key, error: this.formatError(error) });
3731
3820
  }
3732
- isNegativeStoredValue(stored) {
3733
- return isStoredValueEnvelope(stored) && stored.kind === "empty";
3734
- }
3735
3821
  emitError(operation, context) {
3736
3822
  this.logger.error?.(operation, context);
3737
3823
  if (this.listenerCount("error") > 0) {