@trieb.work/nextjs-turbo-redis-cache 1.15.0 → 1.15.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 (246) hide show
  1. package/.cursor/skills/testing/SKILL.md +121 -0
  2. package/.github/workflows/ci.yml +155 -70
  3. package/ARCHITECTURE.md +431 -0
  4. package/CHANGELOG.md +7 -0
  5. package/README.md +72 -47
  6. package/dist/index.js +78 -14
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +78 -14
  9. package/dist/index.mjs.map +1 -1
  10. package/eslint.config.mjs +1 -1
  11. package/package.json +14 -16
  12. package/playwright.config.ts +5 -3
  13. package/src/CacheComponentsHandler.ts +57 -2
  14. package/src/SyncedMap.ts +62 -13
  15. package/test/README.md +179 -0
  16. package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/package.json +6 -6
  17. package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-15-4-11}/pnpm-lock.yaml +89 -105
  18. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/package.json +3 -3
  19. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/pnpm-lock.yaml +53 -53
  20. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/package.json +28 -0
  21. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/pnpm-lock.yaml +4206 -0
  22. package/test/nextjs-test-projects/next-app-16-2-6/eslint.config.mjs +18 -0
  23. package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/package.json +3 -3
  24. package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/pnpm-lock.yaml +54 -60
  25. package/test/nextjs-test-projects/next-app-16-2-6/postcss.config.mjs +7 -0
  26. package/test/nextjs-test-projects/next-app-16-2-6/src/app/favicon.ico +0 -0
  27. package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-2-6}/tsconfig.json +9 -2
  28. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/cache-handler.js +3 -0
  29. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/next.config.ts +13 -0
  30. package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6-cache-components}/package.json +3 -3
  31. package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6-cache-components}/pnpm-lock.yaml +54 -60
  32. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-static-fetch/route.ts +19 -0
  33. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-cachelife/route.ts +24 -0
  34. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-tag/route.ts +21 -0
  35. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidate-tag/route.ts +19 -0
  36. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidated-fetch/route.ts +19 -0
  37. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/cachelife-short/page.tsx +110 -0
  38. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/page.tsx +90 -0
  39. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +127 -0
  40. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +130 -0
  41. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +127 -0
  42. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +110 -0
  43. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/layout.tsx +0 -2
  44. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/update-tag-test/page.tsx +22 -0
  45. package/{tests → test/playwright}/update-tag.spec.ts +1 -1
  46. package/test/{integration → vitest/integration}/build-id-prefix.integration.test.ts +2 -2
  47. package/test/{cache-components/cache-components.integration.spec.ts → vitest/integration/cache-components/cache-components.integration.test.ts} +7 -3
  48. package/test/{cache-components/redis-kill-reconnect.spec.ts → vitest/integration/cache-components/redis-kill-reconnect.test.ts} +28 -7
  49. package/test/vitest/integration/cache-components/redis-quit-vs-disconnect.test.ts +59 -0
  50. package/test/vitest/integration/cache-components/redis-subscriber-outage.test.ts +100 -0
  51. package/test/{cache-components → vitest/integration/cache-components}/scripts/redis-kill-reconnect.ts +31 -8
  52. package/test/vitest/integration/cache-components/scripts/redis-quit-vs-disconnect.ts +113 -0
  53. package/test/vitest/integration/cache-components/scripts/redis-subscriber-outage.ts +379 -0
  54. package/test/vitest/integration/cache-components/scripts/redis-test-helpers.ts +155 -0
  55. package/test/{integration → vitest/integration}/nextjs-cache-handler.integration.test.ts +15 -9
  56. package/test/vitest/unit/SyncedMap-reconnect-repro.test.ts +154 -0
  57. package/{src → test/vitest/unit}/index.test.ts +3 -3
  58. package/test/{cache-components/reconnect-socket-already-opened.spec.ts → vitest/unit/reconnect-socket-already-opened.test.ts} +1 -1
  59. package/{src → test/vitest/unit}/serializer.test.ts +7 -3
  60. package/{src → test/vitest/unit}/utils/prefix.test.ts +1 -1
  61. package/vite.config.ts +4 -3
  62. package/vitest.cache-components.config.ts +1 -1
  63. package/test/browser/update-tag.browser.test.ts +0 -41
  64. package/test/integration/next-app-15-0-3/next.config.js +0 -6
  65. package/test/integration/next-app-15-0-3/package-lock.json +0 -5833
  66. package/test/integration/next-app-15-0-3/pnpm-lock.yaml +0 -3710
  67. package/test/integration/next-app-15-3-2/next.config.js +0 -6
  68. package/test/integration/next-app-15-3-2/package.json +0 -33
  69. package/test/integration/next-app-15-3-2/pnpm-lock.yaml +0 -3720
  70. package/test/integration/next-app-15-3-2/postcss.config.mjs +0 -5
  71. package/test/integration/next-app-15-3-2/src/app/api/revalidateTag/route.ts +0 -15
  72. package/test/integration/next-app-15-4-7/eslint.config.mjs +0 -16
  73. package/test/integration/next-app-15-4-7/package-lock.json +0 -5969
  74. package/test/integration/next-app-15-4-7/package.json +0 -33
  75. package/test/integration/next-app-15-4-7/postcss.config.mjs +0 -5
  76. package/test/integration/next-app-15-4-7/src/app/api/revalidateTag/route.ts +0 -15
  77. package/test/integration/next-app-15-4-7/tsconfig.json +0 -27
  78. package/test/integration/next-app-16-0-3/src/app/api/cached-static-fetch/route.ts +0 -18
  79. package/test/integration/next-app-16-0-3/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  80. package/test/integration/next-app-16-0-3/src/app/api/revalidatePath/route.ts +0 -15
  81. package/test/integration/next-app-16-0-3/src/app/api/revalidated-fetch/route.ts +0 -17
  82. package/test/integration/next-app-16-0-3/src/app/api/uncached-fetch/route.ts +0 -15
  83. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  84. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  85. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  86. package/test/integration/next-app-16-0-3/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  87. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  88. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  89. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  90. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  91. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  92. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  93. package/test/integration/next-app-16-2-3/src/app/api/cached-static-fetch/route.ts +0 -18
  94. package/test/integration/next-app-16-2-3/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  95. package/test/integration/next-app-16-2-3/src/app/api/revalidatePath/route.ts +0 -15
  96. package/test/integration/next-app-16-2-3/src/app/api/revalidated-fetch/route.ts +0 -17
  97. package/test/integration/next-app-16-2-3/src/app/api/uncached-fetch/route.ts +0 -15
  98. package/test/integration/next-app-16-2-3/src/app/globals.css +0 -26
  99. package/test/integration/next-app-16-2-3/src/app/layout.tsx +0 -59
  100. package/test/integration/next-app-16-2-3/src/app/page.tsx +0 -755
  101. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  102. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  103. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  104. package/test/integration/next-app-16-2-3/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  105. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  106. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  107. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  108. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  109. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  110. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  111. package/test/integration/next-app-16-2-3/src/app/revalidation-interface.tsx +0 -267
  112. package/test/integration/next-app-16-2-3-cache-components/public/file.svg +0 -1
  113. package/test/integration/next-app-16-2-3-cache-components/public/globe.svg +0 -1
  114. package/test/integration/next-app-16-2-3-cache-components/public/next.svg +0 -1
  115. package/test/integration/next-app-16-2-3-cache-components/public/vercel.svg +0 -1
  116. package/test/integration/next-app-16-2-3-cache-components/public/window.svg +0 -1
  117. package/test/integration/next-app-customized/README.md +0 -36
  118. package/test/integration/next-app-customized/eslint.config.mjs +0 -16
  119. package/vitest.browser.config.ts +0 -10
  120. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/README.md +0 -0
  121. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/eslint.config.mjs +0 -0
  122. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-15-4-11}/next.config.js +0 -0
  123. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-15-4-11}/package-lock.json +0 -0
  124. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/postcss.config.mjs +0 -0
  125. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/file.svg +0 -0
  126. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/globe.svg +0 -0
  127. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/next.svg +0 -0
  128. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/vercel.svg +0 -0
  129. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/window.svg +0 -0
  130. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/cached-static-fetch/route.ts +0 -0
  131. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -0
  132. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidatePath/route.ts +0 -0
  133. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidateTag/route.ts +0 -0
  134. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidated-fetch/route.ts +0 -0
  135. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/uncached-fetch/route.ts +0 -0
  136. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/globals.css +0 -0
  137. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/layout.tsx +0 -0
  138. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/page.tsx +0 -0
  139. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -0
  140. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -0
  141. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  142. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/no-fetch/default-page/page.tsx +0 -0
  143. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -0
  144. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -0
  145. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  146. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -0
  147. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -0
  148. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  149. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/revalidation-interface.tsx +0 -0
  150. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/tsconfig.json +0 -0
  151. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/README.md +0 -0
  152. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/eslint.config.mjs +0 -0
  153. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/next.config.ts +0 -0
  154. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/postcss.config.mjs +0 -0
  155. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/cached-static-fetch/route.ts +0 -0
  156. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -0
  157. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidatePath/route.ts +0 -0
  158. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidateTag/route.ts +0 -0
  159. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidated-fetch/route.ts +0 -0
  160. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/uncached-fetch/route.ts +0 -0
  161. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/src/app/favicon.ico +0 -0
  162. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/globals.css +0 -0
  163. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/layout.tsx +0 -0
  164. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/page.tsx +0 -0
  165. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -0
  166. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -0
  167. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  168. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/no-fetch/default-page/page.tsx +0 -0
  169. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -0
  170. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -0
  171. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  172. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -0
  173. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -0
  174. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  175. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/revalidation-interface.tsx +0 -0
  176. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/src/app/update-tag-test/page.tsx +0 -0
  177. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/tsconfig.json +0 -0
  178. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/README.md +0 -0
  179. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/cache-handler.js +0 -0
  180. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/eslint.config.mjs +0 -0
  181. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/next.config.ts +0 -0
  182. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/postcss.config.mjs +0 -0
  183. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/file.svg +0 -0
  184. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/globe.svg +0 -0
  185. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/next.svg +0 -0
  186. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/vercel.svg +0 -0
  187. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/window.svg +0 -0
  188. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/api/cached-static-fetch/route.ts +0 -0
  189. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/api/cached-with-cachelife/route.ts +0 -0
  190. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/api/cached-with-tag/route.ts +0 -0
  191. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/api/revalidate-tag/route.ts +0 -0
  192. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/api/revalidated-fetch/route.ts +0 -0
  193. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/cachelife-short/page.tsx +0 -0
  194. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/page.tsx +0 -0
  195. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/runtime-data-suspense/page.tsx +0 -0
  196. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/stale-while-revalidate/page.tsx +0 -0
  197. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/tag-invalidation/page.tsx +0 -0
  198. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/use-cache-nondeterministic/page.tsx +0 -0
  199. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/favicon.ico +0 -0
  200. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/globals.css +0 -0
  201. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/layout.tsx +0 -0
  202. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/page.tsx +0 -0
  203. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/revalidation-interface.tsx +0 -0
  204. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/update-tag-test/page.tsx +0 -0
  205. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/tsconfig.json +0 -0
  206. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/README.md +0 -0
  207. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/next.config.ts +0 -0
  208. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/cached-static-fetch/route.ts +0 -0
  209. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -0
  210. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidatePath/route.ts +0 -0
  211. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidateTag/route.ts +0 -0
  212. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidated-fetch/route.ts +0 -0
  213. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/uncached-fetch/route.ts +0 -0
  214. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/globals.css +0 -0
  215. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/layout.tsx +0 -0
  216. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/page.tsx +0 -0
  217. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -0
  218. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -0
  219. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  220. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/no-fetch/default-page/page.tsx +0 -0
  221. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -0
  222. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -0
  223. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  224. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -0
  225. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -0
  226. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  227. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/revalidation-interface.tsx +0 -0
  228. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/src/app/update-tag-test/page.tsx +0 -0
  229. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/README.md +0 -0
  230. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/eslint.config.mjs +0 -0
  231. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/postcss.config.mjs +0 -0
  232. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/file.svg +0 -0
  233. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/globe.svg +0 -0
  234. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/next.svg +0 -0
  235. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/vercel.svg +0 -0
  236. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/window.svg +0 -0
  237. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/favicon.ico +0 -0
  238. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/globals.css +0 -0
  239. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/page.tsx +0 -0
  240. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/revalidation-interface.tsx +0 -0
  241. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/tsconfig.json +0 -0
  242. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-customized}/README.md +0 -0
  243. /package/test/{integration → nextjs-test-projects}/next-app-customized/customized-cache-handler.js +0 -0
  244. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-customized}/eslint.config.mjs +0 -0
  245. /package/test/{integration → nextjs-test-projects}/next-app-customized/next.config.js +0 -0
  246. /package/{tests → test/playwright}/cache-lab.spec.ts +0 -0
