layercache 3.0.0 → 3.1.1
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 +7 -3
- package/dist/{chunk-NBMG7DHT.js → chunk-L6L7QXYF.js} +10 -1
- package/dist/{chunk-5CIBABDH.js → chunk-XMUT66SH.js} +54 -71
- package/dist/cli.js +1 -1
- package/dist/{edge-BDyuPmIq.d.cts → edge-LBUuZAdr.d.cts} +56 -2
- package/dist/{edge-BDyuPmIq.d.ts → edge-LBUuZAdr.d.ts} +56 -2
- package/dist/edge.cjs +53 -71
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +1 -1
- package/dist/index.cjs +274 -141
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +213 -71
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -98,39 +98,6 @@ function cloneNamespaceMetrics(metrics) {
|
|
|
98
98
|
)
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
|
-
function diffNamespaceMetrics(before, after) {
|
|
102
|
-
const latencyByLayer = Object.fromEntries(
|
|
103
|
-
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
104
|
-
layer,
|
|
105
|
-
{
|
|
106
|
-
avgMs: value.avgMs,
|
|
107
|
-
maxMs: value.maxMs,
|
|
108
|
-
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
109
|
-
}
|
|
110
|
-
])
|
|
111
|
-
);
|
|
112
|
-
return {
|
|
113
|
-
hits: after.hits - before.hits,
|
|
114
|
-
misses: after.misses - before.misses,
|
|
115
|
-
fetches: after.fetches - before.fetches,
|
|
116
|
-
sets: after.sets - before.sets,
|
|
117
|
-
deletes: after.deletes - before.deletes,
|
|
118
|
-
backfills: after.backfills - before.backfills,
|
|
119
|
-
invalidations: after.invalidations - before.invalidations,
|
|
120
|
-
staleHits: after.staleHits - before.staleHits,
|
|
121
|
-
refreshes: after.refreshes - before.refreshes,
|
|
122
|
-
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
123
|
-
writeFailures: after.writeFailures - before.writeFailures,
|
|
124
|
-
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
125
|
-
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
126
|
-
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
127
|
-
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
128
|
-
hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
|
|
129
|
-
missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
|
|
130
|
-
latencyByLayer,
|
|
131
|
-
resetAt: after.resetAt
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
101
|
function addNamespaceMetrics(base, delta) {
|
|
135
102
|
return {
|
|
136
103
|
hits: base.hits + delta.hits,
|
|
@@ -166,14 +133,6 @@ function computeNamespaceHitRate(metrics) {
|
|
|
166
133
|
}
|
|
167
134
|
return { overall, byLayer };
|
|
168
135
|
}
|
|
169
|
-
function diffMetricMap(before, after) {
|
|
170
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
171
|
-
const result = {};
|
|
172
|
-
for (const key of keys) {
|
|
173
|
-
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
174
|
-
}
|
|
175
|
-
return result;
|
|
176
|
-
}
|
|
177
136
|
function addMetricMap(base, delta) {
|
|
178
137
|
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
179
138
|
const result = {};
|
|
@@ -210,6 +169,20 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
210
169
|
async getOrSet(key, fetcher, options) {
|
|
211
170
|
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
212
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Returns a namespaced cache entry, or `null` on miss.
|
|
174
|
+
* Unlike `get()`, this distinguishes a stored `null` value from an absent key.
|
|
175
|
+
*/
|
|
176
|
+
async getEntry(key) {
|
|
177
|
+
const entry = await this.trackMetrics(() => this.cache.getEntry(this.qualify(key)));
|
|
178
|
+
if (entry === null) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
...entry,
|
|
183
|
+
key
|
|
184
|
+
};
|
|
185
|
+
}
|
|
213
186
|
/**
|
|
214
187
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
215
188
|
*/
|
|
@@ -444,13 +417,24 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
444
417
|
};
|
|
445
418
|
}
|
|
446
419
|
async trackMetrics(operation) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
420
|
+
let result;
|
|
421
|
+
let metrics;
|
|
422
|
+
try {
|
|
423
|
+
;
|
|
424
|
+
({ result, metrics } = await this.cache.captureMetrics(operation));
|
|
425
|
+
} catch (error) {
|
|
426
|
+
const capturedMetrics = error.metrics;
|
|
427
|
+
if (capturedMetrics) {
|
|
428
|
+
await this.getMetricsMutex().runExclusive(() => {
|
|
429
|
+
this.metrics = addNamespaceMetrics(this.metrics, capturedMetrics);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
await this.getMetricsMutex().runExclusive(() => {
|
|
435
|
+
this.metrics = addNamespaceMetrics(this.metrics, metrics);
|
|
453
436
|
});
|
|
437
|
+
return result;
|
|
454
438
|
}
|
|
455
439
|
getMetricsMutex() {
|
|
456
440
|
const existing = _CacheNamespace.metricsMutexes.get(this.cache);
|
|
@@ -1271,6 +1255,9 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
|
1271
1255
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1272
1256
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1273
1257
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1258
|
+
var SINGLE_FLIGHT_BACKOFF_FACTOR = 2;
|
|
1259
|
+
var SINGLE_FLIGHT_BACKOFF_JITTER = 0.2;
|
|
1260
|
+
var SINGLE_FLIGHT_MAX_POLL_MS = 1e3;
|
|
1274
1261
|
var CacheStackReader = class {
|
|
1275
1262
|
constructor(options) {
|
|
1276
1263
|
this.options = options;
|
|
@@ -1494,6 +1481,7 @@ var CacheStackReader = class {
|
|
|
1494
1481
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
1495
1482
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
1496
1483
|
const deadline = Date.now() + timeoutMs;
|
|
1484
|
+
let nextPollMs = pollIntervalMs;
|
|
1497
1485
|
this.options.metricsCollector.increment("singleFlightWaits");
|
|
1498
1486
|
this.options.emit("stampede-dedupe", { key });
|
|
1499
1487
|
while (Date.now() < deadline) {
|
|
@@ -1502,7 +1490,13 @@ var CacheStackReader = class {
|
|
|
1502
1490
|
this.options.metricsCollector.increment("hits");
|
|
1503
1491
|
return hit.value;
|
|
1504
1492
|
}
|
|
1505
|
-
|
|
1493
|
+
const remainingMs = deadline - Date.now();
|
|
1494
|
+
if (remainingMs <= 0) {
|
|
1495
|
+
break;
|
|
1496
|
+
}
|
|
1497
|
+
const delayMs = Math.min(this.jitterSingleFlightPoll(nextPollMs), remainingMs);
|
|
1498
|
+
await this.options.sleep(delayMs);
|
|
1499
|
+
nextPollMs = Math.min(nextPollMs * SINGLE_FLIGHT_BACKOFF_FACTOR, SINGLE_FLIGHT_MAX_POLL_MS, timeoutMs);
|
|
1506
1500
|
}
|
|
1507
1501
|
if (!this.options.singleFlightCoordinator) {
|
|
1508
1502
|
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
|
|
@@ -1514,12 +1508,18 @@ var CacheStackReader = class {
|
|
|
1514
1508
|
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext)
|
|
1515
1509
|
);
|
|
1516
1510
|
}
|
|
1511
|
+
jitterSingleFlightPoll(delayMs) {
|
|
1512
|
+
const jitterRange = delayMs * SINGLE_FLIGHT_BACKOFF_JITTER;
|
|
1513
|
+
return Math.max(1, Math.round(delayMs - jitterRange + Math.random() * jitterRange * 2));
|
|
1514
|
+
}
|
|
1517
1515
|
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1518
1516
|
key,
|
|
1519
1517
|
currentValue: void 0,
|
|
1520
1518
|
state: "miss"
|
|
1521
1519
|
}) {
|
|
1522
|
-
|
|
1520
|
+
const circuitBreakerOptions = options?.circuitBreaker ?? this.options.circuitBreaker;
|
|
1521
|
+
const breakerKey = this.resolveCircuitBreakerKey(key, circuitBreakerOptions);
|
|
1522
|
+
this.options.circuitBreakerManager.assertClosed(breakerKey, circuitBreakerOptions);
|
|
1523
1523
|
this.options.metricsCollector.increment("fetches");
|
|
1524
1524
|
const fetchStart = Date.now();
|
|
1525
1525
|
let fetched;
|
|
@@ -1529,13 +1529,13 @@ var CacheStackReader = class {
|
|
|
1529
1529
|
{ key, fetcher },
|
|
1530
1530
|
() => fetcher(fetcherContext)
|
|
1531
1531
|
);
|
|
1532
|
-
this.options.circuitBreakerManager.recordSuccess(
|
|
1532
|
+
this.options.circuitBreakerManager.recordSuccess(breakerKey);
|
|
1533
1533
|
this.options.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1534
1534
|
} catch (error) {
|
|
1535
|
-
this.options.recordCircuitFailure(key,
|
|
1535
|
+
this.options.recordCircuitFailure(key, breakerKey, circuitBreakerOptions, error);
|
|
1536
1536
|
throw error;
|
|
1537
1537
|
}
|
|
1538
|
-
if (fetched ===
|
|
1538
|
+
if (fetched === void 0 || fetched === null && !this.shouldCacheNullValues(options)) {
|
|
1539
1539
|
if (!this.shouldNegativeCache(options)) {
|
|
1540
1540
|
return null;
|
|
1541
1541
|
}
|
|
@@ -1577,6 +1577,18 @@ var CacheStackReader = class {
|
|
|
1577
1577
|
await this.options.storeEntry(key, "value", fetched, options);
|
|
1578
1578
|
return fetched;
|
|
1579
1579
|
}
|
|
1580
|
+
resolveCircuitBreakerKey(key, options) {
|
|
1581
|
+
if (!options) {
|
|
1582
|
+
return `key:${key}`;
|
|
1583
|
+
}
|
|
1584
|
+
if (options.breakerKey) {
|
|
1585
|
+
return `custom:${options.breakerKey}`;
|
|
1586
|
+
}
|
|
1587
|
+
if (options.scope === "shared") {
|
|
1588
|
+
return "scope:shared";
|
|
1589
|
+
}
|
|
1590
|
+
return `key:${key}`;
|
|
1591
|
+
}
|
|
1580
1592
|
runScheduleBackgroundRefresh(key, fetcher, options, fetcherContext) {
|
|
1581
1593
|
this.scheduleBackgroundRefresh(key, fetcher, options, fetcherContext);
|
|
1582
1594
|
}
|
|
@@ -1677,6 +1689,9 @@ var CacheStackReader = class {
|
|
|
1677
1689
|
shouldNegativeCache(options) {
|
|
1678
1690
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
1679
1691
|
}
|
|
1692
|
+
shouldCacheNullValues(options) {
|
|
1693
|
+
return options?.cacheNullValues ?? this.options.cacheNullValues ?? false;
|
|
1694
|
+
}
|
|
1680
1695
|
isNegativeStoredValue(stored) {
|
|
1681
1696
|
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
1682
1697
|
}
|
|
@@ -2019,9 +2034,12 @@ function validateRateLimitOptions(name, options) {
|
|
|
2019
2034
|
validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
2020
2035
|
validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
2021
2036
|
validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
2022
|
-
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2037
|
+
if (options.scope !== void 0 && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2023
2038
|
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
2024
2039
|
}
|
|
2040
|
+
if (options.queueOverflow !== void 0 && !["reject", "bypass"].includes(options.queueOverflow)) {
|
|
2041
|
+
throw new Error(`${name}.queueOverflow must be one of "reject" or "bypass".`);
|
|
2042
|
+
}
|
|
2025
2043
|
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
2026
2044
|
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
2027
2045
|
}
|
|
@@ -2102,6 +2120,12 @@ function validateCircuitBreakerOptions(options) {
|
|
|
2102
2120
|
}
|
|
2103
2121
|
validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
2104
2122
|
validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
2123
|
+
if (options.scope !== void 0 && !["key", "shared"].includes(options.scope)) {
|
|
2124
|
+
throw new Error('circuitBreaker.scope must be one of "key" or "shared".');
|
|
2125
|
+
}
|
|
2126
|
+
if (options.breakerKey !== void 0 && options.breakerKey.length === 0) {
|
|
2127
|
+
throw new Error("circuitBreaker.breakerKey must not be empty.");
|
|
2128
|
+
}
|
|
2105
2129
|
}
|
|
2106
2130
|
function validateContextEntryOptions(name, options) {
|
|
2107
2131
|
if (!options) {
|
|
@@ -2215,6 +2239,7 @@ var CircuitBreakerManager = class {
|
|
|
2215
2239
|
// src/internal/FetchRateLimiter.ts
|
|
2216
2240
|
var MAX_BUCKETS = 1e4;
|
|
2217
2241
|
var MAX_QUEUE_PER_BUCKET = 1e4;
|
|
2242
|
+
var DEFAULT_QUEUE_OVERFLOW_POLICY = "reject";
|
|
2218
2243
|
var FetchRateLimiter = class {
|
|
2219
2244
|
buckets = /* @__PURE__ */ new Map();
|
|
2220
2245
|
queuesByBucket = /* @__PURE__ */ new Map();
|
|
@@ -2239,15 +2264,24 @@ var FetchRateLimiter = class {
|
|
|
2239
2264
|
const bucketKey = this.resolveBucketKey(normalized, context);
|
|
2240
2265
|
const queue = this.queuesByBucket.get(bucketKey) ?? [];
|
|
2241
2266
|
if (queue.length >= MAX_QUEUE_PER_BUCKET) {
|
|
2242
|
-
|
|
2243
|
-
|
|
2267
|
+
if ((normalized.queueOverflow ?? DEFAULT_QUEUE_OVERFLOW_POLICY) === "bypass") {
|
|
2268
|
+
this.rateLimitBypasses += 1;
|
|
2269
|
+
task().then(resolve2, reject);
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
reject(new Error(`FetchRateLimiter queue overflow for bucket "${bucketKey}".`));
|
|
2244
2273
|
return;
|
|
2245
2274
|
}
|
|
2246
2275
|
queue.push({
|
|
2247
2276
|
bucketKey,
|
|
2248
2277
|
options: normalized,
|
|
2249
|
-
|
|
2250
|
-
|
|
2278
|
+
run: async () => {
|
|
2279
|
+
try {
|
|
2280
|
+
resolve2(await task());
|
|
2281
|
+
} catch (error) {
|
|
2282
|
+
reject(error);
|
|
2283
|
+
}
|
|
2284
|
+
},
|
|
2251
2285
|
reject
|
|
2252
2286
|
});
|
|
2253
2287
|
this.queuesByBucket.set(bucketKey, queue);
|
|
@@ -2288,7 +2322,8 @@ var FetchRateLimiter = class {
|
|
|
2288
2322
|
intervalMs,
|
|
2289
2323
|
maxPerInterval,
|
|
2290
2324
|
scope: options.scope ?? "global",
|
|
2291
|
-
bucketKey: options.bucketKey
|
|
2325
|
+
bucketKey: options.bucketKey,
|
|
2326
|
+
queueOverflow: options.queueOverflow
|
|
2292
2327
|
};
|
|
2293
2328
|
}
|
|
2294
2329
|
resolveBucketKey(options, context) {
|
|
@@ -2371,7 +2406,7 @@ var FetchRateLimiter = class {
|
|
|
2371
2406
|
if (next.options.intervalMs && next.options.maxPerInterval) {
|
|
2372
2407
|
bucket.startedAt.push(Date.now());
|
|
2373
2408
|
}
|
|
2374
|
-
void next.
|
|
2409
|
+
void next.run().finally(() => {
|
|
2375
2410
|
bucket.active -= 1;
|
|
2376
2411
|
if ((this.queuesByBucket.get(next.bucketKey)?.length ?? 0) > 0) {
|
|
2377
2412
|
this.pendingBuckets.add(next.bucketKey);
|
|
@@ -2473,7 +2508,9 @@ var FetchRateLimiter = class {
|
|
|
2473
2508
|
};
|
|
2474
2509
|
|
|
2475
2510
|
// src/internal/MetricsCollector.ts
|
|
2511
|
+
var import_node_async_hooks = require("async_hooks");
|
|
2476
2512
|
var MetricsCollector = class {
|
|
2513
|
+
captures = new import_node_async_hooks.AsyncLocalStorage();
|
|
2477
2514
|
data = this.empty();
|
|
2478
2515
|
get snapshot() {
|
|
2479
2516
|
return {
|
|
@@ -2486,18 +2523,46 @@ var MetricsCollector = class {
|
|
|
2486
2523
|
increment(field, amount = 1) {
|
|
2487
2524
|
;
|
|
2488
2525
|
this.data[field] += amount;
|
|
2526
|
+
for (const capture of this.captures.getStore() ?? []) {
|
|
2527
|
+
;
|
|
2528
|
+
capture[field] += amount;
|
|
2529
|
+
}
|
|
2489
2530
|
}
|
|
2490
2531
|
incrementLayer(map, layerName) {
|
|
2491
2532
|
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
2533
|
+
for (const capture of this.captures.getStore() ?? []) {
|
|
2534
|
+
capture[map][layerName] = (capture[map][layerName] ?? 0) + 1;
|
|
2535
|
+
}
|
|
2492
2536
|
}
|
|
2493
2537
|
/**
|
|
2494
2538
|
* Records a read latency sample for the given layer.
|
|
2495
2539
|
* Maintains a rolling average and max using Welford's online algorithm.
|
|
2496
2540
|
*/
|
|
2497
2541
|
recordLatency(layerName, durationMs) {
|
|
2498
|
-
|
|
2542
|
+
this.recordLatencySample(this.data, layerName, durationMs);
|
|
2543
|
+
for (const capture of this.captures.getStore() ?? []) {
|
|
2544
|
+
this.recordLatencySample(capture, layerName, durationMs);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
async capture(operation) {
|
|
2548
|
+
const metrics = this.empty();
|
|
2549
|
+
const activeCaptures = this.captures.getStore();
|
|
2550
|
+
const captures = activeCaptures ? [...activeCaptures, metrics] : [metrics];
|
|
2551
|
+
try {
|
|
2552
|
+
const result = await this.captures.run(captures, operation);
|
|
2553
|
+
return { result, metrics };
|
|
2554
|
+
} catch (error) {
|
|
2555
|
+
if ((typeof error === "object" || typeof error === "function") && error !== null) {
|
|
2556
|
+
;
|
|
2557
|
+
error.metrics = metrics;
|
|
2558
|
+
}
|
|
2559
|
+
throw error;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
recordLatencySample(metrics, layerName, durationMs) {
|
|
2563
|
+
const existing = metrics.latencyByLayer[layerName];
|
|
2499
2564
|
if (!existing) {
|
|
2500
|
-
|
|
2565
|
+
metrics.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
2501
2566
|
return;
|
|
2502
2567
|
}
|
|
2503
2568
|
existing.count += 1;
|
|
@@ -2665,30 +2730,34 @@ var TtlResolver = class {
|
|
|
2665
2730
|
};
|
|
2666
2731
|
|
|
2667
2732
|
// src/invalidation/TagIndex.ts
|
|
2668
|
-
var
|
|
2733
|
+
var DEFAULT_TOUCH_REFRESH_INTERVAL_MS = 1e3;
|
|
2669
2734
|
var TagIndex = class {
|
|
2670
2735
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
2671
2736
|
keyToTags = /* @__PURE__ */ new Map();
|
|
2672
2737
|
knownKeys = /* @__PURE__ */ new Map();
|
|
2673
2738
|
maxKnownKeys;
|
|
2739
|
+
touchRefreshIntervalMs;
|
|
2674
2740
|
nextNodeId = 1;
|
|
2675
2741
|
root = this.createTrieNode();
|
|
2676
2742
|
constructor(options = {}) {
|
|
2677
2743
|
this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
|
|
2744
|
+
this.touchRefreshIntervalMs = options.touchRefreshIntervalMs ?? DEFAULT_TOUCH_REFRESH_INTERVAL_MS;
|
|
2678
2745
|
}
|
|
2679
2746
|
/**
|
|
2680
2747
|
* Records a key as known without changing tag assignments.
|
|
2681
2748
|
*/
|
|
2682
2749
|
async touch(key) {
|
|
2683
|
-
this.insertKnownKey(key)
|
|
2684
|
-
|
|
2750
|
+
if (this.insertKnownKey(key)) {
|
|
2751
|
+
this.pruneKnownKeysIfNeeded();
|
|
2752
|
+
}
|
|
2685
2753
|
}
|
|
2686
2754
|
/**
|
|
2687
2755
|
* Replaces the tags associated with a key and records the key as known.
|
|
2688
2756
|
*/
|
|
2689
2757
|
async track(key, tags) {
|
|
2690
|
-
this.insertKnownKey(key)
|
|
2691
|
-
|
|
2758
|
+
if (this.insertKnownKey(key)) {
|
|
2759
|
+
this.pruneKnownKeysIfNeeded();
|
|
2760
|
+
}
|
|
2692
2761
|
if (tags.length === 0) {
|
|
2693
2762
|
return;
|
|
2694
2763
|
}
|
|
@@ -2758,9 +2827,14 @@ var TagIndex = class {
|
|
|
2758
2827
|
* Returns known keys matching a wildcard pattern.
|
|
2759
2828
|
*/
|
|
2760
2829
|
async matchPattern(pattern) {
|
|
2761
|
-
const
|
|
2762
|
-
|
|
2763
|
-
|
|
2830
|
+
const literalPrefix = this.literalPrefix(pattern);
|
|
2831
|
+
const node = this.findNode(literalPrefix);
|
|
2832
|
+
if (!node) {
|
|
2833
|
+
return [];
|
|
2834
|
+
}
|
|
2835
|
+
const candidates = [];
|
|
2836
|
+
this.collectFromNode(node, literalPrefix, candidates);
|
|
2837
|
+
return candidates.filter((key) => PatternMatcher.matches(pattern, key));
|
|
2764
2838
|
}
|
|
2765
2839
|
/**
|
|
2766
2840
|
* Visits known keys matching a wildcard pattern.
|
|
@@ -2790,13 +2864,18 @@ var TagIndex = class {
|
|
|
2790
2864
|
};
|
|
2791
2865
|
}
|
|
2792
2866
|
insertKnownKey(key) {
|
|
2793
|
-
const
|
|
2867
|
+
const previousTouch = this.knownKeys.get(key);
|
|
2868
|
+
const isNew = previousTouch === void 0;
|
|
2869
|
+
const now = Date.now();
|
|
2870
|
+
if (!isNew && now - previousTouch < this.touchRefreshIntervalMs) {
|
|
2871
|
+
return false;
|
|
2872
|
+
}
|
|
2794
2873
|
if (!isNew) {
|
|
2795
2874
|
this.knownKeys.delete(key);
|
|
2796
2875
|
}
|
|
2797
|
-
this.knownKeys.set(key,
|
|
2876
|
+
this.knownKeys.set(key, now);
|
|
2798
2877
|
if (!isNew) {
|
|
2799
|
-
return;
|
|
2878
|
+
return true;
|
|
2800
2879
|
}
|
|
2801
2880
|
let node = this.root;
|
|
2802
2881
|
for (const character of key) {
|
|
@@ -2808,6 +2887,7 @@ var TagIndex = class {
|
|
|
2808
2887
|
node = child;
|
|
2809
2888
|
}
|
|
2810
2889
|
node.terminal = true;
|
|
2890
|
+
return true;
|
|
2811
2891
|
}
|
|
2812
2892
|
findNode(prefix) {
|
|
2813
2893
|
let node = this.root;
|
|
@@ -2820,74 +2900,41 @@ var TagIndex = class {
|
|
|
2820
2900
|
return node;
|
|
2821
2901
|
}
|
|
2822
2902
|
collectFromNode(node, prefix, matches) {
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2903
|
+
const stack = [{ node, prefix }];
|
|
2904
|
+
while (stack.length > 0) {
|
|
2905
|
+
const current = stack.pop();
|
|
2906
|
+
if (!current) {
|
|
2907
|
+
continue;
|
|
2908
|
+
}
|
|
2909
|
+
if (current.node.terminal) {
|
|
2910
|
+
matches.push(current.prefix);
|
|
2911
|
+
}
|
|
2912
|
+
const children = [...current.node.children].reverse();
|
|
2913
|
+
for (const [character, child] of children) {
|
|
2914
|
+
stack.push({ node: child, prefix: `${current.prefix}${character}` });
|
|
2915
|
+
}
|
|
2828
2916
|
}
|
|
2829
2917
|
}
|
|
2830
2918
|
async visitFromNode(node, prefix, visitor) {
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
}
|
|
2837
|
-
}
|
|
2838
|
-
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
2839
|
-
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
2840
|
-
return;
|
|
2841
|
-
}
|
|
2842
|
-
const stateKey = `${node.id}:${patternIndex}`;
|
|
2843
|
-
if (visited.has(stateKey)) {
|
|
2844
|
-
return;
|
|
2845
|
-
}
|
|
2846
|
-
visited.add(stateKey);
|
|
2847
|
-
if (patternIndex === pattern.length) {
|
|
2848
|
-
if (node.terminal) {
|
|
2849
|
-
matches.add(prefix);
|
|
2919
|
+
const stack = [{ node, prefix }];
|
|
2920
|
+
while (stack.length > 0) {
|
|
2921
|
+
const current = stack.pop();
|
|
2922
|
+
if (!current) {
|
|
2923
|
+
continue;
|
|
2850
2924
|
}
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
const patternChar = pattern[patternIndex];
|
|
2854
|
-
if (patternChar === void 0) {
|
|
2855
|
-
return;
|
|
2856
|
-
}
|
|
2857
|
-
if (patternChar === "*") {
|
|
2858
|
-
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
|
|
2859
|
-
for (const [character, child2] of node.children) {
|
|
2860
|
-
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
|
|
2925
|
+
if (current.node.terminal) {
|
|
2926
|
+
await visitor(current.prefix);
|
|
2861
2927
|
}
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
for (const [character, child2] of node.children) {
|
|
2866
|
-
this.collectPatternMatches(
|
|
2867
|
-
child2,
|
|
2868
|
-
`${prefix}${character}`,
|
|
2869
|
-
pattern,
|
|
2870
|
-
patternIndex + 1,
|
|
2871
|
-
matches,
|
|
2872
|
-
visited,
|
|
2873
|
-
depth + 1
|
|
2874
|
-
);
|
|
2928
|
+
const children = [...current.node.children].reverse();
|
|
2929
|
+
for (const [character, child] of children) {
|
|
2930
|
+
stack.push({ node: child, prefix: `${current.prefix}${character}` });
|
|
2875
2931
|
}
|
|
2876
|
-
return;
|
|
2877
|
-
}
|
|
2878
|
-
const child = node.children.get(patternChar);
|
|
2879
|
-
if (child) {
|
|
2880
|
-
this.collectPatternMatches(
|
|
2881
|
-
child,
|
|
2882
|
-
`${prefix}${patternChar}`,
|
|
2883
|
-
pattern,
|
|
2884
|
-
patternIndex + 1,
|
|
2885
|
-
matches,
|
|
2886
|
-
visited,
|
|
2887
|
-
depth + 1
|
|
2888
|
-
);
|
|
2889
2932
|
}
|
|
2890
2933
|
}
|
|
2934
|
+
literalPrefix(pattern) {
|
|
2935
|
+
const wildcardIndex = pattern.search(/[*?]/);
|
|
2936
|
+
return wildcardIndex === -1 ? pattern : pattern.slice(0, wildcardIndex);
|
|
2937
|
+
}
|
|
2891
2938
|
pruneKnownKeysIfNeeded() {
|
|
2892
2939
|
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
2893
2940
|
return;
|
|
@@ -3195,7 +3242,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3195
3242
|
emitError: (operation, context) => this.emitError(operation, context),
|
|
3196
3243
|
formatError: (error) => this.formatError(error),
|
|
3197
3244
|
storeEntry: (key, kind, value, options2) => this.storeEntry(key, kind, value, options2),
|
|
3198
|
-
recordCircuitFailure: (key, options2, error) => this.recordCircuitFailure(key, options2, error),
|
|
3245
|
+
recordCircuitFailure: (key, breakerKey, options2, error) => this.recordCircuitFailure(key, breakerKey, options2, error),
|
|
3199
3246
|
resolveLayerMs: (layerName, override, globalDefault, fallback) => this.resolveLayerMs(layerName, override, globalDefault, fallback),
|
|
3200
3247
|
sleep: (ms) => this.sleep(ms),
|
|
3201
3248
|
withTimeout: (promise, ms, createError) => this.withTimeout(promise, ms, createError),
|
|
@@ -3210,6 +3257,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3210
3257
|
singleFlightRenewIntervalMs: options.singleFlightRenewIntervalMs,
|
|
3211
3258
|
backgroundRefreshTimeoutMs: options.backgroundRefreshTimeoutMs,
|
|
3212
3259
|
negativeCaching: options.negativeCaching,
|
|
3260
|
+
cacheNullValues: options.cacheNullValues,
|
|
3213
3261
|
refreshAhead: options.refreshAhead,
|
|
3214
3262
|
circuitBreaker: options.circuitBreaker,
|
|
3215
3263
|
fetcherRateLimit: options.fetcherRateLimit
|
|
@@ -3262,6 +3310,64 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3262
3310
|
async getOrSet(key, fetcher, options) {
|
|
3263
3311
|
return this.get(key, fetcher, options);
|
|
3264
3312
|
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Returns a discriminated cache entry, or `null` on miss.
|
|
3315
|
+
* Unlike `get()`, this distinguishes a stored `null` value from an absent key.
|
|
3316
|
+
*/
|
|
3317
|
+
async getEntry(key) {
|
|
3318
|
+
return this.observeOperation("layercache.get_entry", { "layercache.key": String(key ?? "") }, async () => {
|
|
3319
|
+
const userKey = validateCacheKey(key);
|
|
3320
|
+
const normalizedKey = this.qualifyKey(userKey);
|
|
3321
|
+
await this.awaitStartup("getEntry");
|
|
3322
|
+
let sawRetainableValue = false;
|
|
3323
|
+
for (let index = 0; index < this.layers.length; index += 1) {
|
|
3324
|
+
const layer = this.layers[index];
|
|
3325
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
3326
|
+
continue;
|
|
3327
|
+
}
|
|
3328
|
+
const readStart = performance.now();
|
|
3329
|
+
const stored = await this.readLayerEntry(layer, normalizedKey);
|
|
3330
|
+
this.metricsCollector.recordLatency(layer.name, performance.now() - readStart);
|
|
3331
|
+
if (stored === null) {
|
|
3332
|
+
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
3333
|
+
continue;
|
|
3334
|
+
}
|
|
3335
|
+
const resolved = resolveStoredValue(stored);
|
|
3336
|
+
if (resolved.state === "expired") {
|
|
3337
|
+
await layer.delete(normalizedKey);
|
|
3338
|
+
continue;
|
|
3339
|
+
}
|
|
3340
|
+
sawRetainableValue = true;
|
|
3341
|
+
await this.tagIndex.touch(normalizedKey);
|
|
3342
|
+
await this.reader.backfill(normalizedKey, stored, index - 1);
|
|
3343
|
+
this.metricsCollector.increment("hits");
|
|
3344
|
+
if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
|
|
3345
|
+
this.metricsCollector.increment("staleHits");
|
|
3346
|
+
}
|
|
3347
|
+
this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
|
|
3348
|
+
this.logger.debug?.("hit", { key: normalizedKey, layer: layer.name, state: resolved.state });
|
|
3349
|
+
this.emit("hit", {
|
|
3350
|
+
key: normalizedKey,
|
|
3351
|
+
layer: layer.name,
|
|
3352
|
+
state: resolved.state
|
|
3353
|
+
});
|
|
3354
|
+
return {
|
|
3355
|
+
key: userKey,
|
|
3356
|
+
value: resolved.value,
|
|
3357
|
+
kind: resolved.envelope?.kind ?? "value",
|
|
3358
|
+
state: resolved.state,
|
|
3359
|
+
layer: layer.name
|
|
3360
|
+
};
|
|
3361
|
+
}
|
|
3362
|
+
if (!sawRetainableValue) {
|
|
3363
|
+
await this.tagIndex.remove(normalizedKey);
|
|
3364
|
+
}
|
|
3365
|
+
this.metricsCollector.increment("misses");
|
|
3366
|
+
this.logger.debug?.("miss", { key: normalizedKey, mode: "getEntry" });
|
|
3367
|
+
this.emit("miss", { key: normalizedKey, mode: "getEntry" });
|
|
3368
|
+
return null;
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3265
3371
|
/**
|
|
3266
3372
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
3267
3373
|
* Useful when the value is expected to exist or the fetcher is expected to
|
|
@@ -3726,6 +3832,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3726
3832
|
getMetrics() {
|
|
3727
3833
|
return this.metricsCollector.snapshot;
|
|
3728
3834
|
}
|
|
3835
|
+
/**
|
|
3836
|
+
* Runs an operation while collecting only the metrics emitted by its async context.
|
|
3837
|
+
* Used by namespaces so metrics tracking does not serialize the operation itself.
|
|
3838
|
+
*/
|
|
3839
|
+
async captureMetrics(operation) {
|
|
3840
|
+
return this.metricsCollector.capture(operation);
|
|
3841
|
+
}
|
|
3729
3842
|
/**
|
|
3730
3843
|
* Returns metrics plus layer degradation state and active background refresh count.
|
|
3731
3844
|
*/
|
|
@@ -4029,7 +4142,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
4029
4142
|
for (const key of keys) {
|
|
4030
4143
|
await this.tagIndex.remove(key);
|
|
4031
4144
|
this.ttlResolver.deleteProfile(key);
|
|
4032
|
-
this.circuitBreakerManager.delete(key);
|
|
4145
|
+
this.circuitBreakerManager.delete(`key:${key}`);
|
|
4033
4146
|
}
|
|
4034
4147
|
this.metricsCollector.increment("deletes", keys.length);
|
|
4035
4148
|
this.metricsCollector.increment("invalidations");
|
|
@@ -4048,7 +4161,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
4048
4161
|
}
|
|
4049
4162
|
await this.tagIndex.remove(key);
|
|
4050
4163
|
this.ttlResolver.deleteProfile(key);
|
|
4051
|
-
this.circuitBreakerManager.delete(key);
|
|
4164
|
+
this.circuitBreakerManager.delete(`key:${key}`);
|
|
4052
4165
|
}
|
|
4053
4166
|
this.metricsCollector.increment("invalidations");
|
|
4054
4167
|
this.logger.debug?.("expire", { keys });
|
|
@@ -4090,7 +4203,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
4090
4203
|
for (const key of keys) {
|
|
4091
4204
|
await this.tagIndex.remove(key);
|
|
4092
4205
|
this.ttlResolver.deleteProfile(key);
|
|
4093
|
-
this.circuitBreakerManager.delete(key);
|
|
4206
|
+
this.circuitBreakerManager.delete(`key:${key}`);
|
|
4094
4207
|
}
|
|
4095
4208
|
}
|
|
4096
4209
|
}
|
|
@@ -4335,15 +4448,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
4335
4448
|
isGracefulDegradationEnabled() {
|
|
4336
4449
|
return Boolean(this.options.gracefulDegradation);
|
|
4337
4450
|
}
|
|
4338
|
-
recordCircuitFailure(key, options, error) {
|
|
4451
|
+
recordCircuitFailure(key, breakerKey, options, error) {
|
|
4339
4452
|
if (!options) {
|
|
4340
4453
|
return;
|
|
4341
4454
|
}
|
|
4342
|
-
this.circuitBreakerManager.recordFailure(
|
|
4343
|
-
if (this.circuitBreakerManager.isOpen(
|
|
4455
|
+
this.circuitBreakerManager.recordFailure(breakerKey, options);
|
|
4456
|
+
if (this.circuitBreakerManager.isOpen(breakerKey)) {
|
|
4344
4457
|
this.metricsCollector.increment("circuitBreakerTrips");
|
|
4345
4458
|
}
|
|
4346
|
-
this.emitError("fetch", { key, error: this.formatError(error) });
|
|
4459
|
+
this.emitError("fetch", { key, breakerKey, error: this.formatError(error) });
|
|
4347
4460
|
}
|
|
4348
4461
|
emitError(operation, context) {
|
|
4349
4462
|
this.logger.error?.(operation, context);
|
|
@@ -5989,6 +6102,7 @@ var PayloadProtectionError = class extends Error {
|
|
|
5989
6102
|
|
|
5990
6103
|
// src/layers/DiskLayer.ts
|
|
5991
6104
|
var FILE_SCAN_CONCURRENCY = 32;
|
|
6105
|
+
var DEFAULT_MAX_WRITE_QUEUE_DEPTH = 1e4;
|
|
5992
6106
|
var DiskLayer = class {
|
|
5993
6107
|
name;
|
|
5994
6108
|
defaultTtl;
|
|
@@ -5997,8 +6111,10 @@ var DiskLayer = class {
|
|
|
5997
6111
|
serializer;
|
|
5998
6112
|
maxFiles;
|
|
5999
6113
|
maxEntryBytes;
|
|
6114
|
+
maxWriteQueueDepth;
|
|
6000
6115
|
protection;
|
|
6001
6116
|
writeQueue = Promise.resolve();
|
|
6117
|
+
writeQueueDepth = 0;
|
|
6002
6118
|
/**
|
|
6003
6119
|
* Creates a disk-backed cache layer.
|
|
6004
6120
|
*/
|
|
@@ -6009,6 +6125,7 @@ var DiskLayer = class {
|
|
|
6009
6125
|
this.serializer = options.serializer ?? new JsonSerializer();
|
|
6010
6126
|
this.maxFiles = this.normalizeMaxFiles(options.maxFiles);
|
|
6011
6127
|
this.maxEntryBytes = this.normalizeMaxEntryBytes(options.maxEntryBytes);
|
|
6128
|
+
this.maxWriteQueueDepth = this.normalizeMaxWriteQueueDepth(options.maxWriteQueueDepth);
|
|
6012
6129
|
this.protection = new PayloadProtection({
|
|
6013
6130
|
encryptionKey: options.encryptionKey,
|
|
6014
6131
|
signingKey: options.signingKey
|
|
@@ -6224,6 +6341,16 @@ var DiskLayer = class {
|
|
|
6224
6341
|
}
|
|
6225
6342
|
return normalized;
|
|
6226
6343
|
}
|
|
6344
|
+
normalizeMaxWriteQueueDepth(maxWriteQueueDepth) {
|
|
6345
|
+
if (maxWriteQueueDepth === false) {
|
|
6346
|
+
return false;
|
|
6347
|
+
}
|
|
6348
|
+
const normalized = maxWriteQueueDepth ?? DEFAULT_MAX_WRITE_QUEUE_DEPTH;
|
|
6349
|
+
if (!Number.isInteger(normalized) || normalized <= 0) {
|
|
6350
|
+
throw new Error("DiskLayer.maxWriteQueueDepth must be a positive integer or false.");
|
|
6351
|
+
}
|
|
6352
|
+
return normalized;
|
|
6353
|
+
}
|
|
6227
6354
|
async readEntryFile(filePath) {
|
|
6228
6355
|
let handle;
|
|
6229
6356
|
try {
|
|
@@ -6336,7 +6463,13 @@ var DiskLayer = class {
|
|
|
6336
6463
|
}
|
|
6337
6464
|
}
|
|
6338
6465
|
enqueueWrite(operation) {
|
|
6339
|
-
|
|
6466
|
+
if (this.maxWriteQueueDepth !== false && this.writeQueueDepth >= this.maxWriteQueueDepth) {
|
|
6467
|
+
return Promise.reject(new Error(`DiskLayer write queue limit (${this.maxWriteQueueDepth}) exceeded.`));
|
|
6468
|
+
}
|
|
6469
|
+
this.writeQueueDepth += 1;
|
|
6470
|
+
const next = this.writeQueue.then(operation, operation).finally(() => {
|
|
6471
|
+
this.writeQueueDepth -= 1;
|
|
6472
|
+
});
|
|
6340
6473
|
this.writeQueue = next.catch(() => void 0);
|
|
6341
6474
|
return next;
|
|
6342
6475
|
}
|