layercache 2.1.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,
@@ -1157,7 +1158,9 @@ var CacheStackMaintenance = class {
1157
1158
  }
1158
1159
  bumpKeyEpochs(keys) {
1159
1160
  for (const key of keys) {
1160
- 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);
1161
1164
  }
1162
1165
  this.pruneKeyEpochsIfNeeded();
1163
1166
  }
@@ -1216,10 +1219,13 @@ var CacheStackMaintenance = class {
1216
1219
  if (this.keyEpochs.size <= MAX_KEY_EPOCHS) {
1217
1220
  return;
1218
1221
  }
1219
- const sorted = [...this.keyEpochs.entries()].sort((a, b) => a[1] - b[1]);
1220
- const toDelete = Math.ceil(sorted.length * 0.1);
1222
+ const toDelete = Math.ceil(this.keyEpochs.size * 0.1);
1221
1223
  for (let i = 0; i < toDelete; i++) {
1222
- 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);
1223
1229
  }
1224
1230
  }
1225
1231
  };
@@ -1353,22 +1359,28 @@ var CacheStackReader = class {
1353
1359
  if (upToIndex < 0) {
1354
1360
  return;
1355
1361
  }
1362
+ const operations = [];
1356
1363
  for (let index = 0; index <= upToIndex; index += 1) {
1357
1364
  const layer = this.options.layers[index];
1358
1365
  if (!layer || this.options.shouldSkipLayer(layer)) {
1359
1366
  continue;
1360
1367
  }
1361
1368
  const ttl = remainingStoredTtlMs(stored) ?? this.options.resolveLayerMs(layer.name, options?.ttl, void 0, layer.defaultTtl);
1362
- try {
1363
- await layer.set(key, stored, ttl);
1364
- } catch (error) {
1365
- await this.options.handleLayerFailure(layer, "backfill", error);
1366
- continue;
1367
- }
1368
- this.options.metricsCollector.increment("backfills");
1369
- this.options.logger.debug?.("backfill", { key, layer: layer.name });
1370
- 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
+ );
1371
1382
  }
1383
+ await Promise.all(operations);
1372
1384
  }
1373
1385
  abortAllRefreshes() {
1374
1386
  for (const key of this.backgroundRefreshAbort.keys()) {
@@ -1492,7 +1504,15 @@ var CacheStackReader = class {
1492
1504
  }
1493
1505
  await this.options.sleep(pollIntervalMs);
1494
1506
  }
1495
- 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
+ );
1496
1516
  }
1497
1517
  async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
1498
1518
  key,
@@ -2544,6 +2564,7 @@ var TtlResolver = class {
2544
2564
  const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
2545
2565
  profile.hits += 1;
2546
2566
  profile.lastAccessAt = Date.now();
2567
+ this.accessProfiles.delete(key);
2547
2568
  this.accessProfiles.set(key, profile);
2548
2569
  this.pruneIfNeeded();
2549
2570
  }
@@ -2633,12 +2654,12 @@ var TtlResolver = class {
2633
2654
  return;
2634
2655
  }
2635
2656
  const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
2636
- const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
2637
- for (let i = 0; i < toRemove && i < sorted.length; i++) {
2638
- const entry = sorted[i];
2639
- if (entry) {
2640
- 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;
2641
2661
  }
2662
+ this.accessProfiles.delete(oldestKey);
2642
2663
  }
2643
2664
  }
2644
2665
  };
@@ -2770,6 +2791,9 @@ var TagIndex = class {
2770
2791
  }
2771
2792
  insertKnownKey(key) {
2772
2793
  const isNew = !this.knownKeys.has(key);
2794
+ if (!isNew) {
2795
+ this.knownKeys.delete(key);
2796
+ }
2773
2797
  this.knownKeys.set(key, Date.now());
2774
2798
  if (!isNew) {
2775
2799
  return;
@@ -2868,13 +2892,13 @@ var TagIndex = class {
2868
2892
  if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
2869
2893
  return;
2870
2894
  }
2871
- const sorted = [...this.knownKeys.entries()].sort((a, b) => a[1] - b[1]);
2872
2895
  const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
2873
- for (let i = 0; i < toRemove && i < sorted.length; i += 1) {
2874
- const entry = sorted[i];
2875
- if (entry) {
2876
- 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;
2877
2900
  }
2901
+ this.removeKnownKey(oldestKey);
2878
2902
  }
2879
2903
  }
2880
2904
  removeKey(key) {
@@ -3775,6 +3799,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
3775
3799
  }
3776
3800
  return this.currentGeneration;
3777
3801
  }
