@trieb.work/nextjs-turbo-redis-cache 1.18.0 → 1.18.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.
Files changed (318) hide show
  1. package/dist/index.js +27 -20
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +27 -20
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +6 -1
  6. package/.cursor/skills/testing/SKILL.md +0 -121
  7. package/.github/workflows/ci.yml +0 -309
  8. package/.github/workflows/pages.yml +0 -38
  9. package/.github/workflows/release.yml +0 -58
  10. package/.prettierrc.json +0 -6
  11. package/.vscode/settings.json +0 -10
  12. package/ARCHITECTURE.md +0 -470
  13. package/commitlint.config.cjs +0 -4
  14. package/docker/redis.conf +0 -1
  15. package/docs/index.html +0 -140
  16. package/docs/robots.txt +0 -4
  17. package/docs/sitemap.xml +0 -8
  18. package/docs/styles.css +0 -141
  19. package/eslint.config.mjs +0 -26
  20. package/hooks/commit-msg +0 -10
  21. package/hooks/pre-commit +0 -11
  22. package/playwright.config.ts +0 -18
  23. package/release.config.cjs +0 -12
  24. package/scripts/prepare.sh +0 -7
  25. package/scripts/setup-git.sh +0 -8
  26. package/scripts/setup-repo-name.sh +0 -17
  27. package/scripts/vitest-run-staged.cjs +0 -44
  28. package/src/CacheComponentsHandler.ts +0 -597
  29. package/src/CachedHandler.ts +0 -53
  30. package/src/DeduplicatedRequestHandler.ts +0 -110
  31. package/src/RedisStringsHandler.ts +0 -949
  32. package/src/SyncedMap.ts +0 -496
  33. package/src/ZodHandler.ts +0 -45
  34. package/src/index.ts +0 -23
  35. package/src/serializer.ts +0 -59
  36. package/src/utils/cacheTtl.ts +0 -71
  37. package/src/utils/compareAndUnlink.ts +0 -29
  38. package/src/utils/debug.ts +0 -30
  39. package/src/utils/json.ts +0 -49
  40. package/src/utils/prefix.ts +0 -44
  41. package/src/utils/redisConnection.ts +0 -17
  42. package/src/utils/tagRevalidation.ts +0 -162
  43. package/test/README.md +0 -194
  44. package/test/nextjs-test-projects/next-app-15-4-11/README.md +0 -36
  45. package/test/nextjs-test-projects/next-app-15-4-11/eslint.config.mjs +0 -16
  46. package/test/nextjs-test-projects/next-app-15-4-11/next.config.js +0 -9
  47. package/test/nextjs-test-projects/next-app-15-4-11/package-lock.json +0 -5969
  48. package/test/nextjs-test-projects/next-app-15-4-11/package.json +0 -29
  49. package/test/nextjs-test-projects/next-app-15-4-11/pnpm-lock.yaml +0 -3726
  50. package/test/nextjs-test-projects/next-app-15-4-11/postcss.config.mjs +0 -5
  51. package/test/nextjs-test-projects/next-app-15-4-11/public/file.svg +0 -1
  52. package/test/nextjs-test-projects/next-app-15-4-11/public/globe.svg +0 -1
  53. package/test/nextjs-test-projects/next-app-15-4-11/public/next.svg +0 -1
  54. package/test/nextjs-test-projects/next-app-15-4-11/public/vercel.svg +0 -1
  55. package/test/nextjs-test-projects/next-app-15-4-11/public/window.svg +0 -1
  56. package/test/nextjs-test-projects/next-app-15-4-11/src/app/api/cached-static-fetch/route.ts +0 -18
  57. package/test/nextjs-test-projects/next-app-15-4-11/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  58. package/test/nextjs-test-projects/next-app-15-4-11/src/app/api/revalidatePath/route.ts +0 -15
  59. package/test/nextjs-test-projects/next-app-15-4-11/src/app/api/revalidateTag/route.ts +0 -15
  60. package/test/nextjs-test-projects/next-app-15-4-11/src/app/api/revalidated-fetch/route.ts +0 -17
  61. package/test/nextjs-test-projects/next-app-15-4-11/src/app/api/uncached-fetch/route.ts +0 -15
  62. package/test/nextjs-test-projects/next-app-15-4-11/src/app/globals.css +0 -26
  63. package/test/nextjs-test-projects/next-app-15-4-11/src/app/layout.tsx +0 -59
  64. package/test/nextjs-test-projects/next-app-15-4-11/src/app/page.tsx +0 -755
  65. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  66. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  67. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  68. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  69. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  70. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  71. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  72. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  73. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  74. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  75. package/test/nextjs-test-projects/next-app-15-4-11/src/app/revalidation-interface.tsx +0 -267
  76. package/test/nextjs-test-projects/next-app-15-4-11/tsconfig.json +0 -27
  77. package/test/nextjs-test-projects/next-app-16-0-11/README.md +0 -36
  78. package/test/nextjs-test-projects/next-app-16-0-11/eslint.config.mjs +0 -18
  79. package/test/nextjs-test-projects/next-app-16-0-11/next.config.ts +0 -10
  80. package/test/nextjs-test-projects/next-app-16-0-11/package.json +0 -28
  81. package/test/nextjs-test-projects/next-app-16-0-11/pnpm-lock.yaml +0 -4163
  82. package/test/nextjs-test-projects/next-app-16-0-11/postcss.config.mjs +0 -7
  83. package/test/nextjs-test-projects/next-app-16-0-11/src/app/api/cached-static-fetch/route.ts +0 -18
  84. package/test/nextjs-test-projects/next-app-16-0-11/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  85. package/test/nextjs-test-projects/next-app-16-0-11/src/app/api/revalidatePath/route.ts +0 -15
  86. package/test/nextjs-test-projects/next-app-16-0-11/src/app/api/revalidateTag/route.ts +0 -20
  87. package/test/nextjs-test-projects/next-app-16-0-11/src/app/api/revalidated-fetch/route.ts +0 -17
  88. package/test/nextjs-test-projects/next-app-16-0-11/src/app/api/uncached-fetch/route.ts +0 -15
  89. package/test/nextjs-test-projects/next-app-16-0-11/src/app/favicon.ico +0 -0
  90. package/test/nextjs-test-projects/next-app-16-0-11/src/app/globals.css +0 -26
  91. package/test/nextjs-test-projects/next-app-16-0-11/src/app/layout.tsx +0 -59
  92. package/test/nextjs-test-projects/next-app-16-0-11/src/app/page.tsx +0 -755
  93. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  94. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  95. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  96. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  97. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  98. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  99. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  100. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  101. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  102. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  103. package/test/nextjs-test-projects/next-app-16-0-11/src/app/revalidation-interface.tsx +0 -267
  104. package/test/nextjs-test-projects/next-app-16-0-11/src/app/update-tag-test/page.tsx +0 -25
  105. package/test/nextjs-test-projects/next-app-16-0-11/tsconfig.json +0 -34
  106. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/README.md +0 -36
  107. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/cache-handler.js +0 -3
  108. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/eslint.config.mjs +0 -18
  109. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/next.config.ts +0 -15
  110. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/package.json +0 -28
  111. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/pnpm-lock.yaml +0 -4206
  112. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/postcss.config.mjs +0 -7
  113. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/public/file.svg +0 -1
  114. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/public/globe.svg +0 -1
  115. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/public/next.svg +0 -1
  116. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/public/vercel.svg +0 -1
  117. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/public/window.svg +0 -1
  118. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/cached-static-fetch/route.ts +0 -19
  119. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/cached-with-cachelife/route.ts +0 -24
  120. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/cached-with-tag/route.ts +0 -21
  121. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/expire-matrix/revalidate/route.ts +0 -34
  122. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/expire-matrix/route.ts +0 -23
  123. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/revalidate-tag/route.ts +0 -19
  124. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/revalidated-fetch/route.ts +0 -19
  125. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/cachelife-short/page.tsx +0 -110
  126. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/page.tsx +0 -112
  127. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +0 -106
  128. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +0 -127
  129. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +0 -130
  130. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +0 -127
  131. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +0 -110
  132. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +0 -85
  133. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/favicon.ico +0 -0
  134. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/globals.css +0 -26
  135. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/layout.tsx +0 -57
  136. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/page.tsx +0 -755
  137. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/revalidation-interface.tsx +0 -267
  138. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/update-tag-test/page.tsx +0 -22
  139. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/tsconfig.json +0 -34
  140. package/test/nextjs-test-projects/next-app-16-2-6/README.md +0 -36
  141. package/test/nextjs-test-projects/next-app-16-2-6/eslint.config.mjs +0 -18
  142. package/test/nextjs-test-projects/next-app-16-2-6/next.config.ts +0 -7
  143. package/test/nextjs-test-projects/next-app-16-2-6/package.json +0 -28
  144. package/test/nextjs-test-projects/next-app-16-2-6/pnpm-lock.yaml +0 -4165
  145. package/test/nextjs-test-projects/next-app-16-2-6/postcss.config.mjs +0 -7
  146. package/test/nextjs-test-projects/next-app-16-2-6/src/app/api/cached-static-fetch/route.ts +0 -18
  147. package/test/nextjs-test-projects/next-app-16-2-6/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  148. package/test/nextjs-test-projects/next-app-16-2-6/src/app/api/revalidatePath/route.ts +0 -15
  149. package/test/nextjs-test-projects/next-app-16-2-6/src/app/api/revalidateTag/route.ts +0 -20
  150. package/test/nextjs-test-projects/next-app-16-2-6/src/app/api/revalidated-fetch/route.ts +0 -17
  151. package/test/nextjs-test-projects/next-app-16-2-6/src/app/api/uncached-fetch/route.ts +0 -15
  152. package/test/nextjs-test-projects/next-app-16-2-6/src/app/favicon.ico +0 -0
  153. package/test/nextjs-test-projects/next-app-16-2-6/src/app/globals.css +0 -26
  154. package/test/nextjs-test-projects/next-app-16-2-6/src/app/layout.tsx +0 -59
  155. package/test/nextjs-test-projects/next-app-16-2-6/src/app/page.tsx +0 -755
  156. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  157. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  158. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  159. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  160. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  161. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  162. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  163. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  164. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  165. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  166. package/test/nextjs-test-projects/next-app-16-2-6/src/app/revalidation-interface.tsx +0 -267
  167. package/test/nextjs-test-projects/next-app-16-2-6/src/app/update-tag-test/page.tsx +0 -25
  168. package/test/nextjs-test-projects/next-app-16-2-6/tsconfig.json +0 -34
  169. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/README.md +0 -36
  170. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/cache-handler.js +0 -3
  171. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/eslint.config.mjs +0 -18
  172. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/next.config.ts +0 -15
  173. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/package.json +0 -28
  174. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/pnpm-lock.yaml +0 -4165
  175. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/postcss.config.mjs +0 -7
  176. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/public/file.svg +0 -1
  177. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/public/globe.svg +0 -1
  178. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/public/next.svg +0 -1
  179. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/public/vercel.svg +0 -1
  180. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/public/window.svg +0 -1
  181. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-static-fetch/route.ts +0 -19
  182. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-cachelife/route.ts +0 -24
  183. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-tag/route.ts +0 -21
  184. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/expire-matrix/revalidate/route.ts +0 -34
  185. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/expire-matrix/route.ts +0 -23
  186. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidate-tag/route.ts +0 -19
  187. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidated-fetch/route.ts +0 -19
  188. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/cachelife-short/page.tsx +0 -110
  189. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/page.tsx +0 -112
  190. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +0 -106
  191. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +0 -127
  192. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +0 -130
  193. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +0 -127
  194. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +0 -110
  195. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +0 -85
  196. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/favicon.ico +0 -0
  197. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/globals.css +0 -26
  198. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/layout.tsx +0 -57
  199. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/page.tsx +0 -755
  200. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/revalidation-interface.tsx +0 -267
  201. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/update-tag-test/page.tsx +0 -22
  202. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/tsconfig.json +0 -34
  203. package/test/nextjs-test-projects/next-app-16-3-0/README.md +0 -36
  204. package/test/nextjs-test-projects/next-app-16-3-0/eslint.config.mjs +0 -18
  205. package/test/nextjs-test-projects/next-app-16-3-0/next.config.ts +0 -7
  206. package/test/nextjs-test-projects/next-app-16-3-0/package.json +0 -28
  207. package/test/nextjs-test-projects/next-app-16-3-0/pnpm-lock.yaml +0 -4284
  208. package/test/nextjs-test-projects/next-app-16-3-0/postcss.config.mjs +0 -7
  209. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/cached-static-fetch/route.ts +0 -18
  210. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  211. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidatePath/route.ts +0 -15
  212. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidateTag/route.ts +0 -20
  213. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidated-fetch/route.ts +0 -17
  214. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/uncached-fetch/route.ts +0 -15
  215. package/test/nextjs-test-projects/next-app-16-3-0/src/app/favicon.ico +0 -0
  216. package/test/nextjs-test-projects/next-app-16-3-0/src/app/globals.css +0 -26
  217. package/test/nextjs-test-projects/next-app-16-3-0/src/app/layout.tsx +0 -59
  218. package/test/nextjs-test-projects/next-app-16-3-0/src/app/page.tsx +0 -755
  219. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  220. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  221. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  222. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  223. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  224. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  225. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  226. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  227. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  228. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  229. package/test/nextjs-test-projects/next-app-16-3-0/src/app/revalidation-interface.tsx +0 -267
  230. package/test/nextjs-test-projects/next-app-16-3-0/src/app/update-tag-test/page.tsx +0 -25
  231. package/test/nextjs-test-projects/next-app-16-3-0/tsconfig.json +0 -34
  232. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/README.md +0 -36
  233. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/cache-handler.js +0 -3
  234. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/eslint.config.mjs +0 -18
  235. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/next.config.ts +0 -15
  236. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/package.json +0 -28
  237. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/pnpm-lock.yaml +0 -4284
  238. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/postcss.config.mjs +0 -7
  239. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/file.svg +0 -1
  240. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/globe.svg +0 -1
  241. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/next.svg +0 -1
  242. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/vercel.svg +0 -1
  243. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/window.svg +0 -1
  244. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-static-fetch/route.ts +0 -19
  245. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-with-cachelife/route.ts +0 -24
  246. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-with-tag/route.ts +0 -21
  247. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/expire-matrix/revalidate/route.ts +0 -34
  248. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/expire-matrix/route.ts +0 -23
  249. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/revalidate-tag/route.ts +0 -19
  250. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/revalidated-fetch/route.ts +0 -19
  251. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/cachelife-short/page.tsx +0 -110
  252. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/page.tsx +0 -112
  253. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +0 -106
  254. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +0 -127
  255. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +0 -130
  256. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +0 -127
  257. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +0 -110
  258. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +0 -85
  259. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/favicon.ico +0 -0
  260. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/globals.css +0 -26
  261. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/layout.tsx +0 -57
  262. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/page.tsx +0 -755
  263. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/revalidation-interface.tsx +0 -267
  264. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/update-tag-test/page.tsx +0 -22
  265. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/tsconfig.json +0 -34
  266. package/test/nextjs-test-projects/next-app-customized/README.md +0 -36
  267. package/test/nextjs-test-projects/next-app-customized/customized-cache-handler.js +0 -34
  268. package/test/nextjs-test-projects/next-app-customized/eslint.config.mjs +0 -16
  269. package/test/nextjs-test-projects/next-app-customized/next.config.js +0 -6
  270. package/test/nextjs-test-projects/next-pages-16-2-6/README.md +0 -16
  271. package/test/nextjs-test-projects/next-pages-16-2-6/eslint.config.mjs +0 -18
  272. package/test/nextjs-test-projects/next-pages-16-2-6/next.config.ts +0 -7
  273. package/test/nextjs-test-projects/next-pages-16-2-6/package.json +0 -26
  274. package/test/nextjs-test-projects/next-pages-16-2-6/pnpm-lock.yaml +0 -3896
  275. package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/api/revalidate.ts +0 -24
  276. package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/index.tsx +0 -11
  277. package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/isr/[slug].tsx +0 -49
  278. package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/static-forever.tsx +0 -20
  279. package/test/nextjs-test-projects/next-pages-16-2-6/tsconfig.json +0 -34
  280. package/test/nextjs-test-projects/next-pages-16-3-0/README.md +0 -16
  281. package/test/nextjs-test-projects/next-pages-16-3-0/eslint.config.mjs +0 -18
  282. package/test/nextjs-test-projects/next-pages-16-3-0/next.config.ts +0 -7
  283. package/test/nextjs-test-projects/next-pages-16-3-0/package.json +0 -26
  284. package/test/nextjs-test-projects/next-pages-16-3-0/pnpm-lock.yaml +0 -3939
  285. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/api/revalidate.ts +0 -24
  286. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/index.tsx +0 -11
  287. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/isr/[slug].tsx +0 -49
  288. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/static-forever.tsx +0 -20
  289. package/test/nextjs-test-projects/next-pages-16-3-0/tsconfig.json +0 -34
  290. package/test/playwright/cache-lab.spec.ts +0 -232
  291. package/test/playwright/update-tag.spec.ts +0 -33
  292. package/test/vitest/integration/build-id-prefix.integration.test.ts +0 -102
  293. package/test/vitest/integration/cache-components/cache-components.integration.test.ts +0 -389
  294. package/test/vitest/integration/cache-components/redis-kill-reconnect.test.ts +0 -65
  295. package/test/vitest/integration/cache-components/redis-quit-vs-disconnect.test.ts +0 -59
  296. package/test/vitest/integration/cache-components/redis-subscriber-outage.test.ts +0 -100
  297. package/test/vitest/integration/cache-components/scripts/redis-kill-reconnect.ts +0 -243
  298. package/test/vitest/integration/cache-components/scripts/redis-quit-vs-disconnect.ts +0 -113
  299. package/test/vitest/integration/cache-components/scripts/redis-subscriber-outage.ts +0 -379
  300. package/test/vitest/integration/cache-components/scripts/redis-test-helpers.ts +0 -155
  301. package/test/vitest/integration/nextjs-cache-handler.integration.test.ts +0 -959
  302. package/test/vitest/integration/pages-router.integration.test.ts +0 -430
  303. package/test/vitest/unit/SyncedMap-orphan-cleanup.test.ts +0 -111
  304. package/test/vitest/unit/SyncedMap-reconnect-repro.test.ts +0 -154
  305. package/test/vitest/unit/cache-components-update-tags.test.ts +0 -350
  306. package/test/vitest/unit/cache-ttl.test.ts +0 -67
  307. package/test/vitest/unit/compare-and-unlink.test.ts +0 -22
  308. package/test/vitest/unit/index.test.ts +0 -190
  309. package/test/vitest/unit/pages-router-kinds.test.ts +0 -303
  310. package/test/vitest/unit/reconnect-socket-already-opened.test.ts +0 -117
  311. package/test/vitest/unit/redis-connection.test.ts +0 -39
  312. package/test/vitest/unit/serializer.test.ts +0 -182
  313. package/test/vitest/unit/tag-revalidation.test.ts +0 -178
  314. package/test/vitest/unit/utils/prefix.test.ts +0 -115
  315. package/tsconfig.json +0 -18
  316. package/tsup.config.ts +0 -14
  317. package/vite.config.ts +0 -34
  318. package/vitest.cache-components.config.ts +0 -16
