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