3802
+ /**
3803
+ * Returns the active generation prefix number used for future cache keys.
3804
+ */
3805
+ getGeneration() {
3806
+ return this.currentGeneration;
3807
+ }
3778
3808
  /**
3779
3809
  * Returns detailed metadata about a single cache key: which layers contain it,
3780
3810
  * remaining fresh/stale/error TTLs, and associated tags.
@@ -4332,12 +4362,65 @@ var CacheStack = class extends import_node_events.EventEmitter {
4332
4362
  }
4333
4363
  };
4334
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
+
4335
4416
  // src/invalidation/RedisInvalidationBus.ts
4417
+ var import_node_crypto3 = require("crypto");
4336
4418
  var RedisInvalidationBus = class {
4337
4419
  channel;
4338
4420
  publisher;
4339
4421
  subscriber;
4340
4422
  logger;
4423
+ signingKey;
4341
4424
  handlers = /* @__PURE__ */ new Set();
4342
4425
  sharedListener;
4343
4426
  subscribePromise;
@@ -4346,6 +4429,7 @@ var RedisInvalidationBus = class {
4346
4429
  this.subscriber = options.subscriber ?? options.publisher.duplicate();
4347
4430
  this.channel = options.channel ?? "layercache:invalidation";
4348
4431
  this.logger = options.logger;
4432
+ this.signingKey = options.signingSecret ? normalizeSigningSecret(options.signingSecret) : void 0;
4349
4433
  }
4350
4434
  /**
4351
4435
  * Subscribes to invalidation messages and returns an unsubscribe function.
@@ -4385,7 +4469,7 @@ var RedisInvalidationBus = class {
4385
4469
  * Publishes an invalidation message to other subscribers.
4386
4470
  */
4387
4471
  async publish(message) {
4388
- await this.publisher.publish(this.channel, JSON.stringify(message));
4472
+ await this.publisher.publish(this.channel, JSON.stringify(this.signingKey ? this.signMessage(message) : message));
4389
4473
  }
4390
4474
  async dispatchToHandlers(payload) {
4391
4475
  let message;
@@ -4396,10 +4480,11 @@ var RedisInvalidationBus = class {
4396
4480
  maxNodes: 1e4,
4397
4481
  createObject: () => /* @__PURE__ */ Object.create(null)
4398
4482
  });
