layercache 2.0.0 → 3.0.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 +22 -6
- package/dist/{chunk-FFZCC7EQ.js → chunk-5CIBABDH.js} +149 -19
- package/dist/{chunk-7KMKQ6QZ.js → chunk-NBMG7DHT.js} +118 -13
- package/dist/cli.cjs +186 -25
- package/dist/cli.js +69 -13
- package/dist/{edge-D2FpRlyS.d.cts → edge-BDyuPmIq.d.cts} +509 -0
- package/dist/{edge-D2FpRlyS.d.ts → edge-BDyuPmIq.d.ts} +509 -0
- package/dist/edge.cjs +148 -19
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +1 -1
- package/dist/index.cjs +797 -82
- package/dist/index.d.cts +289 -3
- package/dist/index.d.ts +289 -3
- package/dist/index.js +528 -47
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -39,6 +39,7 @@ __export(index_exports, {
|
|
|
39
39
|
MemoryLayer: () => MemoryLayer,
|
|
40
40
|
MsgpackSerializer: () => MsgpackSerializer,
|
|
41
41
|
PatternMatcher: () => PatternMatcher,
|
|
42
|
+
RedisGenerationStore: () => RedisGenerationStore,
|
|
42
43
|
RedisInvalidationBus: () => RedisInvalidationBus,
|
|
43
44
|
RedisLayer: () => RedisLayer,
|
|
44
45
|
RedisSingleFlightCoordinator: () => RedisSingleFlightCoordinator,
|
|
@@ -184,6 +185,9 @@ function addMetricMap(base, delta) {
|
|
|
184
185
|
|
|
185
186
|
// src/CacheNamespace.ts
|
|
186
187
|
var CacheNamespace = class _CacheNamespace {
|
|
188
|
+
/**
|
|
189
|
+
* Creates a namespace backed by an existing cache stack.
|
|
190
|
+
*/
|
|
187
191
|
constructor(cache, prefix) {
|
|
188
192
|
this.cache = cache;
|
|
189
193
|
this.prefix = prefix;
|
|
@@ -193,9 +197,16 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
193
197
|
prefix;
|
|
194
198
|
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
195
199
|
metrics = createEmptyNamespaceMetrics();
|
|
200
|
+
/**
|
|
201
|
+
* Reads a key inside this namespace and optionally runs a read-through fetcher
|
|
202
|
+
* on miss or refresh.
|
|
203
|
+
*/
|
|
196
204
|
async get(key, fetcher, options) {
|
|
197
205
|
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
198
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Alias for `get(key, fetcher, options)` that makes the get-or-set behavior explicit.
|
|
209
|
+
*/
|
|
199
210
|
async getOrSet(key, fetcher, options) {
|
|
200
211
|
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
201
212
|
}
|
|
@@ -205,24 +216,69 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
205
216
|
async getOrThrow(key, fetcher, options) {
|
|
206
217
|
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
207
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Returns true when the namespaced key exists and has not expired in any layer.
|
|
221
|
+
*/
|
|
208
222
|
async has(key) {
|
|
209
223
|
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
210
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Returns the remaining TTL in milliseconds for the namespaced key.
|
|
227
|
+
*/
|
|
211
228
|
async ttl(key) {
|
|
212
229
|
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
213
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Stores a value under a namespaced key.
|
|
233
|
+
*/
|
|
214
234
|
async set(key, value, options) {
|
|
215
235
|
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
|
|
216
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Deletes a namespaced key from all layers.
|
|
239
|
+
*/
|
|
217
240
|
async delete(key) {
|
|
218
241
|
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
219
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Deletes multiple namespaced keys from all layers.
|
|
245
|
+
*/
|
|
220
246
|
async mdelete(keys) {
|
|
221
247
|
await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
|
|
222
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Alias for `delete(key)` scoped to this namespace.
|
|
251
|
+
*/
|
|
252
|
+
async invalidateByKey(key) {
|
|
253
|
+
await this.trackMetrics(() => this.cache.invalidateByKey(this.qualify(key)));
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Alias for `mdelete(keys)` scoped to this namespace.
|
|
257
|
+
*/
|
|
258
|
+
async invalidateByKeys(keys) {
|
|
259
|
+
await this.trackMetrics(() => this.cache.invalidateByKeys(keys.map((k) => this.qualify(k))));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Marks one exact namespaced key expired without deleting its stale value.
|
|
263
|
+
*/
|
|
264
|
+
async expireByKey(key) {
|
|
265
|
+
await this.trackMetrics(() => this.cache.expireByKey(this.qualify(key)));
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Marks multiple exact namespaced keys expired without deleting their stale values.
|
|
269
|
+
*/
|
|
270
|
+
async expireByKeys(keys) {
|
|
271
|
+
await this.trackMetrics(() => this.cache.expireByKeys(keys.map((k) => this.qualify(k))));
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Clears all keys in this namespace by invalidating the namespace prefix.
|
|
275
|
+
*/
|
|
223
276
|
async clear() {
|
|
224
277
|
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
|
|
225
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Reads many namespaced keys concurrently.
|
|
281
|
+
*/
|
|
226
282
|
async mget(entries) {
|
|
227
283
|
return this.trackMetrics(
|
|
228
284
|
() => this.cache.mget(
|
|
@@ -234,6 +290,9 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
234
290
|
)
|
|
235
291
|
);
|
|
236
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Writes many namespaced entries concurrently.
|
|
295
|
+
*/
|
|
237
296
|
async mset(entries) {
|
|
238
297
|
await this.trackMetrics(
|
|
239
298
|
() => this.cache.mset(
|
|
@@ -245,12 +304,21 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
245
304
|
)
|
|
246
305
|
);
|
|
247
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Deletes keys associated with a tag scoped to this namespace.
|
|
309
|
+
*/
|
|
248
310
|
async invalidateByTag(tag) {
|
|
249
311
|
await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
|
|
250
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Expires keys associated with a tag scoped to this namespace while preserving stale windows.
|
|
315
|
+
*/
|
|
251
316
|
async expireByTag(tag) {
|
|
252
317
|
await this.trackMetrics(() => this.cache.expireByTag(this.qualifyTag(tag)));
|
|
253
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Deletes keys associated with any or all namespace-scoped tags.
|
|
321
|
+
*/
|
|
254
322
|
async invalidateByTags(tags, mode = "any") {
|
|
255
323
|
await this.trackMetrics(
|
|
256
324
|
() => this.cache.invalidateByTags(
|
|
@@ -259,6 +327,9 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
259
327
|
)
|
|
260
328
|
);
|
|
261
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Expires keys associated with any or all namespace-scoped tags while preserving stale windows.
|
|
332
|
+
*/
|
|
262
333
|
async expireByTags(tags, mode = "any") {
|
|
263
334
|
await this.trackMetrics(
|
|
264
335
|
() => this.cache.expireByTags(
|
|
@@ -267,15 +338,27 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
267
338
|
)
|
|
268
339
|
);
|
|
269
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Deletes namespaced keys matching a wildcard pattern.
|
|
343
|
+
*/
|
|
270
344
|
async invalidateByPattern(pattern) {
|
|
271
345
|
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
272
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Expires namespaced keys matching a wildcard pattern while preserving stale windows.
|
|
349
|
+
*/
|
|
273
350
|
async expireByPattern(pattern) {
|
|
274
351
|
await this.trackMetrics(() => this.cache.expireByPattern(this.qualify(pattern)));
|
|
275
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Deletes namespaced keys with the provided prefix.
|
|
355
|
+
*/
|
|
276
356
|
async invalidateByPrefix(prefix) {
|
|
277
357
|
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
|
|
278
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Expires namespaced keys with the provided prefix while preserving stale windows.
|
|
361
|
+
*/
|
|
279
362
|
async expireByPrefix(prefix) {
|
|
280
363
|
await this.trackMetrics(() => this.cache.expireByPrefix(this.qualify(prefix)));
|
|
281
364
|
}
|
|
@@ -292,9 +375,15 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
292
375
|
tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
|
|
293
376
|
};
|
|
294
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* Returns a cached wrapper whose generated keys are scoped to this namespace.
|
|
380
|
+
*/
|
|
295
381
|
wrap(keyPrefix, fetcher, options) {
|
|
296
382
|
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
|
|
297
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* Warms entries after qualifying each key and tag with this namespace prefix.
|
|
386
|
+
*/
|
|
298
387
|
warm(entries, options) {
|
|
299
388
|
return this.cache.warm(
|
|
300
389
|
entries.map((entry) => ({
|
|
@@ -305,9 +394,15 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
305
394
|
options
|
|
306
395
|
);
|
|
307
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Returns metrics accumulated by operations performed through this namespace.
|
|
399
|
+
*/
|
|
308
400
|
getMetrics() {
|
|
309
401
|
return cloneNamespaceMetrics(this.metrics);
|
|
310
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* Returns hit-rate statistics for operations performed through this namespace.
|
|
405
|
+
*/
|
|
311
406
|
getHitRate() {
|
|
312
407
|
return computeNamespaceHitRate(this.metrics);
|
|
313
408
|
}
|
|
@@ -324,6 +419,9 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
324
419
|
validateNamespaceKey(childPrefix);
|
|
325
420
|
return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
|
|
326
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* Qualifies a raw key with this namespace prefix.
|
|
424
|
+
*/
|
|
327
425
|
qualify(key) {
|
|
328
426
|
return `${this.prefix}:${key}`;
|
|
329
427
|
}
|
|
@@ -1060,7 +1158,9 @@ var CacheStackMaintenance = class {
|
|
|
1060
1158
|
}
|
|
1061
1159
|
bumpKeyEpochs(keys) {
|
|
1062
1160
|
for (const key of keys) {
|
|
1063
|
-
|
|
1161
|
+
const nextEpoch = this.currentKeyEpoch(key) + 1;
|
|
1162
|
+
this.keyEpochs.delete(key);
|
|
1163
|
+
this.keyEpochs.set(key, nextEpoch);
|
|
1064
1164
|
}
|
|
1065
1165
|
this.pruneKeyEpochsIfNeeded();
|
|
1066
1166
|
}
|
|
@@ -1119,10 +1219,13 @@ var CacheStackMaintenance = class {
|
|
|
1119
1219
|
if (this.keyEpochs.size <= MAX_KEY_EPOCHS) {
|
|
1120
1220
|
return;
|
|
1121
1221
|
}
|
|
1122
|
-
const
|
|
1123
|
-
const toDelete = Math.ceil(sorted.length * 0.1);
|
|
1222
|
+
const toDelete = Math.ceil(this.keyEpochs.size * 0.1);
|
|
1124
1223
|
for (let i = 0; i < toDelete; i++) {
|
|
1125
|
-
this.keyEpochs.
|
|
1224
|
+
const oldestKey = this.keyEpochs.keys().next().value;
|
|
1225
|
+
if (oldestKey === void 0) {
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
this.keyEpochs.delete(oldestKey);
|
|
1126
1229
|
}
|
|
1127
1230
|
}
|
|
1128
1231
|
};
|
|
@@ -1256,22 +1359,28 @@ var CacheStackReader = class {
|
|
|
1256
1359
|
if (upToIndex < 0) {
|
|
1257
1360
|
return;
|
|
1258
1361
|
}
|
|
1362
|
+
const operations = [];
|
|
1259
1363
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
1260
1364
|
const layer = this.options.layers[index];
|
|
1261
1365
|
if (!layer || this.options.shouldSkipLayer(layer)) {
|
|
1262
1366
|
continue;
|
|
1263
1367
|
}
|
|
1264
1368
|
const ttl = remainingStoredTtlMs(stored) ?? this.options.resolveLayerMs(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1369
|
+
operations.push(
|
|
1370
|
+
(async () => {
|
|
1371
|
+
try {
|
|
1372
|
+
await layer.set(key, stored, ttl);
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
await this.options.handleLayerFailure(layer, "backfill", error);
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
this.options.metricsCollector.increment("backfills");
|
|
1378
|
+
this.options.logger.debug?.("backfill", { key, layer: layer.name });
|
|
1379
|
+
this.options.emit("backfill", { key, layer: layer.name });
|
|
1380
|
+
})()
|
|
1381
|
+
);
|
|
1274
1382
|
}
|
|
1383
|
+
await Promise.all(operations);
|
|
1275
1384
|
}
|
|
1276
1385
|
abortAllRefreshes() {
|
|
1277
1386
|
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
@@ -1395,7 +1504,15 @@ var CacheStackReader = class {
|
|
|
1395
1504
|
}
|
|
1396
1505
|
await this.options.sleep(pollIntervalMs);
|
|
1397
1506
|
}
|
|
1398
|
-
|
|
1507
|
+
if (!this.options.singleFlightCoordinator) {
|
|
1508
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
|
|
1509
|
+
}
|
|
1510
|
+
return this.options.singleFlightCoordinator.execute(
|
|
1511
|
+
key,
|
|
1512
|
+
this.resolveSingleFlightOptions(),
|
|
1513
|
+
() => this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext),
|
|
1514
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext)
|
|
1515
|
+
);
|
|
1399
1516
|
}
|
|
1400
1517
|
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1401
1518
|
key,
|
|
@@ -2447,6 +2564,7 @@ var TtlResolver = class {
|
|
|
2447
2564
|
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
2448
2565
|
profile.hits += 1;
|
|
2449
2566
|
profile.lastAccessAt = Date.now();
|
|
2567
|
+
this.accessProfiles.delete(key);
|
|
2450
2568
|
this.accessProfiles.set(key, profile);
|
|
2451
2569
|
this.pruneIfNeeded();
|
|
2452
2570
|
}
|
|
@@ -2536,12 +2654,12 @@ var TtlResolver = class {
|
|
|
2536
2654
|
return;
|
|
2537
2655
|
}
|
|
2538
2656
|
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
this.accessProfiles.delete(entry[0]);
|
|
2657
|
+
for (let i = 0; i < toRemove; i++) {
|
|
2658
|
+
const oldestKey = this.accessProfiles.keys().next().value;
|
|
2659
|
+
if (oldestKey === void 0) {
|
|
2660
|
+
break;
|
|
2544
2661
|
}
|
|
2662
|
+
this.accessProfiles.delete(oldestKey);
|
|
2545
2663
|
}
|
|
2546
2664
|
}
|
|
2547
2665
|
};
|
|
@@ -2558,10 +2676,16 @@ var TagIndex = class {
|
|
|
2558
2676
|
constructor(options = {}) {
|
|
2559
2677
|
this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
|
|
2560
2678
|
}
|
|
2679
|
+
/**
|
|
2680
|
+
* Records a key as known without changing tag assignments.
|
|
2681
|
+
*/
|
|
2561
2682
|
async touch(key) {
|
|
2562
2683
|
this.insertKnownKey(key);
|
|
2563
2684
|
this.pruneKnownKeysIfNeeded();
|
|
2564
2685
|
}
|
|
2686
|
+
/**
|
|
2687
|
+
* Replaces the tags associated with a key and records the key as known.
|
|
2688
|
+
*/
|
|
2565
2689
|
async track(key, tags) {
|
|
2566
2690
|
this.insertKnownKey(key);
|
|
2567
2691
|
this.pruneKnownKeysIfNeeded();
|
|
@@ -2582,17 +2706,29 @@ var TagIndex = class {
|
|
|
2582
2706
|
this.tagToKeys.set(tag, keys);
|
|
2583
2707
|
}
|
|
2584
2708
|
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Removes a key from all tag mappings and known-key tracking.
|
|
2711
|
+
*/
|
|
2585
2712
|
async remove(key) {
|
|
2586
2713
|
this.removeKey(key);
|
|
2587
2714
|
}
|
|
2715
|
+
/**
|
|
2716
|
+
* Returns keys currently associated with a tag.
|
|
2717
|
+
*/
|
|
2588
2718
|
async keysForTag(tag) {
|
|
2589
2719
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
2590
2720
|
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Visits keys currently associated with a tag.
|
|
2723
|
+
*/
|
|
2591
2724
|
async forEachKeyForTag(tag, visitor) {
|
|
2592
2725
|
for (const key of this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()) {
|
|
2593
2726
|
await visitor(key);
|
|
2594
2727
|
}
|
|
2595
2728
|
}
|
|
2729
|
+
/**
|
|
2730
|
+
* Returns known keys that start with a prefix.
|
|
2731
|
+
*/
|
|
2596
2732
|
async keysForPrefix(prefix) {
|
|
2597
2733
|
const node = this.findNode(prefix);
|
|
2598
2734
|
if (!node) {
|
|
@@ -2602,6 +2738,9 @@ var TagIndex = class {
|
|
|
2602
2738
|
this.collectFromNode(node, prefix, matches);
|
|
2603
2739
|
return matches;
|
|
2604
2740
|
}
|
|
2741
|
+
/**
|
|
2742
|
+
* Visits known keys that start with a prefix.
|
|
2743
|
+
*/
|
|
2605
2744
|
async forEachKeyForPrefix(prefix, visitor) {
|
|
2606
2745
|
const node = this.findNode(prefix);
|
|
2607
2746
|
if (!node) {
|
|
@@ -2609,20 +2748,32 @@ var TagIndex = class {
|
|
|
2609
2748
|
}
|
|
2610
2749
|
await this.visitFromNode(node, prefix, visitor);
|
|
2611
2750
|
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Returns the tags currently associated with a key.
|
|
2753
|
+
*/
|
|
2612
2754
|
async tagsForKey(key) {
|
|
2613
2755
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
2614
2756
|
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Returns known keys matching a wildcard pattern.
|
|
2759
|
+
*/
|
|
2615
2760
|
async matchPattern(pattern) {
|
|
2616
2761
|
const matches = /* @__PURE__ */ new Set();
|
|
2617
2762
|
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
2618
2763
|
return [...matches];
|
|
2619
2764
|
}
|
|
2765
|
+
/**
|
|
2766
|
+
* Visits known keys matching a wildcard pattern.
|
|
2767
|
+
*/
|
|
2620
2768
|
async forEachKeyMatchingPattern(pattern, visitor) {
|
|
2621
2769
|
const matches = await this.matchPattern(pattern);
|
|
2622
2770
|
for (const key of matches) {
|
|
2623
2771
|
await visitor(key);
|
|
2624
2772
|
}
|
|
2625
2773
|
}
|
|
2774
|
+
/**
|
|
2775
|
+
* Clears all tag and known-key index state.
|
|
2776
|
+
*/
|
|
2626
2777
|
async clear() {
|
|
2627
2778
|
this.tagToKeys.clear();
|
|
2628
2779
|
this.keyToTags.clear();
|
|
@@ -2640,6 +2791,9 @@ var TagIndex = class {
|
|
|
2640
2791
|
}
|
|
2641
2792
|
insertKnownKey(key) {
|
|
2642
2793
|
const isNew = !this.knownKeys.has(key);
|
|
2794
|
+
if (!isNew) {
|
|
2795
|
+
this.knownKeys.delete(key);
|
|
2796
|
+
}
|
|
2643
2797
|
this.knownKeys.set(key, Date.now());
|
|
2644
2798
|
if (!isNew) {
|
|
2645
2799
|
return;
|
|
@@ -2738,13 +2892,13 @@ var TagIndex = class {
|
|
|
2738
2892
|
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
2739
2893
|
return;
|
|
2740
2894
|
}
|
|
2741
|
-
const sorted = [...this.knownKeys.entries()].sort((a, b) => a[1] - b[1]);
|
|
2742
2895
|
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
2743
|
-
for (let i = 0; i < toRemove
|
|
2744
|
-
const
|
|
2745
|
-
if (
|
|
2746
|
-
|
|
2896
|
+
for (let i = 0; i < toRemove; i += 1) {
|
|
2897
|
+
const oldestKey = this.knownKeys.keys().next().value;
|
|
2898
|
+
if (oldestKey === void 0) {
|
|
2899
|
+
break;
|
|
2747
2900
|
}
|
|
2901
|
+
this.removeKnownKey(oldestKey);
|
|
2748
2902
|
}
|
|
2749
2903
|
}
|
|
2750
2904
|
removeKey(key) {
|
|
@@ -2797,9 +2951,15 @@ var TagIndex = class {
|
|
|
2797
2951
|
|
|
2798
2952
|
// src/serialization/JsonSerializer.ts
|
|
2799
2953
|
var JsonSerializer = class {
|
|
2954
|
+
/**
|
|
2955
|
+
* Serializes a value to JSON.
|
|
2956
|
+
*/
|
|
2800
2957
|
serialize(value) {
|
|
2801
2958
|
return JSON.stringify(value);
|
|
2802
2959
|
}
|
|
2960
|
+
/**
|
|
2961
|
+
* Parses JSON and sanitizes the result before returning it.
|
|
2962
|
+
*/
|
|
2803
2963
|
deserialize(payload) {
|
|
2804
2964
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
2805
2965
|
let parsed;
|
|
@@ -2826,6 +2986,9 @@ var StampedeGuard = class {
|
|
|
2826
2986
|
this.maxInFlight = options.maxInFlight ?? 1e4;
|
|
2827
2987
|
this.entryTimeoutMs = options.entryTimeoutMs;
|
|
2828
2988
|
}
|
|
2989
|
+
/**
|
|
2990
|
+
* Deduplicates concurrent work for the same key in this process.
|
|
2991
|
+
*/
|
|
2829
2992
|
async execute(key, task) {
|
|
2830
2993
|
const existing = this.inFlight.get(key);
|
|
2831
2994
|
if (existing) {
|
|
@@ -2925,6 +3088,9 @@ var DebugLogger = class {
|
|
|
2925
3088
|
}
|
|
2926
3089
|
};
|
|
2927
3090
|
var CacheStack = class extends import_node_events.EventEmitter {
|
|
3091
|
+
/**
|
|
3092
|
+
* Creates a cache stack from ordered layers and optional global behavior settings.
|
|
3093
|
+
*/
|
|
2928
3094
|
constructor(layers, options = {}) {
|
|
2929
3095
|
super();
|
|
2930
3096
|
this.layers = layers;
|
|
@@ -3190,6 +3356,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3190
3356
|
});
|
|
3191
3357
|
});
|
|
3192
3358
|
}
|
|
3359
|
+
/**
|
|
3360
|
+
* Clears every configured layer, removes tag metadata, resets internal TTL
|
|
3361
|
+
* profiles, and broadcasts a clear invalidation message.
|
|
3362
|
+
*/
|
|
3193
3363
|
async clear() {
|
|
3194
3364
|
await this.awaitStartup("clear");
|
|
3195
3365
|
this.maintenance.beginClearEpoch();
|
|
@@ -3219,6 +3389,58 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3219
3389
|
operation: "delete"
|
|
3220
3390
|
});
|
|
3221
3391
|
}
|
|
3392
|
+
/**
|
|
3393
|
+
* Alias for `delete(key)` that matches the `invalidateBy*` API family.
|
|
3394
|
+
*/
|
|
3395
|
+
async invalidateByKey(key) {
|
|
3396
|
+
await this.delete(key);
|
|
3397
|
+
}
|
|
3398
|
+
/**
|
|
3399
|
+
* Alias for `mdelete(keys)` that matches the `invalidateBy*` API family.
|
|
3400
|
+
*/
|
|
3401
|
+
async invalidateByKeys(keys) {
|
|
3402
|
+
await this.mdelete(keys);
|
|
3403
|
+
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Marks one exact key expired without deleting its stale value.
|
|
3406
|
+
*/
|
|
3407
|
+
async expireByKey(key) {
|
|
3408
|
+
await this.observeOperation("layercache.expire_by_key", { "layercache.key": String(key ?? "") }, async () => {
|
|
3409
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
3410
|
+
await this.awaitStartup("expireByKey");
|
|
3411
|
+
await this.expireKeys([normalizedKey]);
|
|
3412
|
+
await this.publishInvalidation({
|
|
3413
|
+
scope: "key",
|
|
3414
|
+
keys: [normalizedKey],
|
|
3415
|
+
sourceId: this.instanceId,
|
|
3416
|
+
operation: "expire"
|
|
3417
|
+
});
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
/**
|
|
3421
|
+
* Marks multiple exact keys expired without deleting their stale values.
|
|
3422
|
+
*/
|
|
3423
|
+
async expireByKeys(keys) {
|
|
3424
|
+
await this.observeOperation("layercache.expire_by_keys", void 0, async () => {
|
|
3425
|
+
if (keys.length === 0) {
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3428
|
+
const normalizedKeys = keys.map((k) => validateCacheKey(k));
|
|
3429
|
+
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
3430
|
+
await this.awaitStartup("expireByKeys");
|
|
3431
|
+
await this.expireKeys(cacheKeys);
|
|
3432
|
+
await this.publishInvalidation({
|
|
3433
|
+
scope: "keys",
|
|
3434
|
+
keys: cacheKeys,
|
|
3435
|
+
sourceId: this.instanceId,
|
|
3436
|
+
operation: "expire"
|
|
3437
|
+
});
|
|
3438
|
+
});
|
|
3439
|
+
}
|
|
3440
|
+
/**
|
|
3441
|
+
* Reads many keys concurrently. Simple reads use layer-level bulk fast paths;
|
|
3442
|
+
* entries with fetchers or options fall back to per-entry read-through logic.
|
|
3443
|
+
*/
|
|
3222
3444
|
async mget(entries) {
|
|
3223
3445
|
return this.observeOperation("layercache.mget", void 0, async () => {
|
|
3224
3446
|
this.assertActive("mget");
|
|
@@ -3306,6 +3528,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3306
3528
|
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
3307
3529
|
});
|
|
3308
3530
|
}
|
|
3531
|
+
/**
|
|
3532
|
+
* Writes many entries concurrently using each layer's bulk write fast path
|
|
3533
|
+
* when available.
|
|
3534
|
+
*/
|
|
3309
3535
|
async mset(entries) {
|
|
3310
3536
|
await this.observeOperation("layercache.mset", void 0, async () => {
|
|
3311
3537
|
this.assertActive("mset");
|
|
@@ -3318,6 +3544,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3318
3544
|
await this.writeBatch(normalizedEntries);
|
|
3319
3545
|
});
|
|
3320
3546
|
}
|
|
3547
|
+
/**
|
|
3548
|
+
* Pre-populates cache entries by running their fetchers with bounded
|
|
3549
|
+
* concurrency. Higher-priority entries run first.
|
|
3550
|
+
*/
|
|
3321
3551
|
async warm(entries, options = {}) {
|
|
3322
3552
|
this.assertActive("warm");
|
|
3323
3553
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
@@ -3368,6 +3598,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3368
3598
|
validateNamespaceKey(prefix);
|
|
3369
3599
|
return new CacheNamespace(this, prefix);
|
|
3370
3600
|
}
|
|
3601
|
+
/**
|
|
3602
|
+
* Deletes every key currently associated with `tag` and broadcasts an
|
|
3603
|
+
* invalidation message.
|
|
3604
|
+
*/
|
|
3371
3605
|
async invalidateByTag(tag) {
|
|
3372
3606
|
await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
|
|
3373
3607
|
validateTag(tag);
|
|
@@ -3377,6 +3611,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3377
3611
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3378
3612
|
});
|
|
3379
3613
|
}
|
|
3614
|
+
/**
|
|
3615
|
+
* Marks every key associated with `tag` as expired while preserving stale
|
|
3616
|
+
* windows for stale serving.
|
|
3617
|
+
*/
|
|
3380
3618
|
async expireByTag(tag) {
|
|
3381
3619
|
await this.observeOperation("layercache.expire_by_tag", void 0, async () => {
|
|
3382
3620
|
validateTag(tag);
|
|
@@ -3386,6 +3624,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3386
3624
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
3387
3625
|
});
|
|
3388
3626
|
}
|
|
3627
|
+
/**
|
|
3628
|
+
* Deletes keys associated with any or all of the provided tags and broadcasts
|
|
3629
|
+
* an invalidation message.
|
|
3630
|
+
*/
|
|
3389
3631
|
async invalidateByTags(tags, mode = "any") {
|
|
3390
3632
|
await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
|
|
3391
3633
|
if (tags.length === 0) {
|
|
@@ -3402,6 +3644,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3402
3644
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3403
3645
|
});
|
|
3404
3646
|
}
|
|
3647
|
+
/**
|
|
3648
|
+
* Marks keys associated with any or all of the provided tags as expired while
|
|
3649
|
+
* preserving stale windows for stale serving.
|
|
3650
|
+
*/
|
|
3405
3651
|
async expireByTags(tags, mode = "any") {
|
|
3406
3652
|
await this.observeOperation("layercache.expire_by_tags", void 0, async () => {
|
|
3407
3653
|
if (tags.length === 0) {
|
|
@@ -3418,6 +3664,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3418
3664
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
3419
3665
|
});
|
|
3420
3666
|
}
|
|
3667
|
+
/**
|
|
3668
|
+
* Deletes keys matching a wildcard pattern such as `user:*`.
|
|
3669
|
+
*/
|
|
3421
3670
|
async invalidateByPattern(pattern) {
|
|
3422
3671
|
await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
|
|
3423
3672
|
validatePattern(pattern);
|
|
@@ -3430,6 +3679,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3430
3679
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3431
3680
|
});
|
|
3432
3681
|
}
|
|
3682
|
+
/**
|
|
3683
|
+
* Marks keys matching a wildcard pattern as expired while preserving stale
|
|
3684
|
+
* windows for stale serving.
|
|
3685
|
+
*/
|
|
3433
3686
|
async expireByPattern(pattern) {
|
|
3434
3687
|
await this.observeOperation("layercache.expire_by_pattern", void 0, async () => {
|
|
3435
3688
|
validatePattern(pattern);
|
|
@@ -3442,6 +3695,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3442
3695
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
3443
3696
|
});
|
|
3444
3697
|
}
|
|
3698
|
+
/**
|
|
3699
|
+
* Deletes keys that start with the provided prefix.
|
|
3700
|
+
*/
|
|
3445
3701
|
async invalidateByPrefix(prefix) {
|
|
3446
3702
|
await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
|
|
3447
3703
|
await this.awaitStartup("invalidateByPrefix");
|
|
@@ -3451,6 +3707,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3451
3707
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3452
3708
|
});
|
|
3453
3709
|
}
|
|
3710
|
+
/**
|
|
3711
|
+
* Marks keys that start with the provided prefix as expired while preserving
|
|
3712
|
+
* stale windows for stale serving.
|
|
3713
|
+
*/
|
|
3454
3714
|
async expireByPrefix(prefix) {
|
|
3455
3715
|
await this.observeOperation("layercache.expire_by_prefix", void 0, async () => {
|
|
3456
3716
|
await this.awaitStartup("expireByPrefix");
|
|
@@ -3460,9 +3720,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3460
3720
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
3461
3721
|
});
|
|
3462
3722
|
}
|
|
3723
|
+
/**
|
|
3724
|
+
* Returns cumulative cache metrics since startup or the last `resetMetrics()`.
|
|
3725
|
+
*/
|
|
3463
3726
|
getMetrics() {
|
|
3464
3727
|
return this.metricsCollector.snapshot;
|
|
3465
3728
|
}
|
|
3729
|
+
/**
|
|
3730
|
+
* Returns metrics plus layer degradation state and active background refresh count.
|
|
3731
|
+
*/
|
|
3466
3732
|
getStats() {
|
|
3467
3733
|
return {
|
|
3468
3734
|
metrics: this.getMetrics(),
|
|
@@ -3474,6 +3740,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3474
3740
|
backgroundRefreshes: this.reader.activeRefreshCount
|
|
3475
3741
|
};
|
|
3476
3742
|
}
|
|
3743
|
+
/**
|
|
3744
|
+
* Resets cumulative metrics counters.
|
|
3745
|
+
*/
|
|
3477
3746
|
resetMetrics() {
|
|
3478
3747
|
this.metricsCollector.reset();
|
|
3479
3748
|
}
|
|
@@ -3483,6 +3752,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3483
3752
|
getHitRate() {
|
|
3484
3753
|
return this.metricsCollector.hitRate();
|
|
3485
3754
|
}
|
|
3755
|
+
/**
|
|
3756
|
+
* Runs each layer's `ping()` hook when available and returns per-layer health
|
|
3757
|
+
* and latency information.
|
|
3758
|
+
*/
|
|
3486
3759
|
async healthCheck() {
|
|
3487
3760
|
await this.startup;
|
|
3488
3761
|
return Promise.all(
|
|
@@ -3526,6 +3799,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3526
3799
|
}
|
|
3527
3800
|
return this.currentGeneration;
|
|
3528
3801
|
}
|
|
3802
|
+
/**
|
|
3803
|
+
* Returns the active generation prefix number used for future cache keys.
|
|
3804
|
+
*/
|
|
3805
|
+
getGeneration() {
|
|
3806
|
+
return this.currentGeneration;
|
|
3807
|
+
}
|
|
3529
3808
|
/**
|
|
3530
3809
|
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
3531
3810
|
* remaining fresh/stale/error TTLs, and associated tags.
|
|
@@ -3567,22 +3846,38 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3567
3846
|
const tags = await this.getTagsForKey(normalizedKey);
|
|
3568
3847
|
return { key: userKey, foundInLayers, freshTtlMs, staleTtlMs, errorTtlMs, isStale, tags };
|
|
3569
3848
|
}
|
|
3849
|
+
/**
|
|
3850
|
+
* Exports cache entries from configured layers for process-local snapshots.
|
|
3851
|
+
*/
|
|
3570
3852
|
async exportState() {
|
|
3571
3853
|
await this.awaitStartup("exportState");
|
|
3572
3854
|
return this.snapshots.exportState(this.snapshotMaxEntries());
|
|
3573
3855
|
}
|
|
3856
|
+
/**
|
|
3857
|
+
* Imports entries produced by `exportState()` into the configured layers.
|
|
3858
|
+
*/
|
|
3574
3859
|
async importState(entries) {
|
|
3575
3860
|
await this.awaitStartup("importState");
|
|
3576
3861
|
await this.snapshots.importState(entries);
|
|
3577
3862
|
}
|
|
3863
|
+
/**
|
|
3864
|
+
* Writes a snapshot file containing current cache entries.
|
|
3865
|
+
*/
|
|
3578
3866
|
async persistToFile(filePath) {
|
|
3579
3867
|
this.assertActive("persistToFile");
|
|
3580
3868
|
await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
|
|
3581
3869
|
}
|
|
3870
|
+
/**
|
|
3871
|
+
* Restores cache entries from a snapshot file.
|
|
3872
|
+
*/
|
|
3582
3873
|
async restoreFromFile(filePath) {
|
|
3583
3874
|
this.assertActive("restoreFromFile");
|
|
3584
3875
|
await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
|
|
3585
3876
|
}
|
|
3877
|
+
/**
|
|
3878
|
+
* Flushes background work, unsubscribes from buses, disposes timers, and then
|
|
3879
|
+
* disposes each layer that provides `dispose()`.
|
|
3880
|
+
*/
|
|
3586
3881
|
async disconnect() {
|
|
3587
3882
|
if (!this.disconnectPromise) {
|
|
3588
3883
|
this.isDisconnecting = true;
|
|
@@ -4067,12 +4362,65 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
4067
4362
|
}
|
|
4068
4363
|
};
|
|
4069
4364
|
|
|
4365
|
+
// src/generation/RedisGenerationStore.ts
|
|
4366
|
+
var DEFAULT_GENERATION_KEY = "layercache:generation";
|
|
4367
|
+
var RedisGenerationStore = class {
|
|
4368
|
+
client;
|
|
4369
|
+
key;
|
|
4370
|
+
constructor(options) {
|
|
4371
|
+
this.client = options.client;
|
|
4372
|
+
this.key = options.key ?? DEFAULT_GENERATION_KEY;
|
|
4373
|
+
}
|
|
4374
|
+
async get() {
|
|
4375
|
+
const stored = await this.client.get(this.key);
|
|
4376
|
+
if (stored === null) {
|
|
4377
|
+
return void 0;
|
|
4378
|
+
}
|
|
4379
|
+
return this.parseGeneration(stored);
|
|
4380
|
+
}
|
|
4381
|
+
async getOrInitialize(initialGeneration = 0) {
|
|
4382
|
+
this.assertGeneration(initialGeneration);
|
|
4383
|
+
await this.client.set(this.key, String(initialGeneration), "NX");
|
|
4384
|
+
const generation = await this.get();
|
|
4385
|
+
if (generation === void 0) {
|
|
4386
|
+
throw new Error(`RedisGenerationStore failed to initialize generation key "${this.key}".`);
|
|
4387
|
+
}
|
|
4388
|
+
return generation;
|
|
4389
|
+
}
|
|
4390
|
+
async set(generation) {
|
|
4391
|
+
this.assertGeneration(generation);
|
|
4392
|
+
await this.client.set(this.key, String(generation));
|
|
4393
|
+
}
|
|
4394
|
+
async bump() {
|
|
4395
|
+
const generation = await this.client.incr(this.key);
|
|
4396
|
+
this.assertGeneration(generation);
|
|
4397
|
+
return generation;
|
|
4398
|
+
}
|
|
4399
|
+
parseGeneration(value) {
|
|
4400
|
+
const generation = Number.parseInt(value, 10);
|
|
4401
|
+
if (String(generation) !== value || !this.isGeneration(generation)) {
|
|
4402
|
+
throw new Error(`RedisGenerationStore found invalid persisted generation value for key "${this.key}".`);
|
|
4403
|
+
}
|
|
4404
|
+
return generation;
|
|
4405
|
+
}
|
|
4406
|
+
assertGeneration(value) {
|
|
4407
|
+
if (!this.isGeneration(value)) {
|
|
4408
|
+
throw new Error("RedisGenerationStore generation must be a non-negative safe integer.");
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
isGeneration(value) {
|
|
4412
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
4413
|
+
}
|
|
4414
|
+
};
|
|
4415
|
+
|
|
4070
4416
|
// src/invalidation/RedisInvalidationBus.ts
|
|
4417
|
+
var import_node_crypto3 = require("crypto");
|
|
4071
4418
|
var RedisInvalidationBus = class {
|
|
4072
4419
|
channel;
|
|
4073
4420
|
publisher;
|
|
4074
4421
|
subscriber;
|
|
4075
4422
|
logger;
|
|
4423
|
+
signingKey;
|
|
4076
4424
|
handlers = /* @__PURE__ */ new Set();
|
|
4077
4425
|
sharedListener;
|
|
4078
4426
|
subscribePromise;
|
|
@@ -4081,7 +4429,11 @@ var RedisInvalidationBus = class {
|
|
|
4081
4429
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
4082
4430
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
4083
4431
|
this.logger = options.logger;
|
|
4432
|
+
this.signingKey = options.signingSecret ? normalizeSigningSecret(options.signingSecret) : void 0;
|
|
4084
4433
|
}
|
|
4434
|
+
/**
|
|
4435
|
+
* Subscribes to invalidation messages and returns an unsubscribe function.
|
|
4436
|
+
*/
|
|
4085
4437
|
async subscribe(handler) {
|
|
4086
4438
|
const previousPromise = this.subscribePromise;
|
|
4087
4439
|
let resolveThis;
|
|
@@ -4113,8 +4465,11 @@ var RedisInvalidationBus = class {
|
|
|
4113
4465
|
}
|
|
4114
4466
|
};
|
|
4115
4467
|
}
|
|
4468
|
+
/**
|
|
4469
|
+
* Publishes an invalidation message to other subscribers.
|
|
4470
|
+
*/
|
|
4116
4471
|
async publish(message) {
|
|
4117
|
-
await this.publisher.publish(this.channel, JSON.stringify(message));
|
|
4472
|
+
await this.publisher.publish(this.channel, JSON.stringify(this.signingKey ? this.signMessage(message) : message));
|
|
4118
4473
|
}
|
|
4119
4474
|
async dispatchToHandlers(payload) {
|
|
4120
4475
|
let message;
|
|
@@ -4125,10 +4480,11 @@ var RedisInvalidationBus = class {
|
|
|
4125
4480
|
maxNodes: 1e4,
|
|
4126
4481
|
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
4127
4482
|
});
|
|
4128
|
-
|
|
4483
|
+
const candidate = this.signingKey ? this.verifySignedEnvelope(parsed) : parsed;
|
|
4484
|
+
if (!this.isInvalidationMessage(candidate)) {
|
|
4129
4485
|
throw new Error("Invalid invalidation payload shape.");
|
|
4130
4486
|
}
|
|
4131
|
-
message =
|
|
4487
|
+
message = candidate;
|
|
4132
4488
|
} catch (error) {
|
|
4133
4489
|
this.reportError("invalid invalidation payload", error);
|
|
4134
4490
|
return;
|
|
@@ -4153,6 +4509,34 @@ var RedisInvalidationBus = class {
|
|
|
4153
4509
|
const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
|
|
4154
4510
|
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
4155
4511
|
}
|
|
4512
|
+
signMessage(message) {
|
|
4513
|
+
const payload = JSON.stringify(message);
|
|
4514
|
+
return {
|
|
4515
|
+
payload: message,
|
|
4516
|
+
signature: this.createSignature(payload)
|
|
4517
|
+
};
|
|
4518
|
+
}
|
|
4519
|
+
verifySignedEnvelope(value) {
|
|
4520
|
+
if (!value || typeof value !== "object") {
|
|
4521
|
+
throw new Error("Signed invalidation envelope must be an object.");
|
|
4522
|
+
}
|
|
4523
|
+
const envelope = value;
|
|
4524
|
+
if (!envelope.payload || typeof envelope.payload !== "object" || typeof envelope.signature !== "string") {
|
|
4525
|
+
throw new Error("Signed invalidation envelope is missing payload or signature.");
|
|
4526
|
+
}
|
|
4527
|
+
const payload = JSON.stringify(envelope.payload);
|
|
4528
|
+
const expected = this.createSignature(payload);
|
|
4529
|
+
if (!isEqualSignature(envelope.signature, expected)) {
|
|
4530
|
+
throw new Error("Invalid invalidation message signature.");
|
|
4531
|
+
}
|
|
4532
|
+
return envelope.payload;
|
|
4533
|
+
}
|
|
4534
|
+
createSignature(payload) {
|
|
4535
|
+
if (!this.signingKey) {
|
|
4536
|
+
throw new Error("RedisInvalidationBus signing key is not configured.");
|
|
4537
|
+
}
|
|
4538
|
+
return (0, import_node_crypto3.createHmac)("sha256", this.signingKey).update(payload).digest("hex");
|
|
4539
|
+
}
|
|
4156
4540
|
reportError(message, error) {
|
|
4157
4541
|
if (this.logger?.error) {
|
|
4158
4542
|
this.logger.error(message, { error });
|
|
@@ -4161,22 +4545,41 @@ var RedisInvalidationBus = class {
|
|
|
4161
4545
|
console.error(`[layercache] ${message}`, error);
|
|
4162
4546
|
}
|
|
4163
4547
|
};
|
|
4548
|
+
function normalizeSigningSecret(secret) {
|
|
4549
|
+
const raw = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, "utf8");
|
|
4550
|
+
return (0, import_node_crypto3.createHash)("sha256").update(raw).digest();
|
|
4551
|
+
}
|
|
4552
|
+
function isEqualSignature(actual, expected) {
|
|
4553
|
+
const actualBuffer = Buffer.from(actual, "hex");
|
|
4554
|
+
const expectedBuffer = Buffer.from(expected, "hex");
|
|
4555
|
+
return actualBuffer.length === expectedBuffer.length && (0, import_node_crypto3.timingSafeEqual)(actualBuffer, expectedBuffer);
|
|
4556
|
+
}
|
|
4164
4557
|
|
|
4165
4558
|
// src/invalidation/RedisTagIndex.ts
|
|
4559
|
+
var DEFAULT_KNOWN_KEYS_SHARDS = 16;
|
|
4166
4560
|
var RedisTagIndex = class {
|
|
4167
4561
|
client;
|
|
4168
4562
|
prefix;
|
|
4169
4563
|
scanCount;
|
|
4170
4564
|
knownKeysShards;
|
|
4565
|
+
logger;
|
|
4566
|
+
warnedLegacyKnownKeys = false;
|
|
4171
4567
|
constructor(options) {
|
|
4172
4568
|
this.client = options.client;
|
|
4173
4569
|
this.prefix = options.prefix ?? "layercache:tag-index";
|
|
4174
4570
|
this.scanCount = options.scanCount ?? 100;
|
|
4175
4571
|
this.knownKeysShards = normalizeKnownKeysShards(options.knownKeysShards);
|
|
4572
|
+
this.logger = options.logger;
|
|
4176
4573
|
}
|
|
4574
|
+
/**
|
|
4575
|
+
* Records a key as known without changing tag assignments.
|
|
4576
|
+
*/
|
|
4177
4577
|
async touch(key) {
|
|
4178
4578
|
await this.client.sadd(this.knownKeysKeyFor(key), key);
|
|
4179
4579
|
}
|
|
4580
|
+
/**
|
|
4581
|
+
* Replaces the tags associated with a key and records the key as known.
|
|
4582
|
+
*/
|
|
4180
4583
|
async track(key, tags) {
|
|
4181
4584
|
const keyTagsKey = this.keyTagsKey(key);
|
|
4182
4585
|
const existingTags = await this.client.smembers(keyTagsKey);
|
|
@@ -4194,20 +4597,32 @@ var RedisTagIndex = class {
|
|
|
4194
4597
|
}
|
|
4195
4598
|
await pipeline.exec();
|
|
4196
4599
|
}
|
|
4600
|
+
/**
|
|
4601
|
+
* Removes a key from all tag mappings and known-key tracking.
|
|
4602
|
+
*/
|
|
4197
4603
|
async remove(key) {
|
|
4198
4604
|
const keyTagsKey = this.keyTagsKey(key);
|
|
4199
4605
|
const existingTags = await this.client.smembers(keyTagsKey);
|
|
4200
4606
|
const pipeline = this.client.pipeline();
|
|
4201
4607
|
pipeline.srem(this.knownKeysKeyFor(key), key);
|
|
4608
|
+
if (this.knownKeysShards > 1) {
|
|
4609
|
+
pipeline.srem(this.legacyKnownKeysKey(), key);
|
|
4610
|
+
}
|
|
4202
4611
|
pipeline.del(keyTagsKey);
|
|
4203
4612
|
for (const tag of existingTags) {
|
|
4204
4613
|
pipeline.srem(this.tagKeysKey(tag), key);
|
|
4205
4614
|
}
|
|
4206
4615
|
await pipeline.exec();
|
|
4207
4616
|
}
|
|
4617
|
+
/**
|
|
4618
|
+
* Returns keys currently associated with a tag.
|
|
4619
|
+
*/
|
|
4208
4620
|
async keysForTag(tag) {
|
|
4209
4621
|
return this.client.smembers(this.tagKeysKey(tag));
|
|
4210
4622
|
}
|
|
4623
|
+
/**
|
|
4624
|
+
* Visits keys currently associated with a tag.
|
|
4625
|
+
*/
|
|
4211
4626
|
async forEachKeyForTag(tag, visitor) {
|
|
4212
4627
|
let cursor = "0";
|
|
4213
4628
|
const tagKey = this.tagKeysKey(tag);
|
|
@@ -4219,38 +4634,56 @@ var RedisTagIndex = class {
|
|
|
4219
4634
|
}
|
|
4220
4635
|
} while (cursor !== "0");
|
|
4221
4636
|
}
|
|
4637
|
+
/**
|
|
4638
|
+
* Returns known keys that start with a prefix.
|
|
4639
|
+
*/
|
|
4222
4640
|
async keysForPrefix(prefix) {
|
|
4223
|
-
const matches =
|
|
4224
|
-
for (const knownKeysKey of this.
|
|
4641
|
+
const matches = /* @__PURE__ */ new Set();
|
|
4642
|
+
for (const knownKeysKey of await this.knownKeysKeysForRead()) {
|
|
4225
4643
|
let cursor = "0";
|
|
4226
4644
|
do {
|
|
4227
4645
|
const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
|
|
4228
4646
|
cursor = nextCursor;
|
|
4229
|
-
|
|
4647
|
+
for (const key of keys) {
|
|
4648
|
+
if (key.startsWith(prefix)) {
|
|
4649
|
+
matches.add(key);
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4230
4652
|
} while (cursor !== "0");
|
|
4231
4653
|
}
|
|
4232
|
-
return matches;
|
|
4654
|
+
return [...matches];
|
|
4233
4655
|
}
|
|
4656
|
+
/**
|
|
4657
|
+
* Visits known keys that start with a prefix.
|
|
4658
|
+
*/
|
|
4234
4659
|
async forEachKeyForPrefix(prefix, visitor) {
|
|
4235
|
-
|
|
4660
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4661
|
+
for (const knownKeysKey of await this.knownKeysKeysForRead()) {
|
|
4236
4662
|
let cursor = "0";
|
|
4237
4663
|
do {
|
|
4238
4664
|
const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
|
|
4239
4665
|
cursor = nextCursor;
|
|
4240
4666
|
for (const key of keys) {
|
|
4241
|
-
if (key.startsWith(prefix)) {
|
|
4667
|
+
if (key.startsWith(prefix) && !visited.has(key)) {
|
|
4668
|
+
visited.add(key);
|
|
4242
4669
|
await visitor(key);
|
|
4243
4670
|
}
|
|
4244
4671
|
}
|
|
4245
4672
|
} while (cursor !== "0");
|
|
4246
4673
|
}
|
|
4247
4674
|
}
|
|
4675
|
+
/**
|
|
4676
|
+
* Returns the tags currently associated with a key.
|
|
4677
|
+
*/
|
|
4248
4678
|
async tagsForKey(key) {
|
|
4249
4679
|
return this.client.smembers(this.keyTagsKey(key));
|
|
4250
4680
|
}
|
|
4681
|
+
/**
|
|
4682
|
+
* Returns known keys matching a wildcard pattern.
|
|
4683
|
+
*/
|
|
4251
4684
|
async matchPattern(pattern) {
|
|
4252
|
-
const matches =
|
|
4253
|
-
for (const knownKeysKey of this.
|
|
4685
|
+
const matches = /* @__PURE__ */ new Set();
|
|
4686
|
+
for (const knownKeysKey of await this.knownKeysKeysForRead()) {
|
|
4254
4687
|
let cursor = "0";
|
|
4255
4688
|
do {
|
|
4256
4689
|
const [nextCursor, keys] = await this.client.sscan(
|
|
@@ -4262,13 +4695,21 @@ var RedisTagIndex = class {
|
|
|
4262
4695
|
this.scanCount
|
|
4263
4696
|
);
|
|
4264
4697
|
cursor = nextCursor;
|
|
4265
|
-
|
|
4698
|
+
for (const key of keys) {
|
|
4699
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
4700
|
+
matches.add(key);
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4266
4703
|
} while (cursor !== "0");
|
|
4267
4704
|
}
|
|
4268
|
-
return matches;
|
|
4705
|
+
return [...matches];
|
|
4269
4706
|
}
|
|
4707
|
+
/**
|
|
4708
|
+
* Visits known keys matching a wildcard pattern.
|
|
4709
|
+
*/
|
|
4270
4710
|
async forEachKeyMatchingPattern(pattern, visitor) {
|
|
4271
|
-
|
|
4711
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4712
|
+
for (const knownKeysKey of await this.knownKeysKeysForRead()) {
|
|
4272
4713
|
let cursor = "0";
|
|
4273
4714
|
do {
|
|
4274
4715
|
const [nextCursor, keys] = await this.client.sscan(
|
|
@@ -4281,13 +4722,17 @@ var RedisTagIndex = class {
|
|
|
4281
4722
|
);
|
|
4282
4723
|
cursor = nextCursor;
|
|
4283
4724
|
for (const key of keys) {
|
|
4284
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
4725
|
+
if (PatternMatcher.matches(pattern, key) && !visited.has(key)) {
|
|
4726
|
+
visited.add(key);
|
|
4285
4727
|
await visitor(key);
|
|
4286
4728
|
}
|
|
4287
4729
|
}
|
|
4288
4730
|
} while (cursor !== "0");
|
|
4289
4731
|
}
|
|
4290
4732
|
}
|
|
4733
|
+
/**
|
|
4734
|
+
* Clears all Redis tag-index state under this prefix.
|
|
4735
|
+
*/
|
|
4291
4736
|
async clear() {
|
|
4292
4737
|
const indexKeys = await this.scanIndexKeys();
|
|
4293
4738
|
if (indexKeys.length === 0) {
|
|
@@ -4295,6 +4740,31 @@ var RedisTagIndex = class {
|
|
|
4295
4740
|
}
|
|
4296
4741
|
await this.client.del(...indexKeys);
|
|
4297
4742
|
}
|
|
4743
|
+
async migrateLegacyKnownKeys() {
|
|
4744
|
+
if (this.knownKeysShards === 1) {
|
|
4745
|
+
return { migratedKeys: 0 };
|
|
4746
|
+
}
|
|
4747
|
+
const legacyKey = this.legacyKnownKeysKey();
|
|
4748
|
+
let cursor = "0";
|
|
4749
|
+
let migratedKeys = 0;
|
|
4750
|
+
do {
|
|
4751
|
+
const [nextCursor, keys] = await this.client.sscan(legacyKey, cursor, "COUNT", this.scanCount);
|
|
4752
|
+
cursor = nextCursor;
|
|
4753
|
+
if (keys.length === 0) {
|
|
4754
|
+
continue;
|
|
4755
|
+
}
|
|
4756
|
+
const pipeline = this.client.pipeline();
|
|
4757
|
+
for (const key of keys) {
|
|
4758
|
+
pipeline.sadd(this.knownKeysKeyFor(key), key);
|
|
4759
|
+
}
|
|
4760
|
+
await pipeline.exec();
|
|
4761
|
+
migratedKeys += keys.length;
|
|
4762
|
+
} while (cursor !== "0");
|
|
4763
|
+
if (migratedKeys > 0) {
|
|
4764
|
+
await this.client.del(legacyKey);
|
|
4765
|
+
}
|
|
4766
|
+
return { migratedKeys };
|
|
4767
|
+
}
|
|
4298
4768
|
async scanIndexKeys() {
|
|
4299
4769
|
const matches = [];
|
|
4300
4770
|
let cursor = "0";
|
|
@@ -4312,12 +4782,40 @@ var RedisTagIndex = class {
|
|
|
4312
4782
|
}
|
|
4313
4783
|
return `${this.prefix}:keys:${simpleHash(key) % this.knownKeysShards}`;
|
|
4314
4784
|
}
|
|
4785
|
+
async knownKeysKeysForRead() {
|
|
4786
|
+
if (this.knownKeysShards === 1) {
|
|
4787
|
+
return [this.legacyKnownKeysKey()];
|
|
4788
|
+
}
|
|
4789
|
+
const shardedKeys = this.knownKeysKeys();
|
|
4790
|
+
const legacyKey = this.legacyKnownKeysKey();
|
|
4791
|
+
const legacyExists = await this.client.exists(legacyKey) > 0;
|
|
4792
|
+
if (!legacyExists) {
|
|
4793
|
+
return shardedKeys;
|
|
4794
|
+
}
|
|
4795
|
+
this.warnLegacyKnownKeys(legacyKey);
|
|
4796
|
+
return [legacyKey, ...shardedKeys];
|
|
4797
|
+
}
|
|
4315
4798
|
knownKeysKeys() {
|
|
4316
4799
|
if (this.knownKeysShards === 1) {
|
|
4317
4800
|
return [`${this.prefix}:keys`];
|
|
4318
4801
|
}
|
|
4319
4802
|
return Array.from({ length: this.knownKeysShards }, (_, index) => `${this.prefix}:keys:${index}`);
|
|
4320
4803
|
}
|
|
4804
|
+
legacyKnownKeysKey() {
|
|
4805
|
+
return `${this.prefix}:keys`;
|
|
4806
|
+
}
|
|
4807
|
+
warnLegacyKnownKeys(legacyKey) {
|
|
4808
|
+
if (this.warnedLegacyKnownKeys) {
|
|
4809
|
+
return;
|
|
4810
|
+
}
|
|
4811
|
+
this.warnedLegacyKnownKeys = true;
|
|
4812
|
+
const message = "RedisTagIndex detected a legacy RedisTagIndex known-key set. Run `layercache migrate-tag-index` to migrate keys into the sharded layout.";
|
|
4813
|
+
if (this.logger?.warn) {
|
|
4814
|
+
this.logger.warn(message, { legacyKey, knownKeysShards: this.knownKeysShards });
|
|
4815
|
+
return;
|
|
4816
|
+
}
|
|
4817
|
+
console.warn(`[layercache] ${message}`, { legacyKey, knownKeysShards: this.knownKeysShards });
|
|
4818
|
+
}
|
|
4321
4819
|
keyTagsKey(key) {
|
|
4322
4820
|
return `${this.prefix}:key:${encodeURIComponent(key)}`;
|
|
4323
4821
|
}
|
|
@@ -4327,7 +4825,7 @@ var RedisTagIndex = class {
|
|
|
4327
4825
|
};
|
|
4328
4826
|
function normalizeKnownKeysShards(value) {
|
|
4329
4827
|
if (value === void 0) {
|
|
4330
|
-
return
|
|
4828
|
+
return DEFAULT_KNOWN_KEYS_SHARDS;
|
|
4331
4829
|
}
|
|
4332
4830
|
if (!Number.isInteger(value) || value <= 0) {
|
|
4333
4831
|
throw new Error("RedisTagIndex.knownKeysShards must be a positive integer.");
|
|
@@ -4423,6 +4921,41 @@ function createFastifyLayercachePlugin(cache, options = {}) {
|
|
|
4423
4921
|
};
|
|
4424
4922
|
}
|
|
4425
4923
|
|
|
4924
|
+
// src/integrations/httpCacheKeys.ts
|
|
4925
|
+
var SENSITIVE_QUERY_PARAMETERS = /* @__PURE__ */ new Set([
|
|
4926
|
+
"access_token",
|
|
4927
|
+
"api_key",
|
|
4928
|
+
"apikey",
|
|
4929
|
+
"auth",
|
|
4930
|
+
"authorization",
|
|
4931
|
+
"code",
|
|
4932
|
+
"credentials",
|
|
4933
|
+
"id_token",
|
|
4934
|
+
"jwt",
|
|
4935
|
+
"password",
|
|
4936
|
+
"private_key",
|
|
4937
|
+
"refresh_token",
|
|
4938
|
+
"secret",
|
|
4939
|
+
"session",
|
|
4940
|
+
"sessionid",
|
|
4941
|
+
"session_id",
|
|
4942
|
+
"token"
|
|
4943
|
+
]);
|
|
4944
|
+
function normalizeHttpCacheUrl(url) {
|
|
4945
|
+
try {
|
|
4946
|
+
const parsed = new URL(url, "http://localhost");
|
|
4947
|
+
for (const name of [...parsed.searchParams.keys()]) {
|
|
4948
|
+
if (SENSITIVE_QUERY_PARAMETERS.has(name.toLowerCase())) {
|
|
4949
|
+
parsed.searchParams.delete(name);
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
parsed.searchParams.sort();
|
|
4953
|
+
return parsed.pathname + parsed.search;
|
|
4954
|
+
} catch {
|
|
4955
|
+
return url;
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4426
4959
|
// src/integrations/express.ts
|
|
4427
4960
|
function createExpressCacheMiddleware(cache, options = {}) {
|
|
4428
4961
|
const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
|
|
@@ -4438,7 +4971,7 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
4438
4971
|
return;
|
|
4439
4972
|
}
|
|
4440
4973
|
const rawUrl = req.originalUrl ?? req.url ?? "/";
|
|
4441
|
-
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${
|
|
4974
|
+
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeHttpCacheUrl(rawUrl)}`;
|
|
4442
4975
|
const cached = await cache.get(key, void 0, options);
|
|
4443
4976
|
if (cached !== null) {
|
|
4444
4977
|
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
@@ -4454,12 +4987,14 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
4454
4987
|
if (originalJson) {
|
|
4455
4988
|
res.json = (body) => {
|
|
4456
4989
|
res.setHeader?.("x-cache", "MISS");
|
|
4457
|
-
|
|
4458
|
-
cache.
|
|
4459
|
-
|
|
4460
|
-
|
|
4990
|
+
if (isSuccessfulStatus(res.statusCode)) {
|
|
4991
|
+
cache.set(key, body, options).catch((err) => {
|
|
4992
|
+
cache.emit("error", {
|
|
4993
|
+
operation: "set",
|
|
4994
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4995
|
+
});
|
|
4461
4996
|
});
|
|
4462
|
-
}
|
|
4997
|
+
}
|
|
4463
4998
|
return originalJson(body);
|
|
4464
4999
|
};
|
|
4465
5000
|
}
|
|
@@ -4469,14 +5004,8 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
4469
5004
|
}
|
|
4470
5005
|
};
|
|
4471
5006
|
}
|
|
4472
|
-
function
|
|
4473
|
-
|
|
4474
|
-
const parsed = new URL(url, "http://localhost");
|
|
4475
|
-
parsed.searchParams.sort();
|
|
4476
|
-
return parsed.pathname + parsed.search;
|
|
4477
|
-
} catch {
|
|
4478
|
-
return url;
|
|
4479
|
-
}
|
|
5007
|
+
function isSuccessfulStatus(statusCode) {
|
|
5008
|
+
return statusCode === void 0 || statusCode >= 200 && statusCode < 300;
|
|
4480
5009
|
}
|
|
4481
5010
|
|
|
4482
5011
|
// src/integrations/graphql.ts
|
|
@@ -4507,35 +5036,39 @@ function createHonoCacheMiddleware(cache, options = {}) {
|
|
|
4507
5036
|
return;
|
|
4508
5037
|
}
|
|
4509
5038
|
const rawPath = context.req.path ?? context.req.url ?? "/";
|
|
4510
|
-
const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${
|
|
5039
|
+
const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${normalizeHttpCacheUrl(rawPath)}`;
|
|
4511
5040
|
const cached = await cache.get(key, void 0, options);
|
|
4512
5041
|
if (cached !== null) {
|
|
4513
5042
|
context.header?.("x-cache", "HIT");
|
|
4514
5043
|
context.header?.("content-type", "application/json; charset=utf-8");
|
|
4515
5044
|
return context.json(cached);
|
|
4516
5045
|
}
|
|
5046
|
+
let currentStatus;
|
|
5047
|
+
const originalStatus = context.status?.bind(context);
|
|
5048
|
+
if (originalStatus) {
|
|
5049
|
+
context.status = (status) => {
|
|
5050
|
+
currentStatus = status;
|
|
5051
|
+
return originalStatus(status);
|
|
5052
|
+
};
|
|
5053
|
+
}
|
|
4517
5054
|
const originalJson = context.json.bind(context);
|
|
4518
5055
|
context.json = (body, status) => {
|
|
4519
5056
|
context.header?.("x-cache", "MISS");
|
|
4520
|
-
|
|
4521
|
-
cache.
|
|
4522
|
-
|
|
4523
|
-
|
|
5057
|
+
if (isSuccessfulStatus2(status ?? currentStatus)) {
|
|
5058
|
+
cache.set(key, body, options).catch((err) => {
|
|
5059
|
+
cache.emit("error", {
|
|
5060
|
+
operation: "set",
|
|
5061
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5062
|
+
});
|
|
4524
5063
|
});
|
|
4525
|
-
}
|
|
5064
|
+
}
|
|
4526
5065
|
return originalJson(body, status);
|
|
4527
5066
|
};
|
|
4528
5067
|
await next();
|
|
4529
5068
|
};
|
|
4530
5069
|
}
|
|
4531
|
-
function
|
|
4532
|
-
|
|
4533
|
-
const parsed = new URL(url, "http://localhost");
|
|
4534
|
-
parsed.searchParams.sort();
|
|
4535
|
-
return parsed.pathname + parsed.search;
|
|
4536
|
-
} catch {
|
|
4537
|
-
return url;
|
|
4538
|
-
}
|
|
5070
|
+
function isSuccessfulStatus2(statusCode) {
|
|
5071
|
+
return statusCode === void 0 || statusCode >= 200 && statusCode < 300;
|
|
4539
5072
|
}
|
|
4540
5073
|
|
|
4541
5074
|
// src/integrations/opentelemetry.ts
|
|
@@ -4627,6 +5160,9 @@ var MemoryLayer = class {
|
|
|
4627
5160
|
onEvict;
|
|
4628
5161
|
entries = /* @__PURE__ */ new Map();
|
|
4629
5162
|
cleanupTimer;
|
|
5163
|
+
/**
|
|
5164
|
+
* Creates an in-memory cache layer.
|
|
5165
|
+
*/
|
|
4630
5166
|
constructor(options = {}) {
|
|
4631
5167
|
this.name = options.name ?? "memory";
|
|
4632
5168
|
this.defaultTtl = options.ttl;
|
|
@@ -4640,10 +5176,16 @@ var MemoryLayer = class {
|
|
|
4640
5176
|
this.cleanupTimer.unref?.();
|
|
4641
5177
|
}
|
|
4642
5178
|
}
|
|
5179
|
+
/**
|
|
5180
|
+
* Reads and unwraps a fresh value from memory.
|
|
5181
|
+
*/
|
|
4643
5182
|
async get(key) {
|
|
4644
5183
|
const value = await this.getEntry(key);
|
|
4645
5184
|
return unwrapStoredValue(value);
|
|
4646
5185
|
}
|
|
5186
|
+
/**
|
|
5187
|
+
* Reads the raw stored value or envelope from memory.
|
|
5188
|
+
*/
|
|
4647
5189
|
async getEntry(key) {
|
|
4648
5190
|
const entry = this.entries.get(key);
|
|
4649
5191
|
if (!entry) {
|
|
@@ -4662,12 +5204,21 @@ var MemoryLayer = class {
|
|
|
4662
5204
|
}
|
|
4663
5205
|
return entry.value;
|
|
4664
5206
|
}
|
|
5207
|
+
/**
|
|
5208
|
+
* Reads many raw entries from memory.
|
|
5209
|
+
*/
|
|
4665
5210
|
async getMany(keys) {
|
|
4666
5211
|
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
4667
5212
|
}
|
|
5213
|
+
/**
|
|
5214
|
+
* Writes many entries to memory.
|
|
5215
|
+
*/
|
|
4668
5216
|
async setMany(entries) {
|
|
4669
5217
|
await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.ttl)));
|
|
4670
5218
|
}
|
|
5219
|
+
/**
|
|
5220
|
+
* Stores a value in memory using the provided TTL or layer default TTL.
|
|
5221
|
+
*/
|
|
4671
5222
|
async set(key, value, ttl = this.defaultTtl) {
|
|
4672
5223
|
this.entries.delete(key);
|
|
4673
5224
|
this.entries.set(key, {
|
|
@@ -4680,6 +5231,9 @@ var MemoryLayer = class {
|
|
|
4680
5231
|
this.evict();
|
|
4681
5232
|
}
|
|
4682
5233
|
}
|
|
5234
|
+
/**
|
|
5235
|
+
* Returns true when the key exists and has not expired.
|
|
5236
|
+
*/
|
|
4683
5237
|
async has(key) {
|
|
4684
5238
|
const entry = this.entries.get(key);
|
|
4685
5239
|
if (!entry) {
|
|
@@ -4691,6 +5245,9 @@ var MemoryLayer = class {
|
|
|
4691
5245
|
}
|
|
4692
5246
|
return true;
|
|
4693
5247
|
}
|
|
5248
|
+
/**
|
|
5249
|
+
* Returns remaining TTL in milliseconds, or null when absent or non-expiring.
|
|
5250
|
+
*/
|
|
4694
5251
|
async ttl(key) {
|
|
4695
5252
|
const entry = this.entries.get(key);
|
|
4696
5253
|
if (!entry) {
|
|
@@ -4705,40 +5262,67 @@ var MemoryLayer = class {
|
|
|
4705
5262
|
}
|
|
4706
5263
|
return Math.max(0, Math.ceil(entry.expiresAt - Date.now()));
|
|
4707
5264
|
}
|
|
5265
|
+
/**
|
|
5266
|
+
* Returns the number of currently retained, non-expired entries.
|
|
5267
|
+
*/
|
|
4708
5268
|
async size() {
|
|
4709
5269
|
this.pruneExpired();
|
|
4710
5270
|
return this.entries.size;
|
|
4711
5271
|
}
|
|
5272
|
+
/**
|
|
5273
|
+
* Deletes a key from memory.
|
|
5274
|
+
*/
|
|
4712
5275
|
async delete(key) {
|
|
4713
5276
|
this.entries.delete(key);
|
|
4714
5277
|
}
|
|
5278
|
+
/**
|
|
5279
|
+
* Deletes multiple keys from memory.
|
|
5280
|
+
*/
|
|
4715
5281
|
async deleteMany(keys) {
|
|
4716
5282
|
for (const key of keys) {
|
|
4717
5283
|
this.entries.delete(key);
|
|
4718
5284
|
}
|
|
4719
5285
|
}
|
|
5286
|
+
/**
|
|
5287
|
+
* Removes all entries from memory.
|
|
5288
|
+
*/
|
|
4720
5289
|
async clear() {
|
|
4721
5290
|
this.entries.clear();
|
|
4722
5291
|
}
|
|
5292
|
+
/**
|
|
5293
|
+
* Health check hook that always succeeds for the in-process layer.
|
|
5294
|
+
*/
|
|
4723
5295
|
async ping() {
|
|
4724
5296
|
return true;
|
|
4725
5297
|
}
|
|
5298
|
+
/**
|
|
5299
|
+
* Stops the cleanup timer, when one is active.
|
|
5300
|
+
*/
|
|
4726
5301
|
async dispose() {
|
|
4727
5302
|
if (this.cleanupTimer) {
|
|
4728
5303
|
clearInterval(this.cleanupTimer);
|
|
4729
5304
|
this.cleanupTimer = void 0;
|
|
4730
5305
|
}
|
|
4731
5306
|
}
|
|
5307
|
+
/**
|
|
5308
|
+
* Returns all currently retained, non-expired keys.
|
|
5309
|
+
*/
|
|
4732
5310
|
async keys() {
|
|
4733
5311
|
this.pruneExpired();
|
|
4734
5312
|
return [...this.entries.keys()];
|
|
4735
5313
|
}
|
|
5314
|
+
/**
|
|
5315
|
+
* Visits all currently retained, non-expired keys.
|
|
5316
|
+
*/
|
|
4736
5317
|
async forEachKey(visitor) {
|
|
4737
5318
|
this.pruneExpired();
|
|
4738
5319
|
for (const key of this.entries.keys()) {
|
|
4739
5320
|
await visitor(key);
|
|
4740
5321
|
}
|
|
4741
5322
|
}
|
|
5323
|
+
/**
|
|
5324
|
+
* Exports memory entries for process-local snapshots.
|
|
5325
|
+
*/
|
|
4742
5326
|
exportState() {
|
|
4743
5327
|
this.pruneExpired();
|
|
4744
5328
|
return [...this.entries.entries()].map(([key, entry]) => ({
|
|
@@ -4747,6 +5331,9 @@ var MemoryLayer = class {
|
|
|
4747
5331
|
expiresAt: entry.expiresAt
|
|
4748
5332
|
}));
|
|
4749
5333
|
}
|
|
5334
|
+
/**
|
|
5335
|
+
* Imports entries previously produced by `exportState()`.
|
|
5336
|
+
*/
|
|
4750
5337
|
importState(entries) {
|
|
4751
5338
|
for (const entry of entries) {
|
|
4752
5339
|
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
@@ -4826,6 +5413,9 @@ var RedisLayer = class {
|
|
|
4826
5413
|
decompressionMaxBytes;
|
|
4827
5414
|
commandTimeoutMs;
|
|
4828
5415
|
disconnectOnDispose;
|
|
5416
|
+
/**
|
|
5417
|
+
* Creates a Redis cache layer using an existing ioredis client.
|
|
5418
|
+
*/
|
|
4829
5419
|
constructor(options) {
|
|
4830
5420
|
this.client = options.client;
|
|
4831
5421
|
this.defaultTtl = options.ttl;
|
|
@@ -4840,10 +5430,16 @@ var RedisLayer = class {
|
|
|
4840
5430
|
this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
|
|
4841
5431
|
this.disconnectOnDispose = options.disconnectOnDispose ?? false;
|
|
4842
5432
|
}
|
|
5433
|
+
/**
|
|
5434
|
+
* Reads and unwraps a fresh value from Redis.
|
|
5435
|
+
*/
|
|
4843
5436
|
async get(key) {
|
|
4844
5437
|
const payload = await this.getEntry(key);
|
|
4845
5438
|
return unwrapStoredValue(payload);
|
|
4846
5439
|
}
|
|
5440
|
+
/**
|
|
5441
|
+
* Reads the raw stored value or envelope from Redis.
|
|
5442
|
+
*/
|
|
4847
5443
|
async getEntry(key) {
|
|
4848
5444
|
this.validateKey(key);
|
|
4849
5445
|
const payload = await this.runCommand(
|
|
@@ -4855,6 +5451,9 @@ var RedisLayer = class {
|
|
|
4855
5451
|
}
|
|
4856
5452
|
return this.deserializeOrDelete(key, payload);
|
|
4857
5453
|
}
|
|
5454
|
+
/**
|
|
5455
|
+
* Reads many raw entries from Redis using a pipeline.
|
|
5456
|
+
*/
|
|
4858
5457
|
async getMany(keys) {
|
|
4859
5458
|
if (keys.length === 0) {
|
|
4860
5459
|
return [];
|
|
@@ -4880,6 +5479,9 @@ var RedisLayer = class {
|
|
|
4880
5479
|
})
|
|
4881
5480
|
);
|
|
4882
5481
|
}
|
|
5482
|
+
/**
|
|
5483
|
+
* Writes many entries to Redis using a pipeline.
|
|
5484
|
+
*/
|
|
4883
5485
|
async setMany(entries) {
|
|
4884
5486
|
if (entries.length === 0) {
|
|
4885
5487
|
return;
|
|
@@ -4900,6 +5502,9 @@ var RedisLayer = class {
|
|
|
4900
5502
|
}
|
|
4901
5503
|
await this.runCommand(`mset(${entries.length})`, () => pipeline.exec());
|
|
4902
5504
|
}
|
|
5505
|
+
/**
|
|
5506
|
+
* Stores a value in Redis using the provided TTL or layer default TTL.
|
|
5507
|
+
*/
|
|
4903
5508
|
async set(key, value, ttl = this.defaultTtl) {
|
|
4904
5509
|
this.validateKey(key);
|
|
4905
5510
|
const serialized = this.primarySerializer().serialize(value);
|
|
@@ -4914,10 +5519,16 @@ var RedisLayer = class {
|
|
|
4914
5519
|
}
|
|
4915
5520
|
await this.runCommand(`set(${this.displayKey(key)})`, () => this.client.set(normalizedKey, payload));
|
|
4916
5521
|
}
|
|
5522
|
+
/**
|
|
5523
|
+
* Deletes a key from Redis.
|
|
5524
|
+
*/
|
|
4917
5525
|
async delete(key) {
|
|
4918
5526
|
this.validateKey(key);
|
|
4919
5527
|
await this.runCommand(`delete(${this.displayKey(key)})`, () => this.client.del(this.withPrefix(key)));
|
|
4920
5528
|
}
|
|
5529
|
+
/**
|
|
5530
|
+
* Deletes multiple keys from Redis in batches.
|
|
5531
|
+
*/
|
|
4921
5532
|
async deleteMany(keys) {
|
|
4922
5533
|
if (keys.length === 0) {
|
|
4923
5534
|
return;
|
|
@@ -4930,11 +5541,17 @@ var RedisLayer = class {
|
|
|
4930
5541
|
() => this.client.del(...keys.map((key) => this.withPrefix(key)))
|
|
4931
5542
|
);
|
|
4932
5543
|
}
|
|
5544
|
+
/**
|
|
5545
|
+
* Returns true when the key exists in Redis.
|
|
5546
|
+
*/
|
|
4933
5547
|
async has(key) {
|
|
4934
5548
|
this.validateKey(key);
|
|
4935
5549
|
const exists = await this.runCommand(`has(${this.displayKey(key)})`, () => this.client.exists(this.withPrefix(key)));
|
|
4936
5550
|
return exists > 0;
|
|
4937
5551
|
}
|
|
5552
|
+
/**
|
|
5553
|
+
* Returns remaining Redis TTL in milliseconds, or null when absent or non-expiring.
|
|
5554
|
+
*/
|
|
4938
5555
|
async ttl(key) {
|
|
4939
5556
|
this.validateKey(key);
|
|
4940
5557
|
const remaining = await this.runCommand(
|
|
@@ -4946,6 +5563,9 @@ var RedisLayer = class {
|
|
|
4946
5563
|
}
|
|
4947
5564
|
return remaining;
|
|
4948
5565
|
}
|
|
5566
|
+
/**
|
|
5567
|
+
* Returns the number of keys under this layer's prefix.
|
|
5568
|
+
*/
|
|
4949
5569
|
async size() {
|
|
4950
5570
|
if (!this.prefix) {
|
|
4951
5571
|
return this.runCommand("dbsize()", () => this.client.dbsize());
|
|
@@ -4963,6 +5583,9 @@ var RedisLayer = class {
|
|
|
4963
5583
|
} while (cursor !== "0");
|
|
4964
5584
|
return count;
|
|
4965
5585
|
}
|
|
5586
|
+
/**
|
|
5587
|
+
* Runs a Redis ping command.
|
|
5588
|
+
*/
|
|
4966
5589
|
async ping() {
|
|
4967
5590
|
try {
|
|
4968
5591
|
return await this.runCommand("ping()", () => this.client.ping()) === "PONG";
|
|
@@ -4970,6 +5593,9 @@ var RedisLayer = class {
|
|
|
4970
5593
|
return false;
|
|
4971
5594
|
}
|
|
4972
5595
|
}
|
|
5596
|
+
/**
|
|
5597
|
+
* Disconnects the Redis client when `disconnectOnDispose` is enabled.
|
|
5598
|
+
*/
|
|
4973
5599
|
async dispose() {
|
|
4974
5600
|
if (this.disconnectOnDispose) {
|
|
4975
5601
|
this.client.disconnect();
|
|
@@ -5002,6 +5628,9 @@ var RedisLayer = class {
|
|
|
5002
5628
|
}
|
|
5003
5629
|
} while (cursor !== "0");
|
|
5004
5630
|
}
|
|
5631
|
+
/**
|
|
5632
|
+
* Returns keys under this layer's prefix without the prefix included.
|
|
5633
|
+
*/
|
|
5005
5634
|
async keys() {
|
|
5006
5635
|
const pattern = `${this.prefix}*`;
|
|
5007
5636
|
const keys = await this.scanKeys(pattern);
|
|
@@ -5010,6 +5639,9 @@ var RedisLayer = class {
|
|
|
5010
5639
|
}
|
|
5011
5640
|
return keys.map((key) => key.slice(this.prefix.length));
|
|
5012
5641
|
}
|
|
5642
|
+
/**
|
|
5643
|
+
* Visits keys under this layer's prefix without materializing all results.
|
|
5644
|
+
*/
|
|
5013
5645
|
async forEachKey(visitor) {
|
|
5014
5646
|
const pattern = `${this.prefix}*`;
|
|
5015
5647
|
let cursor = "0";
|
|
@@ -5231,12 +5863,12 @@ var RedisLayer = class {
|
|
|
5231
5863
|
};
|
|
5232
5864
|
|
|
5233
5865
|
// src/layers/DiskLayer.ts
|
|
5234
|
-
var
|
|
5866
|
+
var import_node_crypto5 = require("crypto");
|
|
5235
5867
|
var import_node_fs2 = require("fs");
|
|
5236
5868
|
var import_node_path = require("path");
|
|
5237
5869
|
|
|
5238
5870
|
// src/internal/PayloadProtection.ts
|
|
5239
|
-
var
|
|
5871
|
+
var import_node_crypto4 = require("crypto");
|
|
5240
5872
|
var MAGIC_ENCRYPTED = Buffer.from("LCP1:");
|
|
5241
5873
|
var MAGIC_SIGNED = Buffer.from("LCS1:");
|
|
5242
5874
|
var ALGORITHM = "aes-256-gcm";
|
|
@@ -5249,11 +5881,11 @@ var PayloadProtection = class {
|
|
|
5249
5881
|
constructor(options) {
|
|
5250
5882
|
if (options.encryptionKey) {
|
|
5251
5883
|
const raw = Buffer.isBuffer(options.encryptionKey) ? options.encryptionKey : Buffer.from(options.encryptionKey, "utf8");
|
|
5252
|
-
this.encryptionKey = (0,
|
|
5884
|
+
this.encryptionKey = (0, import_node_crypto4.createHash)("sha256").update(raw).digest();
|
|
5253
5885
|
}
|
|
5254
5886
|
if (options.signingKey && !options.encryptionKey) {
|
|
5255
5887
|
const raw = Buffer.isBuffer(options.signingKey) ? options.signingKey : Buffer.from(options.signingKey, "utf8");
|
|
5256
|
-
this.signingKey = (0,
|
|
5888
|
+
this.signingKey = (0, import_node_crypto4.createHash)("sha256").update(raw).digest();
|
|
5257
5889
|
}
|
|
5258
5890
|
}
|
|
5259
5891
|
/** Returns `true` when any protection (encryption or signing) is configured. */
|
|
@@ -5300,8 +5932,8 @@ var PayloadProtection = class {
|
|
|
5300
5932
|
}
|
|
5301
5933
|
// ── Encryption (AES-256-GCM) ──────────────────────────────────────────
|
|
5302
5934
|
encrypt(plaintext, key) {
|
|
5303
|
-
const iv = (0,
|
|
5304
|
-
const cipher = (0,
|
|
5935
|
+
const iv = (0, import_node_crypto4.randomBytes)(IV_LENGTH);
|
|
5936
|
+
const cipher = (0, import_node_crypto4.createCipheriv)(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
5305
5937
|
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
5306
5938
|
const authTag = cipher.getAuthTag();
|
|
5307
5939
|
return Buffer.concat([MAGIC_ENCRYPTED, iv, authTag, encrypted]);
|
|
@@ -5312,7 +5944,7 @@ var PayloadProtection = class {
|
|
|
5312
5944
|
const authTag = payload.subarray(headerEnd + IV_LENGTH, headerEnd + IV_LENGTH + AUTH_TAG_LENGTH);
|
|
5313
5945
|
const ciphertext = payload.subarray(headerEnd + IV_LENGTH + AUTH_TAG_LENGTH);
|
|
5314
5946
|
try {
|
|
5315
|
-
const decipher = (0,
|
|
5947
|
+
const decipher = (0, import_node_crypto4.createDecipheriv)(ALGORITHM, key, iv, {
|
|
5316
5948
|
authTagLength: AUTH_TAG_LENGTH
|
|
5317
5949
|
});
|
|
5318
5950
|
decipher.setAuthTag(authTag);
|
|
@@ -5325,15 +5957,15 @@ var PayloadProtection = class {
|
|
|
5325
5957
|
}
|
|
5326
5958
|
// ── Signing (HMAC-SHA256) ─────────────────────────────────────────────
|
|
5327
5959
|
sign(payload, key) {
|
|
5328
|
-
const hmac = (0,
|
|
5960
|
+
const hmac = (0, import_node_crypto4.createHmac)("sha256", key).update(payload).digest();
|
|
5329
5961
|
return Buffer.concat([MAGIC_SIGNED, hmac, payload]);
|
|
5330
5962
|
}
|
|
5331
5963
|
verify(payload, key) {
|
|
5332
5964
|
const headerEnd = MAGIC_SIGNED.length;
|
|
5333
5965
|
const receivedHmac = payload.subarray(headerEnd, headerEnd + HMAC_LENGTH);
|
|
5334
5966
|
const data = payload.subarray(headerEnd + HMAC_LENGTH);
|
|
5335
|
-
const expectedHmac = (0,
|
|
5336
|
-
if (receivedHmac.length !== HMAC_LENGTH || !(0,
|
|
5967
|
+
const expectedHmac = (0, import_node_crypto4.createHmac)("sha256", key).update(data).digest();
|
|
5968
|
+
if (receivedHmac.length !== HMAC_LENGTH || !(0, import_node_crypto4.timingSafeEqual)(receivedHmac, expectedHmac)) {
|
|
5337
5969
|
throw new PayloadProtectionError(
|
|
5338
5970
|
"HMAC verification failed. The data may have been tampered with or the signingKey is incorrect."
|
|
5339
5971
|
);
|
|
@@ -5367,6 +5999,9 @@ var DiskLayer = class {
|
|
|
5367
5999
|
maxEntryBytes;
|
|
5368
6000
|
protection;
|
|
5369
6001
|
writeQueue = Promise.resolve();
|
|
6002
|
+
/**
|
|
6003
|
+
* Creates a disk-backed cache layer.
|
|
6004
|
+
*/
|
|
5370
6005
|
constructor(options) {
|
|
5371
6006
|
this.directory = this.resolveDirectory(options.directory);
|
|
5372
6007
|
this.defaultTtl = options.ttl;
|
|
@@ -5379,9 +6014,15 @@ var DiskLayer = class {
|
|
|
5379
6014
|
signingKey: options.signingKey
|
|
5380
6015
|
});
|
|
5381
6016
|
}
|
|
6017
|
+
/**
|
|
6018
|
+
* Reads and unwraps a fresh value from disk.
|
|
6019
|
+
*/
|
|
5382
6020
|
async get(key) {
|
|
5383
6021
|
return unwrapStoredValue(await this.getEntry(key));
|
|
5384
6022
|
}
|
|
6023
|
+
/**
|
|
6024
|
+
* Reads the raw stored value or envelope from disk.
|
|
6025
|
+
*/
|
|
5385
6026
|
async getEntry(key) {
|
|
5386
6027
|
const filePath = this.keyToPath(key);
|
|
5387
6028
|
const raw = await this.readEntryFile(filePath);
|
|
@@ -5401,6 +6042,9 @@ var DiskLayer = class {
|
|
|
5401
6042
|
}
|
|
5402
6043
|
return entry.value;
|
|
5403
6044
|
}
|
|
6045
|
+
/**
|
|
6046
|
+
* Stores a value on disk using the provided TTL or layer default TTL.
|
|
6047
|
+
*/
|
|
5404
6048
|
async set(key, value, ttl = this.defaultTtl) {
|
|
5405
6049
|
await this.enqueueWrite(async () => {
|
|
5406
6050
|
await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
|
|
@@ -5413,7 +6057,7 @@ var DiskLayer = class {
|
|
|
5413
6057
|
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
|
|
5414
6058
|
const protectedPayload = this.protection.protect(raw);
|
|
5415
6059
|
const targetPath = this.keyToPath(key);
|
|
5416
|
-
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${(0,
|
|
6060
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${(0, import_node_crypto5.randomBytes)(8).toString("hex")}.tmp`;
|
|
5417
6061
|
try {
|
|
5418
6062
|
await import_node_fs2.promises.writeFile(tempPath, protectedPayload);
|
|
5419
6063
|
await import_node_fs2.promises.rename(tempPath, targetPath);
|
|
@@ -5426,16 +6070,28 @@ var DiskLayer = class {
|
|
|
5426
6070
|
}
|
|
5427
6071
|
});
|
|
5428
6072
|
}
|
|
6073
|
+
/**
|
|
6074
|
+
* Reads many raw entries from disk.
|
|
6075
|
+
*/
|
|
5429
6076
|
async getMany(keys) {
|
|
5430
6077
|
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
5431
6078
|
}
|
|
6079
|
+
/**
|
|
6080
|
+
* Writes many entries to disk.
|
|
6081
|
+
*/
|
|
5432
6082
|
async setMany(entries) {
|
|
5433
6083
|
await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.ttl)));
|
|
5434
6084
|
}
|
|
6085
|
+
/**
|
|
6086
|
+
* Returns true when the key exists and has not expired.
|
|
6087
|
+
*/
|
|
5435
6088
|
async has(key) {
|
|
5436
6089
|
const value = await this.getEntry(key);
|
|
5437
6090
|
return value !== null;
|
|
5438
6091
|
}
|
|
6092
|
+
/**
|
|
6093
|
+
* Returns remaining TTL in milliseconds, or null when absent or non-expiring.
|
|
6094
|
+
*/
|
|
5439
6095
|
async ttl(key) {
|
|
5440
6096
|
const filePath = this.keyToPath(key);
|
|
5441
6097
|
const raw = await this.readEntryFile(filePath);
|
|
@@ -5458,14 +6114,23 @@ var DiskLayer = class {
|
|
|
5458
6114
|
}
|
|
5459
6115
|
return remaining;
|
|
5460
6116
|
}
|
|
6117
|
+
/**
|
|
6118
|
+
* Deletes a key from disk.
|
|
6119
|
+
*/
|
|
5461
6120
|
async delete(key) {
|
|
5462
6121
|
await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
|
|
5463
6122
|
}
|
|
6123
|
+
/**
|
|
6124
|
+
* Deletes multiple keys from disk.
|
|
6125
|
+
*/
|
|
5464
6126
|
async deleteMany(keys) {
|
|
5465
6127
|
await this.enqueueWrite(async () => {
|
|
5466
6128
|
await this.deletePathsWithConcurrency(keys.map((key) => this.keyToPath(key)));
|
|
5467
6129
|
});
|
|
5468
6130
|
}
|
|
6131
|
+
/**
|
|
6132
|
+
* Removes all cache entry files from this layer's directory.
|
|
6133
|
+
*/
|
|
5469
6134
|
async clear() {
|
|
5470
6135
|
await this.enqueueWrite(async () => {
|
|
5471
6136
|
let entries;
|
|
@@ -5490,11 +6155,17 @@ var DiskLayer = class {
|
|
|
5490
6155
|
});
|
|
5491
6156
|
return keys;
|
|
5492
6157
|
}
|
|
6158
|
+
/**
|
|
6159
|
+
* Visits all non-expired keys stored on disk.
|
|
6160
|
+
*/
|
|
5493
6161
|
async forEachKey(visitor) {
|
|
5494
6162
|
await this.scanEntries(async (entry) => {
|
|
5495
6163
|
await visitor(entry.key);
|
|
5496
6164
|
});
|
|
5497
6165
|
}
|
|
6166
|
+
/**
|
|
6167
|
+
* Returns the number of non-expired entries stored on disk.
|
|
6168
|
+
*/
|
|
5498
6169
|
async size() {
|
|
5499
6170
|
let count = 0;
|
|
5500
6171
|
await this.scanEntries(async () => {
|
|
@@ -5502,6 +6173,9 @@ var DiskLayer = class {
|
|
|
5502
6173
|
});
|
|
5503
6174
|
return count;
|
|
5504
6175
|
}
|
|
6176
|
+
/**
|
|
6177
|
+
* Verifies the cache directory can be created.
|
|
6178
|
+
*/
|
|
5505
6179
|
async ping() {
|
|
5506
6180
|
try {
|
|
5507
6181
|
await import_node_fs2.promises.mkdir(this.directory, { recursive: true });
|
|
@@ -5510,10 +6184,13 @@ var DiskLayer = class {
|
|
|
5510
6184
|
return false;
|
|
5511
6185
|
}
|
|
5512
6186
|
}
|
|
6187
|
+
/**
|
|
6188
|
+
* Reserved for interface compatibility; DiskLayer does not hold persistent handles.
|
|
6189
|
+
*/
|
|
5513
6190
|
async dispose() {
|
|
5514
6191
|
}
|
|
5515
6192
|
keyToPath(key) {
|
|
5516
|
-
const hash = (0,
|
|
6193
|
+
const hash = (0, import_node_crypto5.createHash)("sha256").update(key).digest("hex");
|
|
5517
6194
|
return (0, import_node_path.join)(this.directory, `${hash}.lc`);
|
|
5518
6195
|
}
|
|
5519
6196
|
resolveDirectory(directory) {
|
|
@@ -5713,6 +6390,9 @@ var MemcachedLayer = class {
|
|
|
5713
6390
|
client;
|
|
5714
6391
|
keyPrefix;
|
|
5715
6392
|
serializer;
|
|
6393
|
+
/**
|
|
6394
|
+
* Creates a Memcached cache layer using a compatible client.
|
|
6395
|
+
*/
|
|
5716
6396
|
constructor(options) {
|
|
5717
6397
|
this.client = options.client;
|
|
5718
6398
|
this.defaultTtl = options.ttl;
|
|
@@ -5720,9 +6400,15 @@ var MemcachedLayer = class {
|
|
|
5720
6400
|
this.keyPrefix = options.keyPrefix ?? "";
|
|
5721
6401
|
this.serializer = options.serializer ?? new JsonSerializer();
|
|
5722
6402
|
}
|
|
6403
|
+
/**
|
|
6404
|
+
* Reads and unwraps a fresh value from Memcached.
|
|
6405
|
+
*/
|
|
5723
6406
|
async get(key) {
|
|
5724
6407
|
return unwrapStoredValue(await this.getEntry(key));
|
|
5725
6408
|
}
|
|
6409
|
+
/**
|
|
6410
|
+
* Reads the raw stored value or envelope from Memcached.
|
|
6411
|
+
*/
|
|
5726
6412
|
async getEntry(key) {
|
|
5727
6413
|
this.validateKey(key);
|
|
5728
6414
|
const result = await this.client.get(this.withPrefix(key));
|
|
@@ -5735,9 +6421,15 @@ var MemcachedLayer = class {
|
|
|
5735
6421
|
return null;
|
|
5736
6422
|
}
|
|
5737
6423
|
}
|
|
6424
|
+
/**
|
|
6425
|
+
* Reads many raw entries from Memcached.
|
|
6426
|
+
*/
|
|
5738
6427
|
async getMany(keys) {
|
|
5739
6428
|
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
5740
6429
|
}
|
|
6430
|
+
/**
|
|
6431
|
+
* Stores a value in Memcached using the provided TTL or layer default TTL.
|
|
6432
|
+
*/
|
|
5741
6433
|
async set(key, value, ttl = this.defaultTtl) {
|
|
5742
6434
|
this.validateKey(key);
|
|
5743
6435
|
const payload = this.serializer.serialize(value);
|
|
@@ -5745,18 +6437,30 @@ var MemcachedLayer = class {
|
|
|
5745
6437
|
expires: ttl && ttl > 0 ? Math.ceil(ttl / 1e3) : void 0
|
|
5746
6438
|
});
|
|
5747
6439
|
}
|
|
6440
|
+
/**
|
|
6441
|
+
* Returns true when the key exists in Memcached.
|
|
6442
|
+
*/
|
|
5748
6443
|
async has(key) {
|
|
5749
6444
|
this.validateKey(key);
|
|
5750
6445
|
const result = await this.client.get(this.withPrefix(key));
|
|
5751
6446
|
return result !== null && result.value !== null;
|
|
5752
6447
|
}
|
|
6448
|
+
/**
|
|
6449
|
+
* Deletes a key from Memcached.
|
|
6450
|
+
*/
|
|
5753
6451
|
async delete(key) {
|
|
5754
6452
|
this.validateKey(key);
|
|
5755
6453
|
await this.client.delete(this.withPrefix(key));
|
|
5756
6454
|
}
|
|
6455
|
+
/**
|
|
6456
|
+
* Deletes multiple keys from Memcached.
|
|
6457
|
+
*/
|
|
5757
6458
|
async deleteMany(keys) {
|
|
5758
6459
|
await Promise.all(keys.map((key) => this.delete(key)));
|
|
5759
6460
|
}
|
|
6461
|
+
/**
|
|
6462
|
+
* Always throws because Memcached has no safe prefix clear primitive.
|
|
6463
|
+
*/
|
|
5760
6464
|
async clear() {
|
|
5761
6465
|
throw new Error(
|
|
5762
6466
|
"MemcachedLayer.clear() is not supported. Use a key prefix and rotate it to effectively invalidate all keys."
|
|
@@ -5784,9 +6488,15 @@ var MemcachedLayer = class {
|
|
|
5784
6488
|
// src/serialization/MsgpackSerializer.ts
|
|
5785
6489
|
var import_msgpack = require("@msgpack/msgpack");
|
|
5786
6490
|
var MsgpackSerializer = class {
|
|
6491
|
+
/**
|
|
6492
|
+
* Serializes a value to MessagePack bytes.
|
|
6493
|
+
*/
|
|
5787
6494
|
serialize(value) {
|
|
5788
6495
|
return Buffer.from((0, import_msgpack.encode)(value));
|
|
5789
6496
|
}
|
|
6497
|
+
/**
|
|
6498
|
+
* Decodes MessagePack bytes and sanitizes the result before returning it.
|
|
6499
|
+
*/
|
|
5790
6500
|
deserialize(payload) {
|
|
5791
6501
|
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
|
|
5792
6502
|
return sanitizeStructuredData((0, import_msgpack.decode)(normalized), {
|
|
@@ -5798,7 +6508,7 @@ var MsgpackSerializer = class {
|
|
|
5798
6508
|
};
|
|
5799
6509
|
|
|
5800
6510
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
5801
|
-
var
|
|
6511
|
+
var import_node_crypto6 = require("crypto");
|
|
5802
6512
|
var RELEASE_SCRIPT = `
|
|
5803
6513
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
5804
6514
|
return redis.call("del", KEYS[1])
|
|
@@ -5820,9 +6530,13 @@ var RedisSingleFlightCoordinator = class {
|
|
|
5820
6530
|
this.prefix = options.prefix ?? "layercache:singleflight";
|
|
5821
6531
|
this.commandTimeoutMs = this.normalizeCommandTimeoutMs(options.commandTimeoutMs);
|
|
5822
6532
|
}
|
|
6533
|
+
/**
|
|
6534
|
+
* Executes `worker` when this process acquires the Redis lock; otherwise runs
|
|
6535
|
+
* `waiter` while another process owns the work.
|
|
6536
|
+
*/
|
|
5823
6537
|
async execute(key, options, worker, waiter) {
|
|
5824
6538
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
5825
|
-
const token = (0,
|
|
6539
|
+
const token = (0, import_node_crypto6.randomUUID)();
|
|
5826
6540
|
const acquired = await this.runCommand(
|
|
5827
6541
|
`acquire("${key}")`,
|
|
5828
6542
|
() => this.client.set(lockKey, token, "PX", options.leaseMs, "NX")
|
|
@@ -5976,6 +6690,7 @@ function sanitizeLabel(value) {
|
|
|
5976
6690
|
MemoryLayer,
|
|
5977
6691
|
MsgpackSerializer,
|
|
5978
6692
|
PatternMatcher,
|
|
6693
|
+
RedisGenerationStore,
|
|
5979
6694
|
RedisInvalidationBus,
|
|
5980
6695
|
RedisLayer,
|
|
5981
6696
|
RedisSingleFlightCoordinator,
|