@@ -6,6 +6,7 @@ import {
6
6
  redisErrorHandler,
7
7
  } from './RedisStringsHandler';
8
8
  import { SyncedMap } from './SyncedMap';
9
+ import { DeduplicatedRequestHandler } from './DeduplicatedRequestHandler';
9
10
  import { debug } from './utils/debug';
10
11
  import { resolveKeyPrefix } from './utils/prefix';
11
12
 
@@ -87,8 +88,19 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
87
88
  private client: Client;
88
89
  private revalidatedTagsMap: SyncedMap<number>;
89
90
  private sharedTagsMap: SyncedMap<string[]>;
91
+ private inMemoryDeduplicationCache: SyncedMap<
92
+ Promise<ReturnType<Client['get']>>
93
+ >;
90
94
  private keyPrefix: string;
91
95
  private getTimeoutMs: number;
96
+ private redisGet: Client['get'];
97
+ private redisDeduplicationHandler: DeduplicatedRequestHandler<
98
+ Client['get'],
99
+ string | Buffer | null
100
+ >;
101
+ private deduplicatedRedisGet: (key: string) => Client['get'];
102
+ private redisGetDeduplication: boolean;
103
+ private inMemoryCachingTime: number;
92
104
 
93
105
  constructor({
94
106
  redisUrl = process.env.REDIS_URL
@@ -103,6 +115,8 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
103
115
  : 500,
104
116
  revalidateTagQuerySize = 250,
105
117
  avgResyncIntervalMs = 60 * 60 * 1_000,
118
+ redisGetDeduplication = true,
119
+ inMemoryCachingTime = 10_000,
106
120
  socketOptions,
107
121
  clientOptions,
108
122
  killContainerOnErrorThreshold = process.env
@@ -118,6 +132,8 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
118
132
  env: process.env,
119
133
  });
