@trieb.work/nextjs-turbo-redis-cache 1.16.2 → 1.18.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.
Files changed (125) hide show
  1. package/.github/workflows/ci.yml +11 -2
  2. package/.github/workflows/release.yml +8 -157
  3. package/ARCHITECTURE.md +86 -47
  4. package/README.md +91 -25
  5. package/dist/index.d.mts +40 -1
  6. package/dist/index.d.ts +40 -1
  7. package/dist/index.js +264 -96
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +261 -95
  10. package/dist/index.mjs.map +1 -1
  11. package/package.json +5 -7
  12. package/release.config.cjs +6 -25
  13. package/src/CacheComponentsHandler.ts +141 -111
  14. package/src/RedisStringsHandler.ts +37 -25
  15. package/src/SyncedMap.ts +16 -2
  16. package/src/index.ts +7 -0
  17. package/src/utils/cacheTtl.ts +71 -0
  18. package/src/utils/compareAndUnlink.ts +29 -0
  19. package/src/utils/redisConnection.ts +17 -0
  20. package/src/utils/tagRevalidation.ts +162 -0
  21. package/test/README.md +13 -13
  22. package/test/nextjs-test-projects/next-app-15-4-11/pnpm-lock.yaml +1 -1
  23. package/test/nextjs-test-projects/next-app-16-0-11/pnpm-lock.yaml +1 -1
  24. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/next.config.ts +2 -0
  25. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/pnpm-lock.yaml +1 -1
  26. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
  27. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/expire-matrix/route.ts +23 -0
  28. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/page.tsx +22 -0
  29. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
  30. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
  31. package/test/nextjs-test-projects/next-app-16-2-6/pnpm-lock.yaml +1 -1
  32. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/next.config.ts +2 -0
  33. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/pnpm-lock.yaml +1 -1
  34. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
  35. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/expire-matrix/route.ts +23 -0
  36. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/page.tsx +22 -0
  37. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
  38. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
  39. package/test/nextjs-test-projects/next-app-16-3-0/README.md +36 -0
  40. package/test/nextjs-test-projects/next-app-16-3-0/eslint.config.mjs +18 -0
  41. package/test/nextjs-test-projects/next-app-16-3-0/next.config.ts +7 -0
  42. package/test/nextjs-test-projects/next-app-16-3-0/package.json +28 -0
  43. package/test/nextjs-test-projects/next-app-16-3-0/pnpm-lock.yaml +4284 -0
  44. package/test/nextjs-test-projects/next-app-16-3-0/postcss.config.mjs +7 -0
  45. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/cached-static-fetch/route.ts +18 -0
  46. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +27 -0
  47. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidatePath/route.ts +15 -0
  48. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidateTag/route.ts +20 -0
  49. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidated-fetch/route.ts +17 -0
  50. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/uncached-fetch/route.ts +15 -0
  51. package/test/nextjs-test-projects/next-app-16-3-0/src/app/favicon.ico +0 -0
  52. package/test/nextjs-test-projects/next-app-16-3-0/src/app/globals.css +26 -0
  53. package/test/nextjs-test-projects/next-app-16-3-0/src/app/layout.tsx +59 -0
  54. package/test/nextjs-test-projects/next-app-16-3-0/src/app/page.tsx +755 -0
  55. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +19 -0
  56. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +34 -0
  57. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  58. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/no-fetch/default-page/page.tsx +55 -0
  59. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +19 -0
  60. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +35 -0
  61. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  62. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +19 -0
  63. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +32 -0
  64. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  65. package/test/nextjs-test-projects/next-app-16-3-0/src/app/revalidation-interface.tsx +267 -0
  66. package/test/nextjs-test-projects/next-app-16-3-0/src/app/update-tag-test/page.tsx +25 -0
  67. package/test/nextjs-test-projects/next-app-16-3-0/tsconfig.json +34 -0
  68. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/README.md +36 -0
  69. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/cache-handler.js +3 -0
  70. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/eslint.config.mjs +18 -0
  71. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/next.config.ts +15 -0
  72. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/package.json +28 -0
  73. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/pnpm-lock.yaml +4284 -0
  74. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/postcss.config.mjs +7 -0
  75. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/file.svg +1 -0
  76. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/globe.svg +1 -0
  77. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/next.svg +1 -0
  78. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/vercel.svg +1 -0
  79. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/window.svg +1 -0
  80. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-static-fetch/route.ts +19 -0
  81. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-with-cachelife/route.ts +24 -0
  82. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-with-tag/route.ts +21 -0
  83. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
  84. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/expire-matrix/route.ts +23 -0
  85. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/revalidate-tag/route.ts +19 -0
  86. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/revalidated-fetch/route.ts +19 -0
  87. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/cachelife-short/page.tsx +110 -0
  88. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/page.tsx +112 -0
  89. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
  90. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +127 -0
  91. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +130 -0
  92. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +127 -0
  93. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +110 -0
  94. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
  95. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/favicon.ico +0 -0
  96. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/globals.css +26 -0
  97. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/layout.tsx +57 -0
  98. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/page.tsx +755 -0
  99. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/revalidation-interface.tsx +267 -0
  100. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/update-tag-test/page.tsx +22 -0
  101. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/tsconfig.json +34 -0
  102. package/test/nextjs-test-projects/next-pages-16-2-6/pnpm-lock.yaml +4 -4
  103. package/test/nextjs-test-projects/next-pages-16-3-0/README.md +16 -0
  104. package/test/nextjs-test-projects/next-pages-16-3-0/eslint.config.mjs +18 -0
  105. package/test/nextjs-test-projects/next-pages-16-3-0/next.config.ts +7 -0
  106. package/test/nextjs-test-projects/next-pages-16-3-0/package.json +26 -0
  107. package/test/nextjs-test-projects/next-pages-16-3-0/pnpm-lock.yaml +3939 -0
  108. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/api/revalidate.ts +24 -0
  109. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/index.tsx +11 -0
  110. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/isr/[slug].tsx +49 -0
  111. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/static-forever.tsx +20 -0
  112. package/test/nextjs-test-projects/next-pages-16-3-0/tsconfig.json +34 -0
  113. package/test/playwright/cache-lab.spec.ts +75 -0
  114. package/test/vitest/integration/cache-components/cache-components.integration.test.ts +194 -36
  115. package/test/vitest/integration/nextjs-cache-handler.integration.test.ts +67 -34
  116. package/test/vitest/integration/pages-router.integration.test.ts +16 -6
  117. package/test/vitest/unit/SyncedMap-orphan-cleanup.test.ts +111 -0
  118. package/test/vitest/unit/cache-components-update-tags.test.ts +350 -0
  119. package/test/vitest/unit/cache-ttl.test.ts +67 -0
  120. package/test/vitest/unit/compare-and-unlink.test.ts +22 -0
  121. package/test/vitest/unit/index.test.ts +17 -0
  122. package/test/vitest/unit/pages-router-kinds.test.ts +11 -0
  123. package/test/vitest/unit/redis-connection.test.ts +39 -0
  124. package/test/vitest/unit/tag-revalidation.test.ts +178 -0
  125. package/CHANGELOG.md +0 -404
