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/README.md +1 -1
- package/dist/{edge-CUHTP9Bc.d.cts → edge-DKkrQ_Ky.d.cts} +3 -14
- package/dist/{edge-CUHTP9Bc.d.ts → edge-DKkrQ_Ky.d.ts} +3 -14
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/index.cjs +411 -325
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +411 -325
- package/package.json +2 -2
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
|
|
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.
|
|
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
|
-
|
|
3089
|
-
this.backgroundRefreshAbort.set(key, true);
|
|
3090
|
-
}
|
|
3436
|
+
this.reader.abortAllRefreshes();
|
|
3091
3437
|
await Promise.allSettled(
|
|
3092
|
-
|
|
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
|
-
|
|
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) {
|