@trieb.work/nextjs-turbo-redis-cache 1.7.1 → 1.8.0-beta.1

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.js CHANGED
@@ -194,18 +194,22 @@ var SyncedMap = class {
194
194
  };
195
195
  try {
196
196
  await this.subscriberClient.connect().catch(async () => {
197
- await this.subscriberClient.connect();
197
+ console.error("Failed to connect subscriber client. Retrying...");
198
+ await this.subscriberClient.connect().catch((error) => {
199
+ console.error("Failed to connect subscriber client.", error);
200
+ throw error;
201
+ });
198
202
  });
199
203
  if ((process.env.SKIP_KEYSPACE_CONFIG_CHECK || "").toUpperCase() !== "TRUE") {
200
204
  const keyspaceEventConfig = (await this.subscriberClient.configGet("notify-keyspace-events"))?.["notify-keyspace-events"];
201
205
  if (!keyspaceEventConfig.includes("E")) {
202
206
  throw new Error(
203
- "Keyspace event configuration has to include 'E' for Keyevent events, published with __keyevent@<db>__ prefix. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`"
207
+ 'Keyspace event configuration is set to "' + keyspaceEventConfig + "\" but has to include 'E' for Keyevent events, published with __keyevent@<db>__ prefix. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`"
204
208
  );
205
209
  }
206
210
  if (!keyspaceEventConfig.includes("A") && !(keyspaceEventConfig.includes("x") && keyspaceEventConfig.includes("e"))) {
207
211
  throw new Error(
208
- "Keyspace event configuration has to include 'A' or 'x' and 'e' for expired and evicted events. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`"
212
+ 'Keyspace event configuration is set to "' + keyspaceEventConfig + "\" but has to include 'A' or 'x' and 'e' for expired and evicted events. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`"
209
213
  );
210
214
  }
211
215
  }
@@ -286,8 +290,6 @@ var SyncedMap = class {
286
290
  );
287
291
  await Promise.all(operations);
288
292
  }