package/dist/index.js CHANGED
@@ -36,7 +36,9 @@ __export(index_exports, {
36
36
  default: () => index_default,
37
37
  getRedisCacheComponentsHandler: () => getRedisCacheComponentsHandler,
38
38
  jsonCacheValueSerializer: () => jsonCacheValueSerializer,
39
- redisCacheHandler: () => redisCacheHandler
39
+ redisCacheHandler: () => redisCacheHandler,
40
+ resolveCacheEntryTtlSeconds: () => resolveCacheEntryTtlSeconds,
41
+ shouldDeferRedisConnection: () => shouldDeferRedisConnection
40
42
  });
41
43
  module.exports = __toCommonJS(index_exports);
42
44
 
@@ -64,6 +66,12 @@ function debugVerbose(color, ...args) {
64
66
  }
65
67
  }
66
68
 
69
+ // src/utils/redisConnection.ts
70
+ var PHASE_PRODUCTION_BUILD = "phase-production-build";
71
+ function shouldDeferRedisConnection() {
72
+ return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD;
73
+ }
74
+
67
75
  // src/SyncedMap.ts
68
76
  var SYNC_CHANNEL_SUFFIX = ":sync-channel:";
69
77
  var SyncedMap = class {
@@ -89,6 +97,10 @@ var SyncedMap = class {
89
97
  });
