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/README.md +157 -81
- package/dist/{chunk-GJBKCFE6.js → chunk-5RCAX2BQ.js} +9 -9
- package/dist/{chunk-BQLL6IM5.js → chunk-BORDQ3LA.js} +135 -0
- package/dist/cli.cjs +77 -5
- package/dist/cli.js +37 -7
- 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.cjs +9 -9
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +1 -1
- package/dist/index.cjs +511 -387
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +491 -480
- package/package.json +2 -2
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
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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,
|
|
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 =
|
|
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
|
|
1134
|
-
const resolved =
|
|
1135
|
-
const baseDir = snapshotBaseDir === false ? false :
|
|
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,
|
|
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,
|
|
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 =
|
|
1152
|
-
const existingAncestor = await findExistingAncestor(parentDir, fs3,
|
|
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,
|
|
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,
|
|
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 =
|
|
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 =
|
|
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
|
|
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 = (
|
|
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
|
|
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
|
-
|
|
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
|
|
2237
|
-
|
|
2238
|
-
if (
|
|
2239
|
-
|
|
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
|
|
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
|
-
|
|
2667
|
+
path.push([node, character]);
|
|
2275
2668
|
node = child;
|
|
2276
2669
|
}
|
|
2277
2670
|
node.terminal = false;
|
|
2278
|
-
for (let index =
|
|
2279
|
-
const entry =
|
|
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
|
|
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
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
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.
|
|
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
|
-
|
|
3056
|
-
this.backgroundRefreshAbort.set(key, true);
|
|
3057
|
-
}
|
|
3436
|
+
this.reader.abortAllRefreshes();
|
|
3058
3437
|
await Promise.allSettled(
|
|
3059
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4999
|
+
var import_node_crypto4 = require("crypto");
|
|
4876
5000
|
var import_node_fs2 = require("fs");
|
|
4877
|
-
var
|
|
5001
|
+
var import_node_path = require("path");
|
|
4878
5002
|
|
|
4879
5003
|
// src/internal/PayloadProtection.ts
|
|
4880
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
4945
|
-
const cipher = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
4977
|
-
if (receivedHmac.length !== HMAC_LENGTH || !(0,
|
|
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,
|
|
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,
|
|
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,
|
|
5158
|
-
return (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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")
|