120
134
  this.getTimeoutMs = getTimeoutMs;
135
+ this.redisGetDeduplication = redisGetDeduplication;
136
+ this.inMemoryCachingTime = inMemoryCachingTime;
121
137
 
122
138
  this.client = createClient({
123
139
  url: redisUrl,
@@ -208,6 +224,29 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
208
224
  avgResyncIntervalMs / 10 +
209
225
  Math.random() * (avgResyncIntervalMs / 10),
210
226
  });
227
+
228
+ this.inMemoryDeduplicationCache = new SyncedMap({
229
+ client: this.client,
230
+ keyPrefix: this.keyPrefix,
231
+ redisKey: '__cacheComponents_inMemoryDeduplicationCache__',
232
+ database,
233
+ querySize: revalidateTagQuerySize,
234
+ filterKeys,
235
+ customizedSync: {
236
+ withoutRedisHashmap: true,
237
+ withoutSetSync: true,
238
+ },
239
+ });
240
+
241
+ const redisGet: Client['get'] = this.client.get.bind(this.client);
242
+ this.redisDeduplicationHandler = new DeduplicatedRequestHandler(
243
+ redisGet,
244
+ inMemoryCachingTime,
245
+ this.inMemoryDeduplicationCache,
246
+ );
247
+ this.redisGet = redisGet;
248
+ this.deduplicatedRedisGet =
249
+ this.redisDeduplicationHandler.deduplicatedFunction;
211
250
  } catch (error) {
212
251
  console.error('RedisCacheComponentsHandler constructor error', error);
213
252
  throw error;
@@ -253,12 +292,18 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
253
292
  try {
254
293
  await this.assertClientIsReady();
255
294
 
295
+ const clientGet = this.redisGetDeduplication
296
+ ? this.deduplicatedRedisGet(cacheKey)
297
+ : this.redisGet;
298
+
256
299
  const serialized = await redisErrorHandler(
257
- 'RedisCacheComponentsHandler.get(), operation: get ' +
300
+ 'RedisCacheComponentsHandler.get(), operation: get' +
301
+ (this.redisGetDeduplication ? ' deduplicated' : '') +
302
+ ' ' +
258
303
  this.getTimeoutMs +
259
304
  'ms ' +
260
305
  redisKey,
261
- this.client.get(
306
+ clientGet(
262
307
  commandOptions({ signal: AbortSignal.timeout(this.getTimeoutMs) }),
263
308
  redisKey,
264
309
  ),
@@ -355,6 +400,10 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
355
400
  throw jsonError;
356
401
  }
357
402
 
403
+ if (this.redisGetDeduplication) {
404
+ this.redisDeduplicationHandler.seedRequestReturn(cacheKey, serialized);
405
+ }
406
+
358
407
  // expire is already a duration in seconds, use it directly
359
408
  const ttlSeconds =
360
409
  Number.isFinite(stored.expire) && stored.expire > 0
@@ -450,6 +499,12 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
450
499
  this.client.unlink(fullRedisKeys),
451
500
  );
452
501
 
502
+ if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
503
+ for (const key of keysToDelete) {
504
+ this.inMemoryDeduplicationCache.delete(key);
505
+ }
506
+ }
507
+
453
508
  // Delete from sharedTagsMap
454
509
  const deleteTagsOperation = this.sharedTagsMap.delete(cacheKeys);
455
510
  await deleteTagsOperation;
package/src/SyncedMap.ts CHANGED
@@ -41,6 +41,7 @@ export class SyncedMap<V> {
41
41
 
42
42
  private setupLock: Promise<void>;
43
43
  private setupLockResolve!: () => void;
44
+ private isReconnecting = false;
44
45
 
45
46
  constructor(options: SyncedMapOptions) {
46
47
  this.client = options.client;
@@ -209,6 +210,16 @@ export class SyncedMap<V> {
209
210
  };
210
211
 
211
212
  try {
213
+ // Attach error handler BEFORE subscribing so that errors during
214
+ // subscribe or after connection loss are always caught and trigger
215
+ // a reconnection. Previously this was attached after the subscribe()
216
+ // calls, which meant if subscribe threw during an outage the handler
217
+ // was never attached and the subscriber was permanently dead.
218
+ this.subscriberClient.on('error', async (err) => {
219
+ console.error('Subscriber client error:', err);
220
+ await this.reconnectSubscriber();
221
+ });
222
+
212
223
  const connectIfNeeded = async () => {
213
224
  // Avoid overlapping connect() calls which can lead to
214
225
  // "Socket already opened" errors when a connect is already in progress
@@ -241,7 +252,9 @@ export class SyncedMap<V> {
241
252
  throw new Error(
242
253
  'Keyspace event configuration is set to "' +
243
254
  keyspaceEventConfig +
244
- "\" but has to include 'E' for Keyevent events, published with __keyevent@<db>__ prefix. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`",
255
+ " but has to include 'E' for Keyevent events, " +
256
+ 'published with __keyevent@<db>__ prefix. We recommend to set it to ' +
257
+ "'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`",
245
258
  );
246
259
  }
247
260
  if (
@@ -254,7 +267,9 @@ export class SyncedMap<V> {
254
267
  throw new Error(
255
268
  'Keyspace event configuration is set to "' +
256
269
  keyspaceEventConfig +
257
- "\" but has to include 'A' or 'x' and 'e' for expired and evicted events. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`",
270
+ " but has to include 'A' or 'x' and 'e' for expired and " +
271
+ 'evicted events. We recommend to set it to ' +
272
+ "'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`",
258
273
  );
259
274
  }
260
275
  }
@@ -275,24 +290,58 @@ export class SyncedMap<V> {
275
290
  keyEventHandler,
276
291
  ),
277
292
  ]);
293
+ } catch (error) {
294
+ console.error('Error setting up pub/sub client:', error);
295
+ throw error;
296
+ }
297
+ }
278
298
 
279
- // Error handling for reconnection
280
- this.subscriberClient.on('error', async (err) => {
281
- console.error('Subscriber client error:', err);
299
+ /**
300
+ * Reconnect the subscriber client with exponential backoff.
301
+ * Guarded by `isReconnecting` to prevent concurrent reconnection
302
+ * attempts when multiple error events fire during an outage.
303
+ * Retries indefinitely (with capped delay) so the subscriber
304
+ * eventually recovers once Redis is available again.
305
+ */
306
+ private async reconnectSubscriber(): Promise<void> {
307
+ if (this.isReconnecting) return;
308
+ this.isReconnecting = true;
309
+
310
+ try {
311
+ let attempt = 0;
312
+ while (true) {
282
313
  try {
283
- await this.subscriberClient.quit();
314
+ // Forcibly tear down the old subscriber so its internal reconnect
315
+ // loop cannot come back to life once Redis recovers. `quit()` waits
316
+ // for the server and can silently fail during an outage, leaving a
317
+ // zombie client that still fires our error handler. Remove that
318
+ // handler first so a delayed error from the old socket cannot tear
319
+ // down the replacement subscriber we are about to create.
320
+ try {
321
+ this.subscriberClient.removeAllListeners('error');
322
+ await this.subscriberClient.disconnect();
323
+ } catch {
324
+ // expected during outage
325
+ }
326
+ // Create a fresh subscriber client
284
327
  this.subscriberClient = this.client.duplicate();
285
328
  await this.setupPubSub();
286
- } catch (reconnectError) {
329
+ return; // success
330
+ } catch (error) {
331
+ const delay = Math.min(
332
+ 500 * Math.pow(2, Math.min(attempt, 5)),
333
+ 10_000,
334
+ );
335
+ attempt++;
287
336
  console.error(
288
- 'Failed to reconnect subscriber client:',
289
- reconnectError,
337
+ `Subscriber reconnect attempt ${attempt} failed, retrying in ${delay}ms`,
338
+ error,
290
339
  );
340
+ await new Promise((r) => setTimeout(r, delay));
291
341
  }
292
- });
293
- } catch (error) {
294
- console.error('Error setting up pub/sub client:', error);
295
- throw error;
342
+ }
343
+ } finally {
344
+ this.isReconnecting = false;
296
345
  }
297
346
  }
298
347
 
package/test/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # Test Structure
2
+
3
+ ```
4
+ test/
5
+ ├── playwright/ # E2E tests — browser automation (Playwright)
6
+ ├── vitest/ # Unit + Integration tests (Vitest)
7
+ │ ├── unit/ # Pure unit tests (no external deps)
8
+ │ └── integration/ # Integration tests (Redis + Next.js server)
9
+ │ └── cache-components/ # Cache Components integration (Next.js 16+)
10
+ └── nextjs-test-projects/ # Next.js app fixtures (shared across test types)
11
+ ```
12
+
13
+ ## Overview
14
+
15
+ | Layer | Runner | What it validates | Needs Redis? | Needs Next.js app? |
16
+ | --------------- | ---------- | ----------------------------------------------------------------------- | ------------ | ------------------ |
17
+ | **Unit** | Vitest | Logic in isolation (serializer, prefix resolution, reconnect handling) | No | No |
18
+ | **Integration** | Vitest | Cache handler ↔ Redis ↔ Next.js HTTP responses (server-side plumbing) | Yes | Yes |
19
+ | **E2E** | Playwright | User-facing behavior in a real browser (Cache Components / `use cache`) | Yes | Yes |
20
+
21
+ ---
22
+
23
+ ## Unit Tests (`test/vitest/unit/`)
24
+
25
+ **Runner:** Vitest
26
+ **Command:** `pnpm test:unit`
27
+ **Config:** `vite.config.ts`
28
+
29
+ Fast tests with no external dependencies. Mocks are used where needed.
30
+
31
+ | File | What it tests |
32
+ | ----------------------------------------- | -------------------------------------------------------------------------------- |
33
+ | `serializer.test.ts` | `CacheValueSerializer` interface, JSON round-trips, singleton stability |
34
+ | `index.test.ts` | `RedisStringsHandler` constructor options, default behaviors |
35
+ | `utils/prefix.test.ts` | `resolveKeyPrefix` logic (BUILD_ID fallback, env var precedence) |
36
+ | `reconnect-socket-already-opened.test.ts` | Regression: reconnect logic doesn't call `connect()` when socket is already open |
37
+
38
+ ```bash
39
+ pnpm test:unit # single run
40
+ pnpm test:unit:watch # watch mode
41
+ pnpm test:unit:coverage # with coverage report (used in CI)
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Integration Tests (`test/vitest/integration/`)
47
+
48
+ **Runner:** Vitest
49
+ **Command:** `pnpm test:integration`
50
+ **Config:** `vite.config.ts`
51
+ **Requires:** Redis on localhost:6379, pre-built Next.js test app
52
+
53
+ These tests spawn a real Next.js server as a child process, make `fetch()` requests against it, and verify both HTTP responses and Redis state directly.
54
+
55
+ ### Standard Integration (`nextjs-cache-handler.integration.test.ts`)
56
+
57
+ Full cache lifecycle: static pages, fetch caching, revalidation, tag invalidation, TTL behavior. In CI this runs against a matrix of Next.js versions (15.4–16.2).
58
+
59
+ ```bash
60
+ pnpm test:integration
61
+ ```
62
+
63
+ ### BUILD_ID Prefix (`build-id-prefix.integration.test.ts`)
64
+
65
+ Verifies that when neither `KEY_PREFIX` nor `VERCEL_URL` is set, the handler falls back to `.next/BUILD_ID` as the Redis key prefix. Runs in its own CI job because it needs a clean environment without those env vars.
66
+
67
+ ```bash
68
+ pnpm test:integration:build-id-prefix
69
+ ```
70
+
71
+ ### Cache Components (`cache-components/`)
72
+
73
+ Integration tests specific to Next.js 16 Cache Components (`use cache`). Uses `next-app-16-2-6-cache-components` as the test app.
74
+
75
+ | File | What it tests |
76
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------------ |
77
+ | `cache-components.integration.test.ts` | `use cache` lifecycle: store, retrieve, tag invalidation, `cacheLife` expiry |
78
+ | `redis-kill-reconnect.test.ts` | Graceful recovery when Redis drops and reconnects (main client ping only) |
79
+ | `redis-subscriber-outage.test.ts` | Issue #86: subscriber PubSub does not recover after Redis outage (expected to fail until bug is fixed) |
80
+ | `redis-quit-vs-disconnect.test.ts` | `quit()` does not reliably close a subscriber during an outage; `disconnect()` does |
81
+
82
+ `redis-kill-reconnect.test.ts` uses a container runtime and auto-detects `podman` first, then `docker`. You can force runtime selection with `CONTAINER_RUNTIME=podman` or `CONTAINER_RUNTIME=docker`.
83
+
84
+ ```bash
85
+ pnpm test:integration:cache-components
86
+ ```
87
+
88
+ **Prerequisites for all integration tests:**
89
+
90
+ ```bash
91
+ cd test/nextjs-test-projects/<app-name>
92
+ pnpm install && pnpm build
93
+ ```
94
+
95
+ ---
96
+
97
+ ## E2E Tests (`test/playwright/`)
98
+
99
+ **Runner:** Playwright
100
+ **Command:** `pnpm test:e2e`
101
+ **Config:** `playwright.config.ts`
102
+ **Requires:** Redis on localhost:6379 (Playwright auto-starts `next-app-16-2-6-cache-components` via `webServer`)
103
+
104
+ Browser-based tests that validate Cache Components behavior from the user's perspective. Playwright was introduced because the Cache Components (`use cache`) feature in Next.js 16 relies on interactions that `fetch()` alone cannot reproduce:
105
+
106
+ - **Server Actions** are triggered by form submissions / button clicks — requires a real browser context
107
+ - `**updateTag`\*\* must be called from within a Server Action — needs actual UI interaction to verify
108
+ - **Cookie-based cache keys** depend on the browser sending cookies during navigation
109
+ - **Stale-while-revalidate** effects are only observable through page reloads and DOM diffing
110
+
111
+ The Vitest cache-components integration tests verify the server-side plumbing (Redis state, HTTP responses). Playwright closes the gap by testing the full user-facing flow.
112
+
113
+ | File | What it tests |
114
+ | -------------------- | -------------------------------------------------------------------------------------------------------------------- |
115
+ | `cache-lab.spec.ts` | `use cache` stability, tag invalidation (`updateTag`/`revalidateTag`), runtime cookie-based cache keys, SWR behavior |
116
+ | `update-tag.spec.ts` | `updateTag` via Server Actions (button click → action → UI update) |
117
+
118
+ ```bash
119
+ pnpm test:e2e
120
+
121
+ # Test against a specific Next.js version:
122
+ PLAYWRIGHT_TEST_APP=next-app-16-0-11-cache-components pnpm test:e2e
123
+
124
+ # Or point at an already-running server:
125
+ PLAYWRIGHT_BASE_URL=http://localhost:3001 pnpm test:e2e
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Next.js Test Projects (`test/nextjs-test-projects/`)
131
+
132
+ Minimal Next.js applications used as fixtures. They are not test runners — they provide the server that tests run against.
133
+
134
+ | App | Next.js | Used by |
135
+ | ----------------------------------- | ------- | -------------------------------------------------------------- |
136
+ | `next-app-15-4-11` | 15.4.11 | Integration (matrix, default for local), build-id-prefix |
137
+ | `next-app-16-0-11` | 16.0.11 | Integration (matrix) |
138
+ | `next-app-16-2-6` | 16.2.6 | Integration (matrix) |
139
+ | `next-app-16-0-11-cache-components` | 16.0.11 | Integration (cache-components matrix), E2E (Playwright matrix) |
140
+ | `next-app-16-2-6-cache-components` | 16.2.6 | Integration (cache-components matrix), E2E (Playwright matrix) |
141
+ | `next-app-customized` | — | Example of custom config (referenced in project README) |
142
+
143
+ ---
144
+
145
+ ## CI Jobs
146
+
147
+ The CI workflow (`.github/workflows/ci.yml`) is structured as:
148
+
149
+ ```
150
+ lint-and-unit → Lint + Unit Tests + Coverage
151
+ ├── integration → Matrix: 3 Next.js versions (15.4–16.2)
152
+ ├── integration-build-id-prefix → Isolated BUILD_ID prefix test
153
+ ├── integration-cache-components → Matrix: 16.0.11 + 16.2.6 cache-components
154
+ └── e2e → Matrix: Playwright against 16.0.11 + 16.2.6
155
+ ```
156
+
157
+ `lint-and-unit` runs first as a gate. All other jobs run in parallel after it passes.
158
+
159
+ | CI Job | Test App(s) | What runs |
160
+ | ------------------------------ | --------------------------------------------------------- | --------------------------------------------------------------- |
161
+ | `lint-and-unit` | — | `pnpm lint` + `pnpm test:unit:coverage` |
162
+ | `integration` | `next-app-15-4-11`, `next-app-16-0-11`, `next-app-16-2-6` | `pnpm test:integration` (per matrix entry) |
163
+ | `integration-build-id-prefix` | `next-app-15-4-11` | `pnpm test:integration:build-id-prefix` |
164
+ | `integration-cache-components` | `next-app-16-{0-3,2-3}-cache-components` | `pnpm test:integration:cache-components` + Redis kill/reconnect |
165
+ | `e2e` | `next-app-16-{0-3,2-3}-cache-components` | `pnpm test:e2e` (Playwright) |
166
+
167
+ ---
168
+
169
+ ## Environment Variables
170
+
171
+ | Variable | Used by | Description |
172
+ | ----------------------- | ------------------------------ | ------------------------------------------------------------------------- |
173
+ | `NEXT_TEST_APP` | Integration | Which test app to use (default: `next-app-15-4-11`) |
174
+ | `CACHE_COMPONENTS_APP` | Integration (cache-components) | Which cache-components app (default: `next-app-16-2-6-cache-components`) |
175
+ | `PLAYWRIGHT_TEST_APP` | E2E | Which app Playwright starts (default: `next-app-16-2-6-cache-components`) |
176
+ | `PLAYWRIGHT_BASE_URL` | E2E | Override base URL (skips `webServer` auto-start) |
177
+ | `SKIP_BUILD` | Integration | Skip Next.js build if app is pre-built |
178
+ | `DEBUG_INTEGRATION` | Integration | Print child process stdout/stderr |
179
+ | `CACHE_COMPONENTS_PORT` | Integration (cache-components) | Port for the server (default: 3065) |
@@ -3,27 +3,27 @@
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "scripts": {
6
- "dev": "export VERCEL_URL=\"dev-test-$RANDOM\" && echo $VERCEL_URL && cd ../../.. && pnpm build && cd test/integration/next-app-15-0-3 && pnpm i && pnpm build && VERCEL_ENV=production REDISHOST=localhost REDISPORT=6379 DEBUG_CACHE_HANDLER=true pnpm start",
6
+ "dev": "export VERCEL_URL=\"dev-test-$RANDOM\" && echo $VERCEL_URL && cd ../../.. && pnpm build && cd test/nextjs-test-projects/next-app-15-4-11 && pnpm i && pnpm build && VERCEL_ENV=production REDISHOST=localhost REDISPORT=6379 DEBUG_CACHE_HANDLER=true pnpm start",
7
7
  "build": "next build",
8
8
  "start": "next start",
9
9
  "lint": "next lint"
10
10
  },
11
11
  "dependencies": {
12
- "react": "19.0.0-rc-66855b96-20241106",
13
- "react-dom": "19.0.0-rc-66855b96-20241106",
14
- "next": "15.0.3",
12
+ "react": "19.2.0",
13
+ "react-dom": "19.2.0",
14
+ "next": "15.4.11",
15
15
  "redis": "4.7.0",
16
16
  "@trieb.work/nextjs-turbo-redis-cache": "file:../../../"
17
17
  },
18
18
  "devDependencies": {
19
19
  "typescript": "^5",
20
- "@types/node": "^20",
20
+ "@types/node": "^22",
21
21
  "@types/react": "^19",
22
22
  "@types/react-dom": "^19",
23
23
  "@tailwindcss/postcss": "^4",
24
24
  "tailwindcss": "^4",
25
25
  "eslint": "^9",
26
- "eslint-config-next": "15.3.0",
26
+ "eslint-config-next": "15.4.11",
27
27
  "@eslint/eslintrc": "^3"
28
28
  }
29
29
  }