90
98
  }
91
99
  async setup() {
100
+ if (shouldDeferRedisConnection()) {
101
+ this.setupLockResolve();
102
+ return;
103
+ }
92
104
  let setupPromises = [];
93
105
  if (!this.customizedSync?.withoutRedisHashmap) {
94
106
  setupPromises.push(this.initialSync());
@@ -119,7 +131,9 @@ var SyncedMap = class {
119
131
  }
120
132
  cursor = remoteItems.cursor;
121
133
  } while (cursor !== 0);
122
- await this.cleanupKeysNotInRedis();
134
+ if (!this.customizedSync?.withoutOrphanCleanup) {
135
+ await this.cleanupKeysNotInRedis();
136
+ }
123
137
  } catch (error) {
124
138
  console.error("Error during initial sync:", error);
125
139
  throw error;
@@ -531,6 +545,37 @@ var jsonCacheValueSerializer = {
531
545
  }
532
546
  };
533
547
 
548
+ // src/utils/cacheTtl.ts
549
+ function resolveCacheEntryTtlSeconds(ctx, data, options) {
550
+ const expire = ctx.cacheControl?.expire;
551
+ if (Number.isFinite(expire) && expire > 0) {
552
+ return Math.max(1, Math.ceil(expire));
553
+ }
554
+ const revalidate = resolveRevalidateValue(ctx, data);
555
+ if (revalidate === false) {
556
+ return options.estimateExpireAge(options.defaultStaleAge);
557
+ }
558
+ if (revalidate && Number.isSafeInteger(revalidate) && revalidate > 0) {
559
+ return options.estimateExpireAge(revalidate);
560
+ }
561
+ return void 0;
562
+ }
563
+ function resolveRevalidateValue(ctx, data) {
564
+ if (data?.kind === "FETCH" && data.revalidate !== void 0) {
565
+ return data.revalidate;
566
+ }
567
+ if (ctx.revalidate !== void 0) {
568
+ return ctx.revalidate;
569
+ }
570
+ if (ctx.cacheControl?.revalidate !== void 0) {
571
+ return ctx.cacheControl.revalidate;
572
+ }
573
+ if (data?.revalidate !== void 0) {
574
+ return data.revalidate;
575
+ }
576
+ return void 0;
577
+ }
578
+
534
579
  // src/RedisStringsHandler.ts
535
580
  function redisErrorHandler(debugInfo, redisCommandResult) {
536
581
  const beforeTimestamp = performance.now();
@@ -608,6 +653,7 @@ var RedisStringsHandler = class {
608
653
  this.killContainerOnErrorThreshold = killContainerOnErrorThreshold;
609
654
  this.getTimeoutMs = getTimeoutMs;
610
655
  this.valueSerializer = valueSerializer;
656
+ this.redisConnectionDeferred = shouldDeferRedisConnection();
611
657
  try {
612
658
  this.client = (0, import_redis.createClient)({
613
659
  url: redisUrl,
@@ -646,12 +692,14 @@ var RedisStringsHandler = class {
646
692
  }, 500);
647
693
  }
648
694
  });
649
- this.client.connect().then(() => {
650
- debug("green", "Redis client connected.");
651
- }).catch((error) => {
652
- console.error("Failed to connect Redis client:", error);
653
- throw error;
654
- });
695
+ if (!this.redisConnectionDeferred) {
696
+ this.client.connect().then(() => {
697
+ debug("green", "Redis client connected.");
698
+ }).catch((error) => {
699
+ console.error("Failed to connect Redis client:", error);
700
+ throw error;
701
+ });
702
+ }
655
703
  } catch (error) {
656
704
  console.error("Failed to initialize Redis client");
657
705
  throw error;
@@ -673,6 +721,7 @@ var RedisStringsHandler = class {
673
721
  database,
674
722
  querySize: revalidateTagQuerySize,
675
723
  filterKeys,
724
+ customizedSync: { withoutOrphanCleanup: true },
676
725
  resyncIntervalMs: avgResyncIntervalMs + avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
677
726
  });
