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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "layercache",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Production-ready multi-layer caching for Node.js. Stack memory + Redis + disk behind one API with stampede prevention, tag invalidation, stale serving, and full observability.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cache",
|
|
@@ -74,7 +74,15 @@
|
|
|
74
74
|
"lint": "biome check .",
|
|
75
75
|
"lint:fix": "biome check --write .",
|
|
76
76
|
"bench:latency": "tsx benchmarks/latency.ts",
|
|
77
|
-
"bench:stampede": "tsx benchmarks/stampede.ts"
|
|
77
|
+
"bench:stampede": "tsx benchmarks/stampede.ts",
|
|
78
|
+
"bench:direct": "tsx benchmarks/direct.ts",
|
|
79
|
+
"bench:http": "tsx benchmarks/http.ts",
|
|
80
|
+
"bench:edge": "tsx benchmarks/edge.ts",
|
|
81
|
+
"bench:slow-redis": "tsx benchmarks/slow-redis.ts",
|
|
82
|
+
"bench:memory-pressure": "tsx benchmarks/memory-pressure.ts",
|
|
83
|
+
"bench:queue-amplification": "tsx benchmarks/queue-amplification.ts",
|
|
84
|
+
"bench:multi-process-fanout": "tsx benchmarks/multi-process-fanout.ts",
|
|
85
|
+
"bench:all": "npm run bench:direct && npm run bench:edge && npm run bench:slow-redis && npm run bench:queue-amplification && npm run bench:http && npm run bench:multi-process-fanout"
|
|
78
86
|
},
|
|
79
87
|
"dependencies": {
|
|
80
88
|
"@msgpack/msgpack": "^3.0.0",
|
|
@@ -90,8 +98,10 @@
|
|
|
90
98
|
"@biomejs/biome": "^1.9.4",
|
|
91
99
|
"@nestjs/common": "^11.1.0",
|
|
92
100
|
"@nestjs/core": "^11.1.0",
|
|
101
|
+
"@types/autocannon": "^7.12.7",
|
|
93
102
|
"@types/node": "^22.15.2",
|
|
94
103
|
"@vitest/coverage-v8": "^4.1.2",
|
|
104
|
+
"autocannon": "^8.0.0",
|
|
95
105
|
"ioredis": "^5.6.1",
|
|
96
106
|
"ioredis-mock": "^8.13.0",
|
|
97
107
|
"reflect-metadata": "^0.2.2",
|
|
@@ -724,7 +724,7 @@ function normalizeForSerialization(value) {
|
|
|
724
724
|
}
|
|
725
725
|
function serializeKeyPart(value) {
|
|
726
726
|
if (typeof value === "string") {
|
|
727
|
-
return `s:${value}`;
|
|
727
|
+
return `s:${value.replace(/%/g, "%25").replace(/:/g, "%3A")}`;
|
|
728
728
|
}
|
|
729
729
|
if (typeof value === "number") {
|
|
730
730
|
return `n:${value}`;
|
|
@@ -1113,6 +1113,7 @@ var CacheStackLayerWriter = class {
|
|
|
1113
1113
|
}
|
|
1114
1114
|
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
1115
1115
|
const failures = results.filter((result) => result.status === "rejected");
|
|
1116
|
+
const degraded = results.filter((result) => result.status === "fulfilled");
|
|
1116
1117
|
if (failures.length === 0) {
|
|
1117
1118
|
return;
|
|
1118
1119
|
}
|
|
@@ -1291,6 +1292,7 @@ function planFreshReadPolicies({
|
|
|
1291
1292
|
}
|
|
1292
1293
|
|
|
1293
1294
|
// ../../src/internal/CacheStackSnapshotManager.ts
|
|
1295
|
+
var import_node_crypto = require("crypto");
|
|
1294
1296
|
var import_node_fs = require("fs");
|
|
1295
1297
|
var import_node_path = __toESM(require("path"), 1);
|
|
1296
1298
|
|
|
@@ -1390,6 +1392,42 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
|
1390
1392
|
return Buffer.concat(chunks).toString("utf8");
|
|
1391
1393
|
}
|
|
1392
1394
|
|
|
1395
|
+
// ../../src/internal/StructuredDataSanitizer.ts
|
|
1396
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1397
|
+
function sanitizeStructuredData(value, options) {
|
|
1398
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
1399
|
+
}
|
|
1400
|
+
function sanitizeValue(value, depth, state, options) {
|
|
1401
|
+
state.count += 1;
|
|
1402
|
+
if (state.count > options.maxNodes) {
|
|
1403
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
1404
|
+
}
|
|
1405
|
+
if (depth > options.maxDepth) {
|
|
1406
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
1407
|
+
}
|
|
1408
|
+
if (Array.isArray(value)) {
|
|
1409
|
+
const sanitized2 = [];
|
|
1410
|
+
for (const entry of value) {
|
|
1411
|
+
sanitized2.push(sanitizeValue(entry, depth + 1, state, options));
|
|
1412
|
+
}
|
|
1413
|
+
return sanitized2;
|
|
1414
|
+
}
|
|
1415
|
+
if (!isPlainObject(value)) {
|
|
1416
|
+
return value;
|
|
1417
|
+
}
|
|
1418
|
+
const sanitized = options.createObject?.() ?? {};
|
|
1419
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1420
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
1424
|
+
}
|
|
1425
|
+
return sanitized;
|
|
1426
|
+
}
|
|
1427
|
+
function isPlainObject(value) {
|
|
1428
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1393
1431
|
// ../../src/internal/CacheStackSnapshotManager.ts
|
|
1394
1432
|
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1395
1433
|
var CacheStackSnapshotManager = class {
|
|
@@ -1414,7 +1452,16 @@ var CacheStackSnapshotManager = class {
|
|
|
1414
1452
|
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
1415
1453
|
await Promise.all(
|
|
1416
1454
|
batch.map(async (entry) => {
|
|
1417
|
-
await Promise.all(
|
|
1455
|
+
await Promise.all(
|
|
1456
|
+
this.options.layers.map(async (layer) => {
|
|
1457
|
+
if (this.options.shouldSkipLayer(layer)) return;
|
|
1458
|
+
try {
|
|
1459
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1460
|
+
} catch (error) {
|
|
1461
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1462
|
+
}
|
|
1463
|
+
})
|
|
1464
|
+
);
|
|
1418
1465
|
await this.options.tagIndex.touch(entry.key);
|
|
1419
1466
|
})
|
|
1420
1467
|
);
|
|
@@ -1424,7 +1471,7 @@ var CacheStackSnapshotManager = class {
|
|
|
1424
1471
|
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1425
1472
|
const tempPath = import_node_path.default.join(
|
|
1426
1473
|
import_node_path.default.dirname(targetPath),
|
|
1427
|
-
`.layercache-${process.pid}-${Date.now()}-${
|
|
1474
|
+
`.layercache-${process.pid}-${Date.now()}-${(0, import_node_crypto.randomBytes)(8).toString("hex")}.tmp`
|
|
1428
1475
|
);
|
|
1429
1476
|
let handle;
|
|
1430
1477
|
try {
|
|
@@ -1524,7 +1571,13 @@ var CacheStackSnapshotManager = class {
|
|
|
1524
1571
|
});
|
|
1525
1572
|
}
|
|
1526
1573
|
sanitizeSnapshotValue(value) {
|
|
1527
|
-
|
|
1574
|
+
const roundTripped = this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
1575
|
+
return sanitizeStructuredData(roundTripped, {
|
|
1576
|
+
label: "Snapshot value",
|
|
1577
|
+
maxDepth: 64,
|
|
1578
|
+
maxNodes: 1e4,
|
|
1579
|
+
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
1580
|
+
});
|
|
1528
1581
|
}
|
|
1529
1582
|
};
|
|
1530
1583
|
|
|
@@ -1904,7 +1957,13 @@ var FetchRateLimiter = class {
|
|
|
1904
1957
|
this.pendingBuckets.add(next.bucketKey);
|
|
1905
1958
|
}
|
|
1906
1959
|
this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
|
|
1907
|
-
this.
|
|
1960
|
+
if (!this.drainTimer) {
|
|
1961
|
+
this.drainTimer = setTimeout(() => {
|
|
1962
|
+
this.drainTimer = void 0;
|
|
1963
|
+
this.drain();
|
|
1964
|
+
}, 0);
|
|
1965
|
+
this.drainTimer.unref?.();
|
|
1966
|
+
}
|
|
1908
1967
|
});
|
|
1909
1968
|
}
|
|
1910
1969
|
}
|
|
@@ -1946,6 +2005,9 @@ var FetchRateLimiter = class {
|
|
|
1946
2005
|
}
|
|
1947
2006
|
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1948
2007
|
this.evictIdleBuckets();
|
|
2008
|
+
if (this.buckets.size >= MAX_BUCKETS) {
|
|
2009
|
+
throw new Error(`FetchRateLimiter bucket limit (${MAX_BUCKETS}) exceeded.`);
|
|
2010
|
+
}
|
|
1949
2011
|
}
|
|
1950
2012
|
const bucket = { active: 0, startedAt: [] };
|
|
1951
2013
|
this.buckets.set(bucketKey, bucket);
|
|
@@ -2424,38 +2486,6 @@ var TagIndex = class {
|
|
|
2424
2486
|
}
|
|
2425
2487
|
};
|
|
2426
2488
|
|
|
2427
|
-
// ../../src/internal/StructuredDataSanitizer.ts
|
|
2428
|
-
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2429
|
-
function sanitizeStructuredData(value, options) {
|
|
2430
|
-
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
2431
|
-
}
|
|
2432
|
-
function sanitizeValue(value, depth, state, options) {
|
|
2433
|
-
state.count += 1;
|
|
2434
|
-
if (state.count > options.maxNodes) {
|
|
2435
|
-
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
2436
|
-
}
|
|
2437
|
-
if (depth > options.maxDepth) {
|
|
2438
|
-
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
2439
|
-
}
|
|
2440
|
-
if (Array.isArray(value)) {
|
|
2441
|
-
return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
|
|
2442
|
-
}
|
|
2443
|
-
if (!isPlainObject(value)) {
|
|
2444
|
-
return value;
|
|
2445
|
-
}
|
|
2446
|
-
const sanitized = options.createObject?.() ?? {};
|
|
2447
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
2448
|
-
if (DANGEROUS_KEYS.has(key)) {
|
|
2449
|
-
continue;
|
|
2450
|
-
}
|
|
2451
|
-
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
2452
|
-
}
|
|
2453
|
-
return sanitized;
|
|
2454
|
-
}
|
|
2455
|
-
function isPlainObject(value) {
|
|
2456
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
2489
|
// ../../src/serialization/JsonSerializer.ts
|
|
2460
2490
|
var JsonSerializer = class {
|
|
2461
2491
|
serialize(value) {
|
|
@@ -2473,27 +2503,34 @@ var JsonSerializer = class {
|
|
|
2473
2503
|
|
|
2474
2504
|
// ../../src/stampede/StampedeGuard.ts
|
|
2475
2505
|
var StampedeGuard = class {
|
|
2476
|
-
|
|
2506
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
2477
2507
|
async execute(key, task) {
|
|
2478
|
-
const
|
|
2508
|
+
const existing = this.inFlight.get(key);
|
|
2509
|
+
if (existing) {
|
|
2510
|
+
existing.references += 1;
|
|
2511
|
+
try {
|
|
2512
|
+
return await existing.promise;
|
|
2513
|
+
} finally {
|
|
2514
|
+
this.releaseEntry(key, existing);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
const entry = {
|
|
2518
|
+
promise: Promise.resolve().then(task),
|
|
2519
|
+
references: 1
|
|
2520
|
+
};
|
|
2521
|
+
this.inFlight.set(key, entry);
|
|
2479
2522
|
try {
|
|
2480
|
-
return await entry.
|
|
2523
|
+
return await entry.promise;
|
|
2481
2524
|
} finally {
|
|
2482
|
-
|
|
2483
|
-
const current = this.mutexes.get(key);
|
|
2484
|
-
if (current === entry && entry.references === 0 && !entry.mutex.isLocked()) {
|
|
2485
|
-
this.mutexes.delete(key);
|
|
2486
|
-
}
|
|
2525
|
+
this.releaseEntry(key, entry);
|
|
2487
2526
|
}
|
|
2488
2527
|
}
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
this.
|
|
2528
|
+
releaseEntry(key, entry) {
|
|
2529
|
+
entry.references -= 1;
|
|
2530
|
+
const current = this.inFlight.get(key);
|
|
2531
|
+
if (current === entry && entry.references === 0) {
|
|
2532
|
+
this.inFlight.delete(key);
|
|
2494
2533
|
}
|
|
2495
|
-
entry.references += 1;
|
|
2496
|
-
return entry;
|
|
2497
2534
|
}
|
|
2498
2535
|
};
|
|
2499
2536
|
|
|
@@ -2619,6 +2656,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2619
2656
|
tagIndex: this.tagIndex,
|
|
2620
2657
|
snapshotSerializer: this.snapshotSerializer,
|
|
2621
2658
|
readLayerEntry: this.readLayerEntry.bind(this),
|
|
2659
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2660
|
+
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
2622
2661
|
qualifyKey: this.qualifyKey.bind(this),
|
|
2623
2662
|
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
2624
2663
|
validateCacheKey,
|
|
@@ -2643,6 +2682,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2643
2682
|
layerWriter;
|
|
2644
2683
|
snapshots;
|
|
2645
2684
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
2685
|
+
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
2646
2686
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2647
2687
|
maintenance = new CacheStackMaintenance();
|
|
2648
2688
|
ttlResolver;
|
|
@@ -2705,7 +2745,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2705
2745
|
if (!fetcher) {
|
|
2706
2746
|
return null;
|
|
2707
2747
|
}
|
|
2708
|
-
return this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
2748
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
|
|
2709
2749
|
}
|
|
2710
2750
|
/**
|
|
2711
2751
|
* Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
|
|
@@ -2887,7 +2927,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2887
2927
|
}
|
|
2888
2928
|
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2889
2929
|
const layer = this.layers[layerIndex];
|
|
2890
|
-
if (!layer) continue;
|
|
2930
|
+
if (!layer || this.shouldSkipLayer(layer)) continue;
|
|
2891
2931
|
const keys = [...pending];
|
|
2892
2932
|
if (keys.length === 0) {
|
|
2893
2933
|
break;
|
|
@@ -2904,6 +2944,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2904
2944
|
await layer.delete(key);
|
|
2905
2945
|
continue;
|
|
2906
2946
|
}
|
|
2947
|
+
if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
|
|
2948
|
+
this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
|
|
2949
|
+
}
|
|
2907
2950
|
await this.tagIndex.touch(key);
|
|
2908
2951
|
await this.backfill(key, stored, layerIndex - 1);
|
|
2909
2952
|
resultsByKey.set(key, resolved.value);
|
|
@@ -3159,7 +3202,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3159
3202
|
await this.unsubscribeInvalidation?.();
|
|
3160
3203
|
await this.flushWriteBehindQueue();
|
|
3161
3204
|
await this.maintenance.waitForGenerationCleanup();
|
|
3162
|
-
|
|
3205
|
+
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
3206
|
+
this.backgroundRefreshAbort.set(key, true);
|
|
3207
|
+
}
|
|
3208
|
+
await Promise.allSettled(
|
|
3209
|
+
[...this.backgroundRefreshes.values()].map((promise) => {
|
|
3210
|
+
let timer;
|
|
3211
|
+
return Promise.race([
|
|
3212
|
+
promise,
|
|
3213
|
+
new Promise((resolve) => {
|
|
3214
|
+
timer = setTimeout(resolve, 5e3);
|
|
3215
|
+
timer.unref?.();
|
|
3216
|
+
})
|
|
3217
|
+
]).finally(() => {
|
|
3218
|
+
if (timer) clearTimeout(timer);
|
|
3219
|
+
});
|
|
3220
|
+
})
|
|
3221
|
+
);
|
|
3222
|
+
this.backgroundRefreshes.clear();
|
|
3223
|
+
this.backgroundRefreshAbort.clear();
|
|
3163
3224
|
this.maintenance.disposeWriteBehindTimer();
|
|
3164
3225
|
this.fetchRateLimiter.dispose();
|
|
3165
3226
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
@@ -3175,12 +3236,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3175
3236
|
await this.handleInvalidationMessage(message);
|
|
3176
3237
|
});
|
|
3177
3238
|
}
|
|
3178
|
-
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
3239
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
|
|
3179
3240
|
const fetchTask = async () => {
|
|
3180
|
-
const
|
|
3181
|
-
if (
|
|
3182
|
-
this.
|
|
3183
|
-
|
|
3241
|
+
const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
|
|
3242
|
+
if (shouldRecheckFreshLayers) {
|
|
3243
|
+
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
3244
|
+
if (secondHit.found) {
|
|
3245
|
+
this.metricsCollector.increment("hits");
|
|
3246
|
+
return secondHit.value;
|
|
3247
|
+
}
|
|
3184
3248
|
}
|
|
3185
3249
|
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
3186
3250
|
};
|
|
@@ -3188,12 +3252,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3188
3252
|
if (!this.options.singleFlightCoordinator) {
|
|
3189
3253
|
return fetchTask();
|
|
3190
3254
|
}
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3255
|
+
try {
|
|
3256
|
+
return await this.options.singleFlightCoordinator.execute(
|
|
3257
|
+
key,
|
|
3258
|
+
this.resolveSingleFlightOptions(),
|
|
3259
|
+
fetchTask,
|
|
3260
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
3261
|
+
);
|
|
3262
|
+
} catch (error) {
|
|
3263
|
+
if (!this.isGracefulDegradationEnabled()) {
|
|
3264
|
+
throw error;
|
|
3265
|
+
}
|
|
3266
|
+
this.metricsCollector.increment("degradedOperations");
|
|
3267
|
+
this.logger.warn?.("single-flight-coordinator-degraded", { key, error: this.formatError(error) });
|
|
3268
|
+
this.emitError("single-flight", { key, degraded: true, error: this.formatError(error) });
|
|
3269
|
+
return fetchTask();
|
|
3270
|
+
}
|
|
3197
3271
|
};
|
|
3198
3272
|
if (this.options.stampedePrevention === false) {
|
|
3199
3273
|
return singleFlightTask();
|
|
@@ -3426,15 +3500,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3426
3500
|
}
|
|
3427
3501
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3428
3502
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3503
|
+
this.backgroundRefreshAbort.set(key, false);
|
|
3429
3504
|
const refresh = (async () => {
|
|
3430
3505
|
this.metricsCollector.increment("refreshes");
|
|
3431
3506
|
try {
|
|
3507
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3432
3508
|
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
3433
3509
|
} catch (error) {
|
|
3510
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3434
3511
|
this.metricsCollector.increment("refreshErrors");
|
|
3435
3512
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
3436
3513
|
} finally {
|
|
3437
3514
|
this.backgroundRefreshes.delete(key);
|
|
3515
|
+
this.backgroundRefreshAbort.delete(key);
|
|
3438
3516
|
}
|
|
3439
3517
|
})();
|
|
3440
3518
|
this.backgroundRefreshes.set(key, refresh);
|
|
@@ -3537,7 +3615,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3537
3615
|
timer.unref?.();
|
|
3538
3616
|
})
|
|
3539
3617
|
]);
|
|
3540
|
-
if (result && typeof result === "object" && "kind" in result) {
|
|
3618
|
+
if (result !== null && result !== void 0 && typeof result === "object" && "kind" in result) {
|
|
3541
3619
|
if (result.kind === "error") {
|
|
3542
3620
|
throw result.error;
|
|
3543
3621
|
}
|
|
@@ -3555,7 +3633,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3555
3633
|
}
|
|
3556
3634
|
async observeOperation(name, attributes, execute) {
|
|
3557
3635
|
const id = this.nextOperationId;
|
|
3558
|
-
this.nextOperationId
|
|
3636
|
+
this.nextOperationId = (this.nextOperationId + 1) % Number.MAX_SAFE_INTEGER;
|
|
3559
3637
|
this.emit("operation-start", { id, name, attributes });
|
|
3560
3638
|
try {
|
|
3561
3639
|
const result = await execute();
|
|
@@ -443,6 +443,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
443
443
|
private readonly layerWriter;
|
|
444
444
|
private readonly snapshots;
|
|
445
445
|
private readonly backgroundRefreshes;
|
|
446
|
+
private readonly backgroundRefreshAbort;
|
|
446
447
|
private readonly layerDegradedUntil;
|
|
447
448
|
private readonly maintenance;
|
|
448
449
|
private readonly ttlResolver;
|
|
@@ -443,6 +443,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
443
443
|
private readonly layerWriter;
|
|
444
444
|
private readonly snapshots;
|
|
445
445
|
private readonly backgroundRefreshes;
|
|
446
|
+
private readonly backgroundRefreshAbort;
|
|
446
447
|
private readonly layerDegradedUntil;
|
|
447
448
|
private readonly maintenance;
|
|
448
449
|
private readonly ttlResolver;
|