@@ -1,949 +0,0 @@
1
- import { commandOptions, createClient, RedisClientOptions } from 'redis';
2
- import { SyncedMap } from './SyncedMap';
3
- import { DeduplicatedRequestHandler } from './DeduplicatedRequestHandler';
4
- import { debug } from './utils/debug';
5
- import { CacheValueSerializer, jsonCacheValueSerializer } from './serializer';
6
- import { resolveCacheEntryTtlSeconds } from './utils/cacheTtl';
7
- import { shouldDeferRedisConnection } from './utils/redisConnection';
8
-
9
- export type CommandOptions = ReturnType<typeof commandOptions>;
10
- export type Client = ReturnType<typeof createClient>;
11
-
12
- export type CacheEntry = {
13
- value: unknown;
14
- lastModified: number;
15
- tags: string[];
16
- };
17
-
18
- /** Discriminated union of the `ctx` argument Next.js passes to
19
- * {@link RedisStringsHandler.get}. The Pages Router (`PAGES`) has no PPR
20
- * concept, so `isRoutePPREnabled` is optional for it.
21
- *
22
- * Note: `REDIRECT` is intentionally absent. Next.js always calls `get()` with
23
- * `kind: 'PAGES'` for Pages Router routes, even when the stored value has
24
- * `kind: 'REDIRECT'`. The handler returns whatever value is cached and Next.js
25
- * interprets it accordingly. */
26
- export type GetContext =
27
- | {
28
- kind: 'APP_ROUTE' | 'APP_PAGE';
29
- isRoutePPREnabled: boolean;
30
- isFallback: boolean;
31
- }
32
- | {
33
- kind: 'PAGES';
34
- isRoutePPREnabled?: boolean;
35
- isFallback: boolean;
36
- }
37
- | {
38
- kind: 'FETCH';
39
- revalidate: number;
40
- fetchUrl: string;
41
- fetchIdx: number;
42
- tags: string[];
43
- softTags: string[];
44
- isFallback: boolean;
45
- };
46
-
47
- /** Discriminated union of cache payloads accepted by
48
- * {@link RedisStringsHandler.set}. `null` is a valid payload: the Pages Router
49
- * stores `notFound: true` results as a cache entry with a null value. */
50
- export type SetCacheValue =
51
- | {
52
- kind: 'APP_PAGE';
53
- status?: number;
54
- headers: {
55
- 'x-nextjs-stale-time': string; // timestamp in ms
56
- 'x-next-cache-tags': string; // comma separated paths (tags)
57
- };
58
- html: string;
59
- rscData: Buffer;
60
- segmentData: unknown;
61
- postboned: unknown;
62
- }
63
- | {
64
- kind: 'APP_ROUTE';
65
- status: number;
66
- headers: {
67
- 'cache-control'?: string;
68
- 'x-nextjs-stale-time': string; // timestamp in ms
69
- 'x-next-cache-tags': string; // comma separated paths (tags)
70
- };
71
- body: Buffer;
72
- }
73
- | {
74
- kind: 'PAGES';
75
- html: string;
76
- pageData: Record<string, unknown>;
77
- headers?: Record<string, number | string | string[] | undefined>;
78
- status?: number;
79
- }
80
- | {
81
- kind: 'REDIRECT';
82
- props: Record<string, unknown>;
83
- }
84
- | {
85
- kind: 'FETCH';
86
- data: {
87
- headers: Record<string, string>;
88
- body: string; // base64 encoded
89
- status: number;
90
- url: string;
91
- };
92
- revalidate: number | false;
93
- }
94
- | null;
95
-
96
- export function redisErrorHandler<T extends Promise<unknown>>(
97
- debugInfo: string,
98
- redisCommandResult: T,
99
- ): T {
100
- const beforeTimestamp = performance.now();
101
- return redisCommandResult.catch((error) => {
102
- console.error(
103
- 'Redis command error',
104
- (performance.now() - beforeTimestamp).toFixed(2),
105
- 'ms',
106
- debugInfo,
107
- error,
108
- );
109
- throw error;
110
- }) as T;
111
- }
112
-
113
- function isAbortError(error: unknown): boolean {
114
- if (!error || typeof error !== 'object') return false;
115
- const err = error as { name?: unknown; code?: unknown };
116
- return err.name === 'AbortError' || err.code === 'ABORT_ERR';
117
- }
118
-
119
- if (process.env.DEBUG_CACHE_HANDLER) {
120
- // This is a test to check if the event loop is lagging. If it lags, increase CPU of container
121
- setInterval(() => {
122
- const start = performance.now();
123
- setImmediate(() => {
124
- const duration = performance.now() - start;
125
- if (duration > 100) {
126
- debug(
127
- 'yellow',
128
- `RedisStringsHandler detected an event loop lag of: ${duration.toFixed(2)}ms. If your container is hosted in a cloud provider with container suspension this is normal. If not you should increase the CPU of your container.`,
129
- );
130
- }
131
- });
132
- }, 500);
133
- }
134
-
135
- export type CreateRedisStringsHandlerOptions = {
136
- /** Redis redisUrl to use.
137
- * @default process.env.REDIS_URL? process.env.REDIS_URL : process.env.REDISHOST
138
- ? `redis://${process.env.REDISHOST}:${process.env.REDISPORT}`
139
- : 'redis://localhost:6379'
140
- */
141
- redisUrl?: string;
142
- /** Redis database number to use. Uses DB 0 for production, DB 1 otherwise
143
- * @default process.env.VERCEL_ENV === 'production' ? 0 : 1
144
- */
145
- database?: number;
146
- /** Prefix added to all Redis keys
147
- * @default process.env.KEY_PREFIX || process.env.VERCEL_URL || 'UNDEFINED_URL_'
148
- */
149
- keyPrefix?: string;
150
- /** Timeout in milliseconds for time critical Redis operations (during cache get, which blocks site rendering).
151
- * If redis get is not fulfilled within this time, the cache handler will return null so site rendering will
152
- * not be blocked further and site can fallback to re-render/re-fetch the content.
153
- * @default 500
154
- */
155
- getTimeoutMs?: number;
156
- /** Number of entries to query in one batch during full sync of shared tags hash map
157
- * @default 250
158
- */
159
- revalidateTagQuerySize?: number;
160
- /** Key used to store shared tags hash map in Redis
161
- * @default '__sharedTags__'
162
- */
163
- sharedTagsKey?: string;
164
- /** Average interval in milliseconds between tag map full re-syncs
165
- * @default 3600000 (1 hour)
166
- */
167
- avgResyncIntervalMs?: number;
168
- /** Enable deduplication of Redis get requests via internal in-memory cache
169
- * @default true
170
- */
171
- redisGetDeduplication?: boolean;
172
- /** Time in milliseconds to cache Redis get results in memory. Set this to 0 to disable in-memory caching completely
173
- * @default 10000
174
- */
175
- inMemoryCachingTime?: number;
176
- /** Default stale age in seconds for cached items
177
- * @default 1209600 (14 days)
178
- */
179
- defaultStaleAge?: number;
180
- /** Function to calculate expire age (redis TTL value) from stale age
181
- * @default Production: staleAge * 2, Other: staleAge * 1.2
182
- */
183
- estimateExpireAge?: (staleAge: number) => number;
184
- /** Kill container on Redis client error if error threshold is reached
185
- * @default 0 (0 means no error threshold)
186
- */
187
- killContainerOnErrorThreshold?: number;
188
- /** Additional Redis client socket options
189
- * @example { tls: true, rejectUnauthorized: false }
190
- */
191
- socketOptions?: RedisClientOptions['socket'];
192
- /** Additional Redis client options to be passed directly to createClient
193
- * @example { username: 'user', password: 'pass' }
194
- */
195
- clientOptions?: Omit<RedisClientOptions, 'url' | 'database' | 'socket'>;
196
- /** Pluggable wire-format codec for Redis string values. Lets you plug in
197
- * compression (gzip/brotli), encryption, or any other custom encoding without
198
- * forking this package or losing the existing dedup / batch / keyspace features.
199
- *
200
- * Both `serialize` and `deserialize` may return a `Promise`, enabling
201
- * non-blocking async codecs (e.g. `zlib.brotliCompress`) that don't block the
202
- * Node.js event loop. Synchronous implementations continue to work unchanged.
203
- *
204
- * Only the main cache-entry storage path is routed through the serializer.
205
- * The shared-tags map and the revalidated-tags map are not. The in-memory
206
- * deduplication cache stores the wire-format string verbatim - its contents
207
- * change with the serializer, but the cache itself is not re-encoded.
208
- *
209
- * Operational note: changing the serializer (or any of its parameters such as
210
- * a compression level or encryption key) makes existing Redis keys unreadable.
211
- * Either flush the affected keys or bump `keyPrefix` before deploying.
212
- *
213
- * @default jsonCacheValueSerializer (JSON.stringify with bufferAndMapReplacer
214
- * so native Buffer and Map values inside a CacheEntry round-trip transparently)
215
- */
216
- valueSerializer?: CacheValueSerializer;
217
- };
218
-
219
- // Identifier prefix used by Next.js to mark automatically generated cache tags
220
- // These tags are created internally by Next.js for route-based invalidation
221
- const NEXT_CACHE_IMPLICIT_TAG_ID = '_N_T_';
222
-
223
- // Redis key used to store a map of tags and their last revalidation timestamps
224
- // This helps track when specific tags were last invalidated
225
- const REVALIDATED_TAGS_KEY = '__revalidated_tags__';
226
-
227
- // Cache kinds this handler is designed and tested for. Kept as the single
228
- // source of truth for both payload validation and the warning message, so
229
- // adding a kind is a one-line change here instead of edits scattered across
230
- // the type union, the guard chain, and the log string.
231
- const SUPPORTED_GET_KINDS = [
232
- 'APP_ROUTE',
233
- 'APP_PAGE',
234
- 'PAGES',
235
- 'FETCH',
236
- ] as const;
237
- const SUPPORTED_SET_KINDS = [
238
- 'APP_ROUTE',
239
- 'APP_PAGE',
240
- 'PAGES',
241
- 'REDIRECT',
242
- 'FETCH',
243
- ] as const;
244
-
245
- let killContainerOnErrorCount: number = 0;
246
- export default class RedisStringsHandler {
247
- private client: Client;
248
- private sharedTagsMap: SyncedMap<string[]>;
249
- private revalidatedTagsMap: SyncedMap<number>;
250
- private inMemoryDeduplicationCache: SyncedMap<
251
- Promise<ReturnType<Client['get']>>
252
- >;
253
- private getTimeoutMs: number;
254
- private redisGet: Client['get'];
255
- private redisDeduplicationHandler: DeduplicatedRequestHandler<
256
- Client['get'],
257
- string | Buffer | null
258
- >;
259
- private deduplicatedRedisGet: (key: string) => Client['get'];
260
- private keyPrefix: string;
261
- private redisGetDeduplication: boolean;
262
- private inMemoryCachingTime: number;
263
- private defaultStaleAge: number;
264
- private estimateExpireAge: (staleAge: number) => number;
265
- private killContainerOnErrorThreshold: number;
266
- private valueSerializer: CacheValueSerializer;
267
- private redisConnectionDeferred: boolean;
268
-
269
- constructor({
270
- redisUrl = process.env.REDIS_URL
271
- ? process.env.REDIS_URL
272
- : process.env.REDISHOST
273
- ? `redis://${process.env.REDISHOST}:${process.env.REDISPORT}`
274
- : 'redis://localhost:6379',
275
- database = process.env.VERCEL_ENV === 'production' ? 0 : 1,
276
- keyPrefix = process.env.KEY_PREFIX ||
277
- process.env.VERCEL_URL ||
278
- 'UNDEFINED_URL_',
279
- sharedTagsKey = '__sharedTags__',
280
- getTimeoutMs = process.env.REDIS_COMMAND_TIMEOUT_MS
281
- ? (Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS) ?? 500)
282
- : 500,
283
- revalidateTagQuerySize = 250,
284
- avgResyncIntervalMs = 60 * 60 * 1_000,
285
- redisGetDeduplication = true,
286
- inMemoryCachingTime = 10_000,
287
- defaultStaleAge = 60 * 60 * 24 * 14,
288
- estimateExpireAge = (staleAge) =>
289
- process.env.VERCEL_ENV === 'production' ? staleAge * 2 : staleAge * 1.2,
290
- killContainerOnErrorThreshold = process.env
291
- .KILL_CONTAINER_ON_ERROR_THRESHOLD
292
- ? (Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD) ?? 0)
293
- : 0,
294
- socketOptions,
295
- clientOptions,
296
- valueSerializer = jsonCacheValueSerializer,
297
- }: CreateRedisStringsHandlerOptions) {
298
- try {
299
- this.keyPrefix = keyPrefix;
300
- this.redisGetDeduplication = redisGetDeduplication;
301
- this.inMemoryCachingTime = inMemoryCachingTime;
302
- this.defaultStaleAge = defaultStaleAge;
303
- this.estimateExpireAge = estimateExpireAge;
304
- this.killContainerOnErrorThreshold = killContainerOnErrorThreshold;
305
- this.getTimeoutMs = getTimeoutMs;
306
- this.valueSerializer = valueSerializer;
307
- this.redisConnectionDeferred = shouldDeferRedisConnection();
308
-
309
- try {
310
- // Create Redis client with properly typed configuration
311
- this.client = createClient({
312
- url: redisUrl,
313
- pingInterval: 10_000, // Useful with Redis deployments that do not use TCP Keep-Alive. Restarts the connection if it is idle for too long.
314
- ...(database !== 0 ? { database } : {}),
315
- ...(socketOptions ? { socket: { ...socketOptions } } : {}),
316
- ...(clientOptions || {}),
317
- });
318
-
319
- this.client.on('error', (error) => {
320
- console.error(
321
- 'Redis client error',
322
- error,
323
- killContainerOnErrorCount++,
324
- );
325
- setTimeout(() => {
326
- // Avoid overlapping connect() calls which can lead to
327
- // "Socket already opened" errors when a reconnect is already
328
- // in progress or the socket is still open.
329
- if (!this.client.isOpen && !this.client.isReady) {
330
- this.client.connect().catch((error) => {
331
- console.error(
332
- 'Failed to reconnect Redis client after connection loss:',
333
- error,
334
- );
335
- });
336
- }
337
- }, 1000);
338
- if (
339
- this.killContainerOnErrorThreshold > 0 &&
340
- killContainerOnErrorCount >= this.killContainerOnErrorThreshold
341
- ) {
342
- console.error(
343
- 'Redis client error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)',
344
- error,
345
- killContainerOnErrorCount++,
346
- );
347
- this.client.disconnect();
348
- this.client.quit();
349
- setTimeout(() => {
350
- process.exit(1);
351
- }, 500);
352
- }
353
- });
354
-
355
- if (!this.redisConnectionDeferred) {
356
- this.client
357
- .connect()
358
- .then(() => {
359
- debug('green', 'Redis client connected.');
360
- })
361
- .catch((error) => {
362
- console.error('Failed to connect Redis client:', error);
363
- throw error;
364
- });
365
- }
366
- } catch (error: unknown) {
367
- console.error('Failed to initialize Redis client');
368
- throw error;
369
- }
370
-
371
- const filterKeys = (key: string): boolean =>
372
- key !== REVALIDATED_TAGS_KEY && key !== sharedTagsKey;
373
-
374
- this.sharedTagsMap = new SyncedMap<string[]>({
375
- client: this.client,
376
- keyPrefix,
377
- redisKey: sharedTagsKey,
378
- database,
379
- querySize: revalidateTagQuerySize,
380
- filterKeys,
381
- resyncIntervalMs:
382
- avgResyncIntervalMs -
383
- avgResyncIntervalMs / 10 +
384
- Math.random() * (avgResyncIntervalMs / 10),
385
- });
386
-
387
- this.revalidatedTagsMap = new SyncedMap<number>({
388
- client: this.client,
389
- keyPrefix,
390
- redisKey: REVALIDATED_TAGS_KEY,
391
- database,
392
- querySize: revalidateTagQuerySize,
393
- filterKeys,
394
- customizedSync: { withoutOrphanCleanup: true },
395
- resyncIntervalMs:
396
- avgResyncIntervalMs +
397
- avgResyncIntervalMs / 10 +
398
- Math.random() * (avgResyncIntervalMs / 10),
399
- });
400
-
401
- this.inMemoryDeduplicationCache = new SyncedMap({
402
- client: this.client,
403
- keyPrefix,
404
- redisKey: 'inMemoryDeduplicationCache',
405
- database,
406
- querySize: revalidateTagQuerySize,
407
- filterKeys,
408
- customizedSync: {
409
- withoutRedisHashmap: true,
410
- withoutSetSync: true,
411
- },
412
- });
413
-
414
- const redisGet: Client['get'] = this.client.get.bind(this.client);
415
- this.redisDeduplicationHandler = new DeduplicatedRequestHandler(
416
- redisGet,
417
- inMemoryCachingTime,
418
- this.inMemoryDeduplicationCache,
419
- );
420
- this.redisGet = redisGet;
421
- this.deduplicatedRedisGet =
422
- this.redisDeduplicationHandler.deduplicatedFunction;
423
- } catch (error) {
424
- console.error(
425
- 'RedisStringsHandler constructor error',
426
- error,
427
- killContainerOnErrorCount++,
428
- );
429
- if (
430
- killContainerOnErrorThreshold > 0 &&
431
- killContainerOnErrorCount >= killContainerOnErrorThreshold
432
- ) {
433
- console.error(
434
- 'RedisStringsHandler constructor error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)',
435
- error,
436
- killContainerOnErrorCount++,
437
- );
438
- process.exit(1);
439
- }
440
- throw error;
441
- }
442
- }
443
-
444
- resetRequestCache(): void {}
445
-
446
- private clientReadyCalls = 0;
447
-
448
- private async assertClientIsReady(): Promise<void> {
449
- if (this.clientReadyCalls > 10) {
450
- throw new Error(
451
- 'assertClientIsReady called more than 10 times without being ready.',
452
- );
453
- }
454
- let readyTimeout: ReturnType<typeof setTimeout> | undefined;
455
- try {
456
- await Promise.race([
457
- Promise.all([
458
- this.sharedTagsMap.waitUntilReady(),
459
- this.revalidatedTagsMap.waitUntilReady(),
460
- ]),
461
- new Promise((_, reject) => {
462
- readyTimeout = setTimeout(() => {
463
- reject(
464
- new Error(
465
- 'assertClientIsReady: Timeout waiting for Redis maps to be ready',
466
- ),
467
- );
468
- }, 30_000);
469
- }),
470
- ]);
471
- } finally {
472
- // Always clear the timeout: once the race is won (the common case after
473
- // the initial sync), the timer would otherwise stay pending for the full
474
- // 30s. Timers capture the ambient async context (AsyncLocalStorage),
475
- // so in a Next.js server every leaked timer pins the request's store —
476
- // and with it the whole response object graph — for at least 30s on
477
- // every single cache operation.
478
- clearTimeout(readyTimeout);
479
- }
480
- this.clientReadyCalls = 0;
481
- if (!this.client.isReady) {
482
- if (this.redisConnectionDeferred) {
483
- return;
484
- }
485
- throw new Error(
486
- 'assertClientIsReady: Redis client is not ready yet or connection is lost.',
487
- );
488
- }
489
- }
490
-
491
- private isClientUnavailable(): boolean {
492
- return this.redisConnectionDeferred || !this.client.isReady;
493
- }
494
-
495
- public async get(key: string, ctx: GetContext): Promise<CacheEntry | null> {
496
- try {
497
- if (!(SUPPORTED_GET_KINDS as readonly string[]).includes(ctx.kind)) {
498
- console.warn(
499
- 'RedisStringsHandler.get() called with',
500
- key,
501
- ctx,
502
- `this cache handler is only designed and tested for kinds ${SUPPORTED_GET_KINDS.join(', ')} and not for kind`,
503
- (ctx as { kind: string })?.kind,
504
- );
505
- }
506
-
507
- debug('green', 'RedisStringsHandler.get() called with', key, ctx);
508
- await this.assertClientIsReady();
509
- if (this.isClientUnavailable()) {
510
- return null;
511
- }
512
-
513
- const clientGet = this.redisGetDeduplication
514
- ? this.deduplicatedRedisGet(key)
515
- : this.redisGet;
516
-
517
- const redisGetOperation = clientGet(
518
- commandOptions({ signal: AbortSignal.timeout(this.getTimeoutMs) }),
519
- this.keyPrefix + key,
520
- ).catch((error) => {
521
- if (isAbortError(error)) {
522
- return null;
523
- }
524
- throw error;
525
- });
526
- const serializedCacheEntry = await redisErrorHandler(
527
- 'RedisStringsHandler.get(), operation: get' +
528
- (this.redisGetDeduplication ? 'deduplicated' : '') +
529
- ' ' +
530
- this.getTimeoutMs +
531
- 'ms ' +
532
- this.keyPrefix +
533
- ' ' +
534
- key,
535
- redisGetOperation,
536
- );
537
-
538
- debug(
539
- 'green',
540
- 'RedisStringsHandler.get() finished with result (serializedCacheEntry)',
541
- serializedCacheEntry?.substring(0, 200),
542
- );
543
-
544
- if (!serializedCacheEntry) {
545
- return null;
546
- }
547
-
548
- let cacheEntry: CacheEntry | null;
549
- try {
550
- cacheEntry =
551
- await this.valueSerializer.deserialize(serializedCacheEntry);
552
- } catch (err) {
553
- // A decode failure (e.g. legacy/corrupted entry after swapping codecs,
554
- // bumping a compression level or rotating an encryption key) is treated
555
- // as a cache miss rather than a hard error - this matches the
556
- // null-from-deserialize semantics in the CacheValueSerializer contract
557
- // and prevents a single unreadable entry from incrementing
558
- // killContainerOnErrorCount on every read.
559
- console.warn(
560
- 'RedisStringsHandler.get() valueSerializer.deserialize failed, treating as cache miss',
561
- this.keyPrefix + key,
562
- err,
563
- );
564
- return null;
565
- }
566
-
567
- debug(
568
- 'green',
569
- 'RedisStringsHandler.get() finished with result (cacheEntry)',
570
- JSON.stringify(cacheEntry).substring(0, 200),
571
- );
572
-
573
- if (!cacheEntry) {
574
- return null;
575
- }
576
-
577
- if (!cacheEntry?.tags) {
578
- console.warn(
579
- 'RedisStringsHandler.get() called with',
580
- key,
581
- ctx,
582
- 'cacheEntry is mall formed (missing tags)',
583
- );
584
- }
585
- // value === null is a legitimate entry: the Pages Router stores
586
- // `notFound: true` results as a cache entry with a null value.
587
- // Only an absent value indicates a malformed entry.
588
- if (cacheEntry?.value === undefined) {
589
- console.warn(
590
- 'RedisStringsHandler.get() called with',
591
- key,
592
- ctx,
593
- 'cacheEntry is mall formed (missing value)',
594
- );
595
- }
596
- if (!cacheEntry?.lastModified) {
597
- console.warn(
598
- 'RedisStringsHandler.get() called with',
599
- key,
600
- ctx,
601
- 'cacheEntry is mall formed (missing lastModified)',
602
- );
603
- }
604
-
605
- if (ctx.kind === 'FETCH') {
606
- const combinedTags = new Set([
607
- ...(ctx?.softTags || []),
608
- ...(ctx?.tags || []),
609
- ]);
610
-
611
- if (combinedTags.size === 0) {
612
- return cacheEntry;
613
- }
614
-
615
- // INFO: implicit tags (revalidate of nested fetch in api route/page on revalidatePath call of the page/api route). See revalidateTag() for more information
616
- //
617
- // This code checks if any of the cache tags associated with this entry (normally the internal tag of the parent page/api route containing the fetch request)
618
- // have been revalidated since the entry was last modified. If any tag was revalidated more recently than the entry's
619
- // lastModified timestamp, then the cached content is considered stale (therefore return null) and should be removed.
620
- for (const tag of combinedTags) {
621
- // Get the last revalidation time for this tag from our revalidatedTagsMap
622
- const revalidationTime = this.revalidatedTagsMap.get(tag);
623
-
624
- // If we have a revalidation time for this tag and it's more recent than when
625
- // this cache entry was last modified, the entry is stale
626
- if (revalidationTime && revalidationTime > cacheEntry.lastModified) {
627
- const redisKey = this.keyPrefix + key;
628
-
629
- // We don't await this cleanup since it can happen asynchronously in the background.
630
- // The cache entry is already considered invalid at this point.
631
- this.client
632
- .unlink(redisKey)
633
- .catch((err) => {
634
- // Log error but don't retry deletion since the cache entry will likely be
635
- // updated immediately after via set(). A retry could dangerously execute
636
- // after the new value is set.
637
- console.error(
638
- 'Error occurred while unlinking stale data. Error was:',
639
- err,
640
- );
641
- })
642
- .finally(async () => {
643
- // Clean up our tag tracking maps after the Redis key is removed
644
- await this.sharedTagsMap.delete(key);
645
- await this.revalidatedTagsMap.delete(tag);
646
- });
647
-
648
- debug(
649
- 'green',
650
- 'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.',
651
- tag,
652
- redisKey,
653
- revalidationTime,
654
- cacheEntry,
655
- );
656
-
657
- // Return null to indicate no valid cache entry was found
658
- return null;
659
- }
660
- }
661
- }
662
-
663
- return cacheEntry;
664
- } catch (error) {
665
- // This catch block is necessary to handle any errors that may occur during:
666
- // 1. Redis operations (get, unlink)
667
- // 2. Tag validation and cleanup
668
- // (Deserialization errors are handled locally above and treated as cache misses,
669
- // so they do not reach this branch and do not count toward the kill threshold.)
670
- // If any error occurs, we return null to indicate no valid cache entry was found,
671
- // allowing the application to regenerate the content rather than crash
672
- console.error(
673
- '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:',
674
- error,
675
- killContainerOnErrorCount++,
676
- );
677
-
678
- if (
679
- this.killContainerOnErrorThreshold > 0 &&
680
- killContainerOnErrorCount >= this.killContainerOnErrorThreshold
681
- ) {
682
- console.error(
683
- 'RedisStringsHandler get() error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)',
684
- error,
685
- killContainerOnErrorCount,
686
- );
687
- this.client.disconnect();
688
- this.client.quit();
689
- setTimeout(() => {
690
- process.exit(1);
691
- }, 500);
692
- }
693
- return null;
694
- }
695
- }
696
- public async set(
697
- key: string,
698
- data: SetCacheValue,
699
- ctx: {
700
- isRoutePPREnabled: boolean;
701
- isFallback: boolean;
702
- tags?: string[];
703
- // Different versions of Next.js use different arguments for the same functionality
704
- revalidate?: number | false; // Version 15.0.3
705
- cacheControl?: { revalidate: number | false; expire: number | undefined }; // Version 15.0.3+
706
- },
707
- ) {
708
- try {
709
- if (
710
- data !== null &&
711
- !(SUPPORTED_SET_KINDS as readonly string[]).includes(data.kind)
712
- ) {
713
- console.warn(
714
- 'RedisStringsHandler.set() called with',
715
- key,
716
- ctx,
717
- data,
718
- `this cache handler is only designed and tested for kinds ${SUPPORTED_SET_KINDS.join(', ')} and not for kind`,
719
- (data as { kind: string })?.kind,
720
- );
721
- }
722
-
723
- await this.assertClientIsReady();
724
- if (this.isClientUnavailable()) {
725
- return;
726
- }
727
-
728
- if (data?.kind === 'APP_PAGE' || data?.kind === 'APP_ROUTE') {
729
- const tags = data.headers['x-next-cache-tags']?.split(',');
730
- ctx.tags = [...(ctx.tags || []), ...(tags || [])];
731
- }
732
-
733
- // Pages Router entries (PAGES, REDIRECT and null/notFound results) do not
734
- // carry an x-next-cache-tags header. Attach the implicit path tag
735
- // (_N_T_/<path>) so that revalidatePath()/revalidateTag('_N_T_/<path>')
736
- // invalidates them the same way as App Router entries.
737
- if (data === null || data.kind === 'PAGES' || data.kind === 'REDIRECT') {
738
- const implicitTag = NEXT_CACHE_IMPLICIT_TAG_ID + key;
739
- if (!ctx.tags?.includes(implicitTag)) {
740
- ctx.tags = [...(ctx.tags || []), implicitTag];
741
- }
742
- }
743
-
744
- // Constructing and serializing the value for storing it in redis
745
- const cacheEntry: CacheEntry = {
746
- lastModified: Date.now(),
747
- tags: ctx?.tags || [],
748
- value: data,
749
- };
750
- const serializedCacheEntry =
751
- await this.valueSerializer.serialize(cacheEntry);
752
-
753
- // pre seed data into deduplicated get client. This will reduce redis load by not requesting
754
- // the same value from redis which was just set.
755
- if (this.redisGetDeduplication) {
756
- this.redisDeduplicationHandler.seedRequestReturn(
757
- key,
758
- serializedCacheEntry,
759
- );
760
- }
761
-
762
- const expireAt = resolveCacheEntryTtlSeconds(ctx, data, {
763
- estimateExpireAge: this.estimateExpireAge,
764
- defaultStaleAge: this.defaultStaleAge,
765
- });
766
-
767
- // Setting the cache entry in redis
768
- const setOperation: Promise<string | null> = redisErrorHandler(
769
- 'RedisStringsHandler.set(), operation: set ' +
770
- this.keyPrefix +
771
- ' ' +
772
- key,
773
- this.client.set(this.keyPrefix + key, serializedCacheEntry, {
774
- ...(expireAt ? { EX: expireAt } : {}),
775
- }),
776
- );
777
-
778
- debug(
779
- 'blue',
780
- 'RedisStringsHandler.set() will set the following serializedCacheEntry',
781
- this.keyPrefix,
782
- key,
783
- data,
784
- ctx,
785
- serializedCacheEntry?.substring(0, 200),
786
- expireAt,
787
- );
788
-
789
- // Setting the tags for the cache entry in the sharedTagsMap (locally stored hashmap synced via redis)
790
- let setTagsOperation: Promise<void> | undefined;
791
- if (ctx.tags && ctx.tags.length > 0) {
792
- const currentTags = this.sharedTagsMap.get(key);
793
- const currentIsSameAsNew =
794
- currentTags?.length === ctx.tags.length &&
795
- currentTags.every((v) => ctx.tags!.includes(v)) &&
796
- ctx.tags.every((v) => currentTags.includes(v));
797
-
798
- if (!currentIsSameAsNew) {
799
- setTagsOperation = this.sharedTagsMap.set(
800
- key,
801
- structuredClone(ctx.tags) as string[],
802
- );
803
- }
804
- }
805
-
806
- debug(
807
- 'blue',
808
- 'RedisStringsHandler.set() will set the following sharedTagsMap',
809
- key,
810
- ctx.tags as string[],
811
- );
812
-
813
- await Promise.all([setOperation, setTagsOperation]);
814
- } catch (error) {
815
- console.error(
816
- 'RedisStringsHandler.set() Error occurred while setting cache entry. The original error was:',
817
- error,
818
- killContainerOnErrorCount++,
819
- );
820
- if (
821
- this.killContainerOnErrorThreshold > 0 &&
822
- killContainerOnErrorCount >= this.killContainerOnErrorThreshold
823
- ) {
824
- console.error(
825
- 'RedisStringsHandler set() error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)',
826
- error,
827
- killContainerOnErrorCount,
828
- );
829
- this.client.disconnect();
830
- this.client.quit();
831
- setTimeout(() => {
832
- process.exit(1);
833
- }, 500);
834
- }
835
- throw error;
836
- }
837
- }
838
-
839
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
840
- public async revalidateTag(tagOrTags: string | string[], ...rest: any[]) {
841
- try {
842
- debug(
843
- 'red',
844
- 'RedisStringsHandler.revalidateTag() called with',
845
- tagOrTags,
846
- rest,
847
- );
848
- const tags = new Set([tagOrTags || []].flat());
849
- await this.assertClientIsReady();
850
- if (this.isClientUnavailable()) {
851
- return;
852
- }
853
-
854
- // find all keys that are related to this tag
855
- const keysToDelete: Set<string> = new Set();
856
-
857
- for (const tag of tags) {
858
- // INFO: implicit tags (revalidate of nested fetch in api route/page on revalidatePath call of the page/api route)
859
- //
860
- // Invalidation logic for fetch requests that are related to a invalidated page.
861
- // revalidateTag is called for the page tag (_N_T_...) and the fetch request needs to be invalidated as well
862
- // unfortunately this is not possible since the revalidateTag is not called with any data that would allow us to find the cache entry of the fetch request
863
- // in case of a fetch request get method call, the get method of the cache handler is called with some information about the pages/routes the fetch request is inside
864
- // therefore we only mark the page/route as stale here (with help of the revalidatedTagsMap)
865
- // and delete the cache entry of the fetch request on the next request to the get function
866
- if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) {
867
- const now = Date.now();
868
- debug(
869
- 'red',
870
- 'RedisStringsHandler.revalidateTag() set revalidation time for tag',
871
- tag,
872
- 'to',
873
- now,
874
- );
875
- await this.revalidatedTagsMap.set(tag, now);
876
- }
877
- }
878
-
879
- // Scan the whole sharedTagsMap for keys that are dependent on any of the revalidated tags
880
- for (const [key, sharedTags] of this.sharedTagsMap.entries()) {
881
- if (sharedTags.some((tag) => tags.has(tag))) {
882
- keysToDelete.add(key);
883
- }
884
- }
885
-
886
- debug(
887
- 'red',
888
- 'RedisStringsHandler.revalidateTag() found',
889
- keysToDelete,
890
- 'keys to delete',
891
- );
892
-
893
- // exit early if no keys are related to this tag
894
- if (keysToDelete.size === 0) {
895
- return;
896
- }
897
-
898
- // prepare deletion of all keys in redis that are related to this tag
899
- const redisKeys = Array.from(keysToDelete);
900
- const fullRedisKeys = redisKeys.map((key) => this.keyPrefix + key);
901
- const deleteKeysOperation = redisErrorHandler(
902
- 'RedisStringsHandler.revalidateTag(), operation: unlink ' +
903
- this.keyPrefix +
904
- ' ' +
905
- fullRedisKeys,
906
- this.client.unlink(fullRedisKeys),
907
- );
908
-
909
- // also delete entries from in-memory deduplication cache if they get revalidated
910
- if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
911
- for (const key of keysToDelete) {
912
- this.inMemoryDeduplicationCache.delete(key);
913
- }
914
- }
915
-
916
- // prepare deletion of entries from shared tags map if they get revalidated so that the map will not grow indefinitely
917
- const deleteTagsOperation = this.sharedTagsMap.delete(redisKeys);
918
-
919
- // execute keys and tag maps deletion
920
- await Promise.all([deleteKeysOperation, deleteTagsOperation]);
921
- debug(
922
- 'red',
923
- 'RedisStringsHandler.revalidateTag() finished delete operations',
924
- );
925
- } catch (error) {
926
- console.error(
927
- 'RedisStringsHandler.revalidateTag() Error occurred while revalidating tags. The original error was:',
928
- error,
929
- killContainerOnErrorCount++,
930
- );
931
- if (
932
- this.killContainerOnErrorThreshold > 0 &&
933
- killContainerOnErrorCount >= this.killContainerOnErrorThreshold
934
- ) {
935
- console.error(
936
- 'RedisStringsHandler revalidateTag() error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)',
937
- error,
938
- killContainerOnErrorCount,
939
- );
940
- this.client.disconnect();
941
- this.client.quit();
942
- setTimeout(() => {
943
- process.exit(1);
944
- }, 500);
945
- }
946
- throw error;
947
- }
948
- }
949
- }