layercache 1.2.8 → 1.3.0
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 +11 -4
- package/benchmarks/direct.ts +221 -0
- package/benchmarks/edge-utils.ts +28 -0
- package/benchmarks/edge.ts +491 -0
- package/benchmarks/http.ts +99 -0
- package/benchmarks/memory-pressure.ts +144 -0
- package/benchmarks/multi-process-fanout.ts +231 -0
- package/benchmarks/multi-process-worker.ts +151 -0
- package/benchmarks/paths.ts +25 -0
- package/benchmarks/queue-amplification-utils.ts +48 -0
- package/benchmarks/queue-amplification.ts +230 -0
- package/benchmarks/redis-latency-proxy.ts +100 -0
- package/benchmarks/redis.ts +107 -0
- package/benchmarks/scenario-utils.ts +38 -0
- package/benchmarks/server.ts +157 -0
- package/benchmarks/slow-redis-latency.ts +309 -0
- package/benchmarks/slow-redis-utils.ts +29 -0
- package/benchmarks/slow-redis.ts +47 -0
- package/benchmarks/stats.ts +46 -0
- package/benchmarks/workload.ts +77 -0
- package/dist/cli.cjs +14 -1
- package/dist/cli.js +14 -1
- package/dist/{edge-DBs8Ko5W.d.cts → edge-BXWTKlI1.d.cts} +1 -0
- package/dist/{edge-DBs8Ko5W.d.ts → edge-BXWTKlI1.d.ts} +1 -0
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/index.cjs +304 -112
- package/dist/index.d.cts +17 -4
- package/dist/index.d.ts +17 -4
- package/dist/index.js +301 -109
- package/package.json +12 -2
- package/packages/nestjs/dist/index.cjs +146 -68
- package/packages/nestjs/dist/index.d.cts +1 -0
- package/packages/nestjs/dist/index.d.ts +1 -0
- package/packages/nestjs/dist/index.js +146 -68
package/dist/index.cjs
CHANGED
|
@@ -528,7 +528,7 @@ function normalizeForSerialization(value) {
|
|
|
528
528
|
}
|
|
529
529
|
function serializeKeyPart(value) {
|
|
530
530
|
if (typeof value === "string") {
|
|
531
|
-
return `s:${value}`;
|
|
531
|
+
return `s:${value.replace(/%/g, "%25").replace(/:/g, "%3A")}`;
|
|
532
532
|
}
|
|
533
533
|
if (typeof value === "number") {
|
|
534
534
|
return `n:${value}`;
|
|
@@ -917,6 +917,7 @@ var CacheStackLayerWriter = class {
|
|
|
917
917
|
}
|
|
918
918
|
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
919
919
|
const failures = results.filter((result) => result.status === "rejected");
|
|
920
|
+
const degraded = results.filter((result) => result.status === "fulfilled");
|
|
920
921
|
if (failures.length === 0) {
|
|
921
922
|
return;
|
|
922
923
|
}
|
|
@@ -1095,6 +1096,7 @@ function planFreshReadPolicies({
|
|
|
1095
1096
|
}
|
|
1096
1097
|
|
|
1097
1098
|
// src/internal/CacheStackSnapshotManager.ts
|
|
1099
|
+
var import_node_crypto = require("crypto");
|
|
1098
1100
|
var import_node_fs = require("fs");
|
|
1099
1101
|
var import_node_path = __toESM(require("path"), 1);
|
|
1100
1102
|
|
|
@@ -1194,6 +1196,42 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
|
1194
1196
|
return Buffer.concat(chunks).toString("utf8");
|
|
1195
1197
|
}
|
|
1196
1198
|
|
|
1199
|
+
// src/internal/StructuredDataSanitizer.ts
|
|
1200
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1201
|
+
function sanitizeStructuredData(value, options) {
|
|
1202
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
1203
|
+
}
|
|
1204
|
+
function sanitizeValue(value, depth, state, options) {
|
|
1205
|
+
state.count += 1;
|
|
1206
|
+
if (state.count > options.maxNodes) {
|
|
1207
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
1208
|
+
}
|
|
1209
|
+
if (depth > options.maxDepth) {
|
|
1210
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
1211
|
+
}
|
|
1212
|
+
if (Array.isArray(value)) {
|
|
1213
|
+
const sanitized2 = [];
|
|
1214
|
+
for (const entry of value) {
|
|
1215
|
+
sanitized2.push(sanitizeValue(entry, depth + 1, state, options));
|
|
1216
|
+
}
|
|
1217
|
+
return sanitized2;
|
|
1218
|
+
}
|
|
1219
|
+
if (!isPlainObject(value)) {
|
|
1220
|
+
return value;
|
|
1221
|
+
}
|
|
1222
|
+
const sanitized = options.createObject?.() ?? {};
|
|
1223
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1224
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
1228
|
+
}
|
|
1229
|
+
return sanitized;
|
|
1230
|
+
}
|
|
1231
|
+
function isPlainObject(value) {
|
|
1232
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1197
1235
|
// src/internal/CacheStackSnapshotManager.ts
|
|
1198
1236
|
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1199
1237
|
var CacheStackSnapshotManager = class {
|
|
@@ -1218,7 +1256,16 @@ var CacheStackSnapshotManager = class {
|
|
|
1218
1256
|
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
1219
1257
|
await Promise.all(
|
|
1220
1258
|
batch.map(async (entry) => {
|
|
1221
|
-
await Promise.all(
|
|
1259
|
+
await Promise.all(
|
|
1260
|
+
this.options.layers.map(async (layer) => {
|
|
1261
|
+
if (this.options.shouldSkipLayer(layer)) return;
|
|
1262
|
+
try {
|
|
1263
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1266
|
+
}
|
|
1267
|
+
})
|
|
1268
|
+
);
|
|
1222
1269
|
await this.options.tagIndex.touch(entry.key);
|
|
1223
1270
|
})
|
|
1224
1271
|
);
|
|
@@ -1228,7 +1275,7 @@ var CacheStackSnapshotManager = class {
|
|
|
1228
1275
|
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1229
1276
|
const tempPath = import_node_path.default.join(
|
|
1230
1277
|
import_node_path.default.dirname(targetPath),
|
|
1231
|
-
`.layercache-${process.pid}-${Date.now()}-${
|
|
1278
|
+
`.layercache-${process.pid}-${Date.now()}-${(0, import_node_crypto.randomBytes)(8).toString("hex")}.tmp`
|
|
1232
1279
|
);
|
|
1233
1280
|
let handle;
|
|
1234
1281
|
try {
|
|
@@ -1328,7 +1375,13 @@ var CacheStackSnapshotManager = class {
|
|
|
1328
1375
|
});
|
|
1329
1376
|
}
|
|
1330
1377
|
sanitizeSnapshotValue(value) {
|
|
1331
|
-
|
|
1378
|
+
const roundTripped = this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
1379
|
+
return sanitizeStructuredData(roundTripped, {
|
|
1380
|
+
label: "Snapshot value",
|
|
1381
|
+
maxDepth: 64,
|
|
1382
|
+
maxNodes: 1e4,
|
|
1383
|
+
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
1384
|
+
});
|
|
1332
1385
|
}
|
|
1333
1386
|
};
|
|
1334
1387
|
|
|
@@ -1708,7 +1761,13 @@ var FetchRateLimiter = class {
|
|
|
1708
1761
|
this.pendingBuckets.add(next.bucketKey);
|
|
1709
1762
|
}
|
|
1710
1763
|
this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
|
|
1711
|
-
this.
|
|
1764
|
+
if (!this.drainTimer) {
|
|
1765
|
+
this.drainTimer = setTimeout(() => {
|
|
1766
|
+
this.drainTimer = void 0;
|
|
1767
|
+
this.drain();
|
|
1768
|
+
}, 0);
|
|
1769
|
+
this.drainTimer.unref?.();
|
|
1770
|
+
}
|
|
1712
1771
|
});
|
|
1713
1772
|
}
|
|
1714
1773
|
}
|
|
@@ -1750,6 +1809,9 @@ var FetchRateLimiter = class {
|
|
|
1750
1809
|
}
|
|
1751
1810
|
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1752
1811
|
this.evictIdleBuckets();
|
|
1812
|
+
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1813
|
+
throw new Error(`FetchRateLimiter bucket limit (${MAX_BUCKETS}) exceeded.`);
|
|
1814
|
+
}
|
|
1753
1815
|
}
|
|
1754
1816
|
const bucket = { active: 0, startedAt: [] };
|
|
1755
1817
|
this.buckets.set(bucketKey, bucket);
|
|
@@ -2228,38 +2290,6 @@ var TagIndex = class {
|
|
|
2228
2290
|
}
|
|
2229
2291
|
};
|
|
2230
2292
|
|
|
2231
|
-
// src/internal/StructuredDataSanitizer.ts
|
|
2232
|
-
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2233
|
-
function sanitizeStructuredData(value, options) {
|
|
2234
|
-
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
2235
|
-
}
|
|
2236
|
-
function sanitizeValue(value, depth, state, options) {
|
|
2237
|
-
state.count += 1;
|
|
2238
|
-
if (state.count > options.maxNodes) {
|
|
2239
|
-
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
2240
|
-
}
|
|
2241
|
-
if (depth > options.maxDepth) {
|
|
2242
|
-
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
2243
|
-
}
|
|
2244
|
-
if (Array.isArray(value)) {
|
|
2245
|
-
return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
|
|
2246
|
-
}
|
|
2247
|
-
if (!isPlainObject(value)) {
|
|
2248
|
-
return value;
|
|
2249
|
-
}
|
|
2250
|
-
const sanitized = options.createObject?.() ?? {};
|
|
2251
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
2252
|
-
if (DANGEROUS_KEYS.has(key)) {
|
|
2253
|
-
continue;
|
|
2254
|
-
}
|
|
2255
|
-
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
2256
|
-
}
|
|
2257
|
-
return sanitized;
|
|
2258
|
-
}
|
|
2259
|
-
function isPlainObject(value) {
|
|
2260
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
2293
|
// src/serialization/JsonSerializer.ts
|
|
2264
2294
|
var JsonSerializer = class {
|
|
2265
2295
|
serialize(value) {
|
|
@@ -2276,29 +2306,35 @@ var JsonSerializer = class {
|
|
|
2276
2306
|
};
|
|
2277
2307
|
|
|
2278
2308
|
// src/stampede/StampedeGuard.ts
|
|
2279
|
-
var import_async_mutex2 = require("async-mutex");
|
|
2280
2309
|
var StampedeGuard = class {
|
|
2281
|
-
|
|
2310
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
2282
2311
|
async execute(key, task) {
|
|
2283
|
-
const
|
|
2312
|
+
const existing = this.inFlight.get(key);
|
|
2313
|
+
if (existing) {
|
|
2314
|
+
existing.references += 1;
|
|
2315
|
+
try {
|
|
2316
|
+
return await existing.promise;
|
|
2317
|
+
} finally {
|
|
2318
|
+
this.releaseEntry(key, existing);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
const entry = {
|
|
2322
|
+
promise: Promise.resolve().then(task),
|
|
2323
|
+
references: 1
|
|
2324
|
+
};
|
|
2325
|
+
this.inFlight.set(key, entry);
|
|
2284
2326
|
try {
|
|
2285
|
-
return await entry.
|
|
2327
|
+
return await entry.promise;
|
|
2286
2328
|
} finally {
|
|
2287
|
-
|
|
2288
|
-
const current = this.mutexes.get(key);
|
|
2289
|
-
if (current === entry && entry.references === 0 && !entry.mutex.isLocked()) {
|
|
2290
|
-
this.mutexes.delete(key);
|
|
2291
|
-
}
|
|
2329
|
+
this.releaseEntry(key, entry);
|
|
2292
2330
|
}
|
|
2293
2331
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
this.
|
|
2332
|
+
releaseEntry(key, entry) {
|
|
2333
|
+
entry.references -= 1;
|
|
2334
|
+
const current = this.inFlight.get(key);
|
|
2335
|
+
if (current === entry && entry.references === 0) {
|
|
2336
|
+
this.inFlight.delete(key);
|
|
2299
2337
|
}
|
|
2300
|
-
entry.references += 1;
|
|
2301
|
-
return entry;
|
|
2302
2338
|
}
|
|
2303
2339
|
};
|
|
2304
2340
|
|
|
@@ -2424,6 +2460,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2424
2460
|
tagIndex: this.tagIndex,
|
|
2425
2461
|
snapshotSerializer: this.snapshotSerializer,
|
|
2426
2462
|
readLayerEntry: this.readLayerEntry.bind(this),
|
|
2463
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2464
|
+
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
2427
2465
|
qualifyKey: this.qualifyKey.bind(this),
|
|
2428
2466
|
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
2429
2467
|
validateCacheKey,
|
|
@@ -2448,6 +2486,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2448
2486
|
layerWriter;
|
|
2449
2487
|
snapshots;
|
|
2450
2488
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
2489
|
+
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
2451
2490
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2452
2491
|
maintenance = new CacheStackMaintenance();
|
|
2453
2492
|
ttlResolver;
|
|
@@ -2510,7 +2549,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2510
2549
|
if (!fetcher) {
|
|
2511
2550
|
return null;
|
|
2512
2551
|
}
|
|
2513
|
-
return this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
2552
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
|
|
2514
2553
|
}
|
|
2515
2554
|
/**
|
|
2516
2555
|
* Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
|
|
@@ -2692,7 +2731,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2692
2731
|
}
|
|
2693
2732
|
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2694
2733
|
const layer = this.layers[layerIndex];
|
|
2695
|
-
if (!layer) continue;
|
|
2734
|
+
if (!layer || this.shouldSkipLayer(layer)) continue;
|
|
2696
2735
|
const keys = [...pending];
|
|
2697
2736
|
if (keys.length === 0) {
|
|
2698
2737
|
break;
|
|
@@ -2709,6 +2748,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2709
2748
|
await layer.delete(key);
|
|
2710
2749
|
continue;
|
|
2711
2750
|
}
|
|
2751
|
+
if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
|
|
2752
|
+
this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
|
|
2753
|
+
}
|
|
2712
2754
|
await this.tagIndex.touch(key);
|
|
2713
2755
|
await this.backfill(key, stored, layerIndex - 1);
|
|
2714
2756
|
resultsByKey.set(key, resolved.value);
|
|
@@ -2964,7 +3006,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2964
3006
|
await this.unsubscribeInvalidation?.();
|
|
2965
3007
|
await this.flushWriteBehindQueue();
|
|
2966
3008
|
await this.maintenance.waitForGenerationCleanup();
|
|
2967
|
-
|
|
3009
|
+
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
3010
|
+
this.backgroundRefreshAbort.set(key, true);
|
|
3011
|
+
}
|
|
3012
|
+
await Promise.allSettled(
|
|
3013
|
+
[...this.backgroundRefreshes.values()].map((promise) => {
|
|
3014
|
+
let timer;
|
|
3015
|
+
return Promise.race([
|
|
3016
|
+
promise,
|
|
3017
|
+
new Promise((resolve2) => {
|
|
3018
|
+
timer = setTimeout(resolve2, 5e3);
|
|
3019
|
+
timer.unref?.();
|
|
3020
|
+
})
|
|
3021
|
+
]).finally(() => {
|
|
3022
|
+
if (timer) clearTimeout(timer);
|
|
3023
|
+
});
|
|
3024
|
+
})
|
|
3025
|
+
);
|
|
3026
|
+
this.backgroundRefreshes.clear();
|
|
3027
|
+
this.backgroundRefreshAbort.clear();
|
|
2968
3028
|
this.maintenance.disposeWriteBehindTimer();
|
|
2969
3029
|
this.fetchRateLimiter.dispose();
|
|
2970
3030
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
@@ -2980,12 +3040,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2980
3040
|
await this.handleInvalidationMessage(message);
|
|
2981
3041
|
});
|
|
2982
3042
|
}
|
|
2983
|
-
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
3043
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
|
|
2984
3044
|
const fetchTask = async () => {
|
|
2985
|
-
const
|
|
2986
|
-
if (
|
|
2987
|
-
this.
|
|
2988
|
-
|
|
3045
|
+
const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
|
|
3046
|
+
if (shouldRecheckFreshLayers) {
|
|
3047
|
+
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
3048
|
+
if (secondHit.found) {
|
|
3049
|
+
this.metricsCollector.increment("hits");
|
|
3050
|
+
return secondHit.value;
|
|
3051
|
+
}
|
|
2989
3052
|
}
|
|
2990
3053
|
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2991
3054
|
};
|
|
@@ -2993,12 +3056,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2993
3056
|
if (!this.options.singleFlightCoordinator) {
|
|
2994
3057
|
return fetchTask();
|
|
2995
3058
|
}
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3059
|
+
try {
|
|
3060
|
+
return await this.options.singleFlightCoordinator.execute(
|
|
3061
|
+
key,
|
|
3062
|
+
this.resolveSingleFlightOptions(),
|
|
3063
|
+
fetchTask,
|
|
3064
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
3065
|
+
);
|
|
3066
|
+
} catch (error) {
|
|
3067
|
+
if (!this.isGracefulDegradationEnabled()) {
|
|
3068
|
+
throw error;
|
|
3069
|
+
}
|
|
3070
|
+
this.metricsCollector.increment("degradedOperations");
|
|
3071
|
+
this.logger.warn?.("single-flight-coordinator-degraded", { key, error: this.formatError(error) });
|
|
3072
|
+
this.emitError("single-flight", { key, degraded: true, error: this.formatError(error) });
|
|
3073
|
+
return fetchTask();
|
|
3074
|
+
}
|
|
3002
3075
|
};
|
|
3003
3076
|
if (this.options.stampedePrevention === false) {
|
|
3004
3077
|
return singleFlightTask();
|
|
@@ -3231,15 +3304,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3231
3304
|
}
|
|
3232
3305
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3233
3306
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3307
|
+
this.backgroundRefreshAbort.set(key, false);
|
|
3234
3308
|
const refresh = (async () => {
|
|
3235
3309
|
this.metricsCollector.increment("refreshes");
|
|
3236
3310
|
try {
|
|
3311
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3237
3312
|
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
3238
3313
|
} catch (error) {
|
|
3314
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3239
3315
|
this.metricsCollector.increment("refreshErrors");
|
|
3240
3316
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
3241
3317
|
} finally {
|
|
3242
3318
|
this.backgroundRefreshes.delete(key);
|
|
3319
|
+
this.backgroundRefreshAbort.delete(key);
|
|
3243
3320
|
}
|
|
3244
3321
|
})();
|
|
3245
3322
|
this.backgroundRefreshes.set(key, refresh);
|
|
@@ -3342,7 +3419,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3342
3419
|
timer.unref?.();
|
|
3343
3420
|
})
|
|
3344
3421
|
]);
|
|
3345
|
-
if (result && typeof result === "object" && "kind" in result) {
|
|
3422
|
+
if (result !== null && result !== void 0 && typeof result === "object" && "kind" in result) {
|
|
3346
3423
|
if (result.kind === "error") {
|
|
3347
3424
|
throw result.error;
|
|
3348
3425
|
}
|
|
@@ -3360,7 +3437,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3360
3437
|
}
|
|
3361
3438
|
async observeOperation(name, attributes, execute) {
|
|
3362
3439
|
const id = this.nextOperationId;
|
|
3363
|
-
this.nextOperationId
|
|
3440
|
+
this.nextOperationId = (this.nextOperationId + 1) % Number.MAX_SAFE_INTEGER;
|
|
3364
3441
|
this.emit("operation-start", { id, name, attributes });
|
|
3365
3442
|
try {
|
|
3366
3443
|
const result = await execute();
|
|
@@ -3596,6 +3673,7 @@ var RedisInvalidationBus = class {
|
|
|
3596
3673
|
logger;
|
|
3597
3674
|
handlers = /* @__PURE__ */ new Set();
|
|
3598
3675
|
sharedListener;
|
|
3676
|
+
subscribePromise;
|
|
3599
3677
|
constructor(options) {
|
|
3600
3678
|
this.publisher = options.publisher;
|
|
3601
3679
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
@@ -3603,15 +3681,27 @@ var RedisInvalidationBus = class {
|
|
|
3603
3681
|
this.logger = options.logger;
|
|
3604
3682
|
}
|
|
3605
3683
|
async subscribe(handler) {
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
await
|
|
3684
|
+
const previousPromise = this.subscribePromise;
|
|
3685
|
+
let resolveThis;
|
|
3686
|
+
this.subscribePromise = new Promise((resolve2) => {
|
|
3687
|
+
resolveThis = resolve2;
|
|
3688
|
+
});
|
|
3689
|
+
if (previousPromise) {
|
|
3690
|
+
await previousPromise;
|
|
3691
|
+
}
|
|
3692
|
+
try {
|
|
3693
|
+
if (this.handlers.size === 0) {
|
|
3694
|
+
const listener = (_channel, payload) => {
|
|
3695
|
+
void this.dispatchToHandlers(payload);
|
|
3696
|
+
};
|
|
3697
|
+
this.sharedListener = listener;
|
|
3698
|
+
this.subscriber.on("message", listener);
|
|
3699
|
+
await this.subscriber.subscribe(this.channel);
|
|
3700
|
+
}
|
|
3701
|
+
this.handlers.add(handler);
|
|
3702
|
+
} finally {
|
|
3703
|
+
resolveThis();
|
|
3613
3704
|
}
|
|
3614
|
-
this.handlers.add(handler);
|
|
3615
3705
|
return async () => {
|
|
3616
3706
|
this.handlers.delete(handler);
|
|
3617
3707
|
if (this.handlers.size === 0 && this.sharedListener) {
|
|
@@ -4037,10 +4127,21 @@ function normalizeUrl2(url) {
|
|
|
4037
4127
|
}
|
|
4038
4128
|
|
|
4039
4129
|
// src/integrations/opentelemetry.ts
|
|
4130
|
+
var MAX_SPANS = 1e4;
|
|
4040
4131
|
function createOpenTelemetryPlugin(cache, tracer) {
|
|
4041
4132
|
const spans = /* @__PURE__ */ new Map();
|
|
4042
4133
|
const onStart = (event) => {
|
|
4043
|
-
|
|
4134
|
+
try {
|
|
4135
|
+
if (spans.size >= MAX_SPANS) {
|
|
4136
|
+
const oldest = spans.keys().next().value;
|
|
4137
|
+
if (oldest !== void 0) {
|
|
4138
|
+
spans.get(oldest)?.end();
|
|
4139
|
+
spans.delete(oldest);
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
|
|
4143
|
+
} catch {
|
|
4144
|
+
}
|
|
4044
4145
|
};
|
|
4045
4146
|
const onEnd = (event) => {
|
|
4046
4147
|
const span = spans.get(event.id);
|
|
@@ -4048,12 +4149,15 @@ function createOpenTelemetryPlugin(cache, tracer) {
|
|
|
4048
4149
|
return;
|
|
4049
4150
|
}
|
|
4050
4151
|
spans.delete(event.id);
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4152
|
+
try {
|
|
4153
|
+
span.setAttribute?.("layercache.success", event.success);
|
|
4154
|
+
if (event.result) {
|
|
4155
|
+
span.setAttribute?.("layercache.result", event.result);
|
|
4156
|
+
}
|
|
4157
|
+
if (event.error !== void 0) {
|
|
4158
|
+
span.recordException?.(event.error);
|
|
4159
|
+
}
|
|
4160
|
+
} catch {
|
|
4057
4161
|
}
|
|
4058
4162
|
span.end();
|
|
4059
4163
|
};
|
|
@@ -4308,6 +4412,7 @@ var RedisLayer = class {
|
|
|
4308
4412
|
compression;
|
|
4309
4413
|
compressionThreshold;
|
|
4310
4414
|
decompressionMaxBytes;
|
|
4415
|
+
commandTimeoutMs;
|
|
4311
4416
|
disconnectOnDispose;
|
|
4312
4417
|
constructor(options) {
|
|
4313
4418
|
this.client = options.client;
|
|
@@ -4320,6 +4425,7 @@ var RedisLayer = class {
|
|
|
4320
4425
|
this.compression = options.compression;
|
|
4321
4426
|
this.compressionThreshold = options.compressionThreshold ?? 1024;
|
|
4322
4427
|
this.decompressionMaxBytes = options.decompressionMaxBytes ?? 64 * 1024 * 1024;
|
|
4428
|
+
this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
|
|
4323
4429
|
this.disconnectOnDispose = options.disconnectOnDispose ?? false;
|
|
4324
4430
|
}
|
|
4325
4431
|
async get(key) {
|
|
@@ -4327,7 +4433,7 @@ var RedisLayer = class {
|
|
|
4327
4433
|
return unwrapStoredValue(payload);
|
|
4328
4434
|
}
|
|
4329
4435
|
async getEntry(key) {
|
|
4330
|
-
const payload = await this.client.getBuffer(this.withPrefix(key));
|
|
4436
|
+
const payload = await this.runCommand(`get("${key}")`, () => this.client.getBuffer(this.withPrefix(key)));
|
|
4331
4437
|
if (payload === null) {
|
|
4332
4438
|
return null;
|
|
4333
4439
|
}
|
|
@@ -4341,7 +4447,7 @@ var RedisLayer = class {
|
|
|
4341
4447
|
for (const key of keys) {
|
|
4342
4448
|
pipeline.getBuffer(this.withPrefix(key));
|
|
4343
4449
|
}
|
|
4344
|
-
const results = await pipeline.exec();
|
|
4450
|
+
const results = await this.runCommand(`mget(${keys.length})`, () => pipeline.exec());
|
|
4345
4451
|
if (results === null) {
|
|
4346
4452
|
return keys.map(() => null);
|
|
4347
4453
|
}
|
|
@@ -4370,33 +4476,36 @@ var RedisLayer = class {
|
|
|
4370
4476
|
pipeline.set(normalizedKey, payload);
|
|
4371
4477
|
}
|
|
4372
4478
|
}
|
|
4373
|
-
await pipeline.exec();
|
|
4479
|
+
await this.runCommand(`mset(${entries.length})`, () => pipeline.exec());
|
|
4374
4480
|
}
|
|
4375
4481
|
async set(key, value, ttl = this.defaultTtl) {
|
|
4376
4482
|
const serialized = this.primarySerializer().serialize(value);
|
|
4377
4483
|
const payload = await this.encodePayload(serialized);
|
|
4378
4484
|
const normalizedKey = this.withPrefix(key);
|
|
4379
4485
|
if (ttl && ttl > 0) {
|
|
4380
|
-
await this.client.set(normalizedKey, payload, "EX", ttl);
|
|
4486
|
+
await this.runCommand(`set("${key}")`, () => this.client.set(normalizedKey, payload, "EX", ttl));
|
|
4381
4487
|
return;
|
|
4382
4488
|
}
|
|
4383
|
-
await this.client.set(normalizedKey, payload);
|
|
4489
|
+
await this.runCommand(`set("${key}")`, () => this.client.set(normalizedKey, payload));
|
|
4384
4490
|
}
|
|
4385
4491
|
async delete(key) {
|
|
4386
|
-
await this.client.del(this.withPrefix(key));
|
|
4492
|
+
await this.runCommand(`delete("${key}")`, () => this.client.del(this.withPrefix(key)));
|
|
4387
4493
|
}
|
|
4388
4494
|
async deleteMany(keys) {
|
|
4389
4495
|
if (keys.length === 0) {
|
|
4390
4496
|
return;
|
|
4391
4497
|
}
|
|
4392
|
-
await this.
|
|
4498
|
+
await this.runCommand(
|
|
4499
|
+
`deleteMany(${keys.length})`,
|
|
4500
|
+
() => this.client.del(...keys.map((key) => this.withPrefix(key)))
|
|
4501
|
+
);
|
|
4393
4502
|
}
|
|
4394
4503
|
async has(key) {
|
|
4395
|
-
const exists = await this.client.exists(this.withPrefix(key));
|
|
4504
|
+
const exists = await this.runCommand(`has("${key}")`, () => this.client.exists(this.withPrefix(key)));
|
|
4396
4505
|
return exists > 0;
|
|
4397
4506
|
}
|
|
4398
4507
|
async ttl(key) {
|
|
4399
|
-
const remaining = await this.client.ttl(this.withPrefix(key));
|
|
4508
|
+
const remaining = await this.runCommand(`ttl("${key}")`, () => this.client.ttl(this.withPrefix(key)));
|
|
4400
4509
|
if (remaining < 0) {
|
|
4401
4510
|
return null;
|
|
4402
4511
|
}
|
|
@@ -4404,13 +4513,16 @@ var RedisLayer = class {
|
|
|
4404
4513
|
}
|
|
4405
4514
|
async size() {
|
|
4406
4515
|
if (!this.prefix) {
|
|
4407
|
-
return this.client.dbsize();
|
|
4516
|
+
return this.runCommand("dbsize()", () => this.client.dbsize());
|
|
4408
4517
|
}
|
|
4409
4518
|
const pattern = `${this.prefix}*`;
|
|
4410
4519
|
let cursor = "0";
|
|
4411
4520
|
let count = 0;
|
|
4412
4521
|
do {
|
|
4413
|
-
const [nextCursor, keys] = await this.
|
|
4522
|
+
const [nextCursor, keys] = await this.runCommand(
|
|
4523
|
+
`scan("${pattern}")`,
|
|
4524
|
+
() => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
|
|
4525
|
+
);
|
|
4414
4526
|
cursor = nextCursor;
|
|
4415
4527
|
count += keys.length;
|
|
4416
4528
|
} while (cursor !== "0");
|
|
@@ -4418,7 +4530,7 @@ var RedisLayer = class {
|
|
|
4418
4530
|
}
|
|
4419
4531
|
async ping() {
|
|
4420
4532
|
try {
|
|
4421
|
-
return await this.client.ping() === "PONG";
|
|
4533
|
+
return await this.runCommand("ping()", () => this.client.ping()) === "PONG";
|
|
4422
4534
|
} catch {
|
|
4423
4535
|
return false;
|
|
4424
4536
|
}
|
|
@@ -4441,14 +4553,17 @@ var RedisLayer = class {
|
|
|
4441
4553
|
const pattern = `${this.prefix}*`;
|
|
4442
4554
|
let cursor = "0";
|
|
4443
4555
|
do {
|
|
4444
|
-
const [nextCursor, keys] = await this.
|
|
4556
|
+
const [nextCursor, keys] = await this.runCommand(
|
|
4557
|
+
`scan("${pattern}")`,
|
|
4558
|
+
() => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
|
|
4559
|
+
);
|
|
4445
4560
|
cursor = nextCursor;
|
|
4446
4561
|
if (keys.length === 0) {
|
|
4447
4562
|
continue;
|
|
4448
4563
|
}
|
|
4449
4564
|
for (let i = 0; i < keys.length; i += BATCH_DELETE_SIZE) {
|
|
4450
4565
|
const batch = keys.slice(i, i + BATCH_DELETE_SIZE);
|
|
4451
|
-
await this.client.del(...batch);
|
|
4566
|
+
await this.runCommand(`clear-del(${batch.length})`, () => this.client.del(...batch));
|
|
4452
4567
|
}
|
|
4453
4568
|
} while (cursor !== "0");
|
|
4454
4569
|
}
|
|
@@ -4464,7 +4579,10 @@ var RedisLayer = class {
|
|
|
4464
4579
|
const pattern = `${this.prefix}*`;
|
|
4465
4580
|
let cursor = "0";
|
|
4466
4581
|
do {
|
|
4467
|
-
const [nextCursor, keys] = await this.
|
|
4582
|
+
const [nextCursor, keys] = await this.runCommand(
|
|
4583
|
+
`scan("${pattern}")`,
|
|
4584
|
+
() => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
|
|
4585
|
+
);
|
|
4468
4586
|
cursor = nextCursor;
|
|
4469
4587
|
for (const key of keys) {
|
|
4470
4588
|
await visitor(this.prefix ? key.slice(this.prefix.length) : key);
|
|
@@ -4475,7 +4593,10 @@ var RedisLayer = class {
|
|
|
4475
4593
|
const matches = [];
|
|
4476
4594
|
let cursor = "0";
|
|
4477
4595
|
do {
|
|
4478
|
-
const [nextCursor, keys] = await this.
|
|
4596
|
+
const [nextCursor, keys] = await this.runCommand(
|
|
4597
|
+
`scan("${pattern}")`,
|
|
4598
|
+
() => this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount)
|
|
4599
|
+
);
|
|
4479
4600
|
cursor = nextCursor;
|
|
4480
4601
|
matches.push(...keys);
|
|
4481
4602
|
} while (cursor !== "0");
|
|
@@ -4507,7 +4628,7 @@ var RedisLayer = class {
|
|
|
4507
4628
|
}
|
|
4508
4629
|
async deleteCorruptedKey(key) {
|
|
4509
4630
|
try {
|
|
4510
|
-
await this.client.del(this.withPrefix(key));
|
|
4631
|
+
await this.runCommand(`deleteCorrupted("${key}")`, () => this.client.del(this.withPrefix(key)));
|
|
4511
4632
|
} catch (deleteError) {
|
|
4512
4633
|
console.warn(`[layercache] RedisLayer: failed to delete corrupted key "${key}"`, deleteError);
|
|
4513
4634
|
}
|
|
@@ -4515,12 +4636,15 @@ var RedisLayer = class {
|
|
|
4515
4636
|
async rewriteWithPrimarySerializer(key, value) {
|
|
4516
4637
|
const serialized = this.primarySerializer().serialize(value);
|
|
4517
4638
|
const payload = await this.encodePayload(serialized);
|
|
4518
|
-
const ttl = await this.client.ttl(this.withPrefix(key));
|
|
4639
|
+
const ttl = await this.runCommand(`rewrite-ttl("${key}")`, () => this.client.ttl(this.withPrefix(key)));
|
|
4519
4640
|
if (ttl > 0) {
|
|
4520
|
-
await this.
|
|
4641
|
+
await this.runCommand(
|
|
4642
|
+
`rewrite-set("${key}")`,
|
|
4643
|
+
() => this.client.set(this.withPrefix(key), payload, "EX", ttl)
|
|
4644
|
+
);
|
|
4521
4645
|
return;
|
|
4522
4646
|
}
|
|
4523
|
-
await this.client.set(this.withPrefix(key), payload);
|
|
4647
|
+
await this.runCommand(`rewrite-set("${key}")`, () => this.client.set(this.withPrefix(key), payload));
|
|
4524
4648
|
}
|
|
4525
4649
|
primarySerializer() {
|
|
4526
4650
|
const serializer = this.serializers[0];
|
|
@@ -4616,10 +4740,39 @@ var RedisLayer = class {
|
|
|
4616
4740
|
source.pipe(decompressor);
|
|
4617
4741
|
});
|
|
4618
4742
|
}
|
|
4743
|
+
normalizeCommandTimeoutMs(value) {
|
|
4744
|
+
if (value === void 0) {
|
|
4745
|
+
return void 0;
|
|
4746
|
+
}
|
|
4747
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
4748
|
+
throw new Error("RedisLayer.commandTimeoutMs must be a positive number.");
|
|
4749
|
+
}
|
|
4750
|
+
return value;
|
|
4751
|
+
}
|
|
4752
|
+
async runCommand(operation, command) {
|
|
4753
|
+
const promise = command();
|
|
4754
|
+
if (!this.commandTimeoutMs) {
|
|
4755
|
+
return promise;
|
|
4756
|
+
}
|
|
4757
|
+
let timer;
|
|
4758
|
+
return Promise.race([
|
|
4759
|
+
promise,
|
|
4760
|
+
new Promise((_, reject) => {
|
|
4761
|
+
timer = setTimeout(() => {
|
|
4762
|
+
reject(new Error(`RedisLayer command ${operation} timed out after ${this.commandTimeoutMs}ms.`));
|
|
4763
|
+
}, this.commandTimeoutMs);
|
|
4764
|
+
timer.unref?.();
|
|
4765
|
+
})
|
|
4766
|
+
]).finally(() => {
|
|
4767
|
+
if (timer) {
|
|
4768
|
+
clearTimeout(timer);
|
|
4769
|
+
}
|
|
4770
|
+
});
|
|
4771
|
+
}
|
|
4619
4772
|
};
|
|
4620
4773
|
|
|
4621
4774
|
// src/layers/DiskLayer.ts
|
|
4622
|
-
var
|
|
4775
|
+
var import_node_crypto2 = require("crypto");
|
|
4623
4776
|
var import_node_fs2 = require("fs");
|
|
4624
4777
|
var import_node_path2 = require("path");
|
|
4625
4778
|
var FILE_SCAN_CONCURRENCY = 32;
|
|
@@ -4672,7 +4825,7 @@ var DiskLayer = class {
|
|
|
4672
4825
|
};
|
|
4673
4826
|
const payload = this.serializer.serialize(entry);
|
|
4674
4827
|
const targetPath = this.keyToPath(key);
|
|
4675
|
-
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${
|
|
4828
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${(0, import_node_crypto2.randomBytes)(8).toString("hex")}.tmp`;
|
|
4676
4829
|
try {
|
|
4677
4830
|
await import_node_fs2.promises.writeFile(tempPath, payload);
|
|
4678
4831
|
await import_node_fs2.promises.rename(tempPath, targetPath);
|
|
@@ -4772,7 +4925,7 @@ var DiskLayer = class {
|
|
|
4772
4925
|
async dispose() {
|
|
4773
4926
|
}
|
|
4774
4927
|
keyToPath(key) {
|
|
4775
|
-
const hash = (0,
|
|
4928
|
+
const hash = (0, import_node_crypto2.createHash)("sha256").update(key).digest("hex");
|
|
4776
4929
|
return (0, import_node_path2.join)(this.directory, `${hash}.lc`);
|
|
4777
4930
|
}
|
|
4778
4931
|
resolveDirectory(directory) {
|
|
@@ -5050,7 +5203,7 @@ var MsgpackSerializer = class {
|
|
|
5050
5203
|
};
|
|
5051
5204
|
|
|
5052
5205
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
5053
|
-
var
|
|
5206
|
+
var import_node_crypto3 = require("crypto");
|
|
5054
5207
|
var RELEASE_SCRIPT = `
|
|
5055
5208
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
5056
5209
|
return redis.call("del", KEYS[1])
|
|
@@ -5066,14 +5219,19 @@ return 0
|
|
|
5066
5219
|
var RedisSingleFlightCoordinator = class {
|
|
5067
5220
|
client;
|
|
5068
5221
|
prefix;
|
|
5222
|
+
commandTimeoutMs;
|
|
5069
5223
|
constructor(options) {
|
|
5070
5224
|
this.client = options.client;
|
|
5071
5225
|
this.prefix = options.prefix ?? "layercache:singleflight";
|
|
5226
|
+
this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
|
|
5072
5227
|
}
|
|
5073
5228
|
async execute(key, options, worker, waiter) {
|
|
5074
5229
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
5075
|
-
const token = (0,
|
|
5076
|
-
const acquired = await this.
|
|
5230
|
+
const token = (0, import_node_crypto3.randomUUID)();
|
|
5231
|
+
const acquired = await this.runCommand(
|
|
5232
|
+
`acquire("${key}")`,
|
|
5233
|
+
() => this.client.set(lockKey, token, "PX", options.leaseMs, "NX")
|
|
5234
|
+
);
|
|
5077
5235
|
if (acquired === "OK") {
|
|
5078
5236
|
const renewTimer = this.startLeaseRenewal(lockKey, token, options);
|
|
5079
5237
|
try {
|
|
@@ -5082,7 +5240,7 @@ var RedisSingleFlightCoordinator = class {
|
|
|
5082
5240
|
if (renewTimer) {
|
|
5083
5241
|
clearInterval(renewTimer);
|
|
5084
5242
|
}
|
|
5085
|
-
await this.client.eval(RELEASE_SCRIPT, 1, lockKey, token);
|
|
5243
|
+
await this.runCommand(`release("${key}")`, () => this.client.eval(RELEASE_SCRIPT, 1, lockKey, token));
|
|
5086
5244
|
}
|
|
5087
5245
|
}
|
|
5088
5246
|
return waiter();
|
|
@@ -5093,11 +5251,45 @@ var RedisSingleFlightCoordinator = class {
|
|
|
5093
5251
|
return void 0;
|
|
5094
5252
|
}
|
|
5095
5253
|
const timer = setInterval(() => {
|
|
5096
|
-
void this.
|
|
5254
|
+
void this.runCommand(
|
|
5255
|
+
`renew("${lockKey}")`,
|
|
5256
|
+
() => this.client.eval(RENEW_SCRIPT, 1, lockKey, token, String(options.leaseMs))
|
|
5257
|
+
).catch(() => void 0);
|
|
5097
5258
|
}, renewIntervalMs);
|
|
5098
5259
|
timer.unref?.();
|
|
5099
5260
|
return timer;
|
|
5100
5261
|
}
|
|
5262
|
+
normalizeCommandTimeoutMs(value) {
|
|
5263
|
+
if (value === void 0) {
|
|
5264
|
+
return void 0;
|
|
5265
|
+
}
|
|
5266
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
5267
|
+
throw new Error("RedisSingleFlightCoordinator.commandTimeoutMs must be a positive number.");
|
|
5268
|
+
}
|
|
5269
|
+
return value;
|
|
5270
|
+
}
|
|
5271
|
+
async runCommand(operation, command) {
|
|
5272
|
+
const promise = command();
|
|
5273
|
+
if (!this.commandTimeoutMs) {
|
|
5274
|
+
return promise;
|
|
5275
|
+
}
|
|
5276
|
+
let timer;
|
|
5277
|
+
return Promise.race([
|
|
5278
|
+
promise,
|
|
5279
|
+
new Promise((_, reject) => {
|
|
5280
|
+
timer = setTimeout(() => {
|
|
5281
|
+
reject(
|
|
5282
|
+
new Error(`RedisSingleFlightCoordinator command ${operation} timed out after ${this.commandTimeoutMs}ms.`)
|
|
5283
|
+
);
|
|
5284
|
+
}, this.commandTimeoutMs);
|
|
5285
|
+
timer.unref?.();
|
|
5286
|
+
})
|
|
5287
|
+
]).finally(() => {
|
|
5288
|
+
if (timer) {
|
|
5289
|
+
clearTimeout(timer);
|
|
5290
|
+
}
|
|
5291
|
+
});
|
|
5292
|
+
}
|
|
5101
5293
|
};
|
|
5102
5294
|
|
|
5103
5295
|
// src/metrics/PrometheusExporter.ts
|