layercache 3.0.0 → 3.1.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 +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 +266 -138
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +205 -68
- package/package.json +1 -1
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,8 +2264,12 @@ 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({
|
|
@@ -2288,7 +2317,8 @@ var FetchRateLimiter = class {
|
|
|
2288
2317
|
intervalMs,
|
|
2289
2318
|
maxPerInterval,
|
|
2290
2319
|
scope: options.scope ?? "global",
|
|
2291
|
-
bucketKey: options.bucketKey
|
|
2320
|
+
bucketKey: options.bucketKey,
|
|
2321
|
+
queueOverflow: options.queueOverflow
|
|
2292
2322
|
};
|
|
2293
2323
|
}
|
|
2294
2324
|
resolveBucketKey(options, context) {
|
|
@@ -2473,7 +2503,9 @@ var FetchRateLimiter = class {
|
|
|
2473
2503
|
};
|
|
2474
2504
|
|
|
2475
2505
|
// src/internal/MetricsCollector.ts
|
|
2506
|
+
var import_node_async_hooks = require("async_hooks");
|
|
2476
2507
|
var MetricsCollector = class {
|
|
2508
|
+
captures = new import_node_async_hooks.AsyncLocalStorage();
|
|
2477
2509
|
data = this.empty();
|
|
2478
2510
|
get snapshot() {
|
|
2479
2511
|
return {
|
|
@@ -2486,18 +2518,46 @@ var MetricsCollector = class {
|
|
|
2486
2518
|
increment(field, amount = 1) {
|
|
2487
2519
|
;
|
|
2488
2520
|
this.data[field] += amount;
|
|
2521
|
+
for (const capture of this.captures.getStore() ?? []) {
|
|
2522
|
+
;
|
|
2523
|
+
capture[field] += amount;
|
|
2524
|
+
}
|
|
2489
2525
|
}
|
|
2490
2526
|
incrementLayer(map, layerName) {
|
|
2491
2527
|
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
2528
|
+
for (const capture of this.captures.getStore() ?? []) {
|
|
2529
|
+
capture[map][layerName] = (capture[map][layerName] ?? 0) + 1;
|
|
2530
|
+
}
|
|
2492
2531
|
}
|
|
2493
2532
|
/**
|
|
2494
2533
|
* Records a read latency sample for the given layer.
|
|
2495
2534
|
* Maintains a rolling average and max using Welford's online algorithm.
|
|
2496
2535
|
*/
|
|
2497
2536
|
recordLatency(layerName, durationMs) {
|
|
2498
|
-
|
|
2537
|
+
this.recordLatencySample(this.data, layerName, durationMs);
|
|
2538
|
+
for (const capture of this.captures.getStore() ?? []) {
|
|
2539
|
+
this.recordLatencySample(capture, layerName, durationMs);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
async capture(operation) {
|
|
2543
|
+
const metrics = this.empty();
|
|
2544
|
+
const activeCaptures = this.captures.getStore();
|
|
2545
|
+
const captures = activeCaptures ? [...activeCaptures, metrics] : [metrics];
|
|
2546
|
+
try {
|
|
2547
|
+
const result = await this.captures.run(captures, operation);
|
|
2548
|
+
return { result, metrics };
|
|
2549
|
+
} catch (error) {
|
|
2550
|
+
if ((typeof error === "object" || typeof error === "function") && error !== null) {
|
|
2551
|
+
;
|
|
2552
|
+
error.metrics = metrics;
|
|
2553
|
+
}
|
|
2554
|
+
throw error;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
recordLatencySample(metrics, layerName, durationMs) {
|
|
2558
|
+
const existing = metrics.latencyByLayer[layerName];
|
|
2499
2559
|
if (!existing) {
|
|
2500
|
-
|
|
2560
|
+
metrics.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
2501
2561
|
return;
|
|
2502
2562
|
}
|
|
2503
2563
|
existing.count += 1;
|
|
@@ -2665,30 +2725,34 @@ var TtlResolver = class {
|
|
|
2665
2725
|
};
|
|
2666
2726
|
|
|
2667
2727
|
// src/invalidation/TagIndex.ts
|
|
2668
|
-
var
|
|
2728
|
+
var DEFAULT_TOUCH_REFRESH_INTERVAL_MS = 1e3;
|
|
2669
2729
|
var TagIndex = class {
|
|
2670
2730
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
2671
2731
|
keyToTags = /* @__PURE__ */ new Map();
|
|
2672
2732
|
knownKeys = /* @__PURE__ */ new Map();
|
|
2673
2733
|
maxKnownKeys;
|
|
2734
|
+
touchRefreshIntervalMs;
|
|
2674
2735
|
nextNodeId = 1;
|
|
2675
2736
|
root = this.createTrieNode();
|
|
2676
2737
|
constructor(options = {}) {
|
|
2677
2738
|
this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
|
|
2739
|
+
this.touchRefreshIntervalMs = options.touchRefreshIntervalMs ?? DEFAULT_TOUCH_REFRESH_INTERVAL_MS;
|
|
2678
2740
|
}
|
|
2679
2741
|
/**
|
|
2680
2742
|
* Records a key as known without changing tag assignments.
|
|
2681
2743
|
*/
|
|
2682
2744
|
async touch(key) {
|
|
2683
|
-
this.insertKnownKey(key)
|
|
2684
|
-
|
|
2745
|
+
if (this.insertKnownKey(key)) {
|
|
2746
|
+
this.pruneKnownKeysIfNeeded();
|
|
2747
|
+
}
|
|
2685
2748
|
}
|
|
2686
2749
|
/**
|
|
2687
2750
|
* Replaces the tags associated with a key and records the key as known.
|
|
2688
2751
|
*/
|
|
2689
2752
|
async track(key, tags) {
|
|
2690
|
-
this.insertKnownKey(key)
|
|
2691
|
-
|
|
2753
|
+
if (this.insertKnownKey(key)) {
|
|
2754
|
+
this.pruneKnownKeysIfNeeded();
|
|
2755
|
+
}
|
|
2692
2756
|
if (tags.length === 0) {
|
|
2693
2757
|
return;
|
|
2694
2758
|
}
|
|
@@ -2758,9 +2822,14 @@ var TagIndex = class {
|
|
|
2758
2822
|
* Returns known keys matching a wildcard pattern.
|
|
2759
2823
|
*/
|
|
2760
2824
|
async matchPattern(pattern) {
|
|
2761
|
-
const
|
|
2762
|
-
|
|
2763
|
-
|
|
2825
|
+
const literalPrefix = this.literalPrefix(pattern);
|
|
2826
|
+
const node = this.findNode(literalPrefix);
|
|
2827
|
+
if (!node) {
|
|
2828
|
+
return [];
|
|
2829
|
+
}
|
|
2830
|
+
const candidates = [];
|
|
2831
|
+
this.collectFromNode(node, literalPrefix, candidates);
|
|
2832
|
+
return candidates.filter((key) => PatternMatcher.matches(pattern, key));
|
|
2764
2833
|
}
|
|
2765
2834
|
/**
|
|
2766
2835
|
* Visits known keys matching a wildcard pattern.
|
|
@@ -2790,13 +2859,18 @@ var TagIndex = class {
|
|
|
2790
2859
|
};
|
|
2791
2860
|
}
|
|
2792
2861
|
insertKnownKey(key) {
|
|
2793
|
-
const
|
|
2862
|
+
const previousTouch = this.knownKeys.get(key);
|
|
2863
|
+
const isNew = previousTouch === void 0;
|
|
2864
|
+
const now = Date.now();
|
|
2865
|
+
if (!isNew && now - previousTouch < this.touchRefreshIntervalMs) {
|
|
2866
|
+
return false;
|
|
2867
|
+
}
|
|
2794
2868
|
if (!isNew) {
|
|
2795
2869
|
this.knownKeys.delete(key);
|
|
2796
2870
|
}
|
|
2797
|
-
this.knownKeys.set(key,
|
|
2871
|
+
this.knownKeys.set(key, now);
|
|
2798
2872
|
if (!isNew) {
|
|
2799
|
-
return;
|
|
2873
|
+
return true;
|
|
2800
2874
|
}
|
|
2801
2875
|
let node = this.root;
|
|
2802
2876
|
for (const character of key) {
|
|
@@ -2808,6 +2882,7 @@ var TagIndex = class {
|
|
|
2808
2882
|
node = child;
|
|
2809
2883
|
}
|
|
2810
2884
|
node.terminal = true;
|
|
2885
|
+
return true;
|
|
2811
2886
|
}
|
|
2812
2887
|
findNode(prefix) {
|
|
2813
2888
|
let node = this.root;
|
|
@@ -2820,74 +2895,41 @@ var TagIndex = class {
|
|
|
2820
2895
|
return node;
|
|
2821
2896
|
}
|
|
2822
2897
|
collectFromNode(node, prefix, matches) {
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2898
|
+
const stack = [{ node, prefix }];
|
|
2899
|
+
while (stack.length > 0) {
|
|
2900
|
+
const current = stack.pop();
|
|
2901
|
+
if (!current) {
|
|
2902
|
+
continue;
|
|
2903
|
+
}
|
|
2904
|
+
if (current.node.terminal) {
|
|
2905
|
+
matches.push(current.prefix);
|
|
2906
|
+
}
|
|
2907
|
+
const children = [...current.node.children].reverse();
|
|
2908
|
+
for (const [character, child] of children) {
|
|
2909
|
+
stack.push({ node: child, prefix: `${current.prefix}${character}` });
|
|
2910
|
+
}
|
|
2828
2911
|
}
|
|
2829
2912
|
}
|
|
2830
2913
|
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);
|
|
2914
|
+
const stack = [{ node, prefix }];
|
|
2915
|
+
while (stack.length > 0) {
|
|
2916
|
+
const current = stack.pop();
|
|
2917
|
+
if (!current) {
|
|
2918
|
+
continue;
|
|
2850
2919
|
}
|
|
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);
|
|
2920
|
+
if (current.node.terminal) {
|
|
2921
|
+
await visitor(current.prefix);
|
|
2861
2922
|
}
|
|
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
|
-
);
|
|
2923
|
+
const children = [...current.node.children].reverse();
|
|
2924
|
+
for (const [character, child] of children) {
|
|
2925
|
+
stack.push({ node: child, prefix: `${current.prefix}${character}` });
|
|
2875
2926
|
}
|
|
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
2927
|
}
|
|
2890
2928
|
}
|
|
2929
|
+
literalPrefix(pattern) {
|
|
2930
|
+
const wildcardIndex = pattern.search(/[*?]/);
|
|
2931
|
+
return wildcardIndex === -1 ? pattern : pattern.slice(0, wildcardIndex);
|
|
2932
|
+
}
|
|
2891
2933
|
pruneKnownKeysIfNeeded() {
|
|
2892
2934
|
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
2893
2935
|
return;
|
|
@@ -3195,7 +3237,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3195
3237
|
emitError: (operation, context) => this.emitError(operation, context),
|
|
3196
3238
|
formatError: (error) => this.formatError(error),
|
|
3197
3239
|
storeEntry: (key, kind, value, options2) => this.storeEntry(key, kind, value, options2),
|
|
3198
|
-
recordCircuitFailure: (key, options2, error) => this.recordCircuitFailure(key, options2, error),
|
|
3240
|
+
recordCircuitFailure: (key, breakerKey, options2, error) => this.recordCircuitFailure(key, breakerKey, options2, error),
|
|
3199
3241
|
resolveLayerMs: (layerName, override, globalDefault, fallback) => this.resolveLayerMs(layerName, override, globalDefault, fallback),
|
|
3200
3242
|
sleep: (ms) => this.sleep(ms),
|
|
3201
3243
|
withTimeout: (promise, ms, createError) => this.withTimeout(promise, ms, createError),
|
|
@@ -3210,6 +3252,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3210
3252
|
singleFlightRenewIntervalMs: options.singleFlightRenewIntervalMs,
|
|
3211
3253
|
backgroundRefreshTimeoutMs: options.backgroundRefreshTimeoutMs,
|
|
3212
3254
|
negativeCaching: options.negativeCaching,
|
|
3255
|
+
cacheNullValues: options.cacheNullValues,
|
|
3213
3256
|
refreshAhead: options.refreshAhead,
|
|
3214
3257
|
circuitBreaker: options.circuitBreaker,
|
|
3215
3258
|
fetcherRateLimit: options.fetcherRateLimit
|
|
@@ -3262,6 +3305,64 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3262
3305
|
async getOrSet(key, fetcher, options) {
|
|
3263
3306
|
return this.get(key, fetcher, options);
|
|
3264
3307
|
}
|
|
3308
|
+
/**
|
|
3309
|
+
* Returns a discriminated cache entry, or `null` on miss.
|
|
3310
|
+
* Unlike `get()`, this distinguishes a stored `null` value from an absent key.
|
|
3311
|
+
*/
|
|
3312
|
+
async getEntry(key) {
|
|
3313
|
+
return this.observeOperation("layercache.get_entry", { "layercache.key": String(key ?? "") }, async () => {
|
|
3314
|
+
const userKey = validateCacheKey(key);
|
|
3315
|
+
const normalizedKey = this.qualifyKey(userKey);
|
|
3316
|
+
await this.awaitStartup("getEntry");
|
|
3317
|
+
let sawRetainableValue = false;
|
|
3318
|
+
for (let index = 0; index < this.layers.length; index += 1) {
|
|
3319
|
+
const layer = this.layers[index];
|
|
3320
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
3321
|
+
continue;
|
|
3322
|
+
}
|
|
3323
|
+
const readStart = performance.now();
|
|
3324
|
+
const stored = await this.readLayerEntry(layer, normalizedKey);
|
|
3325
|
+
this.metricsCollector.recordLatency(layer.name, performance.now() - readStart);
|
|
3326
|
+
if (stored === null) {
|
|
3327
|
+
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
3328
|
+
continue;
|
|
3329
|
+
}
|
|
3330
|
+
const resolved = resolveStoredValue(stored);
|
|
3331
|
+
if (resolved.state === "expired") {
|
|
3332
|
+
await layer.delete(normalizedKey);
|
|
3333
|
+
continue;
|
|
3334
|
+
}
|
|
3335
|
+
sawRetainableValue = true;
|
|
3336
|
+
await this.tagIndex.touch(normalizedKey);
|
|
3337
|
+
await this.reader.backfill(normalizedKey, stored, index - 1);
|
|
3338
|
+
this.metricsCollector.increment("hits");
|
|
3339
|
+
if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
|
|
3340
|
+
this.metricsCollector.increment("staleHits");
|
|
3341
|
+
}
|
|
3342
|
+
this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
|
|
3343
|
+
this.logger.debug?.("hit", { key: normalizedKey, layer: layer.name, state: resolved.state });
|
|
3344
|
+
this.emit("hit", {
|
|
3345
|
+
key: normalizedKey,
|
|
3346
|
+
layer: layer.name,
|
|
3347
|
+
state: resolved.state
|
|
3348
|
+
});
|
|
3349
|
+
return {
|
|
3350
|
+
key: userKey,
|
|
3351
|
+
value: resolved.value,
|
|
3352
|
+
kind: resolved.envelope?.kind ?? "value",
|
|
3353
|
+
state: resolved.state,
|
|
3354
|
+
layer: layer.name
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
if (!sawRetainableValue) {
|
|
3358
|
+
await this.tagIndex.remove(normalizedKey);
|
|
3359
|
+
}
|
|
3360
|
+
this.metricsCollector.increment("misses");
|
|
3361
|
+
this.logger.debug?.("miss", { key: normalizedKey, mode: "getEntry" });
|
|
3362
|
+
this.emit("miss", { key: normalizedKey, mode: "getEntry" });
|
|
3363
|
+
return null;
|
|
3364
|
+
});
|
|
3365
|
+
}
|
|
3265
3366
|
/**
|
|
3266
3367
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
3267
3368
|
* Useful when the value is expected to exist or the fetcher is expected to
|
|
@@ -3726,6 +3827,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3726
3827
|
getMetrics() {
|
|
3727
3828
|
return this.metricsCollector.snapshot;
|
|
3728
3829
|
}
|
|
3830
|
+
/**
|
|
3831
|
+
* Runs an operation while collecting only the metrics emitted by its async context.
|
|
3832
|
+
* Used by namespaces so metrics tracking does not serialize the operation itself.
|
|
3833
|
+
*/
|
|
3834
|
+
async captureMetrics(operation) {
|
|
3835
|
+
return this.metricsCollector.capture(operation);
|
|
3836
|
+
}
|
|
3729
3837
|
/**
|
|
3730
3838
|
* Returns metrics plus layer degradation state and active background refresh count.
|
|
3731
3839
|
*/
|
|
@@ -4029,7 +4137,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
4029
4137
|
for (const key of keys) {
|
|
4030
4138
|
await this.tagIndex.remove(key);
|
|
4031
4139
|
this.ttlResolver.deleteProfile(key);
|
|
4032
|
-
this.circuitBreakerManager.delete(key);
|
|
4140
|
+
this.circuitBreakerManager.delete(`key:${key}`);
|
|
4033
4141
|
}
|
|
4034
4142
|
this.metricsCollector.increment("deletes", keys.length);
|
|
4035
4143
|
this.metricsCollector.increment("invalidations");
|
|
@@ -4048,7 +4156,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
4048
4156
|
}
|
|
4049
4157
|
await this.tagIndex.remove(key);
|
|
4050
4158
|
this.ttlResolver.deleteProfile(key);
|
|
4051
|
-
this.circuitBreakerManager.delete(key);
|
|
4159
|
+
this.circuitBreakerManager.delete(`key:${key}`);
|
|
4052
4160
|
}
|
|
4053
4161
|
this.metricsCollector.increment("invalidations");
|
|
4054
4162
|
this.logger.debug?.("expire", { keys });
|
|
@@ -4090,7 +4198,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
4090
4198
|
for (const key of keys) {
|
|
4091
4199
|
await this.tagIndex.remove(key);
|
|
4092
4200
|
this.ttlResolver.deleteProfile(key);
|
|
4093
|
-
this.circuitBreakerManager.delete(key);
|
|
4201
|
+
this.circuitBreakerManager.delete(`key:${key}`);
|
|
4094
4202
|
}
|
|
4095
4203
|
}
|
|
4096
4204
|
}
|
|
@@ -4335,15 +4443,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
4335
4443
|
isGracefulDegradationEnabled() {
|
|
4336
4444
|
return Boolean(this.options.gracefulDegradation);
|
|
4337
4445
|
}
|
|
4338
|
-
recordCircuitFailure(key, options, error) {
|
|
4446
|
+
recordCircuitFailure(key, breakerKey, options, error) {
|
|
4339
4447
|
if (!options) {
|
|
4340
4448
|
return;
|
|
4341
4449
|
}
|
|
4342
|
-
this.circuitBreakerManager.recordFailure(
|
|
4343
|
-
if (this.circuitBreakerManager.isOpen(
|
|
4450
|
+
this.circuitBreakerManager.recordFailure(breakerKey, options);
|
|
4451
|
+
if (this.circuitBreakerManager.isOpen(breakerKey)) {
|
|
4344
4452
|
this.metricsCollector.increment("circuitBreakerTrips");
|
|
4345
4453
|
}
|
|
4346
|
-
this.emitError("fetch", { key, error: this.formatError(error) });
|
|
4454
|
+
this.emitError("fetch", { key, breakerKey, error: this.formatError(error) });
|
|
4347
4455
|
}
|
|
4348
4456
|
emitError(operation, context) {
|
|
4349
4457
|
this.logger.error?.(operation, context);
|
|
@@ -5989,6 +6097,7 @@ var PayloadProtectionError = class extends Error {
|
|
|
5989
6097
|
|
|
5990
6098
|
// src/layers/DiskLayer.ts
|
|
5991
6099
|
var FILE_SCAN_CONCURRENCY = 32;
|
|
6100
|
+
var DEFAULT_MAX_WRITE_QUEUE_DEPTH = 1e4;
|
|
5992
6101
|
var DiskLayer = class {
|
|
5993
6102
|
name;
|
|
5994
6103
|
defaultTtl;
|
|
@@ -5997,8 +6106,10 @@ var DiskLayer = class {
|
|
|
5997
6106
|
serializer;
|
|
5998
6107
|
maxFiles;
|
|
5999
6108
|
maxEntryBytes;
|
|
6109
|
+
maxWriteQueueDepth;
|
|
6000
6110
|
protection;
|
|
6001
6111
|
writeQueue = Promise.resolve();
|
|
6112
|
+
writeQueueDepth = 0;
|
|
6002
6113
|
/**
|
|
6003
6114
|
* Creates a disk-backed cache layer.
|
|
6004
6115
|
*/
|
|
@@ -6009,6 +6120,7 @@ var DiskLayer = class {
|
|
|
6009
6120
|
this.serializer = options.serializer ?? new JsonSerializer();
|
|
6010
6121
|
this.maxFiles = this.normalizeMaxFiles(options.maxFiles);
|
|
6011
6122
|
this.maxEntryBytes = this.normalizeMaxEntryBytes(options.maxEntryBytes);
|
|
6123
|
+
this.maxWriteQueueDepth = this.normalizeMaxWriteQueueDepth(options.maxWriteQueueDepth);
|
|
6012
6124
|
this.protection = new PayloadProtection({
|
|
6013
6125
|
encryptionKey: options.encryptionKey,
|
|
6014
6126
|
signingKey: options.signingKey
|
|
@@ -6224,6 +6336,16 @@ var DiskLayer = class {
|
|
|
6224
6336
|
}
|
|
6225
6337
|
return normalized;
|
|
6226
6338
|
}
|
|
6339
|
+
normalizeMaxWriteQueueDepth(maxWriteQueueDepth) {
|
|
6340
|
+
if (maxWriteQueueDepth === false) {
|
|
6341
|
+
return false;
|
|
6342
|
+
}
|
|
6343
|
+
const normalized = maxWriteQueueDepth ?? DEFAULT_MAX_WRITE_QUEUE_DEPTH;
|
|
6344
|
+
if (!Number.isInteger(normalized) || normalized <= 0) {
|
|
6345
|
+
throw new Error("DiskLayer.maxWriteQueueDepth must be a positive integer or false.");
|
|
6346
|
+
}
|
|
6347
|
+
return normalized;
|
|
6348
|
+
}
|
|
6227
6349
|
async readEntryFile(filePath) {
|
|
6228
6350
|
let handle;
|
|
6229
6351
|
try {
|
|
@@ -6336,7 +6458,13 @@ var DiskLayer = class {
|
|
|
6336
6458
|
}
|
|
6337
6459
|
}
|
|
6338
6460
|
enqueueWrite(operation) {
|
|
6339
|
-
|
|
6461
|
+
if (this.maxWriteQueueDepth !== false && this.writeQueueDepth >= this.maxWriteQueueDepth) {
|
|
6462
|
+
return Promise.reject(new Error(`DiskLayer write queue limit (${this.maxWriteQueueDepth}) exceeded.`));
|
|
6463
|
+
}
|
|
6464
|
+
this.writeQueueDepth += 1;
|
|
6465
|
+
const next = this.writeQueue.then(operation, operation).finally(() => {
|
|
6466
|
+
this.writeQueueDepth -= 1;
|
|
6467
|
+
});
|
|
6340
6468
|
this.writeQueue = next.catch(() => void 0);
|
|
6341
6469
|
return next;
|
|
6342
6470
|
}
|