layercache 2.1.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 +29 -9
- package/dist/{chunk-6X7NV5BG.js → chunk-L6L7QXYF.js} +95 -14
- package/dist/{chunk-IVX6ABFX.js → chunk-XMUT66SH.js} +116 -90
- package/dist/cli.cjs +153 -25
- package/dist/cli.js +69 -13
- package/dist/{edge-BCU8D-Yd.d.cts → edge-LBUuZAdr.d.cts} +61 -2
- package/dist/{edge-BCU8D-Yd.d.ts → edge-LBUuZAdr.d.ts} +61 -2
- package/dist/edge.cjs +114 -90
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +1 -1
- package/dist/index.cjs +578 -220
- package/dist/index.d.cts +55 -3
- package/dist/index.d.ts +55 -3
- package/dist/index.js +366 -113
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12,12 +12,13 @@ import {
|
|
|
12
12
|
validateTag,
|
|
13
13
|
validateTags,
|
|
14
14
|
validateTtlPolicy
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-L6L7QXYF.js";
|
|
16
16
|
import {
|
|
17
17
|
MemoryLayer,
|
|
18
18
|
TagIndex,
|
|
19
|
-
createHonoCacheMiddleware
|
|
20
|
-
|
|
19
|
+
createHonoCacheMiddleware,
|
|
20
|
+
normalizeHttpCacheUrl
|
|
21
|
+
} from "./chunk-XMUT66SH.js";
|
|
21
22
|
import {
|
|
22
23
|
PatternMatcher,
|
|
23
24
|
createStoredValueEnvelope,
|
|
@@ -70,39 +71,6 @@ function cloneNamespaceMetrics(metrics) {
|
|
|
70
71
|
)
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
|
-
function diffNamespaceMetrics(before, after) {
|
|
74
|
-
const latencyByLayer = Object.fromEntries(
|
|
75
|
-
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
76
|
-
layer,
|
|
77
|
-
{
|
|
78
|
-
avgMs: value.avgMs,
|
|
79
|
-
maxMs: value.maxMs,
|
|
80
|
-
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
81
|
-
}
|
|
82
|
-
])
|
|
83
|
-
);
|
|
84
|
-
return {
|
|
85
|
-
hits: after.hits - before.hits,
|
|
86
|
-
misses: after.misses - before.misses,
|
|
87
|
-
fetches: after.fetches - before.fetches,
|
|
88
|
-
sets: after.sets - before.sets,
|
|
89
|
-
deletes: after.deletes - before.deletes,
|
|
90
|
-
backfills: after.backfills - before.backfills,
|
|
91
|
-
invalidations: after.invalidations - before.invalidations,
|
|
92
|
-
staleHits: after.staleHits - before.staleHits,
|
|
93
|
-
refreshes: after.refreshes - before.refreshes,
|
|
94
|
-
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
95
|
-
writeFailures: after.writeFailures - before.writeFailures,
|
|
96
|
-
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
97
|
-
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
98
|
-
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
99
|
-
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
100
|
-
hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
|
|
101
|
-
missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
|
|
102
|
-
latencyByLayer,
|
|
103
|
-
resetAt: after.resetAt
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
74
|
function addNamespaceMetrics(base, delta) {
|
|
107
75
|
return {
|
|
108
76
|
hits: base.hits + delta.hits,
|
|
@@ -138,14 +106,6 @@ function computeNamespaceHitRate(metrics) {
|
|
|
138
106
|
}
|
|
139
107
|
return { overall, byLayer };
|
|
140
108
|
}
|
|
141
|
-
function diffMetricMap(before, after) {
|
|
142
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
143
|
-
const result = {};
|
|
144
|
-
for (const key of keys) {
|
|
145
|
-
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
146
|
-
}
|
|
147
|
-
return result;
|
|
148
|
-
}
|
|
149
109
|
function addMetricMap(base, delta) {
|
|
150
110
|
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
151
111
|
const result = {};
|
|
@@ -182,6 +142,20 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
182
142
|
async getOrSet(key, fetcher, options) {
|
|
183
143
|
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
184
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Returns a namespaced cache entry, or `null` on miss.
|
|
147
|
+
* Unlike `get()`, this distinguishes a stored `null` value from an absent key.
|
|
148
|
+
*/
|
|
149
|
+
async getEntry(key) {
|
|
150
|
+
const entry = await this.trackMetrics(() => this.cache.getEntry(this.qualify(key)));
|
|
151
|
+
if (entry === null) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
...entry,
|
|
156
|
+
key
|
|
157
|
+
};
|
|
158
|
+
}
|
|
185
159
|
/**
|
|
186
160
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
187
161
|
*/
|
|
@@ -416,13 +390,24 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
416
390
|
};
|
|
417
391
|
}
|
|
418
392
|
async trackMetrics(operation) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
393
|
+
let result;
|
|
394
|
+
let metrics;
|
|
395
|
+
try {
|
|
396
|
+
;
|
|
397
|
+
({ result, metrics } = await this.cache.captureMetrics(operation));
|
|
398
|
+
} catch (error) {
|
|
399
|
+
const capturedMetrics = error.metrics;
|
|
400
|
+
if (capturedMetrics) {
|
|
401
|
+
await this.getMetricsMutex().runExclusive(() => {
|
|
402
|
+
this.metrics = addNamespaceMetrics(this.metrics, capturedMetrics);
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
await this.getMetricsMutex().runExclusive(() => {
|
|
408
|
+
this.metrics = addNamespaceMetrics(this.metrics, metrics);
|
|
425
409
|
});
|
|
410
|
+
return result;
|
|
426
411
|
}
|
|
427
412
|
getMetricsMutex() {
|
|
428
413
|
const existing = _CacheNamespace.metricsMutexes.get(this.cache);
|
|
@@ -913,7 +898,9 @@ var CacheStackMaintenance = class {
|
|
|
913
898
|
}
|
|
914
899
|
bumpKeyEpochs(keys) {
|
|
915
900
|
for (const key of keys) {
|
|
916
|
-
|
|
901
|
+
const nextEpoch = this.currentKeyEpoch(key) + 1;
|
|
902
|
+
this.keyEpochs.delete(key);
|
|
903
|
+
this.keyEpochs.set(key, nextEpoch);
|
|
917
904
|
}
|
|
918
905
|
this.pruneKeyEpochsIfNeeded();
|
|
919
906
|
}
|
|
@@ -972,10 +959,13 @@ var CacheStackMaintenance = class {
|
|
|
972
959
|
if (this.keyEpochs.size <= MAX_KEY_EPOCHS) {
|
|
973
960
|
return;
|
|
974
961
|
}
|
|
975
|
-
const
|
|
976
|
-
const toDelete = Math.ceil(sorted.length * 0.1);
|
|
962
|
+
const toDelete = Math.ceil(this.keyEpochs.size * 0.1);
|
|
977
963
|
for (let i = 0; i < toDelete; i++) {
|
|
978
|
-
this.keyEpochs.
|
|
964
|
+
const oldestKey = this.keyEpochs.keys().next().value;
|
|
965
|
+
if (oldestKey === void 0) {
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
this.keyEpochs.delete(oldestKey);
|
|
979
969
|
}
|
|
980
970
|
}
|
|
981
971
|
};
|
|
@@ -1021,6 +1011,9 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
|
1021
1011
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1022
1012
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1023
1013
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1014
|
+
var SINGLE_FLIGHT_BACKOFF_FACTOR = 2;
|
|
1015
|
+
var SINGLE_FLIGHT_BACKOFF_JITTER = 0.2;
|
|
1016
|
+
var SINGLE_FLIGHT_MAX_POLL_MS = 1e3;
|
|
1024
1017
|
var CacheStackReader = class {
|
|
1025
1018
|
constructor(options) {
|
|
1026
1019
|
this.options = options;
|
|
@@ -1109,22 +1102,28 @@ var CacheStackReader = class {
|
|
|
1109
1102
|
if (upToIndex < 0) {
|
|
1110
1103
|
return;
|
|
1111
1104
|
}
|
|
1105
|
+
const operations = [];
|
|
1112
1106
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
1113
1107
|
const layer = this.options.layers[index];
|
|
1114
1108
|
if (!layer || this.options.shouldSkipLayer(layer)) {
|
|
1115
1109
|
continue;
|
|
1116
1110
|
}
|
|
1117
1111
|
const ttl = remainingStoredTtlMs(stored) ?? this.options.resolveLayerMs(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1112
|
+
operations.push(
|
|
1113
|
+
(async () => {
|
|
1114
|
+
try {
|
|
1115
|
+
await layer.set(key, stored, ttl);
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
await this.options.handleLayerFailure(layer, "backfill", error);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
this.options.metricsCollector.increment("backfills");
|
|
1121
|
+
this.options.logger.debug?.("backfill", { key, layer: layer.name });
|
|
1122
|
+
this.options.emit("backfill", { key, layer: layer.name });
|
|
1123
|
+
})()
|
|
1124
|
+
);
|
|
1127
1125
|
}
|
|
1126
|
+
await Promise.all(operations);
|
|
1128
1127
|
}
|
|
1129
1128
|
abortAllRefreshes() {
|
|
1130
1129
|
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
@@ -1238,6 +1237,7 @@ var CacheStackReader = class {
|
|
|
1238
1237
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
1239
1238
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
1240
1239
|
const deadline = Date.now() + timeoutMs;
|
|
1240
|
+
let nextPollMs = pollIntervalMs;
|
|
1241
1241
|
this.options.metricsCollector.increment("singleFlightWaits");
|
|
1242
1242
|
this.options.emit("stampede-dedupe", { key });
|
|
1243
1243
|
while (Date.now() < deadline) {
|
|
@@ -1246,16 +1246,36 @@ var CacheStackReader = class {
|
|
|
1246
1246
|
this.options.metricsCollector.increment("hits");
|
|
1247
1247
|
return hit.value;
|
|
1248
1248
|
}
|
|
1249
|
-
|
|
1249
|
+
const remainingMs = deadline - Date.now();
|
|
1250
|
+
if (remainingMs <= 0) {
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1253
|
+
const delayMs = Math.min(this.jitterSingleFlightPoll(nextPollMs), remainingMs);
|
|
1254
|
+
await this.options.sleep(delayMs);
|
|
1255
|
+
nextPollMs = Math.min(nextPollMs * SINGLE_FLIGHT_BACKOFF_FACTOR, SINGLE_FLIGHT_MAX_POLL_MS, timeoutMs);
|
|
1256
|
+
}
|
|
1257
|
+
if (!this.options.singleFlightCoordinator) {
|
|
1258
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
|
|
1250
1259
|
}
|
|
1251
|
-
return this.
|
|
1260
|
+
return this.options.singleFlightCoordinator.execute(
|
|
1261
|
+
key,
|
|
1262
|
+
this.resolveSingleFlightOptions(),
|
|
1263
|
+
() => this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext),
|
|
1264
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext)
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
jitterSingleFlightPoll(delayMs) {
|
|
1268
|
+
const jitterRange = delayMs * SINGLE_FLIGHT_BACKOFF_JITTER;
|
|
1269
|
+
return Math.max(1, Math.round(delayMs - jitterRange + Math.random() * jitterRange * 2));
|
|
1252
1270
|
}
|
|
1253
1271
|
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1254
1272
|
key,
|
|
1255
1273
|
currentValue: void 0,
|
|
1256
1274
|
state: "miss"
|
|
1257
1275
|
}) {
|
|
1258
|
-
|
|
1276
|
+
const circuitBreakerOptions = options?.circuitBreaker ?? this.options.circuitBreaker;
|
|
1277
|
+
const breakerKey = this.resolveCircuitBreakerKey(key, circuitBreakerOptions);
|
|
1278
|
+
this.options.circuitBreakerManager.assertClosed(breakerKey, circuitBreakerOptions);
|
|
1259
1279
|
this.options.metricsCollector.increment("fetches");
|
|
1260
1280
|
const fetchStart = Date.now();
|
|
1261
1281
|
let fetched;
|
|
@@ -1265,13 +1285,13 @@ var CacheStackReader = class {
|
|
|
1265
1285
|
{ key, fetcher },
|
|
1266
1286
|
() => fetcher(fetcherContext)
|
|
1267
1287
|
);
|
|
1268
|
-
this.options.circuitBreakerManager.recordSuccess(
|
|
1288
|
+
this.options.circuitBreakerManager.recordSuccess(breakerKey);
|
|
1269
1289
|
this.options.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1270
1290
|
} catch (error) {
|
|
1271
|
-
this.options.recordCircuitFailure(key,
|
|
1291
|
+
this.options.recordCircuitFailure(key, breakerKey, circuitBreakerOptions, error);
|
|
1272
1292
|
throw error;
|
|
1273
1293
|
}
|
|
1274
|
-
if (fetched ===
|
|
1294
|
+
if (fetched === void 0 || fetched === null && !this.shouldCacheNullValues(options)) {
|
|
1275
1295
|
if (!this.shouldNegativeCache(options)) {
|
|
1276
1296
|
return null;
|
|
1277
1297
|
}
|
|
@@ -1313,6 +1333,18 @@ var CacheStackReader = class {
|
|
|
1313
1333
|
await this.options.storeEntry(key, "value", fetched, options);
|
|
1314
1334
|
return fetched;
|
|
1315
1335
|
}
|
|
1336
|
+
resolveCircuitBreakerKey(key, options) {
|
|
1337
|
+
if (!options) {
|
|
1338
|
+
return `key:${key}`;
|
|
1339
|
+
}
|
|
1340
|
+
if (options.breakerKey) {
|
|
1341
|
+
return `custom:${options.breakerKey}`;
|
|
1342
|
+
}
|
|
1343
|
+
if (options.scope === "shared") {
|
|
1344
|
+
return "scope:shared";
|
|
1345
|
+
}
|
|
1346
|
+
return `key:${key}`;
|
|
1347
|
+
}
|
|
1316
1348
|
runScheduleBackgroundRefresh(key, fetcher, options, fetcherContext) {
|
|
1317
1349
|
this.scheduleBackgroundRefresh(key, fetcher, options, fetcherContext);
|
|
1318
1350
|
}
|
|
@@ -1413,6 +1445,9 @@ var CacheStackReader = class {
|
|
|
1413
1445
|
shouldNegativeCache(options) {
|
|
1414
1446
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
1415
1447
|
}
|
|
1448
|
+
shouldCacheNullValues(options) {
|
|
1449
|
+
return options?.cacheNullValues ?? this.options.cacheNullValues ?? false;
|
|
1450
|
+
}
|
|
1416
1451
|
isNegativeStoredValue(stored) {
|
|
1417
1452
|
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
1418
1453
|
}
|
|
@@ -1814,6 +1849,7 @@ var CircuitBreakerManager = class {
|
|
|
1814
1849
|
// src/internal/FetchRateLimiter.ts
|
|
1815
1850
|
var MAX_BUCKETS = 1e4;
|
|
1816
1851
|
var MAX_QUEUE_PER_BUCKET = 1e4;
|
|
1852
|
+
var DEFAULT_QUEUE_OVERFLOW_POLICY = "reject";
|
|
1817
1853
|
var FetchRateLimiter = class {
|
|
1818
1854
|
buckets = /* @__PURE__ */ new Map();
|
|
1819
1855
|
queuesByBucket = /* @__PURE__ */ new Map();
|
|
@@ -1838,8 +1874,12 @@ var FetchRateLimiter = class {
|
|
|
1838
1874
|
const bucketKey = this.resolveBucketKey(normalized, context);
|
|
1839
1875
|
const queue = this.queuesByBucket.get(bucketKey) ?? [];
|
|
1840
1876
|
if (queue.length >= MAX_QUEUE_PER_BUCKET) {
|
|
1841
|
-
|
|
1842
|
-
|
|
1877
|
+
if ((normalized.queueOverflow ?? DEFAULT_QUEUE_OVERFLOW_POLICY) === "bypass") {
|
|
1878
|
+
this.rateLimitBypasses += 1;
|
|
1879
|
+
task().then(resolve2, reject);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
reject(new Error(`FetchRateLimiter queue overflow for bucket "${bucketKey}".`));
|
|
1843
1883
|
return;
|
|
1844
1884
|
}
|
|
1845
1885
|
queue.push({
|
|
@@ -1887,7 +1927,8 @@ var FetchRateLimiter = class {
|
|
|
1887
1927
|
intervalMs,
|
|
1888
1928
|
maxPerInterval,
|
|
1889
1929
|
scope: options.scope ?? "global",
|
|
1890
|
-
bucketKey: options.bucketKey
|
|
1930
|
+
bucketKey: options.bucketKey,
|
|
1931
|
+
queueOverflow: options.queueOverflow
|
|
1891
1932
|
};
|
|
1892
1933
|
}
|
|
1893
1934
|
resolveBucketKey(options, context) {
|
|
@@ -2072,7 +2113,9 @@ var FetchRateLimiter = class {
|
|
|
2072
2113
|
};
|
|
2073
2114
|
|
|
2074
2115
|
// src/internal/MetricsCollector.ts
|
|
2116
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2075
2117
|
var MetricsCollector = class {
|
|
2118
|
+
captures = new AsyncLocalStorage();
|
|
2076
2119
|
data = this.empty();
|
|
2077
2120
|
get snapshot() {
|
|
2078
2121
|
return {
|
|
@@ -2085,18 +2128,46 @@ var MetricsCollector = class {
|
|
|
2085
2128
|
increment(field, amount = 1) {
|
|
2086
2129
|
;
|
|
2087
2130
|
this.data[field] += amount;
|
|
2131
|
+
for (const capture of this.captures.getStore() ?? []) {
|
|
2132
|
+
;
|
|
2133
|
+
capture[field] += amount;
|
|
2134
|
+
}
|
|
2088
2135
|
}
|
|
2089
2136
|
incrementLayer(map, layerName) {
|
|
2090
2137
|
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
2138
|
+
for (const capture of this.captures.getStore() ?? []) {
|
|
2139
|
+
capture[map][layerName] = (capture[map][layerName] ?? 0) + 1;
|
|
2140
|
+
}
|
|
2091
2141
|
}
|
|
2092
2142
|
/**
|
|
2093
2143
|
* Records a read latency sample for the given layer.
|
|
2094
2144
|
* Maintains a rolling average and max using Welford's online algorithm.
|
|
2095
2145
|
*/
|
|
2096
2146
|
recordLatency(layerName, durationMs) {
|
|
2097
|
-
|
|
2147
|
+
this.recordLatencySample(this.data, layerName, durationMs);
|
|
2148
|
+
for (const capture of this.captures.getStore() ?? []) {
|
|
2149
|
+
this.recordLatencySample(capture, layerName, durationMs);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
async capture(operation) {
|
|
2153
|
+
const metrics = this.empty();
|
|
2154
|
+
const activeCaptures = this.captures.getStore();
|
|
2155
|
+
const captures = activeCaptures ? [...activeCaptures, metrics] : [metrics];
|
|
2156
|
+
try {
|
|
2157
|
+
const result = await this.captures.run(captures, operation);
|
|
2158
|
+
return { result, metrics };
|
|
2159
|
+
} catch (error) {
|
|
2160
|
+
if ((typeof error === "object" || typeof error === "function") && error !== null) {
|
|
2161
|
+
;
|
|
2162
|
+
error.metrics = metrics;
|
|
2163
|
+
}
|
|
2164
|
+
throw error;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
recordLatencySample(metrics, layerName, durationMs) {
|
|
2168
|
+
const existing = metrics.latencyByLayer[layerName];
|
|
2098
2169
|
if (!existing) {
|
|
2099
|
-
|
|
2170
|
+
metrics.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
2100
2171
|
return;
|
|
2101
2172
|
}
|
|
2102
2173
|
existing.count += 1;
|
|
@@ -2163,6 +2234,7 @@ var TtlResolver = class {
|
|
|
2163
2234
|
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
2164
2235
|
profile.hits += 1;
|
|
2165
2236
|
profile.lastAccessAt = Date.now();
|
|
2237
|
+
this.accessProfiles.delete(key);
|
|
2166
2238
|
this.accessProfiles.set(key, profile);
|
|
2167
2239
|
this.pruneIfNeeded();
|
|
2168
2240
|
}
|
|
@@ -2252,12 +2324,12 @@ var TtlResolver = class {
|
|
|
2252
2324
|
return;
|
|
2253
2325
|
}
|
|
2254
2326
|
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
this.accessProfiles.delete(entry[0]);
|
|
2327
|
+
for (let i = 0; i < toRemove; i++) {
|
|
2328
|
+
const oldestKey = this.accessProfiles.keys().next().value;
|
|
2329
|
+
if (oldestKey === void 0) {
|
|
2330
|
+
break;
|
|
2260
2331
|
}
|
|
2332
|
+
this.accessProfiles.delete(oldestKey);
|
|
2261
2333
|
}
|
|
2262
2334
|
}
|
|
2263
2335
|
};
|
|
@@ -2508,7 +2580,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2508
2580
|
emitError: (operation, context) => this.emitError(operation, context),
|
|
2509
2581
|
formatError: (error) => this.formatError(error),
|
|
2510
2582
|
storeEntry: (key, kind, value, options2) => this.storeEntry(key, kind, value, options2),
|
|
2511
|
-
recordCircuitFailure: (key, options2, error) => this.recordCircuitFailure(key, options2, error),
|
|
2583
|
+
recordCircuitFailure: (key, breakerKey, options2, error) => this.recordCircuitFailure(key, breakerKey, options2, error),
|
|
2512
2584
|
resolveLayerMs: (layerName, override, globalDefault, fallback) => this.resolveLayerMs(layerName, override, globalDefault, fallback),
|
|
2513
2585
|
sleep: (ms) => this.sleep(ms),
|
|
2514
2586
|
withTimeout: (promise, ms, createError) => this.withTimeout(promise, ms, createError),
|
|
@@ -2523,6 +2595,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2523
2595
|
singleFlightRenewIntervalMs: options.singleFlightRenewIntervalMs,
|
|
2524
2596
|
backgroundRefreshTimeoutMs: options.backgroundRefreshTimeoutMs,
|
|
2525
2597
|
negativeCaching: options.negativeCaching,
|
|
2598
|
+
cacheNullValues: options.cacheNullValues,
|
|
2526
2599
|
refreshAhead: options.refreshAhead,
|
|
2527
2600
|
circuitBreaker: options.circuitBreaker,
|
|
2528
2601
|
fetcherRateLimit: options.fetcherRateLimit
|
|
@@ -2575,6 +2648,64 @@ var CacheStack = class extends EventEmitter {
|
|
|
2575
2648
|
async getOrSet(key, fetcher, options) {
|
|
2576
2649
|
return this.get(key, fetcher, options);
|
|
2577
2650
|
}
|
|
2651
|
+
/**
|
|
2652
|
+
* Returns a discriminated cache entry, or `null` on miss.
|
|
2653
|
+
* Unlike `get()`, this distinguishes a stored `null` value from an absent key.
|
|
2654
|
+
*/
|
|
2655
|
+
async getEntry(key) {
|
|
2656
|
+
return this.observeOperation("layercache.get_entry", { "layercache.key": String(key ?? "") }, async () => {
|
|
2657
|
+
const userKey = validateCacheKey(key);
|
|
2658
|
+
const normalizedKey = this.qualifyKey(userKey);
|
|
2659
|
+
await this.awaitStartup("getEntry");
|
|
2660
|
+
let sawRetainableValue = false;
|
|
2661
|
+
for (let index = 0; index < this.layers.length; index += 1) {
|
|
2662
|
+
const layer = this.layers[index];
|
|
2663
|
+
if (!layer || this.shouldSkipLayer(layer)) {
|
|
2664
|
+
continue;
|
|
2665
|
+
}
|
|
2666
|
+
const readStart = performance.now();
|
|
2667
|
+
const stored = await this.readLayerEntry(layer, normalizedKey);
|
|
2668
|
+
this.metricsCollector.recordLatency(layer.name, performance.now() - readStart);
|
|
2669
|
+
if (stored === null) {
|
|
2670
|
+
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
2671
|
+
continue;
|
|
2672
|
+
}
|
|
2673
|
+
const resolved = resolveStoredValue(stored);
|
|
2674
|
+
if (resolved.state === "expired") {
|
|
2675
|
+
await layer.delete(normalizedKey);
|
|
2676
|
+
continue;
|
|
2677
|
+
}
|
|
2678
|
+
sawRetainableValue = true;
|
|
2679
|
+
await this.tagIndex.touch(normalizedKey);
|
|
2680
|
+
await this.reader.backfill(normalizedKey, stored, index - 1);
|
|
2681
|
+
this.metricsCollector.increment("hits");
|
|
2682
|
+
if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
|
|
2683
|
+
this.metricsCollector.increment("staleHits");
|
|
2684
|
+
}
|
|
2685
|
+
this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
|
|
2686
|
+
this.logger.debug?.("hit", { key: normalizedKey, layer: layer.name, state: resolved.state });
|
|
2687
|
+
this.emit("hit", {
|
|
2688
|
+
key: normalizedKey,
|
|
2689
|
+
layer: layer.name,
|
|
2690
|
+
state: resolved.state
|
|
2691
|
+
});
|
|
2692
|
+
return {
|
|
2693
|
+
key: userKey,
|
|
2694
|
+
value: resolved.value,
|
|
2695
|
+
kind: resolved.envelope?.kind ?? "value",
|
|
2696
|
+
state: resolved.state,
|
|
2697
|
+
layer: layer.name
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
if (!sawRetainableValue) {
|
|
2701
|
+
await this.tagIndex.remove(normalizedKey);
|
|
2702
|
+
}
|
|
2703
|
+
this.metricsCollector.increment("misses");
|
|
2704
|
+
this.logger.debug?.("miss", { key: normalizedKey, mode: "getEntry" });
|
|
2705
|
+
this.emit("miss", { key: normalizedKey, mode: "getEntry" });
|
|
2706
|
+
return null;
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2578
2709
|
/**
|
|
2579
2710
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
2580
2711
|
* Useful when the value is expected to exist or the fetcher is expected to
|
|
@@ -3039,6 +3170,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
3039
3170
|
getMetrics() {
|
|
3040
3171
|
return this.metricsCollector.snapshot;
|
|
3041
3172
|
}
|
|
3173
|
+
/**
|
|
3174
|
+
* Runs an operation while collecting only the metrics emitted by its async context.
|
|
3175
|
+
* Used by namespaces so metrics tracking does not serialize the operation itself.
|
|
3176
|
+
*/
|
|
3177
|
+
async captureMetrics(operation) {
|
|
3178
|
+
return this.metricsCollector.capture(operation);
|
|
3179
|
+
}
|
|
3042
3180
|
/**
|
|
3043
3181
|
* Returns metrics plus layer degradation state and active background refresh count.
|
|
3044
3182
|
*/
|
|
@@ -3112,6 +3250,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
3112
3250
|
}
|
|
3113
3251
|
return this.currentGeneration;
|
|
3114
3252
|
}
|
|
3253
|
+
/**
|
|
3254
|
+
* Returns the active generation prefix number used for future cache keys.
|
|
3255
|
+
*/
|
|
3256
|
+
getGeneration() {
|
|
3257
|
+
return this.currentGeneration;
|
|
3258
|
+
}
|
|
3115
3259
|
/**
|
|
3116
3260
|
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
3117
3261
|
* remaining fresh/stale/error TTLs, and associated tags.
|
|
@@ -3336,7 +3480,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
3336
3480
|
for (const key of keys) {
|
|
3337
3481
|
await this.tagIndex.remove(key);
|
|
3338
3482
|
this.ttlResolver.deleteProfile(key);
|
|
3339
|
-
this.circuitBreakerManager.delete(key);
|
|
3483
|
+
this.circuitBreakerManager.delete(`key:${key}`);
|
|
3340
3484
|
}
|
|
3341
3485
|
this.metricsCollector.increment("deletes", keys.length);
|
|
3342
3486
|
this.metricsCollector.increment("invalidations");
|
|
@@ -3355,7 +3499,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
3355
3499
|
}
|
|
3356
3500
|
await this.tagIndex.remove(key);
|
|
3357
3501
|
this.ttlResolver.deleteProfile(key);
|
|
3358
|
-
this.circuitBreakerManager.delete(key);
|
|
3502
|
+
this.circuitBreakerManager.delete(`key:${key}`);
|
|
3359
3503
|
}
|
|
3360
3504
|
this.metricsCollector.increment("invalidations");
|
|
3361
3505
|
this.logger.debug?.("expire", { keys });
|
|
@@ -3397,7 +3541,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
3397
3541
|
for (const key of keys) {
|
|
3398
3542
|
await this.tagIndex.remove(key);
|
|
3399
3543
|
this.ttlResolver.deleteProfile(key);
|
|
3400
|
-
this.circuitBreakerManager.delete(key);
|
|
3544
|
+
this.circuitBreakerManager.delete(`key:${key}`);
|
|
3401
3545
|
}
|
|
3402
3546
|
}
|
|
3403
3547
|
}
|
|
@@ -3642,15 +3786,15 @@ var CacheStack = class extends EventEmitter {
|
|
|
3642
3786
|
isGracefulDegradationEnabled() {
|
|
3643
3787
|
return Boolean(this.options.gracefulDegradation);
|
|
3644
3788
|
}
|
|
3645
|
-
recordCircuitFailure(key, options, error) {
|
|
3789
|
+
recordCircuitFailure(key, breakerKey, options, error) {
|
|
3646
3790
|
if (!options) {
|
|
3647
3791
|
return;
|
|
3648
3792
|
}
|
|
3649
|
-
this.circuitBreakerManager.recordFailure(
|
|
3650
|
-
if (this.circuitBreakerManager.isOpen(
|
|
3793
|
+
this.circuitBreakerManager.recordFailure(breakerKey, options);
|
|
3794
|
+
if (this.circuitBreakerManager.isOpen(breakerKey)) {
|
|
3651
3795
|
this.metricsCollector.increment("circuitBreakerTrips");
|
|
3652
3796
|
}
|
|
3653
|
-
this.emitError("fetch", { key, error: this.formatError(error) });
|
|
3797
|
+
this.emitError("fetch", { key, breakerKey, error: this.formatError(error) });
|
|
3654
3798
|
}
|
|
3655
3799
|
emitError(operation, context) {
|
|
3656
3800
|
this.logger.error?.(operation, context);
|
|
@@ -3669,12 +3813,65 @@ var CacheStack = class extends EventEmitter {
|
|
|
3669
3813
|
}
|
|
3670
3814
|
};
|
|
3671
3815
|
|
|
3816
|
+
// src/generation/RedisGenerationStore.ts
|
|
3817
|
+
var DEFAULT_GENERATION_KEY = "layercache:generation";
|
|
3818
|
+
var RedisGenerationStore = class {
|
|
3819
|
+
client;
|
|
3820
|
+
key;
|
|
3821
|
+
constructor(options) {
|
|
3822
|
+
this.client = options.client;
|
|
3823
|
+
this.key = options.key ?? DEFAULT_GENERATION_KEY;
|
|
3824
|
+
}
|
|
3825
|
+
async get() {
|
|
3826
|
+
const stored = await this.client.get(this.key);
|
|
3827
|
+
if (stored === null) {
|
|
3828
|
+
return void 0;
|
|
3829
|
+
}
|
|
3830
|
+
return this.parseGeneration(stored);
|
|
3831
|
+
}
|
|
3832
|
+
async getOrInitialize(initialGeneration = 0) {
|
|
3833
|
+
this.assertGeneration(initialGeneration);
|
|
3834
|
+
await this.client.set(this.key, String(initialGeneration), "NX");
|
|
3835
|
+
const generation = await this.get();
|
|
3836
|
+
if (generation === void 0) {
|
|
3837
|
+
throw new Error(`RedisGenerationStore failed to initialize generation key "${this.key}".`);
|
|
3838
|
+
}
|
|
3839
|
+
return generation;
|
|
3840
|
+
}
|
|
3841
|
+
async set(generation) {
|
|
3842
|
+
this.assertGeneration(generation);
|
|
3843
|
+
await this.client.set(this.key, String(generation));
|
|
3844
|
+
}
|
|
3845
|
+
async bump() {
|
|
3846
|
+
const generation = await this.client.incr(this.key);
|
|
3847
|
+
this.assertGeneration(generation);
|
|
3848
|
+
return generation;
|
|
3849
|
+
}
|
|
3850
|
+
parseGeneration(value) {
|
|
3851
|
+
const generation = Number.parseInt(value, 10);
|
|
3852
|
+
if (String(generation) !== value || !this.isGeneration(generation)) {
|
|
3853
|
+
throw new Error(`RedisGenerationStore found invalid persisted generation value for key "${this.key}".`);
|
|
3854
|
+
}
|
|
3855
|
+
return generation;
|
|
3856
|
+
}
|
|
3857
|
+
assertGeneration(value) {
|
|
3858
|
+
if (!this.isGeneration(value)) {
|
|
3859
|
+
throw new Error("RedisGenerationStore generation must be a non-negative safe integer.");
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
isGeneration(value) {
|
|
3863
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
3864
|
+
}
|
|
3865
|
+
};
|
|
3866
|
+
|
|
3672
3867
|
// src/invalidation/RedisInvalidationBus.ts
|
|
3868
|
+
import { createHash, createHmac, timingSafeEqual } from "crypto";
|
|
3673
3869
|
var RedisInvalidationBus = class {
|
|
3674
3870
|
channel;
|
|
3675
3871
|
publisher;
|
|
3676
3872
|
subscriber;
|
|
3677
3873
|
logger;
|
|
3874
|
+
signingKey;
|
|
3678
3875
|
handlers = /* @__PURE__ */ new Set();
|
|
3679
3876
|
sharedListener;
|
|
3680
3877
|
subscribePromise;
|
|
@@ -3683,6 +3880,7 @@ var RedisInvalidationBus = class {
|
|
|
3683
3880
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
3684
3881
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
3685
3882
|
this.logger = options.logger;
|
|
3883
|
+
this.signingKey = options.signingSecret ? normalizeSigningSecret(options.signingSecret) : void 0;
|
|
3686
3884
|
}
|
|
3687
3885
|
/**
|
|
3688
3886
|
* Subscribes to invalidation messages and returns an unsubscribe function.
|
|
@@ -3722,7 +3920,7 @@ var RedisInvalidationBus = class {
|
|
|
3722
3920
|
* Publishes an invalidation message to other subscribers.
|
|
3723
3921
|
*/
|
|
3724
3922
|
async publish(message) {
|
|
3725
|
-
await this.publisher.publish(this.channel, JSON.stringify(message));
|
|
3923
|
+
await this.publisher.publish(this.channel, JSON.stringify(this.signingKey ? this.signMessage(message) : message));
|
|
3726
3924
|
}
|
|
3727
3925
|
async dispatchToHandlers(payload) {
|
|
3728
3926
|
let message;
|
|
@@ -3733,10 +3931,11 @@ var RedisInvalidationBus = class {
|
|
|
3733
3931
|
maxNodes: 1e4,
|
|
3734
3932
|
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
3735
3933
|
});
|
|
3736
|
-
|
|
3934
|
+
const candidate = this.signingKey ? this.verifySignedEnvelope(parsed) : parsed;
|
|
3935
|
+
if (!this.isInvalidationMessage(candidate)) {
|
|
3737
3936
|
throw new Error("Invalid invalidation payload shape.");
|
|
3738
3937
|
}
|
|
3739
|
-
message =
|
|
3938
|
+
message = candidate;
|
|
3740
3939
|
} catch (error) {
|
|
3741
3940
|
this.reportError("invalid invalidation payload", error);
|
|
3742
3941
|
return;
|
|
@@ -3761,6 +3960,34 @@ var RedisInvalidationBus = class {
|
|
|
3761
3960
|
const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
|
|
3762
3961
|
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
3763
3962
|
}
|
|
3963
|
+
signMessage(message) {
|
|
3964
|
+
const payload = JSON.stringify(message);
|
|
3965
|
+
return {
|
|
3966
|
+
payload: message,
|
|
3967
|
+
signature: this.createSignature(payload)
|
|
3968
|
+
};
|
|
3969
|
+
}
|
|
3970
|
+
verifySignedEnvelope(value) {
|
|
3971
|
+
if (!value || typeof value !== "object") {
|
|
3972
|
+
throw new Error("Signed invalidation envelope must be an object.");
|
|
3973
|
+
}
|
|
3974
|
+
const envelope = value;
|
|
3975
|
+
if (!envelope.payload || typeof envelope.payload !== "object" || typeof envelope.signature !== "string") {
|
|
3976
|
+
throw new Error("Signed invalidation envelope is missing payload or signature.");
|
|
3977
|
+
}
|
|
3978
|
+
const payload = JSON.stringify(envelope.payload);
|
|
3979
|
+
const expected = this.createSignature(payload);
|
|
3980
|
+
if (!isEqualSignature(envelope.signature, expected)) {
|
|
3981
|
+
throw new Error("Invalid invalidation message signature.");
|
|
3982
|
+
}
|
|
3983
|
+
return envelope.payload;
|
|
3984
|
+
}
|
|
3985
|
+
createSignature(payload) {
|
|
3986
|
+
if (!this.signingKey) {
|
|
3987
|
+
throw new Error("RedisInvalidationBus signing key is not configured.");
|
|
3988
|
+
}
|
|
3989
|
+
return createHmac("sha256", this.signingKey).update(payload).digest("hex");
|
|
3990
|
+
}
|
|
3764
3991
|
reportError(message, error) {
|
|
3765
3992
|
if (this.logger?.error) {
|
|
3766
3993
|
this.logger.error(message, { error });
|
|
@@ -3769,6 +3996,15 @@ var RedisInvalidationBus = class {
|
|
|
3769
3996
|
console.error(`[layercache] ${message}`, error);
|
|
3770
3997
|
}
|
|
3771
3998
|
};
|
|
3999
|
+
function normalizeSigningSecret(secret) {
|
|
4000
|
+
const raw = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, "utf8");
|
|
4001
|
+
return createHash("sha256").update(raw).digest();
|
|
4002
|
+
}
|
|
4003
|
+
function isEqualSignature(actual, expected) {
|
|
4004
|
+
const actualBuffer = Buffer.from(actual, "hex");
|
|
4005
|
+
const expectedBuffer = Buffer.from(expected, "hex");
|
|
4006
|
+
return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
|
|
4007
|
+
}
|
|
3772
4008
|
|
|
3773
4009
|
// src/http/createCacheStatsHandler.ts
|
|
3774
4010
|
function createCacheStatsHandler(cache, options = {}) {
|
|
@@ -3866,7 +4102,7 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
3866
4102
|
return;
|
|
3867
4103
|
}
|
|
3868
4104
|
const rawUrl = req.originalUrl ?? req.url ?? "/";
|
|
3869
|
-
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${
|
|
4105
|
+
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeHttpCacheUrl(rawUrl)}`;
|
|
3870
4106
|
const cached = await cache.get(key, void 0, options);
|
|
3871
4107
|
if (cached !== null) {
|
|
3872
4108
|
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
@@ -3882,12 +4118,14 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
3882
4118
|
if (originalJson) {
|
|
3883
4119
|
res.json = (body) => {
|
|
3884
4120
|
res.setHeader?.("x-cache", "MISS");
|
|
3885
|
-
|
|
3886
|
-
cache.
|
|
3887
|
-
|
|
3888
|
-
|
|
4121
|
+
if (isSuccessfulStatus(res.statusCode)) {
|
|
4122
|
+
cache.set(key, body, options).catch((err) => {
|
|
4123
|
+
cache.emit("error", {
|
|
4124
|
+
operation: "set",
|
|
4125
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4126
|
+
});
|
|
3889
4127
|
});
|
|
3890
|
-
}
|
|
4128
|
+
}
|
|
3891
4129
|
return originalJson(body);
|
|
3892
4130
|
};
|
|
3893
4131
|
}
|
|
@@ -3897,14 +4135,8 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
3897
4135
|
}
|
|
3898
4136
|
};
|
|
3899
4137
|
}
|
|
3900
|
-
function
|
|
3901
|
-
|
|
3902
|
-
const parsed = new URL(url, "http://localhost");
|
|
3903
|
-
parsed.searchParams.sort();
|
|
3904
|
-
return parsed.pathname + parsed.search;
|
|
3905
|
-
} catch {
|
|
3906
|
-
return url;
|
|
3907
|
-
}
|
|
4138
|
+
function isSuccessfulStatus(statusCode) {
|
|
4139
|
+
return statusCode === void 0 || statusCode >= 200 && statusCode < 300;
|
|
3908
4140
|
}
|
|
3909
4141
|
|
|
3910
4142
|
// src/integrations/graphql.ts
|
|
@@ -4471,12 +4703,12 @@ var RedisLayer = class {
|
|
|
4471
4703
|
};
|
|
4472
4704
|
|
|
4473
4705
|
// src/layers/DiskLayer.ts
|
|
4474
|
-
import { createHash as
|
|
4706
|
+
import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
|
|
4475
4707
|
import { promises as fs2 } from "fs";
|
|
4476
4708
|
import { join, resolve } from "path";
|
|
4477
4709
|
|
|
4478
4710
|
// src/internal/PayloadProtection.ts
|
|
4479
|
-
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
4711
|
+
import { createCipheriv, createDecipheriv, createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4480
4712
|
var MAGIC_ENCRYPTED = Buffer.from("LCP1:");
|
|
4481
4713
|
var MAGIC_SIGNED = Buffer.from("LCS1:");
|
|
4482
4714
|
var ALGORITHM = "aes-256-gcm";
|
|
@@ -4489,11 +4721,11 @@ var PayloadProtection = class {
|
|
|
4489
4721
|
constructor(options) {
|
|
4490
4722
|
if (options.encryptionKey) {
|
|
4491
4723
|
const raw = Buffer.isBuffer(options.encryptionKey) ? options.encryptionKey : Buffer.from(options.encryptionKey, "utf8");
|
|
4492
|
-
this.encryptionKey =
|
|
4724
|
+
this.encryptionKey = createHash2("sha256").update(raw).digest();
|
|
4493
4725
|
}
|
|
4494
4726
|
if (options.signingKey && !options.encryptionKey) {
|
|
4495
4727
|
const raw = Buffer.isBuffer(options.signingKey) ? options.signingKey : Buffer.from(options.signingKey, "utf8");
|
|
4496
|
-
this.signingKey =
|
|
4728
|
+
this.signingKey = createHash2("sha256").update(raw).digest();
|
|
4497
4729
|
}
|
|
4498
4730
|
}
|
|
4499
4731
|
/** Returns `true` when any protection (encryption or signing) is configured. */
|
|
@@ -4565,15 +4797,15 @@ var PayloadProtection = class {
|
|
|
4565
4797
|
}
|
|
4566
4798
|
// ── Signing (HMAC-SHA256) ─────────────────────────────────────────────
|
|
4567
4799
|
sign(payload, key) {
|
|
4568
|
-
const hmac =
|
|
4800
|
+
const hmac = createHmac2("sha256", key).update(payload).digest();
|
|
4569
4801
|
return Buffer.concat([MAGIC_SIGNED, hmac, payload]);
|
|
4570
4802
|
}
|
|
4571
4803
|
verify(payload, key) {
|
|
4572
4804
|
const headerEnd = MAGIC_SIGNED.length;
|
|
4573
4805
|
const receivedHmac = payload.subarray(headerEnd, headerEnd + HMAC_LENGTH);
|
|
4574
4806
|
const data = payload.subarray(headerEnd + HMAC_LENGTH);
|
|
4575
|
-
const expectedHmac =
|
|
4576
|
-
if (receivedHmac.length !== HMAC_LENGTH || !
|
|
4807
|
+
const expectedHmac = createHmac2("sha256", key).update(data).digest();
|
|
4808
|
+
if (receivedHmac.length !== HMAC_LENGTH || !timingSafeEqual2(receivedHmac, expectedHmac)) {
|
|
4577
4809
|
throw new PayloadProtectionError(
|
|
4578
4810
|
"HMAC verification failed. The data may have been tampered with or the signingKey is incorrect."
|
|
4579
4811
|
);
|
|
@@ -4597,6 +4829,7 @@ var PayloadProtectionError = class extends Error {
|
|
|
4597
4829
|
|
|
4598
4830
|
// src/layers/DiskLayer.ts
|
|
4599
4831
|
var FILE_SCAN_CONCURRENCY = 32;
|
|
4832
|
+
var DEFAULT_MAX_WRITE_QUEUE_DEPTH = 1e4;
|
|
4600
4833
|
var DiskLayer = class {
|
|
4601
4834
|
name;
|
|
4602
4835
|
defaultTtl;
|
|
@@ -4605,8 +4838,10 @@ var DiskLayer = class {
|
|
|
4605
4838
|
serializer;
|
|
4606
4839
|
maxFiles;
|
|
4607
4840
|
maxEntryBytes;
|
|
4841
|
+
maxWriteQueueDepth;
|
|
4608
4842
|
protection;
|
|
4609
4843
|
writeQueue = Promise.resolve();
|
|
4844
|
+
writeQueueDepth = 0;
|
|
4610
4845
|
/**
|
|
4611
4846
|
* Creates a disk-backed cache layer.
|
|
4612
4847
|
*/
|
|
@@ -4617,6 +4852,7 @@ var DiskLayer = class {
|
|
|
4617
4852
|
this.serializer = options.serializer ?? new JsonSerializer();
|
|
4618
4853
|
this.maxFiles = this.normalizeMaxFiles(options.maxFiles);
|
|
4619
4854
|
this.maxEntryBytes = this.normalizeMaxEntryBytes(options.maxEntryBytes);
|
|
4855
|
+
this.maxWriteQueueDepth = this.normalizeMaxWriteQueueDepth(options.maxWriteQueueDepth);
|
|
4620
4856
|
this.protection = new PayloadProtection({
|
|
4621
4857
|
encryptionKey: options.encryptionKey,
|
|
4622
4858
|
signingKey: options.signingKey
|
|
@@ -4798,7 +5034,7 @@ var DiskLayer = class {
|
|
|
4798
5034
|
async dispose() {
|
|
4799
5035
|
}
|
|
4800
5036
|
keyToPath(key) {
|
|
4801
|
-
const hash =
|
|
5037
|
+
const hash = createHash3("sha256").update(key).digest("hex");
|
|
4802
5038
|
return join(this.directory, `${hash}.lc`);
|
|
4803
5039
|
}
|
|
4804
5040
|
resolveDirectory(directory) {
|
|
@@ -4832,6 +5068,16 @@ var DiskLayer = class {
|
|
|
4832
5068
|
}
|
|
4833
5069
|
return normalized;
|
|
4834
5070
|
}
|
|
5071
|
+
normalizeMaxWriteQueueDepth(maxWriteQueueDepth) {
|
|
5072
|
+
if (maxWriteQueueDepth === false) {
|
|
5073
|
+
return false;
|
|
5074
|
+
}
|
|
5075
|
+
const normalized = maxWriteQueueDepth ?? DEFAULT_MAX_WRITE_QUEUE_DEPTH;
|
|
5076
|
+
if (!Number.isInteger(normalized) || normalized <= 0) {
|
|
5077
|
+
throw new Error("DiskLayer.maxWriteQueueDepth must be a positive integer or false.");
|
|
5078
|
+
}
|
|
5079
|
+
return normalized;
|
|
5080
|
+
}
|
|
4835
5081
|
async readEntryFile(filePath) {
|
|
4836
5082
|
let handle;
|
|
4837
5083
|
try {
|
|
@@ -4944,7 +5190,13 @@ var DiskLayer = class {
|
|
|
4944
5190
|
}
|
|
4945
5191
|
}
|
|
4946
5192
|
enqueueWrite(operation) {
|
|
4947
|
-
|
|
5193
|
+
if (this.maxWriteQueueDepth !== false && this.writeQueueDepth >= this.maxWriteQueueDepth) {
|
|
5194
|
+
return Promise.reject(new Error(`DiskLayer write queue limit (${this.maxWriteQueueDepth}) exceeded.`));
|
|
5195
|
+
}
|
|
5196
|
+
this.writeQueueDepth += 1;
|
|
5197
|
+
const next = this.writeQueue.then(operation, operation).finally(() => {
|
|
5198
|
+
this.writeQueueDepth -= 1;
|
|
5199
|
+
});
|
|
4948
5200
|
this.writeQueue = next.catch(() => void 0);
|
|
4949
5201
|
return next;
|
|
4950
5202
|
}
|
|
@@ -5297,6 +5549,7 @@ export {
|
|
|
5297
5549
|
MemoryLayer,
|
|
5298
5550
|
MsgpackSerializer,
|
|
5299
5551
|
PatternMatcher,
|
|
5552
|
+
RedisGenerationStore,
|
|
5300
5553
|
RedisInvalidationBus,
|
|
5301
5554
|
RedisLayer,
|
|
5302
5555
|
RedisSingleFlightCoordinator,
|