@trieb.work/nextjs-turbo-redis-cache 1.14.1 → 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 (256) hide show
  1. package/.cursor/skills/testing/SKILL.md +121 -0
  2. package/.github/workflows/ci.yml +155 -70
  3. package/.github/workflows/pages.yml +38 -0
  4. package/ARCHITECTURE.md +431 -0
  5. package/CHANGELOG.md +19 -0
  6. package/README.md +220 -48
  7. package/dist/index.d.mts +77 -2
  8. package/dist/index.d.ts +77 -2
  9. package/dist/index.js +109 -23
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +106 -23
  12. package/dist/index.mjs.map +1 -1
  13. package/docs/index.html +139 -0
  14. package/docs/robots.txt +4 -0
  15. package/docs/sitemap.xml +8 -0
  16. package/docs/styles.css +141 -0
  17. package/eslint.config.mjs +1 -1
  18. package/package.json +16 -18
  19. package/playwright.config.ts +5 -3
  20. package/src/CacheComponentsHandler.ts +57 -2
  21. package/src/RedisStringsHandler.ts +48 -11
  22. package/src/SyncedMap.ts +62 -13
  23. package/src/index.ts +4 -0
  24. package/src/serializer.ts +59 -0
  25. package/test/README.md +179 -0
  26. package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/package.json +6 -6
  27. package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-15-4-11}/pnpm-lock.yaml +89 -105
  28. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/package.json +3 -3
  29. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/pnpm-lock.yaml +53 -53
  30. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/package.json +28 -0
  31. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/pnpm-lock.yaml +4206 -0
  32. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/cached-with-cachelife/route.ts +24 -0
  33. package/test/nextjs-test-projects/next-app-16-2-6/eslint.config.mjs +18 -0
  34. package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/package.json +3 -3
  35. package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/pnpm-lock.yaml +54 -60
  36. package/test/nextjs-test-projects/next-app-16-2-6/postcss.config.mjs +7 -0
  37. package/test/nextjs-test-projects/next-app-16-2-6/src/app/favicon.ico +0 -0
  38. package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-2-6}/tsconfig.json +9 -2
  39. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/cache-handler.js +3 -0
  40. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/next.config.ts +13 -0
  41. package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6-cache-components}/package.json +3 -3
  42. 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
  43. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-static-fetch/route.ts +19 -0
  44. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-cachelife/route.ts +24 -0
  45. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-tag/route.ts +21 -0
  46. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidate-tag/route.ts +19 -0
  47. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidated-fetch/route.ts +19 -0
  48. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/cachelife-short/page.tsx +110 -0
  49. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/page.tsx +90 -0
  50. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +127 -0
  51. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +130 -0
  52. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +127 -0
  53. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +110 -0
  54. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/layout.tsx +0 -2
  55. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/update-tag-test/page.tsx +22 -0
  56. package/{tests → test/playwright}/update-tag.spec.ts +1 -1
  57. package/test/{integration → vitest/integration}/build-id-prefix.integration.test.ts +2 -2
  58. package/test/{cache-components/cache-components.integration.spec.ts → vitest/integration/cache-components/cache-components.integration.test.ts} +46 -3
  59. package/test/{cache-components/redis-kill-reconnect.spec.ts → vitest/integration/cache-components/redis-kill-reconnect.test.ts} +28 -7
  60. package/test/vitest/integration/cache-components/redis-quit-vs-disconnect.test.ts +59 -0
  61. package/test/vitest/integration/cache-components/redis-subscriber-outage.test.ts +100 -0
  62. package/test/{cache-components → vitest/integration/cache-components}/scripts/redis-kill-reconnect.ts +31 -8
  63. package/test/vitest/integration/cache-components/scripts/redis-quit-vs-disconnect.ts +113 -0
  64. package/test/vitest/integration/cache-components/scripts/redis-subscriber-outage.ts +379 -0
  65. package/test/vitest/integration/cache-components/scripts/redis-test-helpers.ts +155 -0
  66. package/test/{integration → vitest/integration}/nextjs-cache-handler.integration.test.ts +17 -10
  67. package/test/vitest/unit/SyncedMap-reconnect-repro.test.ts +154 -0
  68. package/{src → test/vitest/unit}/index.test.ts +3 -3
  69. package/test/{cache-components/reconnect-socket-already-opened.spec.ts → vitest/unit/reconnect-socket-already-opened.test.ts} +1 -1
  70. package/test/vitest/unit/serializer.test.ts +182 -0
  71. package/{src → test/vitest/unit}/utils/prefix.test.ts +1 -1
  72. package/vite.config.ts +4 -3
  73. package/vitest.cache-components.config.ts +1 -1
  74. package/test/browser/update-tag.browser.test.ts +0 -41
  75. package/test/integration/next-app-15-0-3/next.config.js +0 -6
  76. package/test/integration/next-app-15-0-3/package-lock.json +0 -5833
  77. package/test/integration/next-app-15-0-3/pnpm-lock.yaml +0 -3710
  78. package/test/integration/next-app-15-3-2/next.config.js +0 -6
  79. package/test/integration/next-app-15-3-2/package.json +0 -33
  80. package/test/integration/next-app-15-3-2/pnpm-lock.yaml +0 -3720
  81. package/test/integration/next-app-15-3-2/postcss.config.mjs +0 -5
  82. package/test/integration/next-app-15-3-2/src/app/api/revalidateTag/route.ts +0 -15
  83. package/test/integration/next-app-15-4-7/eslint.config.mjs +0 -16
  84. package/test/integration/next-app-15-4-7/package-lock.json +0 -5969
  85. package/test/integration/next-app-15-4-7/package.json +0 -33
  86. package/test/integration/next-app-15-4-7/postcss.config.mjs +0 -5
  87. package/test/integration/next-app-15-4-7/src/app/api/revalidateTag/route.ts +0 -15
  88. package/test/integration/next-app-15-4-7/tsconfig.json +0 -27
  89. package/test/integration/next-app-16-0-3/src/app/api/cached-static-fetch/route.ts +0 -18
  90. package/test/integration/next-app-16-0-3/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  91. package/test/integration/next-app-16-0-3/src/app/api/revalidatePath/route.ts +0 -15
  92. package/test/integration/next-app-16-0-3/src/app/api/revalidated-fetch/route.ts +0 -17
  93. package/test/integration/next-app-16-0-3/src/app/api/uncached-fetch/route.ts +0 -15
  94. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  95. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  96. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  97. package/test/integration/next-app-16-0-3/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  98. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  99. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  100. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  101. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  102. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  103. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  104. package/test/integration/next-app-16-2-3/src/app/api/cached-static-fetch/route.ts +0 -18
  105. package/test/integration/next-app-16-2-3/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  106. package/test/integration/next-app-16-2-3/src/app/api/revalidatePath/route.ts +0 -15
  107. package/test/integration/next-app-16-2-3/src/app/api/revalidated-fetch/route.ts +0 -17
  108. package/test/integration/next-app-16-2-3/src/app/api/uncached-fetch/route.ts +0 -15
  109. package/test/integration/next-app-16-2-3/src/app/globals.css +0 -26
  110. package/test/integration/next-app-16-2-3/src/app/layout.tsx +0 -59
  111. package/test/integration/next-app-16-2-3/src/app/page.tsx +0 -755
  112. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  113. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  114. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  115. package/test/integration/next-app-16-2-3/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  116. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  117. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  118. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  119. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  120. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  121. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  122. package/test/integration/next-app-16-2-3/src/app/revalidation-interface.tsx +0 -267
  123. package/test/integration/next-app-16-2-3-cache-components/public/file.svg +0 -1
  124. package/test/integration/next-app-16-2-3-cache-components/public/globe.svg +0 -1
  125. package/test/integration/next-app-16-2-3-cache-components/public/next.svg +0 -1
  126. package/test/integration/next-app-16-2-3-cache-components/public/vercel.svg +0 -1
  127. package/test/integration/next-app-16-2-3-cache-components/public/window.svg +0 -1
  128. package/test/integration/next-app-customized/README.md +0 -36
  129. package/test/integration/next-app-customized/eslint.config.mjs +0 -16
  130. package/vitest.browser.config.ts +0 -10
  131. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/README.md +0 -0
  132. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/eslint.config.mjs +0 -0
  133. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-15-4-11}/next.config.js +0 -0
  134. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-15-4-11}/package-lock.json +0 -0
  135. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/postcss.config.mjs +0 -0
  136. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/file.svg +0 -0
  137. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/globe.svg +0 -0
  138. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/next.svg +0 -0
  139. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/vercel.svg +0 -0
  140. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/window.svg +0 -0
  141. /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
  142. /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
  143. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidatePath/route.ts +0 -0
  144. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidateTag/route.ts +0 -0
  145. /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
  146. /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
  147. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/globals.css +0 -0
  148. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/layout.tsx +0 -0
  149. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/page.tsx +0 -0
  150. /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
  151. /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
  152. /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
  153. /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
  154. /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
  155. /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
  156. /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
  157. /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
  158. /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
  159. /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
  160. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/revalidation-interface.tsx +0 -0
  161. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/tsconfig.json +0 -0
  162. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/README.md +0 -0
  163. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/eslint.config.mjs +0 -0
  164. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/next.config.ts +0 -0
  165. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/postcss.config.mjs +0 -0
  166. /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
  167. /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
  168. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidatePath/route.ts +0 -0
  169. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidateTag/route.ts +0 -0
  170. /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
  171. /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
  172. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/src/app/favicon.ico +0 -0
  173. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/globals.css +0 -0
  174. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/layout.tsx +0 -0
  175. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/page.tsx +0 -0
  176. /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
  177. /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
  178. /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
  179. /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
  180. /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
  181. /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
  182. /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
  183. /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
  184. /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
  185. /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
  186. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/revalidation-interface.tsx +0 -0
  187. /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
  188. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/tsconfig.json +0 -0
  189. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/README.md +0 -0
  190. /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
  191. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/eslint.config.mjs +0 -0
  192. /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
  193. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/postcss.config.mjs +0 -0
  194. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/file.svg +0 -0
  195. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/globe.svg +0 -0
  196. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/next.svg +0 -0
  197. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/vercel.svg +0 -0
  198. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/window.svg +0 -0
  199. /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
  200. /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
  201. /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
  202. /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
  203. /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
  204. /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
  205. /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
  206. /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
  207. /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
  208. /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
  209. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/favicon.ico +0 -0
  210. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/globals.css +0 -0
  211. /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
  212. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/page.tsx +0 -0
  213. /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
  214. /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
  215. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/tsconfig.json +0 -0
  216. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/README.md +0 -0
  217. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/next.config.ts +0 -0
  218. /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
  219. /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
  220. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidatePath/route.ts +0 -0
  221. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidateTag/route.ts +0 -0
  222. /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
  223. /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
  224. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/globals.css +0 -0
  225. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/layout.tsx +0 -0
  226. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/page.tsx +0 -0
  227. /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
  228. /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
  229. /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
  230. /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
  231. /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
  232. /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
  233. /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
  234. /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
  235. /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
  236. /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
  237. /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
  238. /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
  239. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/README.md +0 -0
  240. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/eslint.config.mjs +0 -0
  241. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/postcss.config.mjs +0 -0
  242. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/file.svg +0 -0
  243. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/globe.svg +0 -0
  244. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/next.svg +0 -0
  245. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/vercel.svg +0 -0
  246. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/window.svg +0 -0
  247. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/favicon.ico +0 -0
  248. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/globals.css +0 -0
  249. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/page.tsx +0 -0
  250. /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
  251. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/tsconfig.json +0 -0
  252. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-customized}/README.md +0 -0
  253. /package/test/{integration → nextjs-test-projects}/next-app-customized/customized-cache-handler.js +0 -0
  254. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-customized}/eslint.config.mjs +0 -0
  255. /package/test/{integration → nextjs-test-projects}/next-app-customized/next.config.js +0 -0
  256. /package/{tests → test/playwright}/cache-lab.spec.ts +0 -0
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/src/index.ts CHANGED
@@ -5,6 +5,10 @@ import RedisStringsHandler from './RedisStringsHandler';
5
5
  export { RedisStringsHandler };