678
727
  this.inMemoryDeduplicationCache = new SyncedMap({
@@ -742,11 +791,17 @@ var RedisStringsHandler = class {
742
791
  }
743
792
  this.clientReadyCalls = 0;
744
793
  if (!this.client.isReady) {
794
+ if (this.redisConnectionDeferred) {
795
+ return;
796
+ }
745
797
  throw new Error(
746
798
  "assertClientIsReady: Redis client is not ready yet or connection is lost."
747
799
  );
748
800
  }
749
801
  }
802
+ isClientUnavailable() {
803
+ return this.redisConnectionDeferred || !this.client.isReady;
804
+ }
750
805
  async get(key, ctx) {
751
806
  try {
752
807
  if (!SUPPORTED_GET_KINDS.includes(ctx.kind)) {
@@ -760,6 +815,9 @@ var RedisStringsHandler = class {
760
815
  }
761
816
  debug("green", "RedisStringsHandler.get() called with", key, ctx);
762
817
  await this.assertClientIsReady();
818
+ if (this.isClientUnavailable()) {
819
+ return null;
820
+ }
763
821
  const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(key) : this.redisGet;
764
822
  const redisGetOperation = clientGet(
765
823
  (0, import_redis.commandOptions)({ signal: AbortSignal.timeout(this.getTimeoutMs) }),
@@ -893,6 +951,9 @@ var RedisStringsHandler = class {
893
951
  );
894
952
  }
895
953
  await this.assertClientIsReady();
954
+ if (this.isClientUnavailable()) {
955
+ return;
956
+ }
896
957
  if (data?.kind === "APP_PAGE" || data?.kind === "APP_ROUTE") {
897
958
  const tags = data.headers["x-next-cache-tags"]?.split(",");
898
959
  ctx.tags = [...ctx.tags || [], ...tags || []];
@@ -915,18 +976,14 @@ var RedisStringsHandler = class {
915
976
  serializedCacheEntry
916
977
  );
917
978
  }
918
- const revalidate = (
919
- // 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
920
- data?.kind === "FETCH" && data.revalidate || ctx.revalidate || ctx.cacheControl?.revalidate || // Legacy fallback: older Next.js versions attached `revalidate` directly
921
- // to the data payload. FETCH is already handled above, so this only
922
- // matters for those older, non-discriminated shapes.
923
- data?.revalidate
924
- );
925
- const expireAt = revalidate && Number.isSafeInteger(revalidate) && revalidate > 0 ? this.estimateExpireAge(revalidate) : this.estimateExpireAge(this.defaultStaleAge);
979
+ const expireAt = resolveCacheEntryTtlSeconds(ctx, data, {
980
+ estimateExpireAge: this.estimateExpireAge,
981
+ defaultStaleAge: this.defaultStaleAge
982
+ });
926
983
  const setOperation = redisErrorHandler(
927
984
  "RedisStringsHandler.set(), operation: set " + this.keyPrefix + " " + key,
928
985
  this.client.set(this.keyPrefix + key, serializedCacheEntry, {
929
- EX: expireAt
986
+ ...expireAt ? { EX: expireAt } : {}
930
987
  })
931
988
  );
932
989
  debug(
@@ -989,6 +1046,9 @@ var RedisStringsHandler = class {
989
1046
  );
990
1047
  const tags = new Set([tagOrTags || []].flat());
991
1048
  await this.assertClientIsReady();
1049
+ if (this.isClientUnavailable()) {
1050
+ return;
1051
+ }
992
1052
  const keysToDelete = /* @__PURE__ */ new Set();
993
1053
  for (const tag of tags) {
994
1054
  if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) {
@@ -1127,6 +1187,88 @@ var CachedHandler = class {
1127
1187
 
1128
1188
  // src/CacheComponentsHandler.ts
1129
1189
  var import_redis2 = require("redis");
1190
+
1191
+ // src/utils/compareAndUnlink.ts
1192
+ var COMPARE_AND_UNLINK = `
1193
+ if redis.call('GET', KEYS[1]) == ARGV[1] then
1194
+ return redis.call('UNLINK', KEYS[1])
1195
+ end
1196
+ return 0
1197
+ `;
1198
+ async function compareAndUnlink(client, key, expected) {
1199
+ const deleted = await client.eval(COMPARE_AND_UNLINK, {
1200
+ keys: [key],
1201
+ arguments: [expected]
1202
+ });
1203
+ return deleted === 1 || deleted === 1n;
1204
+ }
1205
+
1206
+ // src/utils/tagRevalidation.ts
1207
+ function normalizeTagManifest(stored, now = Date.now()) {
1208
+ if (typeof stored === "number" && Number.isFinite(stored)) {
1209
+ return { expired: Math.min(stored, now) };
1210
+ }
1211
+ if (stored && typeof stored === "object") {
1212
+ const rec = stored;
1213
+ const stale = typeof rec.stale === "number" && Number.isFinite(rec.stale) ? rec.stale : typeof rec.last === "number" && Number.isFinite(rec.last) ? rec.last : void 0;
1214
+ const expired = typeof rec.expired === "number" && Number.isFinite(rec.expired) ? rec.expired : typeof rec.pending === "number" && Number.isFinite(rec.pending) ? rec.pending : void 0;
1215
+ return {
1216
+ ...stale !== void 0 ? { stale } : {},
1217
+ ...expired !== void 0 ? { expired } : {}
1218
+ };
1219
+ }
1220
+ return {};
1221
+ }
1222
+ function applyTagUpdate(existing, durations, now) {
1223
+ if (durations) {
1224
+ const updates = { ...existing };
1225
+ updates.stale = now;
1226
+ if (durations.expire !== void 0) {
1227
+ updates.expired = now + durations.expire * 1e3;
1228
+ }
1229
+ return updates;
1230
+ }
1231
+ return {
1232
+ ...existing,
1233
+ expired: now
1234
+ };
1235
+ }
1236
+ function persistableTagManifest(entry, now) {
1237
+ if (typeof entry.expired === "number" && entry.expired <= now && (entry.stale === void 0 || entry.stale <= entry.expired)) {
1238
+ return entry.expired;
1239
+ }
1240
+ return entry;
1241
+ }
1242
+ function areTagsExpired(tags, entryTimestamp, now, getEntry) {
1243
+ for (const tag of tags) {
1244
+ const expiredAt = getEntry(tag)?.expired;
1245
+ if (typeof expiredAt === "number" && expiredAt <= now && expiredAt > entryTimestamp) {
1246
+ return true;
1247
+ }
1248
+ }
1249
+ return false;
1250
+ }
1251
+ function areTagsStale(tags, entryTimestamp, getEntry) {
1252
+ for (const tag of tags) {
1253
+ const staleAt = getEntry(tag)?.stale ?? 0;
1254
+ if (typeof staleAt === "number" && staleAt > entryTimestamp) {
1255
+ return true;
1256
+ }
1257
+ }
1258
+ return false;
1259
+ }
1260
+ function maxExpiredTimestamp(tags, getEntry) {
1261
+ let max = 0;
1262
+ for (const tag of tags) {
1263
+ const expired = getEntry(tag)?.expired || 0;
1264
+ if (expired > max) {
1265
+ max = expired;
1266
+ }
1267
+ }
1268
+ return max;
1269
+ }
1270
+
1271
+ // src/CacheComponentsHandler.ts
1130
1272
  var REVALIDATED_TAGS_KEY2 = "__cacheComponents_revalidated_tags__";
1131
1273
  var SHARED_TAGS_KEY = "__cacheComponents_sharedTags__";
1132
1274
  var killContainerOnErrorCount2 = 0;
@@ -1184,6 +1326,7 @@ var RedisCacheComponentsHandler = class {
1184
1326
  this.getTimeoutMs = getTimeoutMs;
1185
1327
  this.redisGetDeduplication = redisGetDeduplication;
1186
1328
  this.inMemoryCachingTime = inMemoryCachingTime;
1329
+ this.redisConnectionDeferred = shouldDeferRedisConnection();
1187
1330
  this.client = (0, import_redis2.createClient)({
1188
1331
  url: redisUrl,
1189
1332
  pingInterval: 1e4,
@@ -1219,18 +1362,20 @@ var RedisCacheComponentsHandler = class {
1219
1362
  }, 500);
1220
1363
  }
1221
1364
  });
1222
- this.client.connect().then(() => {
1223
- debug("green", "RedisCacheComponentsHandler client connected.");
1224
- }).catch(() => {
1225
- this.client.connect().catch((error) => {
1226
- console.error(
1227
- "Failed to connect RedisCacheComponentsHandler client:",
1228
- error
1229
- );
1230
- this.client.disconnect();
1231
- throw error;
1365
+ if (!this.redisConnectionDeferred) {
1366
+ this.client.connect().then(() => {
1367
+ debug("green", "RedisCacheComponentsHandler client connected.");
1368
+ }).catch(() => {
1369
+ this.client.connect().catch((error) => {
1370
+ console.error(
1371
+ "Failed to connect RedisCacheComponentsHandler client:",
1372
+ error
1373
+ );
1374
+ this.client.disconnect();
1375
+ throw error;
1376
+ });
1232
1377
  });
1233
- });
1378
+ }
1234
1379
  const filterKeys = (key) => key !== REVALIDATED_TAGS_KEY2 && key !== SHARED_TAGS_KEY;
1235
1380
  this.revalidatedTagsMap = new SyncedMap({
1236
1381
  client: this.client,
@@ -1239,6 +1384,7 @@ var RedisCacheComponentsHandler = class {
1239
1384
  database,
1240
1385
  querySize: revalidateTagQuerySize,
1241
1386
  filterKeys,
1387
+ customizedSync: { withoutOrphanCleanup: true },
1242
1388
  resyncIntervalMs: avgResyncIntervalMs + avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
1243
1389
  });
1244
1390
  this.sharedTagsMap = new SyncedMap({
@@ -1276,6 +1422,13 @@ var RedisCacheComponentsHandler = class {
1276
1422
  }
1277
1423
  }
1278
1424
  async assertClientIsReady() {
1425
+ if (this.redisConnectionDeferred) {
1426
+ await Promise.all([
1427
+ this.revalidatedTagsMap.waitUntilReady(),
1428
+ this.sharedTagsMap.waitUntilReady()
1429
+ ]);
1430
+ return;
1431
+ }
1279
1432
  if (!this.client.isReady && !this.client.isOpen) {
1280
1433
  await this.client.connect().catch((error) => {
1281
1434
  console.error(
@@ -1290,57 +1443,76 @@ var RedisCacheComponentsHandler = class {
1290
1443
  this.sharedTagsMap.waitUntilReady()
1291
1444
  ]);
1292
1445
  }
1293
- async computeMaxRevalidation(tags) {
1294
- let max = 0;
1295
- for (const tag of tags) {
1296
- const ts = this.revalidatedTagsMap.get(tag);
1297
- if (ts && ts > max) {
1298
- max = ts;
1299
- }
1300
- }
1301
- return max;
1446
+ isClientUnavailable() {
1447
+ return this.redisConnectionDeferred || !this.client.isReady;
1302
1448
  }
1303
- async get(cacheKey, softTags) {
1449
+ tagManifestFor(tag, now = Date.now()) {
1450
+ return normalizeTagManifest(this.revalidatedTagsMap.get(tag), now);
1451
+ }
1452
+ async get(cacheKey, _softTags) {
1453
+ void _softTags;
1304
1454
  const redisKey = `${this.keyPrefix}${cacheKey}`;
1305
1455
  try {
1306
1456
  await this.assertClientIsReady();
1307
- const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(cacheKey) : this.redisGet;
1308
- const serialized = await redisErrorHandler(
1309
- "RedisCacheComponentsHandler.get(), operation: get" + (this.redisGetDeduplication ? " deduplicated" : "") + " " + this.getTimeoutMs + "ms " + redisKey,
1310
- clientGet(
1457
+ if (this.isClientUnavailable()) {
1458
+ return void 0;
1459
+ }
1460
+ const readSerialized = (useDedup) => redisErrorHandler(
1461
+ "RedisCacheComponentsHandler.get(), operation: get" + (useDedup ? " deduplicated" : "") + " " + this.getTimeoutMs + "ms " + redisKey,
1462
+ (useDedup ? this.deduplicatedRedisGet(cacheKey) : this.redisGet)(
1311
1463
  (0, import_redis2.commandOptions)({ signal: AbortSignal.timeout(this.getTimeoutMs) }),
1312
1464
  redisKey
1313
1465
  )
1314
1466
  );
1315
- if (!serialized) {
1316
- return void 0;
1317
- }
1318
- const stored = JSON.parse(serialized);
1319
- const now = Date.now();
1320
- const expiryTime = stored.timestamp + stored.expire * 1e3;
1321
- if (Number.isFinite(stored.expire) && stored.expire > 0 && now > expiryTime) {
1322
- await this.client.unlink(redisKey).catch(() => {
1323
- });
1324
- await this.sharedTagsMap.delete(cacheKey).catch(() => {
1325
- });
1326
- return void 0;
1327
- }
1328
- const maxRevalidation = await this.computeMaxRevalidation([
1329
- ...stored.tags || [],
1330
- ...softTags || []
1331
- ]);
1332
- if (maxRevalidation > 0 && maxRevalidation > stored.timestamp) {
1333
- await this.client.unlink(redisKey).catch(() => {
1334
- });
1335
- await this.sharedTagsMap.delete(cacheKey).catch(() => {
1336
- });
1337
- return void 0;
1467
+ let usedDedup = this.redisGetDeduplication;
1468
+ let serialized = await readSerialized(usedDedup);
1469
+ for (let attempt = 0; attempt < 2; attempt++) {
1470
+ if (!serialized) {
1471
+ return void 0;
1472
+ }
1473
+ const stored = JSON.parse(serialized);
1474
+ const now = Date.now();
1475
+ const tags = stored.tags || [];
1476
+ const expiryTime = stored.timestamp + stored.expire * 1e3;
1477
+ const ttlExpired = Number.isFinite(stored.expire) && stored.expire > 0 && now > expiryTime;
1478
+ const tagsExpired = areTagsExpired(
1479
+ tags,
1480
+ stored.timestamp,
1481
+ now,
1482
+ (tag) => this.tagManifestFor(tag, now)
1483
+ );
1484
+ if (ttlExpired || tagsExpired) {
1485
+ if (usedDedup) {
1486
+ usedDedup = false;
1487
+ serialized = await readSerialized(false);
1488
+ continue;
1489
+ }
1490
+ const dropped = await compareAndUnlink(
1491
+ this.client,
1492
+ redisKey,
1493
+ serialized
1494
+ );
1495
+ if (dropped) {
1496
+ await this.sharedTagsMap.delete(cacheKey).catch(() => {
1497
+ });
1498
+ }
1499
+ return void 0;
1500
+ }
1501
+ const valueBuffer = typeof stored.value === "string" ? new Uint8Array(Buffer.from(stored.value, "base64")) : stored.value;
1502
+ const entry = {
1503
+ ...stored,
1504
+ value: bufferToReadableStream(valueBuffer)
1505
+ };
1506
+ if (areTagsStale(
1507
+ tags,
1508
+ stored.timestamp,
1509
+ (tag) => this.tagManifestFor(tag, now)
1510
+ )) {
1511
+ entry.revalidate = -1;
1512
+ }
1513
+ return entry;
1338
1514
  }
1339
- const valueBuffer = typeof stored.value === "string" ? new Uint8Array(Buffer.from(stored.value, "base64")) : stored.value;
1340
- return {
1341
- ...stored,
1342
- value: bufferToReadableStream(valueBuffer)
1343
- };
1515
+ return void 0;
1344
1516
  } catch (error) {
1345
1517
  console.error(
1346
1518
  "RedisCacheComponentsHandler.get() Error occurred while getting cache entry. Returning undefined so site can continue to serve content while cache is disabled. The original error was:",
@@ -1353,6 +1525,9 @@ var RedisCacheComponentsHandler = class {
1353
1525
  async set(cacheKey, pendingEntry) {
1354
1526
  try {
1355
1527
  await this.assertClientIsReady();
1528
+ if (this.isClientUnavailable()) {
1529
+ return;
1530
+ }
1356
1531
  const entry = await pendingEntry;
1357
1532
  const [storeStream] = entry.value.tee();
1358
1533
  const buffer = await streamToBuffer(storeStream);
@@ -1416,7 +1591,7 @@ var RedisCacheComponentsHandler = class {
1416
1591
  async getExpiration(tags) {
1417
1592
  try {
1418
1593
  await this.assertClientIsReady();
1419
- return this.computeMaxRevalidation(tags || []);
1594
+ return maxExpiredTimestamp(tags || [], (tag) => this.tagManifestFor(tag));
1420
1595
  } catch (error) {
1421
1596
  console.error(
1422
1597
  "RedisCacheComponentsHandler.getExpiration() Error occurred while getting expiration for tags. The original error was:",
@@ -1425,37 +1600,28 @@ var RedisCacheComponentsHandler = class {
1425
1600
  return 0;
1426
1601
  }
1427
1602
  }
1428
- async updateTags(tags, _durations) {
1603
+ async updateTags(tags, durations) {
1429
1604
  try {
1430
- void _durations;
1431
1605
  await this.assertClientIsReady();
1432
- const now = Date.now();
1433
- const tagsSet = new Set(tags || []);
1434
- for (const tag of tagsSet) {
1435
- await this.revalidatedTagsMap.set(tag, now);
1436
- }
1437
- const keysToDelete = /* @__PURE__ */ new Set();
1438
- for (const [key, storedTags] of this.sharedTagsMap.entries()) {
1439
- if (storedTags.some((tag) => tagsSet.has(tag))) {
1440
- keysToDelete.add(key);
1441
- }
1442
- }
1443
- if (keysToDelete.size === 0) {
1606
+ if (this.isClientUnavailable()) {
1444
1607
  return;
1445
1608
  }
1446
- const cacheKeys = Array.from(keysToDelete);
1447
- const fullRedisKeys = cacheKeys.map((key) => `${this.keyPrefix}${key}`);
1448
- await redisErrorHandler(
1449
- "RedisCacheComponentsHandler.updateTags(), operation: unlink",
1450
- this.client.unlink(fullRedisKeys)
1451
- );
1609
+ const now = Date.now();
1610
+ const updated = new Set(tags || []);
1611
+ for (const tag of updated) {
1612
+ const next = applyTagUpdate(this.tagManifestFor(tag), durations, now);
1613
+ await this.revalidatedTagsMap.set(
1614
+ tag,
1615
+ persistableTagManifest(next, now)
1616
+ );
1617
+ }
1452
1618
  if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
1453
- for (const key of keysToDelete) {
1454
- this.inMemoryDeduplicationCache.delete(key);
1619
+ for (const [key, sharedTags] of this.sharedTagsMap.entries()) {
1620
+ if (sharedTags.some((tag) => updated.has(tag))) {
1621
+ await this.inMemoryDeduplicationCache.delete(key, true);
1622
+ }
1455
1623
  }
1456
1624
  }
1457
- const deleteTagsOperation = this.sharedTagsMap.delete(cacheKeys);
1458
- await deleteTagsOperation;
1459
1625
  } catch (error) {
1460
1626
  console.error(
1461
1627
  "RedisCacheComponentsHandler.updateTags() Error occurred while updating tags. The original error was:",
@@ -1499,6 +1665,8 @@ var index_default = CachedHandler;
1499
1665
  bufferAndMapReviver,
1500
1666
  getRedisCacheComponentsHandler,
1501
1667
  jsonCacheValueSerializer,
1502
- redisCacheHandler
1668
+ redisCacheHandler,
1669
+ resolveCacheEntryTtlSeconds,
1670
+ shouldDeferRedisConnection
1503
1671
  });
1504
1672
  //# sourceMappingURL=index.js.map