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/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
- this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
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 sorted = [...this.keyEpochs.entries()].sort((a, b) => a[1] - b[1]);
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.delete(sorted[i][0]);
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
- try {
1266
- await layer.set(key, stored, ttl);
1267
- } catch (error) {
1268
- await this.options.handleLayerFailure(layer, "backfill", error);
1269
- continue;
1270
- }
1271
- this.options.metricsCollector.increment("backfills");
1272
- this.options.logger.debug?.("backfill", { key, layer: layer.name });
1273
- this.options.emit("backfill", { key, layer: layer.name });
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
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
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
- const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
2540
- for (let i = 0; i < toRemove && i < sorted.length; i++) {
2541
- const entry = sorted[i];
2542
- if (entry) {
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 && i < sorted.length; i += 1) {
2744
- const entry = sorted[i];
2745
- if (entry) {
2746
- this.removeKey(entry[0]);
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
- if (!this.isInvalidationMessage(parsed)) {
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 = parsed;
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.knownKeysKeys()) {
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
- matches.push(...keys.filter((key) => key.startsWith(prefix)));
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
- for (const knownKeysKey of this.knownKeysKeys()) {
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.knownKeysKeys()) {
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
- matches.push(...keys.filter((key) => PatternMatcher.matches(pattern, key)));
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
- for (const knownKeysKey of this.knownKeysKeys()) {
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 1;
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}:${normalizeUrl(rawUrl)}`;
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
- cache.set(key, body, options).catch((err) => {
4458
- cache.emit("error", {
4459
- operation: "set",
4460
- error: err instanceof Error ? err.message : String(err)
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 normalizeUrl(url) {
4473
- try {
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}:${normalizeUrl2(rawPath)}`;
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
- cache.set(key, body, options).catch((err) => {
4521
- cache.emit("error", {
4522
- operation: "set",
4523
- error: err instanceof Error ? err.message : String(err)
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 normalizeUrl2(url) {
4532
- try {
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 import_node_crypto4 = require("crypto");
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 import_node_crypto3 = require("crypto");
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, import_node_crypto3.createHash)("sha256").update(raw).digest();
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, import_node_crypto3.createHash)("sha256").update(raw).digest();
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, import_node_crypto3.randomBytes)(IV_LENGTH);
5304
- const cipher = (0, import_node_crypto3.createCipheriv)(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
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, import_node_crypto3.createDecipheriv)(ALGORITHM, key, iv, {
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, import_node_crypto3.createHmac)("sha256", key).update(payload).digest();
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, import_node_crypto3.createHmac)("sha256", key).update(data).digest();
5336
- if (receivedHmac.length !== HMAC_LENGTH || !(0, import_node_crypto3.timingSafeEqual)(receivedHmac, expectedHmac)) {
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, import_node_crypto4.randomBytes)(8).toString("hex")}.tmp`;
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, import_node_crypto4.createHash)("sha256").update(key).digest("hex");
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 import_node_crypto5 = require("crypto");
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, import_node_crypto5.randomUUID)();
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,