289
- // /api/revalidated-fetch
290
- // true
291
293
  async delete(keys, withoutSyncMessage = false) {
292
294
  debugVerbose(
293
295
  "SyncedMap.delete() called with keys",
@@ -441,325 +443,455 @@ var REVALIDATED_TAGS_KEY = "__revalidated_tags__";
441
443
  function getTimeoutRedisCommandOptions(timeoutMs) {
442
444
  return (0, import_redis.commandOptions)({ signal: AbortSignal.timeout(timeoutMs) });
443
445
  }
446
+ var killContainerOnErrorCount = 0;
444
447
  var RedisStringsHandler = class {
445
448
  constructor({
446
449
  redisUrl = process.env.REDIS_URL ? process.env.REDIS_URL : process.env.REDISHOST ? `redis://${process.env.REDISHOST}:${process.env.REDISPORT}` : "redis://localhost:6379",
447
450
  database = process.env.VERCEL_ENV === "production" ? 0 : 1,
448
451
  keyPrefix = process.env.VERCEL_URL || "UNDEFINED_URL_",
449
452
  sharedTagsKey = "__sharedTags__",
450
- timeoutMs = 5e3,
453
+ timeoutMs = process.env.REDIS_COMMAND_TIMEOUT_MS ? Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS) ?? 5e3 : 5e3,
451
454
  revalidateTagQuerySize = 250,
452
455
  avgResyncIntervalMs = 60 * 60 * 1e3,
453
456
  redisGetDeduplication = true,
454
457
  inMemoryCachingTime = 1e4,
455
458
  defaultStaleAge = 60 * 60 * 24 * 14,
456
459
  estimateExpireAge = (staleAge) => process.env.VERCEL_ENV === "production" ? staleAge * 2 : staleAge * 1.2,
460
+ killContainerOnErrorThreshold = process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD ? Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD) ?? 0 : 0,
457
461
  socketOptions,
458
462
  clientOptions
459
463
  }) {
460
- this.keyPrefix = keyPrefix;
461
- this.timeoutMs = timeoutMs;
462
- this.redisGetDeduplication = redisGetDeduplication;
463
- this.inMemoryCachingTime = inMemoryCachingTime;
464
- this.defaultStaleAge = defaultStaleAge;
465
- this.estimateExpireAge = estimateExpireAge;
464
+ this.clientReadyCalls = 0;
466
465
  try {
467
- this.client = (0, import_redis.createClient)({
468
- url: redisUrl,
469
- ...database !== 0 ? { database } : {},
470
- ...socketOptions ? { socket: socketOptions } : {},
471
- ...clientOptions || {}
466
+ this.keyPrefix = keyPrefix;
467
+ this.timeoutMs = timeoutMs;
468
+ this.redisGetDeduplication = redisGetDeduplication;
469
+ this.inMemoryCachingTime = inMemoryCachingTime;
470
+ this.defaultStaleAge = defaultStaleAge;
471
+ this.estimateExpireAge = estimateExpireAge;
472
+ this.killContainerOnErrorThreshold = killContainerOnErrorThreshold;
473
+ try {
474
+ this.client = (0, import_redis.createClient)({
475
+ url: redisUrl,
476
+ pingInterval: 5e3,
477
+ // Useful with Redis deployments that do not use TCP Keep-Alive. Restarts the connection if it is idle for too long.
478
+ ...database !== 0 ? { database } : {},
479
+ ...socketOptions ? { socket: { connectTimeout: timeoutMs, ...socketOptions } } : { connectTimeout: timeoutMs },
480
+ ...clientOptions || {}
481
+ });
482
+ this.client.on("error", (error) => {
483
+ console.error(
484
+ "Redis client error",
485
+ error,
486
+ killContainerOnErrorCount++
487
+ );
488
+ setTimeout(
489
+ () => this.client.connect().catch((error2) => {
490
+ console.error(
491
+ "Failed to reconnect Redis client after connection loss:",
492
+ error2
493
+ );
494
+ }),
495
+ 1e3
496
+ );
497
+ if (this.killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= this.killContainerOnErrorThreshold) {
498
+ console.error(
499
+ "Redis client error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)",
500
+ error,
501
+ killContainerOnErrorCount++
502
+ );
503
+ this.client.disconnect();
504
+ this.client.quit();
505
+ setTimeout(() => {
506
+ process.exit(1);
507
+ }, 500);
508
+ }
509
+ });
510
+ this.client.connect().then(() => {
511
+ console.info("Redis client connected.");
512
+ }).catch(() => {
513
+ this.client.connect().catch((error) => {
514
+ console.error("Failed to connect Redis client:", error);
515
+ this.client.disconnect();
516
+ throw error;
517
+ });
518
+ });
519
+ } catch (error) {
520
+ console.error("Failed to initialize Redis client");
521
+ throw error;
522
+ }
523
+ const filterKeys = (key) => key !== REVALIDATED_TAGS_KEY && key !== sharedTagsKey;
524
+ this.sharedTagsMap = new SyncedMap({
525
+ client: this.client,
526
+ keyPrefix,
527
+ redisKey: sharedTagsKey,
528
+ database,
529
+ timeoutMs,
530
+ querySize: revalidateTagQuerySize,
531
+ filterKeys,
532
+ resyncIntervalMs: avgResyncIntervalMs - avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
472
533
  });
473
- this.client.on("error", (error) => {
474
- console.error("Redis client error", error);
534
+ this.revalidatedTagsMap = new SyncedMap({
535
+ client: this.client,
536
+ keyPrefix,
537
+ redisKey: REVALIDATED_TAGS_KEY,
538
+ database,
539
+ timeoutMs,
540
+ querySize: revalidateTagQuerySize,
541
+ filterKeys,
542
+ resyncIntervalMs: avgResyncIntervalMs + avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
475
543
  });
476
- this.client.connect().then(() => {
477
- console.info("Redis client connected.");
478
- }).catch(() => {
479
- this.client.connect().catch((error) => {
480
- console.error("Failed to connect Redis client:", error);
481
- this.client.disconnect();
482
- throw error;
483
- });
544
+ this.inMemoryDeduplicationCache = new SyncedMap({
545
+ client: this.client,
546
+ keyPrefix,
547
+ redisKey: "inMemoryDeduplicationCache",
548
+ database,
549
+ timeoutMs,
550
+ querySize: revalidateTagQuerySize,
551
+ filterKeys,
552
+ customizedSync: {
553
+ withoutRedisHashmap: true,
554
+ withoutSetSync: true
555
+ }
484
556
  });
557
+ const redisGet = this.client.get.bind(this.client);
558
+ this.redisDeduplicationHandler = new DeduplicatedRequestHandler(
559
+ redisGet,
560
+ inMemoryCachingTime,
561
+ this.inMemoryDeduplicationCache
562
+ );
563
+ this.redisGet = redisGet;
564
+ this.deduplicatedRedisGet = this.redisDeduplicationHandler.deduplicatedFunction;
485
565
  } catch (error) {
486
- console.error("Failed to initialize Redis client");
566
+ console.error(
567
+ "RedisStringsHandler constructor error",
568
+ error,
569
+ killContainerOnErrorCount++
570
+ );
571
+ if (killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= killContainerOnErrorThreshold) {
572
+ console.error(
573
+ "RedisStringsHandler constructor error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)",
574
+ error,
575
+ killContainerOnErrorCount++
576
+ );
577
+ process.exit(1);
578
+ }
487
579
  throw error;
488
580
  }
489
- const filterKeys = (key) => key !== REVALIDATED_TAGS_KEY && key !== sharedTagsKey;
490
- this.sharedTagsMap = new SyncedMap({
491
- client: this.client,
492
- keyPrefix,
493
- redisKey: sharedTagsKey,
494
- database,
495
- timeoutMs,
496
- querySize: revalidateTagQuerySize,
497
- filterKeys,
498
- resyncIntervalMs: avgResyncIntervalMs - avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
499
- });
500
- this.revalidatedTagsMap = new SyncedMap({
501
- client: this.client,
502
- keyPrefix,
503
- redisKey: REVALIDATED_TAGS_KEY,
504
- database,
505
- timeoutMs,
506
- querySize: revalidateTagQuerySize,
507
- filterKeys,
508
- resyncIntervalMs: avgResyncIntervalMs + avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
509
- });
510
- this.inMemoryDeduplicationCache = new SyncedMap({
511
- client: this.client,
512
- keyPrefix,
513
- redisKey: "inMemoryDeduplicationCache",
514
- database,
515
- timeoutMs,
516
- querySize: revalidateTagQuerySize,
517
- filterKeys,
518
- customizedSync: {
519
- withoutRedisHashmap: true,
520
- withoutSetSync: true
521
- }
522
- });
523
- const redisGet = this.client.get.bind(this.client);
524
- this.redisDeduplicationHandler = new DeduplicatedRequestHandler(
525
- redisGet,
526
- inMemoryCachingTime,
527
- this.inMemoryDeduplicationCache
528
- );
529
- this.redisGet = redisGet;
530
- this.deduplicatedRedisGet = this.redisDeduplicationHandler.deduplicatedFunction;
531
581
  }
532
582
  resetRequestCache() {
533
583
  }
534
584
  async assertClientIsReady() {
535
- await Promise.all([
536
- this.sharedTagsMap.waitUntilReady(),
537
- this.revalidatedTagsMap.waitUntilReady()
585
+ if (this.clientReadyCalls > 10) {
586
+ throw new Error(
587
+ "assertClientIsReady called more than 10 times without being ready."
588
+ );
589
+ }
590
+ await Promise.race([
591
+ Promise.all([
592
+ this.sharedTagsMap.waitUntilReady(),
593
+ this.revalidatedTagsMap.waitUntilReady()
594
+ ]),
595
+ new Promise(
596
+ (_, reject) => setTimeout(() => {
597
+ reject(
598
+ new Error(
599
+ "assertClientIsReady: Timeout waiting for Redis maps to be ready"
600
+ )
601
+ );
602
+ }, this.timeoutMs * 5)
603
+ )
538
604
  ]);
605
+ this.clientReadyCalls = 0;
539
606
  if (!this.client.isReady) {
540
- throw new Error("Redis client is not ready yet or connection is lost.");
607
+ throw new Error(
608
+ "assertClientIsReady: Redis client is not ready yet or connection is lost."
609
+ );
541
610
  }
542
611
  }
543
612
  async get(key, ctx) {
544
- if (ctx.kind !== "APP_ROUTE" && ctx.kind !== "APP_PAGE" && ctx.kind !== "FETCH") {
545
- console.warn(
546
- "RedisStringsHandler.get() called with",
547
- key,
548
- ctx,
549
- " this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
550
- ctx?.kind
613
+ try {
614
+ if (ctx.kind !== "APP_ROUTE" && ctx.kind !== "APP_PAGE" && ctx.kind !== "FETCH") {
615
+ console.warn(
616
+ "RedisStringsHandler.get() called with",
617
+ key,
618
+ ctx,
619
+ " this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
620
+ ctx?.kind
621
+ );
622
+ }
623
+ debug("green", "RedisStringsHandler.get() called with", key, ctx);
624
+ await this.assertClientIsReady();
625
+ const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(key) : this.redisGet;
626
+ const serializedCacheEntry = await clientGet(
627
+ getTimeoutRedisCommandOptions(this.timeoutMs),
628
+ this.keyPrefix + key
551
629
  );
552
- }
553
- debug("green", "RedisStringsHandler.get() called with", key, ctx);
554
- await this.assertClientIsReady();
555
- const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(key) : this.redisGet;
556
- const serializedCacheEntry = await clientGet(
557
- getTimeoutRedisCommandOptions(this.timeoutMs),
558
- this.keyPrefix + key
559
- );
560
- debug(
561
- "green",
562
- "RedisStringsHandler.get() finished with result (serializedCacheEntry)",
563
- serializedCacheEntry?.substring(0, 200)
564
- );
565
- if (!serializedCacheEntry) {
566
- return null;
567
- }
568
- const cacheEntry = JSON.parse(
569
- serializedCacheEntry,
570
- bufferReviver
571
- );
572
- debug(
573
- "green",
574
- "RedisStringsHandler.get() finished with result (cacheEntry)",
575
- JSON.stringify(cacheEntry).substring(0, 200)
576
- );
577
- if (!cacheEntry) {
578
- return null;
579
- }
580
- if (!cacheEntry?.tags) {
581
- console.warn(
582
- "RedisStringsHandler.get() called with",
583
- key,
584
- ctx,
585
- "cacheEntry is mall formed (missing tags)"
630
+ debug(
631
+ "green",
632
+ "RedisStringsHandler.get() finished with result (serializedCacheEntry)",
633
+ serializedCacheEntry?.substring(0, 200)
586
634
  );
587
- }
588
- if (!cacheEntry?.value) {
589
- console.warn(
590
- "RedisStringsHandler.get() called with",
591
- key,
592
- ctx,
593
- "cacheEntry is mall formed (missing value)"
635
+ if (!serializedCacheEntry) {
636
+ return null;
637
+ }
638
+ const cacheEntry = JSON.parse(
639
+ serializedCacheEntry,
640
+ bufferReviver
594
641
  );
595
- }
596
- if (!cacheEntry?.lastModified) {
597
- console.warn(
598
- "RedisStringsHandler.get() called with",
599
- key,
600
- ctx,
601
- "cacheEntry is mall formed (missing lastModified)"
642
+ debug(
643
+ "green",
644
+ "RedisStringsHandler.get() finished with result (cacheEntry)",
645
+ JSON.stringify(cacheEntry).substring(0, 200)
602
646
  );
603
- }
604
- if (ctx.kind === "FETCH") {
605
- const combinedTags = /* @__PURE__ */ new Set([
606
- ...ctx?.softTags || [],
607
- ...ctx?.tags || []
608
- ]);
609
- if (combinedTags.size === 0) {
610
- return cacheEntry;
647
+ if (!cacheEntry) {
648
+ return null;
611
649
  }
612
- for (const tag of combinedTags) {
613
- const revalidationTime = this.revalidatedTagsMap.get(tag);
614
- if (revalidationTime && revalidationTime > cacheEntry.lastModified) {
615
- const redisKey = this.keyPrefix + key;
616
- this.client.unlink(getTimeoutRedisCommandOptions(this.timeoutMs), redisKey).catch((err) => {
617
- console.error(
618
- "Error occurred while unlinking stale data. Error was:",
619
- err
650
+ if (!cacheEntry?.tags) {
651
+ console.warn(
652
+ "RedisStringsHandler.get() called with",
653
+ key,
654
+ ctx,
655
+ "cacheEntry is mall formed (missing tags)"
656
+ );
657
+ }
658
+ if (!cacheEntry?.value) {
659
+ console.warn(
660
+ "RedisStringsHandler.get() called with",
661
+ key,
662
+ ctx,
663
+ "cacheEntry is mall formed (missing value)"
664
+ );
665
+ }
666
+ if (!cacheEntry?.lastModified) {
667
+ console.warn(
668
+ "RedisStringsHandler.get() called with",
669
+ key,
670
+ ctx,
671
+ "cacheEntry is mall formed (missing lastModified)"
672
+ );
673
+ }
674
+ if (ctx.kind === "FETCH") {
675
+ const combinedTags = /* @__PURE__ */ new Set([
676
+ ...ctx?.softTags || [],
677
+ ...ctx?.tags || []
678
+ ]);
679
+ if (combinedTags.size === 0) {
680
+ return cacheEntry;
681
+ }
682
+ for (const tag of combinedTags) {
683
+ const revalidationTime = this.revalidatedTagsMap.get(tag);
684
+ if (revalidationTime && revalidationTime > cacheEntry.lastModified) {
685
+ const redisKey = this.keyPrefix + key;
686
+ this.client.unlink(getTimeoutRedisCommandOptions(this.timeoutMs), redisKey).catch((err) => {
687
+ console.error(
688
+ "Error occurred while unlinking stale data. Error was:",
689
+ err
690
+ );
691
+ }).finally(async () => {
692
+ await this.sharedTagsMap.delete(key);
693
+ await this.revalidatedTagsMap.delete(tag);
694
+ });
695
+ debug(
696
+ "green",
697
+ 'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.',
698
+ tag,
699
+ redisKey,
700
+ revalidationTime,
701
+ cacheEntry
620
702
  );
621
- }).finally(async () => {
622
- await this.sharedTagsMap.delete(key);
623
- await this.revalidatedTagsMap.delete(tag);
624
- });
625
- debug(
626
- "green",
627
- 'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.',
628
- tag,
629
- redisKey,
630
- revalidationTime,
631
- cacheEntry
632
- );
633
- return null;
703
+ return null;
704
+ }
634
705
  }
635
706
  }
707
+ return cacheEntry;
708
+ } catch (error) {
709
+ console.error(
710
+ "RedisStringsHandler.get() Error occurred while getting cache entry. Returning null so site can continue to serve content while cache is disabled. The original error was:",
711
+ error,
712
+ killContainerOnErrorCount++
713
+ );
714
+ if (this.killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= this.killContainerOnErrorThreshold) {
715
+ console.error(
716
+ "RedisStringsHandler get() error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)",
717
+ error,
718
+ killContainerOnErrorCount
719
+ );
720
+ this.client.disconnect();
721
+ this.client.quit();
722
+ setTimeout(() => {
723
+ process.exit(1);
724
+ }, 500);
725
+ }
726
+ return null;
636
727
  }
637
- return cacheEntry;
638
728
  }
639
729
  async set(key, data, ctx) {
640
- if (data.kind !== "APP_ROUTE" && data.kind !== "APP_PAGE" && data.kind !== "FETCH") {
641
- console.warn(
642
- "RedisStringsHandler.set() called with",
730
+ try {
731
+ if (data.kind !== "APP_ROUTE" && data.kind !== "APP_PAGE" && data.kind !== "FETCH") {
732
+ console.warn(
733
+ "RedisStringsHandler.set() called with",
734
+ key,
735
+ ctx,
736
+ data,
737
+ " this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
738
+ data?.kind
739
+ );
740
+ }
741
+ await this.assertClientIsReady();
742
+ if (data.kind === "APP_PAGE" || data.kind === "APP_ROUTE") {
743
+ const tags = data.headers["x-next-cache-tags"]?.split(",");
744
+ ctx.tags = [...ctx.tags || [], ...tags || []];
745
+ }
746
+ const cacheEntry = {
747
+ lastModified: Date.now(),
748
+ tags: ctx?.tags || [],
749
+ value: data
750
+ };
751
+ const serializedCacheEntry = JSON.stringify(cacheEntry, bufferReplacer);
752
+ if (this.redisGetDeduplication) {
753
+ this.redisDeduplicationHandler.seedRequestReturn(
754
+ key,
755
+ serializedCacheEntry
756
+ );
757
+ }
758
+ const revalidate = (
759
+ // For fetch requests in newest versions, the revalidate context property is never used, and instead the revalidate property of the passed-in data is used
760
+ data.kind === "FETCH" && data.revalidate || ctx.revalidate || ctx.cacheControl?.revalidate || data?.revalidate
761
+ );
762
+ const expireAt = revalidate && Number.isSafeInteger(revalidate) && revalidate > 0 ? this.estimateExpireAge(revalidate) : this.estimateExpireAge(this.defaultStaleAge);
763
+ const options = getTimeoutRedisCommandOptions(this.timeoutMs);
764
+ const setOperation = this.client.set(
765
+ options,
766
+ this.keyPrefix + key,
767
+ serializedCacheEntry,
768
+ {
769
+ EX: expireAt
770
+ }
771
+ );
772
+ debug(
773
+ "blue",
774
+ "RedisStringsHandler.set() will set the following serializedCacheEntry",
775
+ this.keyPrefix,
643
776
  key,
644
- ctx,
645
777
  data,
646
- " this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
647
- data?.kind
778
+ ctx,
779
+ serializedCacheEntry?.substring(0, 200),
780
+ expireAt
648
781
  );
649
- }
650
- await this.assertClientIsReady();
651
- if (data.kind === "APP_PAGE" || data.kind === "APP_ROUTE") {
652
- const tags = data.headers["x-next-cache-tags"]?.split(",");
653
- ctx.tags = [...ctx.tags || [], ...tags || []];
654
- }
655
- const cacheEntry = {
656
- lastModified: Date.now(),
657
- tags: ctx?.tags || [],
658
- value: data
659
- };
660
- const serializedCacheEntry = JSON.stringify(cacheEntry, bufferReplacer);
661
- if (this.redisGetDeduplication) {
662
- this.redisDeduplicationHandler.seedRequestReturn(
782
+ let setTagsOperation;
783
+ if (ctx.tags && ctx.tags.length > 0) {
784
+ const currentTags = this.sharedTagsMap.get(key);
785
+ const currentIsSameAsNew = currentTags?.length === ctx.tags.length && currentTags.every((v) => ctx.tags.includes(v)) && ctx.tags.every((v) => currentTags.includes(v));
786
+ if (!currentIsSameAsNew) {
787
+ setTagsOperation = this.sharedTagsMap.set(
788
+ key,
789
+ structuredClone(ctx.tags)
790
+ );
791
+ }
792
+ }
793
+ debug(
794
+ "blue",
795
+ "RedisStringsHandler.set() will set the following sharedTagsMap",
663
796
  key,
664
- serializedCacheEntry
797
+ ctx.tags
665
798
  );
666
- }
667
- const revalidate = (
668
- // For fetch requests in newest versions, the revalidate context property is never used, and instead the revalidate property of the passed-in data is used
669
- data.kind === "FETCH" && data.revalidate || ctx.revalidate || ctx.cacheControl?.revalidate
670
- );
671
- const expireAt = revalidate && Number.isSafeInteger(revalidate) && revalidate > 0 ? this.estimateExpireAge(revalidate) : this.estimateExpireAge(this.defaultStaleAge);
672
- const options = getTimeoutRedisCommandOptions(this.timeoutMs);
673
- const setOperation = this.client.set(
674
- options,
675
- this.keyPrefix + key,
676
- serializedCacheEntry,
677
- {
678
- EX: expireAt
679
- }
680
- );
681
- debug(
682
- "blue",
683
- "RedisStringsHandler.set() will set the following serializedCacheEntry",
684
- this.keyPrefix,
685
- key,
686
- data,
687
- ctx,
688
- serializedCacheEntry?.substring(0, 200),
689
- expireAt
690
- );
691
- let setTagsOperation;
692
- if (ctx.tags && ctx.tags.length > 0) {
693
- const currentTags = this.sharedTagsMap.get(key);
694
- const currentIsSameAsNew = currentTags?.length === ctx.tags.length && currentTags.every((v) => ctx.tags.includes(v)) && ctx.tags.every((v) => currentTags.includes(v));
695
- if (!currentIsSameAsNew) {
696
- setTagsOperation = this.sharedTagsMap.set(
697
- key,
698
- structuredClone(ctx.tags)
799
+ await Promise.all([setOperation, setTagsOperation]);
800
+ } catch (error) {
801
+ console.error(
802
+ "RedisStringsHandler.set() Error occurred while setting cache entry. The original error was:",
803
+ error,
804
+ killContainerOnErrorCount++
805
+ );
806
+ if (this.killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= this.killContainerOnErrorThreshold) {
807
+ console.error(
808
+ "RedisStringsHandler set() error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)",
809
+ error,
810
+ killContainerOnErrorCount
699
811
  );
812
+ this.client.disconnect();
813
+ this.client.quit();
814
+ setTimeout(() => {
815
+ process.exit(1);
816
+ }, 500);
700
817
  }
818
+ throw error;
701
819
  }
702
- debug(
703
- "blue",
704
- "RedisStringsHandler.set() will set the following sharedTagsMap",
705
- key,
706
- ctx.tags
707
- );
708
- await Promise.all([setOperation, setTagsOperation]);
709
820
  }
710
821
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
711
822
  async revalidateTag(tagOrTags, ...rest) {
712
- debug(
713
- "red",
714
- "RedisStringsHandler.revalidateTag() called with",
715
- tagOrTags,
716
- rest
717
- );
718
- const tags = new Set([tagOrTags || []].flat());
719
- await this.assertClientIsReady();
720
- const keysToDelete = /* @__PURE__ */ new Set();
721
- for (const tag of tags) {
722
- if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) {
723
- const now = Date.now();
724
- debug(
725
- "red",
726
- "RedisStringsHandler.revalidateTag() set revalidation time for tag",
727
- tag,
728
- "to",
729
- now
730
- );
731
- await this.revalidatedTagsMap.set(tag, now);
823
+ try {
824
+ debug(
825
+ "red",
826
+ "RedisStringsHandler.revalidateTag() called with",
827
+ tagOrTags,
828
+ rest
829
+ );
830
+ const tags = new Set([tagOrTags || []].flat());
831
+ await this.assertClientIsReady();
832
+ const keysToDelete = /* @__PURE__ */ new Set();
833
+ for (const tag of tags) {
834
+ if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) {
835
+ const now = Date.now();
836
+ debug(
837
+ "red",
838
+ "RedisStringsHandler.revalidateTag() set revalidation time for tag",
839
+ tag,
840
+ "to",
841
+ now
842
+ );
843
+ await this.revalidatedTagsMap.set(tag, now);
844
+ }
732
845
  }
733
- }
734
- for (const [key, sharedTags] of this.sharedTagsMap.entries()) {
735
- if (sharedTags.some((tag) => tags.has(tag))) {
736
- keysToDelete.add(key);
846
+ for (const [key, sharedTags] of this.sharedTagsMap.entries()) {
847
+ if (sharedTags.some((tag) => tags.has(tag))) {
848
+ keysToDelete.add(key);
849
+ }
737
850
  }
738
- }
739
- debug(
740
- "red",
741
- "RedisStringsHandler.revalidateTag() found",
742
- keysToDelete,
743
- "keys to delete"
744
- );
745
- if (keysToDelete.size === 0) {
746
- return;
747
- }
748
- const redisKeys = Array.from(keysToDelete);
749
- const fullRedisKeys = redisKeys.map((key) => this.keyPrefix + key);
750
- const options = getTimeoutRedisCommandOptions(this.timeoutMs);
751
- const deleteKeysOperation = this.client.unlink(options, fullRedisKeys);
752
- if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
753
- for (const key of keysToDelete) {
754
- this.inMemoryDeduplicationCache.delete(key);
851
+ debug(
852
+ "red",
853
+ "RedisStringsHandler.revalidateTag() found",
854
+ keysToDelete,
855
+ "keys to delete"
856
+ );
857
+ if (keysToDelete.size === 0) {
858
+ return;
859
+ }
860
+ const redisKeys = Array.from(keysToDelete);
861
+ const fullRedisKeys = redisKeys.map((key) => this.keyPrefix + key);
862
+ const options = getTimeoutRedisCommandOptions(this.timeoutMs);
863
+ const deleteKeysOperation = this.client.unlink(options, fullRedisKeys);
864
+ if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
865
+ for (const key of keysToDelete) {
866
+ this.inMemoryDeduplicationCache.delete(key);
867
+ }
755
868
  }
869
+ const deleteTagsOperation = this.sharedTagsMap.delete(redisKeys);
870
+ await Promise.all([deleteKeysOperation, deleteTagsOperation]);
871
+ debug(
872
+ "red",
873
+ "RedisStringsHandler.revalidateTag() finished delete operations"
874
+ );
875
+ } catch (error) {
876
+ console.error(
877
+ "RedisStringsHandler.revalidateTag() Error occurred while revalidating tags. The original error was:",
878
+ error,
879
+ killContainerOnErrorCount++
880
+ );
881
+ if (this.killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= this.killContainerOnErrorThreshold) {
882
+ console.error(
883
+ "RedisStringsHandler revalidateTag() error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)",
884
+ error,
885
+ killContainerOnErrorCount
886
+ );
887
+ this.client.disconnect();
888
+ this.client.quit();
889
+ setTimeout(() => {
890
+ process.exit(1);
891
+ }, 500);
892
+ }
893
+ throw error;
756
894
  }
757
- const deleteTagsOperation = this.sharedTagsMap.delete(redisKeys);
758
- await Promise.all([deleteKeysOperation, deleteTagsOperation]);
759
- debug(
760
- "red",
761
- "RedisStringsHandler.revalidateTag() finished delete operations"
762
- );
763
895
  }
764
896
  };
765
897