6
6
  export type { CreateRedisStringsHandlerOptions } from './RedisStringsHandler';
7
7
 
8
+ export { jsonCacheValueSerializer } from './serializer';
9
+ export type { CacheValueSerializer } from './serializer';
10
+ export { bufferAndMapReplacer, bufferAndMapReviver } from './utils/json';
11
+
8
12
  import {
9
13
  redisCacheHandler,
10
14
  getRedisCacheComponentsHandler,
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Pluggable wire-format codec for Redis string values used by `RedisStringsHandler`.
3
+ *
4
+ * The serializer is the single point at which an in-memory `CacheEntry` becomes the
5
+ * string written to Redis (and back). Plugging in a custom serializer lets you add
6
+ * compression (gzip/brotli), encryption, or any other custom encoding without forking
7
+ * this package or losing the existing dedup / batch / keyspace features.
8
+ *
9
+ * Both `serialize` and `deserialize` may return either a value directly or a `Promise`,
10
+ * which enables non-blocking async codecs such as stream-based compression
11
+ * (`zlib.brotliCompress`) or encryption (`crypto.subtle`). Synchronous implementations
12
+ * continue to work unchanged - awaiting a plain value is a no-op.
13
+ *
14
+ * The default export {@link jsonCacheValueSerializer} is `JSON.stringify` /
15
+ * `JSON.parse` paired with {@link bufferAndMapReplacer} / {@link bufferAndMapReviver},
16
+ * so native `Buffer` and `Map` values inside a `CacheEntry` round-trip transparently
17
+ * (this is required for Next.js RSC payloads).
18
+ *
19
+ * Operational note: changing the serializer (or any of its parameters such as a
20
+ * compression level or encryption key) makes existing Redis keys unreadable, because
21
+ * the deserializer will fail or return `null` for entries written by the previous
22
+ * format. Either flush the affected keys, bump `keyPrefix`, or migrate values
23
+ * out-of-band before deploying a new serializer.
24
+ */
25
+ import type { CacheEntry } from './RedisStringsHandler';
26
+ import { bufferAndMapReplacer, bufferAndMapReviver } from './utils/json';
27
+
28
+ export type CacheValueSerializer = {
29
+ /**
30
+ * Encode an in-memory `CacheEntry` into the string written to Redis.
31
+ * May return a `Promise` for async codecs (e.g. compression, encryption).
32
+ */
33
+ serialize(value: CacheEntry): string | Promise<string>;
34
+ /**
35
+ * Decode a string read from Redis back into a `CacheEntry`.
36
+ * Returning `null` (or a `Promise<null>`) signals "treat as cache miss" -
37
+ * the handler will return `null` from `get()` without surfacing an error.
38
+ * May return a `Promise` for async codecs.
39
+ */
40
+ deserialize(stored: string): CacheEntry | null | Promise<CacheEntry | null>;
41
+ };
42
+
43
+ /**
44
+ * Default serializer used by `RedisStringsHandler` when no `valueSerializer` is
45
+ * configured. Wraps `JSON.stringify` / `JSON.parse` with this package's
46
+ * {@link bufferAndMapReplacer} / {@link bufferAndMapReviver} so native `Buffer`
47
+ * and `Map` values inside a `CacheEntry` survive the round-trip.
48
+ *
49
+ * Exported as a singleton so consumers can compare against the default by
50
+ * reference (e.g. to detect that no custom serializer was configured).
51
+ */
52
+ export const jsonCacheValueSerializer: CacheValueSerializer = {
53
+ serialize(value) {
54
+ return JSON.stringify(value, bufferAndMapReplacer);
55
+ },
56
+ deserialize(stored) {
57
+ return JSON.parse(stored, bufferAndMapReviver) as CacheEntry | null;
58
+ },
59
+ };
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
  }