@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
@@ -0,0 +1,127 @@
1
+ import Link from 'next/link';
2
+ import { Suspense } from 'react';
3
+ import { cookies } from 'next/headers';
4
+
5
+ export default function RuntimeDataSuspensePage() {
6
+ return (
7
+ <main className="mx-auto max-w-3xl p-10">
8
+ <div className="mb-6">
9
+ <Link
10
+ className="text-sm text-blue-600 hover:underline"
11
+ href="/cache-lab"
12
+ >
13
+ ← Back to Cache Lab
14
+ </Link>
15
+ </div>
16
+
17
+ <header className="mb-6">
18
+ <h1 className="text-2xl font-semibold">Runtime data + Suspense</h1>
19
+ <p className="mt-2 text-sm text-slate-600">
20
+ This page reads <code>cookies()</code> at request time (runtime data),
21
+ then passes a value into a cached function. The runtime read is
22
+ wrapped in <code>{'<Suspense>'}</code> so it is an explicit dynamic
23
+ boundary.
24
+ </p>
25
+ </header>
26
+
27
+ <Suspense
28
+ fallback={
29
+ <div className="rounded-lg border bg-slate-50 p-5 text-sm text-slate-700">
30
+ Loading runtime cookie…
31
+ </div>
32
+ }
33
+ >
34
+ <RuntimeCookieBlock />
35
+ </Suspense>
36
+
37
+ <div className="mt-6 rounded-lg border bg-slate-50 p-5 text-sm text-slate-700">
38
+ <div className="space-y-2">
39
+ <p>
40
+ Expected: changing the cookie changes the <em>input</em> to the
41
+ cached function, producing a different cache entry.
42
+ </p>
43
+ <p>
44
+ The cached function itself uses <code>use cache</code>, so for the
45
+ same cookie value, it should return a stable payload across reloads.
46
+ </p>
47
+ </div>
48
+ </div>
49
+ </main>
50
+ );
51
+ }
52
+
53
+ async function setCookie(value: string) {
54
+ 'use server';
55
+ const store = await cookies();
56
+ store.set('cache_lab_session', value, { path: '/' });
57
+ }
58
+
59
+ async function setCookieUserA() {
60
+ 'use server';
61
+ await setCookie('user-a');
62
+ }
63
+
64
+ async function setCookieUserB() {
65
+ 'use server';
66
+ await setCookie('user-b');
67
+ }
68
+
69
+ async function RuntimeCookieBlock() {
70
+ const store = await cookies();
71
+ const sessionId = store.get('cache_lab_session')?.value || 'anonymous';
72
+
73
+ return (
74
+ <div className="rounded-lg border p-5">
75
+ <div className="mb-4 text-sm">
76
+ <div>
77
+ <span className="font-medium">cookie cache_lab_session:</span>{' '}
78
+ <span data-testid="cookie" className="font-mono text-slate-700">
79
+ {sessionId}
80
+ </span>
81
+ </div>
82
+ </div>
83
+
84
+ <div className="flex flex-col gap-3 sm:flex-row">
85
+ <form action={setCookieUserA}>
86
+ <button
87
+ className="w-full rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
88
+ type="submit"
89
+ >
90
+ Set cookie: user-a
91
+ </button>
92
+ </form>
93
+ <form action={setCookieUserB}>
94
+ <button
95
+ className="w-full rounded-md border border-blue-600 bg-white px-4 py-2 text-sm font-medium text-blue-700 hover:bg-blue-50"
96
+ type="submit"
97
+ >
98
+ Set cookie: user-b
99
+ </button>
100
+ </form>
101
+ </div>
102
+
103
+ <div className="mt-5">
104
+ <CachedPerSession sessionId={sessionId} />
105
+ </div>
106
+ </div>
107
+ );
108
+ }
109
+
110
+ async function CachedPerSession({ sessionId }: { sessionId: string }) {
111
+ 'use cache';
112
+
113
+ const payload = {
114
+ sessionId,
115
+ createdAt: Date.now(),
116
+ random: Math.random(),
117
+ };
118
+
119
+ return (
120
+ <div className="rounded-md bg-slate-50 p-4 text-sm">
121
+ <div className="font-medium">Cached payload (keyed by sessionId)</div>
122
+ <pre data-testid="payload" className="mt-2 overflow-x-auto text-xs">
123
+ {JSON.stringify(payload, null, 2)}
124
+ </pre>
125
+ </div>
126
+ );
127
+ }
@@ -0,0 +1,130 @@
1
+ import Link from 'next/link';
2
+ import { Suspense } from 'react';
3
+ import { cacheLife, cacheTag, revalidateTag } from 'next/cache';
4
+
5
+ function sleep(ms: number): Promise<void> {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+
9
+ async function getSlowTaggedValue() {
10
+ 'use cache';
11
+
12
+ // Keep this short so you can observe SWR behavior quickly.
13
+ // stale: after 2s the entry is considered stale
14
+ // revalidate: after 8s Next may attempt to refresh
15
+ // expire: after 60s it must be recomputed
16
+ cacheLife({ stale: 2, revalidate: 8, expire: 60 });
17
+ cacheTag('cache-lab:swr');
18
+
19
+ // Make regeneration visibly expensive so you can tell whether the request was
20
+ // served from stale cache (fast) or blocked on recomputation (slow).
21
+ await sleep(3000);
22
+
23
+ return {
24
+ computedAt: Date.now(),
25
+ value: Math.random(),
26
+ note: 'This function always sleeps ~3s when it actually executes.',
27
+ };
28
+ }
29
+
30
+ async function triggerRevalidateTag() {
31
+ 'use server';
32
+ revalidateTag('cache-lab:swr', { expire: 1 });
33
+ }
34
+
35
+ async function CachedPanel() {
36
+ const data = await getSlowTaggedValue();
37
+
38
+ return (
39
+ <div className="rounded-lg border p-5">
40
+ <div className="text-sm text-slate-700">
41
+ <div>
42
+ <span className="font-medium">computedAt:</span>{' '}
43
+ <span data-testid="computedAt" className="font-mono">
44
+ {data.computedAt}
45
+ </span>
46
+ </div>
47
+ <div className="mt-1">
48
+ <span className="font-medium">value:</span>{' '}
49
+ <span data-testid="value" className="font-mono">
50
+ {data.value}
51
+ </span>
52
+ </div>
53
+ <div className="mt-1 text-xs text-slate-500">{data.note}</div>
54
+ </div>
55
+ </div>
56
+ );
57
+ }
58
+
59
+ export default function StaleWhileRevalidatePage() {
60
+ return (
61
+ <main className="mx-auto max-w-3xl p-10">
62
+ <div className="mb-6">
63
+ <Link
64
+ className="text-sm text-blue-600 hover:underline"
65
+ href="/cache-lab"
66
+ >
67
+ ← Back to Cache Lab
68
+ </Link>
69
+ </div>
70
+
71
+ <header className="mb-6">
72
+ <h1 className="text-2xl font-semibold">
73
+ SWR: stale-while-revalidate (slow)
74
+ </h1>
75
+ <p className="mt-2 text-sm text-slate-600">
76
+ This page is designed to make stale-while-revalidate behavior visible.
77
+ The cached function sleeps ~3 seconds whenever it really runs.
78
+ </p>
79
+ </header>
80
+
81
+ <Suspense
82
+ fallback={
83
+ <div className="rounded-lg border bg-slate-50 p-5 text-sm text-slate-700">
84
+ Loading cached content… (first time can take ~3s)
85
+ </div>
86
+ }
87
+ >
88
+ <CachedPanel />
89
+ </Suspense>
90
+
91
+ <div className="mt-6 flex flex-col gap-3 sm:flex-row">
92
+ <form action={triggerRevalidateTag}>
93
+ <button
94
+ className="w-full rounded-md border border-blue-600 bg-white px-4 py-2 text-sm font-medium text-blue-700 hover:bg-blue-50"
95
+ type="submit"
96
+ >
97
+ revalidateTag('cache-lab:swr')
98
+ </button>
99
+ </form>
100
+ </div>
101
+
102
+ <div className="mt-6 rounded-lg border bg-slate-50 p-5 text-sm text-slate-700">
103
+ <div className="space-y-2">
104
+ <p className="font-medium">How to observe SWR (non-blocking)</p>
105
+ <p>1) Load this page once (expect ~3s on the very first load).</p>
106
+ <p>
107
+ 2) Wait ~3 seconds so the entry becomes <code>stale</code>{' '}
108
+ (stale=2s).
109
+ </p>
110
+ <p>
111
+ 3) Click <code>revalidateTag</code>.
112
+ </p>
113
+ <p>4) Immediately reload the page:</p>
114
+ <div className="rounded bg-white p-3 text-xs">
115
+ If SWR is working, the reload should be fast and you may still see
116
+ the old computedAt/value (stale served).
117
+ <br />
118
+ Reload again after ~3-4 seconds and you should see a new
119
+ computedAt/value.
120
+ <br />
121
+ If SWR is not working, the reload will block for ~3 seconds.
122
+ </div>
123
+ <div className="text-xs text-slate-500">
124
+ cacheLife: {'{ stale: 2, revalidate: 8, expire: 60 }'}
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </main>
129
+ );
130
+ }
@@ -0,0 +1,127 @@
1
+ import Link from 'next/link';
2
+ import { cacheTag, updateTag, revalidateTag, cacheLife } from 'next/cache';
3
+ import { Suspense } from 'react';
4
+
5
+ async function getTaggedData() {
6
+ 'use cache';
7
+
8
+ cacheLife({ stale: 5, revalidate: 10, expire: 60 });
9
+ cacheTag('cache-lab:tag');
10
+
11
+ return {
12
+ value: Math.random(),
13
+ createdAt: Date.now(),
14
+ };
15
+ }
16
+
17
+ async function doUpdateTag() {
18
+ 'use server';
19
+ updateTag('cache-lab:tag');
20
+ }
21
+
22
+ async function doRevalidateTag() {
23
+ 'use server';
24
+ revalidateTag('cache-lab:tag', { expire: 1 });
25
+ }
26
+
27
+ async function CachedTaggedPanel() {
28
+ const data = await getTaggedData();
29
+ return (
30
+ <div className="rounded-lg border p-5">
31
+ <dl className="grid grid-cols-1 gap-3 text-sm">
32
+ <div>
33
+ <dt className="font-medium">createdAt</dt>
34
+ <dd data-testid="createdAt" className="font-mono text-slate-700">
35
+ {data.createdAt}
36
+ </dd>
37
+ </div>
38
+ <div>
39
+ <dt className="font-medium">value</dt>
40
+ <dd data-testid="value" className="font-mono text-slate-700">
41
+ {data.value}
42
+ </dd>
43
+ </div>
44
+ <div>
45
+ <dt className="font-medium">cacheLife</dt>
46
+ <dd className="font-mono text-slate-700">
47
+ {'{ stale: 5, revalidate: 10, expire: 60 }'}
48
+ </dd>
49
+ </div>
50
+ </dl>
51
+ </div>
52
+ );
53
+ }
54
+
55
+ export default function TagInvalidationPage() {
56
+ return (
57
+ <main className="mx-auto max-w-3xl p-10">
58
+ <div className="mb-6">
59
+ <Link
60
+ className="text-sm text-blue-600 hover:underline"
61
+ href="/cache-lab"
62
+ >
63
+ ← Back to Cache Lab
64
+ </Link>
65
+ </div>
66
+
67
+ <header className="mb-6">
68
+ <h1 className="text-2xl font-semibold">
69
+ Tags: updateTag vs revalidateTag
70
+ </h1>
71
+ <p className="mt-2 text-sm text-slate-600">
72
+ This page caches a function tagged with{' '}
73
+ <code>cacheTag('cache-lab:tag')</code>. Use the buttons to invalidate.
74
+ </p>
75
+ </header>
76
+
77
+ <Suspense
78
+ fallback={
79
+ <div className="rounded-lg border bg-slate-50 p-5 text-sm text-slate-700">
80
+ Loading cached tagged content…
81
+ </div>
82
+ }
83
+ >
84
+ <CachedTaggedPanel />
85
+ </Suspense>
86
+
87
+ <div className="mt-6 flex flex-col gap-3 sm:flex-row">
88
+ <form action={doUpdateTag}>
89
+ <button
90
+ className="w-full rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
91
+ type="submit"
92
+ >
93
+ updateTag('cache-lab:tag')
94
+ </button>
95
+ </form>
96
+
97
+ <form action={doRevalidateTag}>
98
+ <button
99
+ className="w-full rounded-md border border-blue-600 bg-white px-4 py-2 text-sm font-medium text-blue-700 hover:bg-blue-50"
100
+ type="submit"
101
+ >
102
+ revalidateTag('cache-lab:tag')
103
+ </button>
104
+ </form>
105
+ </div>
106
+
107
+ <div className="mt-6 rounded-lg border bg-slate-50 p-5 text-sm text-slate-700">
108
+ <div className="space-y-2">
109
+ <p>Expected: values should stay stable on reloads.</p>
110
+ <p>
111
+ <strong>updateTag</strong> is intended for “expire + immediately
112
+ refresh within the same request” (used after mutations in Server
113
+ Actions).
114
+ </p>
115
+ <p>
116
+ <strong>revalidateTag</strong> invalidates tagged entries with
117
+ stale-while-revalidate behavior.
118
+ </p>
119
+ <p>
120
+ This page deliberately puts the async work behind{' '}
121
+ <code>{'<Suspense>'}</code> to avoid the "Blocking Route" warning.
122
+ </p>
123
+ </div>
124
+ </div>
125
+ </main>
126
+ );
127
+ }
@@ -0,0 +1,110 @@
1
+ import Link from 'next/link';
2
+ import { cacheTag, updateTag } from 'next/cache';
3
+
4
+ async function getCachedNonDeterministicValues() {
5
+ 'use cache';
6
+
7
+ cacheTag('cache-lab:nondet');
8
+
9
+ const random1 = Math.random();
10
+ const random2 = Math.random();
11
+ const now = Date.now();
12
+ const uuid = crypto.randomUUID();
13
+ const bytes = crypto.getRandomValues(new Uint8Array(8));
14
+
15
+ return {
16
+ random1,
17
+ random2,
18
+ now,
19
+ uuid,
20
+ bytes: Array.from(bytes),
21
+ };
22
+ }
23
+
24
+ async function refresh() {
25
+ 'use server';
26
+ updateTag('cache-lab:nondet');
27
+ }
28
+
29
+ export default async function UseCacheNonDeterministicPage() {
30
+ const data = await getCachedNonDeterministicValues();
31
+
32
+ return (
33
+ <main className="mx-auto max-w-3xl p-10">
34
+ <div className="mb-6">
35
+ <Link
36
+ className="text-sm text-blue-600 hover:underline"
37
+ href="/cache-lab"
38
+ >
39
+ ← Back to Cache Lab
40
+ </Link>
41
+ </div>
42
+
43
+ <header className="mb-6">
44
+ <h1 className="text-2xl font-semibold">use cache: non-deterministic</h1>
45
+ <p className="mt-2 text-sm text-slate-600">
46
+ Per docs, non-deterministic operations inside a <code>use cache</code>{' '}
47
+ scope run once and then stay stable for all requests until you
48
+ invalidate.
49
+ </p>
50
+ </header>
51
+
52
+ <form action={refresh} className="mb-6">
53
+ <button
54
+ className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
55
+ type="submit"
56
+ >
57
+ updateTag('cache-lab:nondet')
58
+ </button>
59
+ </form>
60
+
61
+ <div className="rounded-lg border p-5">
62
+ <dl className="grid grid-cols-1 gap-3 text-sm">
63
+ <div>
64
+ <dt className="font-medium">random1</dt>
65
+ <dd data-testid="random1" className="font-mono text-slate-700">
66
+ {data.random1}
67
+ </dd>
68
+ </div>
69
+ <div>
70
+ <dt className="font-medium">random2</dt>
71
+ <dd data-testid="random2" className="font-mono text-slate-700">
72
+ {data.random2}
73
+ </dd>
74
+ </div>
75
+ <div>
76
+ <dt className="font-medium">now (Date.now)</dt>
77
+ <dd data-testid="now" className="font-mono text-slate-700">
78
+ {data.now}
79
+ </dd>
80
+ </div>
81
+ <div>
82
+ <dt className="font-medium">uuid</dt>
83
+ <dd data-testid="uuid" className="font-mono text-slate-700">
84
+ {data.uuid}
85
+ </dd>
86
+ </div>
87
+ <div>
88
+ <dt className="font-medium">bytes</dt>
89
+ <dd data-testid="bytes" className="font-mono text-slate-700">
90
+ {JSON.stringify(data.bytes)}
91
+ </dd>
92
+ </div>
93
+ </dl>
94
+ </div>
95
+
96
+ <div className="mt-6 rounded-lg border bg-slate-50 p-5 text-sm text-slate-700">
97
+ <div className="space-y-2">
98
+ <p>
99
+ Expected: Reloading the page should keep values identical across
100
+ requests.
101
+ </p>
102
+ <p>
103
+ Click the button (Server Action) to run <code>updateTag</code> and
104
+ refresh the cached entry.
105
+ </p>
106
+ </div>
107
+ </div>
108
+ </main>
109
+ );
110
+ }
@@ -50,8 +50,6 @@ export default async function RootLayout({
50
50
  <body
51
51
  className={`${geistSans.variable} ${geistMono.variable} antialiased`}
52
52
  >
53
- Layout TS: {Date.now()}
54
- {/* Layout counter: {data.origin} */}
55
53
  {children}
56
54
  </body>
57
55
  </html>
@@ -0,0 +1,22 @@
1
+ import { updateTag } from 'next/cache';
2
+
3
+ let clicks = 0;
4
+
5
+ async function increment() {
6
+ 'use server';
7
+
8
+ clicks++;
9
+ updateTag('update-tag-test');
10
+ }
11
+
12
+ export default function UpdateTagTestPage() {
13
+ return (
14
+ <main style={{ padding: 40, fontFamily: 'system-ui, sans-serif' }}>
15
+ <h1>UpdateTag Test</h1>
16
+ <form action={increment}>
17
+ <p data-testid="clicks">Clicks: {clicks}</p>
18
+ <button type="submit">Increment</button>
19
+ </form>
20
+ </main>
21
+ );
22
+ }
@@ -1,7 +1,7 @@
1
1
  import { test, expect } from '@playwright/test';
2
2
 
3
3
  // This test assumes the Next 16 app is already running, e.g.:
4
- // cd test/integration/next-app-16-0-3 && pnpm dev
4
+ // cd test/nextjs-test-projects/next-app-16-0-11 && pnpm dev
5
5
  // on the same origin as baseURL.
6
6
 
7
7
  test('button click triggers Server Action using updateTag', async ({
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  import fs from 'fs';
6
6
  import fetch from 'node-fetch';
7
7
 
8
- const NEXT_APP = 'next-app-15-4-7';
8
+ const NEXT_APP = 'next-app-15-4-11';
9
9
  const PORT = 3075;
10
10
  const BASE_URL = `http://localhost:${PORT}`;
11
11
 
@@ -28,7 +28,7 @@ describe('BUILD_ID-based key prefix (Next handlers)', () => {
28
28
  let buildId: string;
29
29
 
30
30
  beforeAll(async () => {
31
- appDir = path.join(__dirname, NEXT_APP);
31
+ appDir = path.join(__dirname, '..', '..', 'nextjs-test-projects', NEXT_APP);
32
32
 
33
33
  // Ensure clean env so BUILD_ID precedence kicks in
34
34
  delete (process.env as Record<string, string | undefined>).KEY_PREFIX;
@@ -11,6 +11,10 @@ describe('Next.js 16 Cache Components Integration', () => {
11
11
  let redisClient: RedisClientType;
12
12
  let keyPrefix: string;
13
13
 
14
+ async function delay(ms: number) {
15
+ return new Promise((resolve) => setTimeout(resolve, ms));
16
+ }
17
+
14
18
  beforeAll(async () => {
15
19
  // Connect to Redis
16
20
  redisClient = createClient({
@@ -23,12 +27,16 @@ describe('Next.js 16 Cache Components Integration', () => {
23
27
  keyPrefix = `cache-components-test-${Math.random().toString(36).substring(7)}`;
24
28
  process.env.VERCEL_URL = keyPrefix;
25
29
 
26
- // Build and start Next.js app
30
+ const cacheComponentsApp =
31
+ process.env.CACHE_COMPONENTS_APP || 'next-app-16-2-6-cache-components';
32
+
27
33
  const appDir = path.join(
28
34
  __dirname,
29
35
  '..',
30
- 'integration',
31
- 'next-app-16-2-3-cache-components',
36
+ '..',
37
+ '..',
38
+ 'nextjs-test-projects',
39
+ cacheComponentsApp,
32
40
  );
33
41
 
34
42
  console.log('Installing Next.js app dependencies...');
@@ -155,6 +163,41 @@ describe('Next.js 16 Cache Components Integration', () => {
155
163
  }, 20_000);
156
164
  });
157
165
 
166
+ describe('cacheLife functionality', () => {
167
+ it('should respect expire window and eventually return refreshed data', async () => {
168
+ const res1 = await fetch(`${BASE_URL}/api/cached-with-cachelife`);
169
+ const data1 = await res1.json();
170
+ expect(data1.counter).toBe(1);
171
+
172
+ const res2 = await fetch(`${BASE_URL}/api/cached-with-cachelife`);
173
+ const data2 = await res2.json();
174
+ expect(data2.counter).toBe(data1.counter);
175
+ expect(data2.timestamp).toBe(data1.timestamp);
176
+
177
+ await delay(6500);
178
+
179
+ let refreshedData: any;
180
+ for (let i = 0; i < 10; i++) {
181
+ const res = await fetch(`${BASE_URL}/api/cached-with-cachelife`);
182
+ const data = await res.json();
183
+
184
+ if (
185
+ data.counter !== data1.counter ||
186
+ data.timestamp !== data1.timestamp
187
+ ) {
188
+ refreshedData = data;
189
+ break;
190
+ }
191
+
192
+ await delay(500);
193
+ }
194
+
195
+ expect(refreshedData).toBeDefined();
196
+ expect(refreshedData.counter).toBeGreaterThan(data1.counter);
197
+ expect(refreshedData.timestamp).not.toBe(data1.timestamp);
198
+ }, 20_000);
199
+ });
200
+
158
201
  describe('Redis cache handler integration', () => {
159
202
  it('should call cache handler get and set methods', async () => {
160
203
  // Make request to trigger cache (don't clear first)
@@ -1,6 +1,21 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { spawn } from 'child_process';
3
3
  import path from 'path';
4
+ import { spawnSync } from 'child_process';
5
+
6
+ function hasContainerRuntime() {
7
+ const podman = spawnSync('podman', ['--version'], {
8
+ encoding: 'utf8',
9
+ timeout: 3_000,
10
+ });
11
+ if (!podman.error && (podman.status ?? 1) === 0) return true;
12
+
13
+ const docker = spawnSync('docker', ['--version'], {
14
+ encoding: 'utf8',
15
+ timeout: 3_000,
16
+ });
17
+ return !docker.error && (docker.status ?? 1) === 0;
18
+ }
4
19
 
5
20
  function runNode(script: string, timeoutMs = 120_000) {
6
21
  return new Promise<{ code: number; stdout: string; stderr: string }>(
@@ -26,13 +41,19 @@ function runNode(script: string, timeoutMs = 120_000) {
26
41
  }
27
42
 
28
43
  describe('redis kill/reconnect end-to-end (both handlers)', () => {
29
- it('survives redis restart without Socket already opened', async () => {
30
- const script = path.join(__dirname, 'scripts', 'redis-kill-reconnect.ts');
44
+ const runOrSkip = hasContainerRuntime() ? it : it.skip;
31
45
 
32
- const res = await runNode(script, 180_000);
46
+ runOrSkip(
47
+ 'survives redis restart without Socket already opened',
48
+ async () => {
49
+ const script = path.join(__dirname, 'scripts', 'redis-kill-reconnect.ts');
33
50
 
34
- expect(res.code).toBe(0);
35
- expect(res.stdout).toContain('OK');
36
- expect(res.stderr).not.toContain('Socket already opened');
37
- }, 180_000);
51
+ const res = await runNode(script, 180_000);
52
+
53
+ expect(res.code).toBe(0);
54
+ expect(res.stdout).toContain('OK');
55
+ expect(res.stderr).not.toContain('Socket already opened');
56
+ },
57
+ 180_000,
58
+ );
38
59
  });