4399
- if (!this.isInvalidationMessage(parsed)) {
4483
+ const candidate = this.signingKey ? this.verifySignedEnvelope(parsed) : parsed;
4484
+ if (!this.isInvalidationMessage(candidate)) {
4400
4485
  throw new Error("Invalid invalidation payload shape.");
4401
4486
  }
4402
- message = parsed;
4487
+ message = candidate;
4403
4488
  } catch (error) {
4404
4489
  this.reportError("invalid invalidation payload", error);
4405
4490
  return;
@@ -4424,6 +4509,34 @@ var RedisInvalidationBus = class {
4424
4509
  const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
4425
4510
  return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
4426
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
+ }
4427
4540
  reportError(message, error) {
4428
4541
  if (this.logger?.error) {
4429
4542
  this.logger.error(message, { error });
@@ -4432,18 +4545,31 @@ var RedisInvalidationBus = class {
4432
4545
  console.error(`[layercache] ${message}`, error);
4433
4546
  }
4434
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
+ }
4435
4557
 
4436
4558
  // src/invalidation/RedisTagIndex.ts
4559
+ var DEFAULT_KNOWN_KEYS_SHARDS = 16;
4437
4560
  var RedisTagIndex = class {
4438
4561
  client;
4439
4562
  prefix;
4440
4563
  scanCount;
4441
4564
  knownKeysShards;
4565
+ logger;
4566
+ warnedLegacyKnownKeys = false;
4442
4567
  constructor(options) {
4443
4568
  this.client = options.client;
4444
4569
  this.prefix = options.prefix ?? "layercache:tag-index";
4445
4570
  this.scanCount = options.scanCount ?? 100;
4446
4571
  this.knownKeysShards = normalizeKnownKeysShards(options.knownKeysShards);
4572
+ this.logger = options.logger;
4447
4573
  }
4448
4574
  /**
4449
4575
  * Records a key as known without changing tag assignments.
@@ -4479,6 +4605,9 @@ var RedisTagIndex = class {
4479
4605
  const existingTags = await this.client.smembers(keyTagsKey);
4480
4606
  const pipeline = this.client.pipeline();
4481
4607
  pipeline.srem(this.knownKeysKeyFor(key), key);
4608
+ if (this.knownKeysShards > 1) {
4609
+ pipeline.srem(this.legacyKnownKeysKey(), key);
4610
+ }
4482
4611
  pipeline.del(keyTagsKey);
4483
4612
  for (const tag of existingTags) {
4484
4613
  pipeline.srem(this.tagKeysKey(tag), key);
@@ -4509,28 +4638,34 @@ var RedisTagIndex = class {
4509
4638
  * Returns known keys that start with a prefix.
4510
4639
  */
4511
4640
  async keysForPrefix(prefix) {
4512
- const matches = [];
4513
- for (const knownKeysKey of this.knownKeysKeys()) {
4641
+ const matches = /* @__PURE__ */ new Set();
4642
+ for (const knownKeysKey of await this.knownKeysKeysForRead()) {
4514
4643
  let cursor = "0";
4515
4644
  do {
4516
4645
  const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
4517
4646
  cursor = nextCursor;
4518
- 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
+ }
4519
4652
  } while (cursor !== "0");
4520
4653
  }
4521
- return matches;
4654
+ return [...matches];
4522
4655
  }
4523
4656
  /**
4524
4657
  * Visits known keys that start with a prefix.
4525
4658
  */
4526
4659
  async forEachKeyForPrefix(prefix, visitor) {
4527
- for (const knownKeysKey of this.knownKeysKeys()) {
4660
+ const visited = /* @__PURE__ */ new Set();
4661
+ for (const knownKeysKey of await this.knownKeysKeysForRead()) {
4528
4662
  let cursor = "0";
4529
4663
  do {
4530
4664
  const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
4531
4665
  cursor = nextCursor;
4532
4666
  for (const key of keys) {
4533
- if (key.startsWith(prefix)) {
4667
+ if (key.startsWith(prefix) && !visited.has(key)) {
4668
+ visited.add(key);
4534
4669
  await visitor(key);
4535
4670
  }
4536
4671
  }
@@ -4547,8 +4682,8 @@ var RedisTagIndex = class {
4547
4682
  * Returns known keys matching a wildcard pattern.
4548
4683
  */
4549
4684
  async matchPattern(pattern) {
4550
- const matches = [];
4551
- for (const knownKeysKey of this.knownKeysKeys()) {
4685
+ const matches = /* @__PURE__ */ new Set();
4686
+ for (const knownKeysKey of await this.knownKeysKeysForRead()) {
4552
4687
  let cursor = "0";
4553
4688
  do {
4554
4689
  const [nextCursor, keys] = await this.client.sscan(
@@ -4560,16 +4695,21 @@ var RedisTagIndex = class {
4560
4695
  this.scanCount
4561
4696
  );
4562
4697
  cursor = nextCursor;
4563
- 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
+ }
4564
4703
  } while (cursor !== "0");
4565
4704
  }
4566
- return matches;
4705
+ return [...matches];
4567
4706
  }
4568
4707
  /**
4569
4708
  * Visits known keys matching a wildcard pattern.
4570
4709
  */
4571
4710
  async forEachKeyMatchingPattern(pattern, visitor) {
4572
- for (const knownKeysKey of this.knownKeysKeys()) {
4711
+ const visited = /* @__PURE__ */ new Set();
4712
+ for (const knownKeysKey of await this.knownKeysKeysForRead()) {
4573
4713
  let cursor = "0";
4574
4714
  do {
4575
4715
  const [nextCursor, keys] = await this.client.sscan(
@@ -4582,7 +4722,8 @@ var RedisTagIndex = class {
4582
4722
  );
4583
4723
  cursor = nextCursor;
4584
4724
  for (const key of keys) {
4585
- if (PatternMatcher.matches(pattern, key)) {
4725
+ if (PatternMatcher.matches(pattern, key) && !visited.has(key)) {
4726
+ visited.add(key);
4586
4727
  await visitor(key);
4587
4728
  }
4588
4729
  }
@@ -4599,6 +4740,31 @@ var RedisTagIndex = class {
4599
4740
  }
4600
4741
  await this.client.del(...indexKeys);
4601
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
+ }
4602
4768
  async scanIndexKeys() {
4603
4769
  const matches = [];
4604
4770
  let cursor = "0";
@@ -4616,12 +4782,40 @@ var RedisTagIndex = class {
4616
4782
  }
4617
4783
  return `${this.prefix}:keys:${simpleHash(key) % this.knownKeysShards}`;
4618
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
+ }
4619
4798
  knownKeysKeys() {
4620
4799
  if (this.knownKeysShards === 1) {
4621
4800
  return [`${this.prefix}:keys`];
4622
4801
  }
4623
4802
  return Array.from({ length: this.knownKeysShards }, (_, index) => `${this.prefix}:keys:${index}`);
4624
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
+ }
4625
4819
  keyTagsKey(key) {
4626
4820
  return `${this.prefix}:key:${encodeURIComponent(key)}`;
4627
4821
  }
@@ -4631,7 +4825,7 @@ var RedisTagIndex = class {
4631
4825
  };
4632
4826
  function normalizeKnownKeysShards(value) {
4633
4827
  if (value === void 0) {
4634
- return 1;
4828
+ return DEFAULT_KNOWN_KEYS_SHARDS;
4635
4829
  }
4636
4830
  if (!Number.isInteger(value) || value <= 0) {
4637
4831
  throw new Error("RedisTagIndex.knownKeysShards must be a positive integer.");
@@ -4727,6 +4921,41 @@ function createFastifyLayercachePlugin(cache, options = {}) {
4727
4921
  };
4728
4922
  }
4729
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
+
4730
4959
  // src/integrations/express.ts
4731
4960
  function createExpressCacheMiddleware(cache, options = {}) {
4732
4961
  const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
@@ -4742,7 +4971,7 @@ function createExpressCacheMiddleware(cache, options = {}) {
4742
4971
  return;
4743
4972
  }
4744
4973
  const rawUrl = req.originalUrl ?? req.url ?? "/";
4745
- const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeUrl(rawUrl)}`;
4974
+ const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeHttpCacheUrl(rawUrl)}`;
4746
4975
  const cached = await cache.get(key, void 0, options);
4747
4976
  if (cached !== null) {
4748
4977
  res.setHeader?.("content-type", "application/json; charset=utf-8");
@@ -4758,12 +4987,14 @@ function createExpressCacheMiddleware(cache, options = {}) {
4758
4987
  if (originalJson) {
4759
4988
  res.json = (body) => {
4760
4989
  res.setHeader?.("x-cache", "MISS");
4761
- cache.set(key, body, options).catch((err) => {
4762
- cache.emit("error", {
4763
- operation: "set",
4764
- 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
+ });
4765
4996
  });
4766
- });
4997
+ }
4767
4998
  return originalJson(body);
4768
4999
  };
4769
5000
  }
@@ -4773,14 +5004,8 @@ function createExpressCacheMiddleware(cache, options = {}) {
4773
5004
  }
4774
5005
  };
4775
5006
  }
4776
- function normalizeUrl(url) {
4777
- try {
4778
- const parsed = new URL(url, "http://localhost");
4779
- parsed.searchParams.sort();
4780
- return parsed.pathname + parsed.search;
4781
- } catch {
4782
- return url;
4783
- }
5007
+ function isSuccessfulStatus(statusCode) {
5008
+ return statusCode === void 0 || statusCode >= 200 && statusCode < 300;
4784
5009
  }
4785
5010
 
4786
5011
  // src/integrations/graphql.ts
@@ -4811,35 +5036,39 @@ function createHonoCacheMiddleware(cache, options = {}) {
4811
5036
  return;
4812
5037
  }
4813
5038
  const rawPath = context.req.path ?? context.req.url ?? "/";
4814
- const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${normalizeUrl2(rawPath)}`;
5039
+ const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${normalizeHttpCacheUrl(rawPath)}`;
4815
5040
  const cached = await cache.get(key, void 0, options);
4816
5041
  if (cached !== null) {
4817
5042
  context.header?.("x-cache", "HIT");
4818
5043
  context.header?.("content-type", "application/json; charset=utf-8");
4819
5044
  return context.json(cached);
4820
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
+ }
4821
5054
  const originalJson = context.json.bind(context);
4822
5055
  context.json = (body, status) => {
4823
5056
  context.header?.("x-cache", "MISS");
4824
- cache.set(key, body, options).catch((err) => {
4825
- cache.emit("error", {
4826
- operation: "set",
4827
- 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
+ });
4828
5063
  });
4829
- });
5064
+ }
4830
5065
  return originalJson(body, status);
4831
5066
  };
4832
5067
  await next();
4833
5068
  };
4834
5069
  }
4835
- function normalizeUrl2(url) {
4836
- try {
4837
- const parsed = new URL(url, "http://localhost");
4838
- parsed.searchParams.sort();
4839
- return parsed.pathname + parsed.search;
4840
- } catch {
4841
- return url;
4842
- }
5070
+ function isSuccessfulStatus2(statusCode) {
5071
+ return statusCode === void 0 || statusCode >= 200 && statusCode < 300;
4843
5072
  }
4844
5073
 
4845
5074
  // src/integrations/opentelemetry.ts
@@ -5634,12 +5863,12 @@ var RedisLayer = class {
5634
5863
  };
5635
5864
 
5636
5865
  // src/layers/DiskLayer.ts
5637
- var import_node_crypto4 = require("crypto");
5866
+ var import_node_crypto5 = require("crypto");
5638
5867
  var import_node_fs2 = require("fs");
5639
5868
  var import_node_path = require("path");
5640
5869
 
5641
5870
  // src/internal/PayloadProtection.ts
5642
- var import_node_crypto3 = require("crypto");
5871
+ var import_node_crypto4 = require("crypto");
5643
5872
  var MAGIC_ENCRYPTED = Buffer.from("LCP1:");
5644
5873
  var MAGIC_SIGNED = Buffer.from("LCS1:");
5645
5874
  var ALGORITHM = "aes-256-gcm";
@@ -5652,11 +5881,11 @@ var PayloadProtection = class {
5652
5881
  constructor(options) {
5653
5882
  if (options.encryptionKey) {
5654
5883
  const raw = Buffer.isBuffer(options.encryptionKey) ? options.encryptionKey : Buffer.from(options.encryptionKey, "utf8");
5655
- this.encryptionKey = (0, import_node_crypto3.createHash)("sha256").update(raw).digest();
5884
+ this.encryptionKey = (0, import_node_crypto4.createHash)("sha256").update(raw).digest();
5656
5885
  }
5657
5886
  if (options.signingKey && !options.encryptionKey) {
5658
5887
  const raw = Buffer.isBuffer(options.signingKey) ? options.signingKey : Buffer.from(options.signingKey, "utf8");
5659
- this.signingKey = (0, import_node_crypto3.createHash)("sha256").update(raw).digest();
5888
+ this.signingKey = (0, import_node_crypto4.createHash)("sha256").update(raw).digest();
5660
5889
  }
5661
5890
  }
5662
5891
  /** Returns `true` when any protection (encryption or signing) is configured. */
@@ -5703,8 +5932,8 @@ var PayloadProtection = class {
5703
5932
  }
5704
5933
  // ── Encryption (AES-256-GCM) ──────────────────────────────────────────
5705
5934
  encrypt(plaintext, key) {
5706
- const iv = (0, import_node_crypto3.randomBytes)(IV_LENGTH);
5707
- 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 });
5708
5937
  const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
5709
5938
  const authTag = cipher.getAuthTag();
5710
5939
  return Buffer.concat([MAGIC_ENCRYPTED, iv, authTag, encrypted]);
@@ -5715,7 +5944,7 @@ var PayloadProtection = class {
5715
5944
  const authTag = payload.subarray(headerEnd + IV_LENGTH, headerEnd + IV_LENGTH + AUTH_TAG_LENGTH);
5716
5945
  const ciphertext = payload.subarray(headerEnd + IV_LENGTH + AUTH_TAG_LENGTH);
5717
5946
  try {
5718
- const decipher = (0, import_node_crypto3.createDecipheriv)(ALGORITHM, key, iv, {
5947
+ const decipher = (0, import_node_crypto4.createDecipheriv)(ALGORITHM, key, iv, {
5719
5948
  authTagLength: AUTH_TAG_LENGTH
5720
5949
  });
5721
5950
  decipher.setAuthTag(authTag);
@@ -5728,15 +5957,15 @@ var PayloadProtection = class {
5728
5957
  }
5729
5958
  // ── Signing (HMAC-SHA256) ─────────────────────────────────────────────
5730
5959
  sign(payload, key) {
5731
- 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();
5732
5961
  return Buffer.concat([MAGIC_SIGNED, hmac, payload]);
5733
5962
  }
5734
5963
  verify(payload, key) {
5735
5964
  const headerEnd = MAGIC_SIGNED.length;
5736
5965
  const receivedHmac = payload.subarray(headerEnd, headerEnd + HMAC_LENGTH);
5737
5966
  const data = payload.subarray(headerEnd + HMAC_LENGTH);
5738
- const expectedHmac = (0, import_node_crypto3.createHmac)("sha256", key).update(data).digest();
5739
- 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)) {
5740
5969
  throw new PayloadProtectionError(
5741
5970
  "HMAC verification failed. The data may have been tampered with or the signingKey is incorrect."
5742
5971
  );
@@ -5828,7 +6057,7 @@ var DiskLayer = class {
5828
6057
  const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
5829
6058
  const protectedPayload = this.protection.protect(raw);
5830
6059
  const targetPath = this.keyToPath(key);
5831
- 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`;
5832
6061
  try {
5833
6062
  await import_node_fs2.promises.writeFile(tempPath, protectedPayload);
5834
6063
  await import_node_fs2.promises.rename(tempPath, targetPath);
@@ -5961,7 +6190,7 @@ var DiskLayer = class {
5961
6190
  async dispose() {
5962
6191
  }
5963
6192
  keyToPath(key) {
5964
- 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");
5965
6194
  return (0, import_node_path.join)(this.directory, `${hash}.lc`);
5966
6195
  }
5967
6196
  resolveDirectory(directory) {
@@ -6279,7 +6508,7 @@ var MsgpackSerializer = class {
6279
6508
  };
6280
6509
 
6281
6510
  // src/singleflight/RedisSingleFlightCoordinator.ts
6282
- var import_node_crypto5 = require("crypto");
6511
+ var import_node_crypto6 = require("crypto");
6283
6512
  var RELEASE_SCRIPT = `
6284
6513
  if redis.call("get", KEYS[1]) == ARGV[1] then
6285
6514
  return redis.call("del", KEYS[1])
@@ -6307,7 +6536,7 @@ var RedisSingleFlightCoordinator = class {
6307
6536
  */
6308
6537
  async execute(key, options, worker, waiter) {
6309
6538
  const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
6310
- const token = (0, import_node_crypto5.randomUUID)();
6539
+ const token = (0, import_node_crypto6.randomUUID)();
6311
6540
  const acquired = await this.runCommand(
6312
6541
  `acquire("${key}")`,
6313
6542
  () => this.client.set(lockKey, token, "PX", options.leaseMs, "NX")
@@ -6461,6 +6690,7 @@ function sanitizeLabel(value) {
6461
6690
  MemoryLayer,
6462
6691
  MsgpackSerializer,
6463
6692
  PatternMatcher,
6693
+ RedisGenerationStore,
6464
6694
  RedisInvalidationBus,
6465
6695
  RedisLayer,
6466
6696
  RedisSingleFlightCoordinator,