@trieb.work/nextjs-turbo-redis-cache 1.15.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/.cursor/skills/testing/SKILL.md +121 -0
  2. package/.github/workflows/ci.yml +199 -69
  3. package/.github/workflows/release.yml +9 -1
  4. package/ARCHITECTURE.md +431 -0
  5. package/CHANGELOG.md +28 -0
  6. package/README.md +77 -49
  7. package/dist/index.d.mts +71 -45
  8. package/dist/index.d.ts +71 -45
  9. package/dist/index.js +107 -21
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +107 -21
  12. package/dist/index.mjs.map +1 -1
  13. package/docs/index.html +7 -6
  14. package/eslint.config.mjs +1 -1
  15. package/package.json +15 -16
  16. package/playwright.config.ts +5 -3
  17. package/src/CacheComponentsHandler.ts +57 -2
  18. package/src/RedisStringsHandler.ts +125 -66
  19. package/src/SyncedMap.ts +62 -13
  20. package/test/README.md +194 -0
  21. package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/package.json +6 -6
  22. package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-15-4-11}/pnpm-lock.yaml +89 -105
  23. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/package.json +3 -3
  24. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/pnpm-lock.yaml +53 -53
  25. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/package.json +28 -0
  26. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/pnpm-lock.yaml +4206 -0
  27. package/test/nextjs-test-projects/next-app-16-2-6/eslint.config.mjs +18 -0
  28. package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/package.json +3 -3
  29. package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/pnpm-lock.yaml +54 -60
  30. package/test/nextjs-test-projects/next-app-16-2-6/postcss.config.mjs +7 -0
  31. package/test/nextjs-test-projects/next-app-16-2-6/src/app/favicon.ico +0 -0
  32. package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-2-6}/tsconfig.json +9 -2
  33. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/cache-handler.js +3 -0
  34. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/next.config.ts +13 -0
  35. package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6-cache-components}/package.json +3 -3
  36. 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
  37. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-static-fetch/route.ts +19 -0
  38. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-cachelife/route.ts +24 -0
  39. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-tag/route.ts +21 -0
  40. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidate-tag/route.ts +19 -0
  41. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidated-fetch/route.ts +19 -0
  42. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/cachelife-short/page.tsx +110 -0
  43. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/page.tsx +90 -0
  44. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +127 -0
  45. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +130 -0
  46. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +127 -0
  47. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +110 -0
  48. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/layout.tsx +0 -2
  49. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/update-tag-test/page.tsx +22 -0
  50. package/test/nextjs-test-projects/next-pages-16-2-6/README.md +16 -0
  51. package/test/nextjs-test-projects/next-pages-16-2-6/eslint.config.mjs +18 -0
  52. package/test/nextjs-test-projects/next-pages-16-2-6/next.config.ts +7 -0
  53. package/test/nextjs-test-projects/next-pages-16-2-6/package.json +26 -0
  54. package/test/nextjs-test-projects/next-pages-16-2-6/pnpm-lock.yaml +3896 -0
  55. package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/api/revalidate.ts +24 -0
  56. package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/index.tsx +11 -0
  57. package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/isr/[slug].tsx +49 -0
  58. package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/static-forever.tsx +20 -0
  59. package/test/nextjs-test-projects/next-pages-16-2-6/tsconfig.json +34 -0
  60. package/{tests → test/playwright}/update-tag.spec.ts +1 -1
  61. package/test/{integration → vitest/integration}/build-id-prefix.integration.test.ts +2 -2
  62. package/test/{cache-components/cache-components.integration.spec.ts → vitest/integration/cache-components/cache-components.integration.test.ts} +7 -3
  63. package/test/{cache-components/redis-kill-reconnect.spec.ts → vitest/integration/cache-components/redis-kill-reconnect.test.ts} +28 -7
  64. package/test/vitest/integration/cache-components/redis-quit-vs-disconnect.test.ts +59 -0
  65. package/test/vitest/integration/cache-components/redis-subscriber-outage.test.ts +100 -0
  66. package/test/{cache-components → vitest/integration/cache-components}/scripts/redis-kill-reconnect.ts +31 -8
  67. package/test/vitest/integration/cache-components/scripts/redis-quit-vs-disconnect.ts +113 -0
  68. package/test/vitest/integration/cache-components/scripts/redis-subscriber-outage.ts +379 -0
  69. package/test/vitest/integration/cache-components/scripts/redis-test-helpers.ts +155 -0
  70. package/test/{integration → vitest/integration}/nextjs-cache-handler.integration.test.ts +15 -9
  71. package/test/vitest/integration/pages-router.integration.test.ts +420 -0
  72. package/test/vitest/unit/SyncedMap-reconnect-repro.test.ts +154 -0
  73. package/{src → test/vitest/unit}/index.test.ts +3 -3
  74. package/test/vitest/unit/pages-router-kinds.test.ts +292 -0
  75. package/test/{cache-components/reconnect-socket-already-opened.spec.ts → vitest/unit/reconnect-socket-already-opened.test.ts} +1 -1
  76. package/{src → test/vitest/unit}/serializer.test.ts +7 -3
  77. package/{src → test/vitest/unit}/utils/prefix.test.ts +1 -1
  78. package/vite.config.ts +4 -3
  79. package/vitest.cache-components.config.ts +1 -1
  80. package/test/browser/update-tag.browser.test.ts +0 -41
  81. package/test/integration/next-app-15-0-3/next.config.js +0 -6
  82. package/test/integration/next-app-15-0-3/package-lock.json +0 -5833
  83. package/test/integration/next-app-15-0-3/pnpm-lock.yaml +0 -3710
  84. package/test/integration/next-app-15-3-2/next.config.js +0 -6
  85. package/test/integration/next-app-15-3-2/package.json +0 -33
  86. package/test/integration/next-app-15-3-2/pnpm-lock.yaml +0 -3720
  87. package/test/integration/next-app-15-3-2/postcss.config.mjs +0 -5
  88. package/test/integration/next-app-15-3-2/src/app/api/revalidateTag/route.ts +0 -15
  89. package/test/integration/next-app-15-4-7/eslint.config.mjs +0 -16
  90. package/test/integration/next-app-15-4-7/package-lock.json +0 -5969
  91. package/test/integration/next-app-15-4-7/package.json +0 -33
  92. package/test/integration/next-app-15-4-7/postcss.config.mjs +0 -5
  93. package/test/integration/next-app-15-4-7/src/app/api/revalidateTag/route.ts +0 -15
  94. package/test/integration/next-app-15-4-7/tsconfig.json +0 -27
  95. package/test/integration/next-app-16-0-3/src/app/api/cached-static-fetch/route.ts +0 -18
  96. package/test/integration/next-app-16-0-3/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  97. package/test/integration/next-app-16-0-3/src/app/api/revalidatePath/route.ts +0 -15
  98. package/test/integration/next-app-16-0-3/src/app/api/revalidated-fetch/route.ts +0 -17
  99. package/test/integration/next-app-16-0-3/src/app/api/uncached-fetch/route.ts +0 -15
  100. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  101. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  102. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  103. package/test/integration/next-app-16-0-3/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  104. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  105. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  106. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  107. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  108. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  109. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  110. package/test/integration/next-app-16-2-3/src/app/api/cached-static-fetch/route.ts +0 -18
  111. package/test/integration/next-app-16-2-3/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  112. package/test/integration/next-app-16-2-3/src/app/api/revalidatePath/route.ts +0 -15
  113. package/test/integration/next-app-16-2-3/src/app/api/revalidated-fetch/route.ts +0 -17
  114. package/test/integration/next-app-16-2-3/src/app/api/uncached-fetch/route.ts +0 -15
  115. package/test/integration/next-app-16-2-3/src/app/globals.css +0 -26
  116. package/test/integration/next-app-16-2-3/src/app/layout.tsx +0 -59
  117. package/test/integration/next-app-16-2-3/src/app/page.tsx +0 -755
  118. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  119. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  120. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  121. package/test/integration/next-app-16-2-3/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  122. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  123. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  124. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  125. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  126. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  127. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  128. package/test/integration/next-app-16-2-3/src/app/revalidation-interface.tsx +0 -267
  129. package/test/integration/next-app-16-2-3-cache-components/public/file.svg +0 -1
  130. package/test/integration/next-app-16-2-3-cache-components/public/globe.svg +0 -1
  131. package/test/integration/next-app-16-2-3-cache-components/public/next.svg +0 -1
  132. package/test/integration/next-app-16-2-3-cache-components/public/vercel.svg +0 -1
  133. package/test/integration/next-app-16-2-3-cache-components/public/window.svg +0 -1
  134. package/test/integration/next-app-customized/README.md +0 -36
  135. package/test/integration/next-app-customized/eslint.config.mjs +0 -16
  136. package/vitest.browser.config.ts +0 -10
  137. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/README.md +0 -0
  138. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/eslint.config.mjs +0 -0
  139. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-15-4-11}/next.config.js +0 -0
  140. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-15-4-11}/package-lock.json +0 -0
  141. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/postcss.config.mjs +0 -0
  142. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/file.svg +0 -0
  143. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/globe.svg +0 -0
  144. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/next.svg +0 -0
  145. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/vercel.svg +0 -0
  146. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/window.svg +0 -0
  147. /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
  148. /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
  149. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidatePath/route.ts +0 -0
  150. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidateTag/route.ts +0 -0
  151. /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
  152. /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
  153. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/globals.css +0 -0
  154. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/layout.tsx +0 -0
  155. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/page.tsx +0 -0
  156. /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
  157. /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
  158. /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
  159. /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
  160. /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
  161. /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
  162. /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
  163. /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
  164. /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
  165. /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
  166. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/revalidation-interface.tsx +0 -0
  167. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/tsconfig.json +0 -0
  168. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/README.md +0 -0
  169. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/eslint.config.mjs +0 -0
  170. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/next.config.ts +0 -0
  171. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/postcss.config.mjs +0 -0
  172. /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
  173. /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
  174. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidatePath/route.ts +0 -0
  175. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidateTag/route.ts +0 -0
  176. /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
  177. /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
  178. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/src/app/favicon.ico +0 -0
  179. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/globals.css +0 -0
  180. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/layout.tsx +0 -0
  181. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/page.tsx +0 -0
  182. /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
  183. /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
  184. /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
  185. /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
  186. /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
  187. /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
  188. /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
  189. /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
  190. /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
  191. /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
  192. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/revalidation-interface.tsx +0 -0
  193. /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
  194. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/tsconfig.json +0 -0
  195. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/README.md +0 -0
  196. /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
  197. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/eslint.config.mjs +0 -0
  198. /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
  199. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/postcss.config.mjs +0 -0
  200. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/file.svg +0 -0
  201. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/globe.svg +0 -0
  202. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/next.svg +0 -0
  203. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/vercel.svg +0 -0
  204. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/window.svg +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/api/cached-static-fetch/route.ts +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/api/cached-with-cachelife/route.ts +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/api/cached-with-tag/route.ts +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/api/revalidate-tag/route.ts +0 -0
  209. /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
  210. /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
  211. /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
  212. /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
  213. /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
  214. /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
  215. /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
  216. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/favicon.ico +0 -0
  217. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/globals.css +0 -0
  218. /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
  219. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/page.tsx +0 -0
  220. /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
  221. /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
  222. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/tsconfig.json +0 -0
  223. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/README.md +0 -0
  224. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/next.config.ts +0 -0
  225. /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
  226. /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
  227. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidatePath/route.ts +0 -0
  228. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidateTag/route.ts +0 -0
  229. /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
  230. /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
  231. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/globals.css +0 -0
  232. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/layout.tsx +0 -0
  233. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/page.tsx +0 -0
  234. /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
  235. /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
  236. /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
  237. /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
  238. /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
  239. /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
  240. /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
  241. /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
  242. /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
  243. /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
  244. /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
  245. /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
  246. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/README.md +0 -0
  247. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/eslint.config.mjs +0 -0
  248. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/postcss.config.mjs +0 -0
  249. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/file.svg +0 -0
  250. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/globe.svg +0 -0
  251. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/next.svg +0 -0
  252. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/vercel.svg +0 -0
  253. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/window.svg +0 -0
  254. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/favicon.ico +0 -0
  255. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/globals.css +0 -0
  256. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/page.tsx +0 -0
  257. /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
  258. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/tsconfig.json +0 -0
  259. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-customized}/README.md +0 -0
  260. /package/test/{integration → nextjs-test-projects}/next-app-customized/customized-cache-handler.js +0 -0
  261. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-customized}/eslint.config.mjs +0 -0
  262. /package/test/{integration → nextjs-test-projects}/next-app-customized/next.config.js +0 -0
  263. /package/{tests → test/playwright}/cache-lab.spec.ts +0 -0
@@ -0,0 +1,431 @@
1
+ # Architecture – Cache Handler Logic
2
+
3
+ This document describes the internal architecture of the cache handler, focusing on the three core functions (`get`, `set`, `revalidateTag` / `updateTags`), the shared hash maps, and the supporting infrastructure (`SyncedMap`, `DeduplicatedRequestHandler`).
4
+
5
+ Two handler implementations exist side by side:
6
+
7
+ | Handler | Next.js API | File |
8
+ | -------------------------- | ------------------------------------------------------ | ------------------------------- |
9
+ | **RedisStringsHandler** | Legacy `cacheHandler` (Next.js 15) | `src/RedisStringsHandler.ts` |
10
+ | **CacheComponentsHandler** | `cacheHandlers.default` (Next.js 16+ Cache Components) | `src/CacheComponentsHandler.ts` |
11
+
12
+ Both follow the same fundamental pattern – store serialized data in Redis strings, maintain two in-memory hash maps for tags, and synchronize those maps across instances – but differ in the data shapes they receive from Next.js and in some performance optimizations.
13
+
14
+ ---
15
+
16
+ ## Table of Contents
17
+
18
+ 1. [High-Level Overview](#high-level-overview)
19
+ 2. [Shared Hash Maps & Why They Exist](#shared-hash-maps--why-they-exist)
20
+ 3. [SyncedMap – The Synchronization Primitive](#syncedmap--the-synchronization-primitive)
21
+ 4. [DeduplicatedRequestHandler](#deduplicatedrequesthandler)
22
+ 5. [Core Function: `get`](#core-function-get)
23
+ 6. [Core Function: `set`](#core-function-set)
24
+ 7. [Core Function: `revalidateTag` / `updateTags`](#core-function-revalidatetag--updatetags)
25
+ 8. [RedisStringsHandler vs CacheComponentsHandler](#redisstringshandler-vs-cachecomponentshandler)
26
+
27
+ ---
28
+
29
+ ## High-Level Overview
30
+
31
+ ```mermaid
32
+ flowchart TB
33
+ subgraph "Next.js Runtime"
34
+ NX["Next.js Page / Route / Fetch / 'use cache'"]
35
+ end
36
+
37
+ subgraph "Cache Handler Instance (per Node.js process)"
38
+ GET["get()"]
39
+ SET["set()"]
40
+ REV["revalidateTag() / updateTags()"]
41
+
42
+ subgraph "In-Memory Maps"
43
+ STM["sharedTagsMap\n(SyncedMap<string[]>)\ncacheKey → tags"]
44
+ RTM["revalidatedTagsMap\n(SyncedMap<number>)\ntag → timestamp"]
45
+ DEDUP["inMemoryDeduplicationCache\n(SyncedMap<Promise>)"]
46
+ end
47
+ end
48
+
49
+ subgraph "Redis"
50
+ RS["Redis Strings\n(cached page/fetch data)"]
51
+ RH1["Redis Hash\n__sharedTags__"]
52
+ RH2["Redis Hash\n__revalidated_tags__"]
53
+ PUBSUB["Pub/Sub Channels\n(sync messages)"]
54
+ KSN["Keyspace Notifications\n(expired / evicted)"]
55
+ end
56
+
57
+ NX -->|"get(key, ctx)"| GET
58
+ NX -->|"set(key, data, ctx)"| SET
59
+ NX -->|"revalidateTag(tags)"| REV
60
+
61
+ GET <-->|"GET key"| RS
62
+ SET -->|"SET key EX ttl"| RS
63
+ REV -->|"UNLINK keys"| RS
64
+
65
+ STM <-->|"HSET / HDEL / HSCAN"| RH1
66
+ RTM <-->|"HSET / HDEL / HSCAN"| RH2
67
+ STM <-->|"PUBLISH / SUBSCRIBE"| PUBSUB
68
+ RTM <-->|"PUBLISH / SUBSCRIBE"| PUBSUB
69
+ KSN -->|"evicted / expired"| STM
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Shared Hash Maps & Why They Exist
75
+
76
+ ### The Problem
77
+
78
+ Next.js calls `revalidateTag("product")` with **only the tag name**. It does **not** provide the list of cache keys that belong to that tag.
79
+
80
+ At the same time:
81
+
82
+ - Redis strings (the actual cache entries) are keyed by a cache key (e.g. `/products/[id]`), not by tag.
83
+ - Redis has no native secondary index that maps a tag to all keys that carry it.
84
+
85
+ Without an additional data structure, the only way to find all keys for a tag would be a `KEYS *` or `SCAN` over the entire keyspace, parsing every entry – far too expensive at scale.
86
+
87
+ ### The Solution: Two SyncedMaps
88
+
89
+ | Map | Key | Value | Purpose |
90
+ | -------------------- | ------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
91
+ | `sharedTagsMap` | cache key (e.g. `/products/42`) | `string[]` of tags | Reverse index: given a tag, iterate this map to find all affected cache keys |
92
+ | `revalidatedTagsMap` | tag name (e.g. `product`) | `number` (timestamp) | Tracks _when_ a tag was last revalidated, used for lazy invalidation of fetch entries (implicit tags / `_N_T_` prefix) |
93
+
94
+ Both maps live **in-memory** in every Node.js process and are **synchronized across instances** through Redis Hash + Pub/Sub (see [SyncedMap](#syncedmap--the-synchronization-primitive)).
95
+
96
+ ```mermaid
97
+ flowchart LR
98
+ subgraph "sharedTagsMap"
99
+ K1["/products/42"] -->|"tags"| V1["['product', '_N_T_/products']"]
100
+ K2["/products/99"] -->|"tags"| V2["['product', '_N_T_/products']"]
101
+ K3["/blog/hello"] -->|"tags"| V3["['blog', '_N_T_/blog']"]
102
+ end
103
+
104
+ subgraph "revalidatedTagsMap"
105
+ T1["_N_T_/products"] -->|"timestamp"| TS1["1716123456789"]
106
+ end
107
+ ```
108
+
109
+ When `revalidateTag("product")` is called, the handler iterates `sharedTagsMap` to find `/products/42` and `/products/99`, then batch-deletes them from Redis in a single `UNLINK` call.
110
+
111
+ ---
112
+
113
+ ## SyncedMap – The Synchronization Primitive
114
+
115
+ `SyncedMap<V>` (`src/SyncedMap.ts`) wraps a standard `Map<string, V>` and keeps it synchronized across all Node.js processes via three mechanisms:
116
+
117
+ ### 1. Redis Hash (persistent state)
118
+
119
+ Every `set()` writes to both the local map **and** a Redis Hash (`HSET`). On startup, `initialSync()` uses `HSCAN` to load the full hash into memory. A periodic re-sync (default ~1 hour, jittered) guards against drift.
120
+
121
+ ### 2. Pub/Sub (real-time cross-instance sync)
122
+
123
+ Every `set()` and `delete()` also publishes a message on a dedicated channel. All other instances subscribe and apply the change to their local map immediately. This avoids the need for polling.
124
+
125
+ ```mermaid
126
+ sequenceDiagram
127
+ participant I1 as Instance 1
128
+ participant Redis as Redis
129
+ participant I2 as Instance 2
130
+
131
+ I1->>Redis: HSET __sharedTags__ "/p/42" '["product"]'
132
+ I1->>Redis: PUBLISH sync-channel {type:"insert", key:"/p/42", value:["product"]}
133
+ Redis-->>I2: message on sync-channel
134
+ I2->>I2: map.set("/p/42", ["product"])
135
+ ```
136
+
137
+ ### 3. Keyspace Notifications (eviction / expiry cleanup)
138
+
139
+ When Redis evicts or expires a cache key, the corresponding entry must be removed from the `sharedTagsMap` as well – otherwise the map would grow indefinitely. `SyncedMap` subscribes to `__keyevent@<db>__:evicted` and `__keyevent@<db>__:expired` and automatically deletes matching entries.
140
+
141
+ This requires Redis to be configured with `notify-keyspace-events Exe`.
142
+
143
+ ```mermaid
144
+ flowchart LR
145
+ REDIS["Redis expires key\n/products/42"] -->|"keyevent notification"| SM["SyncedMap\ndeletes '/products/42'\nfrom local map + hash"]
146
+ ```
147
+
148
+ ### Why not store tags inside the Redis string value?
149
+
150
+ The tag information _is_ stored inside the cached entry value as well. However, during `revalidateTag`, the handler must find _all_ keys that have a given tag without deserializing every cache entry. The in-memory `sharedTagsMap` enables O(n) iteration over a lightweight map instead of O(n) deserialization of potentially large page payloads.
151
+
152
+ ---
153
+
154
+ ## DeduplicatedRequestHandler
155
+
156
+ `DeduplicatedRequestHandler<T, K>` (`src/DeduplicatedRequestHandler.ts`) is a generic request deduplication + short-lived in-memory cache wrapper. It is used by **both** `RedisStringsHandler` and `CacheComponentsHandler`.
157
+
158
+ ### Motivation
159
+
160
+ In a single Next.js request, the same cache key can be read multiple times (e.g. layout + page + multiple fetch calls referencing the same data). Without deduplication, each `get()` would issue a separate `Redis GET`. With deduplication:
161
+
162
+ 1. The first call creates the Redis request promise and stores it in the `inMemoryDeduplicationCache`.
163
+ 2. Subsequent calls for the same key within the caching window (default 10s) return the stored promise.
164
+ 3. After the caching timeout, the entry is evicted.
165
+
166
+ ### Seed on Set
167
+
168
+ When `set()` stores a value in Redis, it also **seeds** the deduplication cache with the serialized value. This means an immediately following `get()` is served from memory without hitting Redis at all.
169
+
170
+ ```mermaid
171
+ sequenceDiagram
172
+ participant NX as Next.js
173
+ participant H as Cache Handler (either)
174
+ participant DC as DeduplicationCache
175
+ participant R as Redis
176
+
177
+ NX->>H: get("key-A")
178
+ H->>DC: has("key-A")? No
179
+ H->>R: GET "prefix:key-A"
180
+ H->>DC: set("key-A", promise)
181
+ R-->>H: serialized value
182
+ H-->>NX: parsed CacheEntry
183
+
184
+ NX->>H: get("key-A") (same or different request within 10s)
185
+ H->>DC: has("key-A")? Yes
186
+ DC-->>H: cached promise
187
+ H-->>NX: structuredClone of result
188
+
189
+ Note over DC: After 10s timeout
190
+ DC->>DC: delete("key-A")
191
+ ```
192
+
193
+ The `inMemoryDeduplicationCache` itself is a `SyncedMap` configured with `withoutRedisHashmap: true` and `withoutSetSync: true` – it only uses the Pub/Sub delete channel so that revalidations on other instances can evict stale entries.
194
+
195
+ ---
196
+
197
+ ## Core Function: `get`
198
+
199
+ ### What Next.js Passes In
200
+
201
+ **RedisStringsHandler:**
202
+
203
+ ```typescript
204
+ get(key: string, ctx: {
205
+ kind: 'APP_ROUTE' | 'APP_PAGE' | 'FETCH';
206
+ tags?: string[]; // explicit tags (FETCH only)
207
+ softTags?: string[]; // implicit tags like _N_T_/path (FETCH only)
208
+ revalidate?: number; // FETCH only
209
+ fetchUrl?: string; // FETCH only
210
+ isFallback: boolean;
211
+ })
212
+ ```
213
+
214
+ **CacheComponentsHandler:**
215
+
216
+ ```typescript
217
+ get(cacheKey: string, softTags: string[])
218
+ ```
219
+
220
+ The Cache Components interface is simpler: it receives only the cache key and soft tags (implicit tags for lazy invalidation).
221
+
222
+ ### What `get` Does
223
+
224
+ ```mermaid
225
+ flowchart TD
226
+ A["get(key, ctx)"] --> B["Redis GET prefix:key"]
227
+ B --> C{Result found?}
228
+ C -->|No| RN["Return null/undefined"]
229
+ C -->|Yes| D["JSON.parse result"]
230
+
231
+ D --> CC{CacheComponents?\nCheck expire}
232
+ CC -->|Expired| DEL1["UNLINK key\nDelete from sharedTagsMap\nReturn undefined"]
233
+ CC -->|Valid| E
234
+
235
+ D --> E["Check revalidatedTagsMap\nfor all tags + softTags"]
236
+ E --> F{Any tag revalidated\nafter entry.lastModified/timestamp?}
237
+ F -->|Yes| G["UNLINK key from Redis\nDelete from sharedTagsMap\nReturn null/undefined"]
238
+ F -->|No| H["Return cache entry"]
239
+
240
+ H --> H2["RedisStringsHandler:\nreturn { value, lastModified, tags }"]
241
+ H --> H3["CacheComponentsHandler:\nConvert base64 → Uint8Array → ReadableStream\nreturn { value, tags, stale, timestamp, expire, revalidate }"]
242
+ ```
243
+
244
+ ### Key Details
245
+
246
+ 1. **Timeout**: Every `Redis GET` uses `AbortSignal.timeout(getTimeoutMs)` (default 500ms). If Redis is slow, the handler returns `null` so the page can be server-rendered instead of waiting.
247
+
248
+ 2. **Deduplication** (both handlers): Before hitting Redis, the deduplication cache is checked for an existing in-flight or recently resolved promise for the same key. Enabled by default (`redisGetDeduplication: true`) with a 10s caching window (`inMemoryCachingTime: 10_000`).
249
+
250
+ 3. **Lazy tag invalidation**: Instead of eagerly deleting all fetch entries when a page tag is revalidated, the handler records the revalidation timestamp in `revalidatedTagsMap`. During `get`, it compares `lastModified` / `timestamp` against the max revalidation timestamp of all related tags. If the entry is stale, it is deleted and `null` is returned. This is necessary because `revalidateTag` for implicit tags (`_N_T_` prefix) does not know which fetch cache keys are affected.
251
+
252
+ 4. **Value transformation** (CacheComponentsHandler): The stored value is a base64-encoded string (from a `ReadableStream<Uint8Array>`). On read, it is decoded back to `Uint8Array` and wrapped in a new `ReadableStream`.
253
+
254
+ ---
255
+
256
+ ## Core Function: `set`
257
+
258
+ ### What Next.js Passes In
259
+
260
+ **RedisStringsHandler:**
261
+
262
+ ```typescript
263
+ set(key: string, data: {
264
+ kind: 'APP_PAGE' | 'APP_ROUTE' | 'FETCH';
265
+ // APP_PAGE: { html, rscData, headers: { 'x-next-cache-tags', 'x-nextjs-stale-time' } }
266
+ // APP_ROUTE: { body, status, headers: { 'x-next-cache-tags', 'cache-control' } }
267
+ // FETCH: { data: { headers, body, status, url }, revalidate }
268
+ }, ctx: {
269
+ tags?: string[];
270
+ revalidate?: number | false;
271
+ cacheControl?: { revalidate: number; expire: number };
272
+ })
273
+ ```
274
+
275
+ **CacheComponentsHandler:**
276
+
277
+ ```typescript
278
+ set(cacheKey: string, pendingEntry: Promise<{
279
+ value: ReadableStream<Uint8Array>;
280
+ tags: string[];
281
+ stale: number;
282
+ timestamp: number;
283
+ expire: number;
284
+ revalidate: number;
285
+ }>)
286
+ ```
287
+
288
+ Note that the Cache Components handler receives a **Promise** of the entry – the value is not yet available when `set` is called.
289
+
290
+ ### What `set` Does
291
+
292
+ ```mermaid
293
+ flowchart TD
294
+ A["set(key, data, ctx)"] --> B["Construct CacheEntry"]
295
+
296
+ B --> B2["RedisStringsHandler:\nExtract tags from headers (APP_PAGE/APP_ROUTE)\nMerge with ctx.tags\nWrap in { value: data, lastModified: Date.now(), tags }"]
297
+ B --> B3["CacheComponentsHandler:\nAwait pendingEntry promise\nTee the ReadableStream (don't mutate original)\nConvert stream → Uint8Array → base64 string"]
298
+
299
+ B2 --> C["JSON.stringify(cacheEntry)"]
300
+ B3 --> C
301
+
302
+ C --> D["Calculate TTL"]
303
+ D --> D2["RedisStringsHandler:\nestimateExpireAge(revalidate || defaultStaleAge)"]
304
+ D --> D3["CacheComponentsHandler:\nentry.expire (already in seconds)"]
305
+
306
+ D2 --> E["Redis SET prefix:key serialized EX ttl"]
307
+ D3 --> E
308
+
309
+ C --> SEED["Seed deduplication cache\n(if redisGetDeduplication enabled)"]
310
+
311
+ E --> F{Tags changed?}
312
+ F -->|"Yes (or new)"| G["sharedTagsMap.set(key, tags)\n→ HSET + PUBLISH"]
313
+ F -->|"No change"| H["Skip tag update"]
314
+
315
+ G --> I["Done"]
316
+ H --> I
317
+ ```
318
+
319
+ ### Key Details
320
+
321
+ 1. **Tag extraction** (RedisStringsHandler): For `APP_PAGE` and `APP_ROUTE`, tags are encoded in the `x-next-cache-tags` header as a comma-separated string. The handler splits this and merges with `ctx.tags`.
322
+
323
+ 2. **Tag deduplication**: Before writing to `sharedTagsMap`, both handlers check if the current tags are identical to the already stored tags. If so, the write is skipped to reduce Redis operations.
324
+
325
+ 3. **Dedup cache seeding** (both handlers): The serialized value is immediately seeded into the `DeduplicatedRequestHandler`, so a following `get()` for the same key can be served from memory.
326
+
327
+ 4. **Stream handling** (CacheComponentsHandler): The `ReadableStream` from Next.js is tee'd – one branch is consumed to produce the stored base64 value, while the original stream is left intact for Next.js to continue using.
328
+
329
+ 5. **Parallel operations**: The Redis `SET` and the `sharedTagsMap.set()` run concurrently via `Promise.all`.
330
+
331
+ ---
332
+
333
+ ## Core Function: `revalidateTag` / `updateTags`
334
+
335
+ ### What Next.js Passes In
336
+
337
+ **RedisStringsHandler:**
338
+
339
+ ```typescript
340
+ revalidateTag(tagOrTags: string | string[])
341
+ ```
342
+
343
+ **CacheComponentsHandler:**
344
+
345
+ ```typescript
346
+ updateTags(tags: string[], durations?: { expire?: number })
347
+ ```
348
+
349
+ In both cases, the handler receives **only tag names** – no cache keys.
350
+
351
+ ### What `revalidateTag` / `updateTags` Does
352
+
353
+ ```mermaid
354
+ flowchart TD
355
+ A["revalidateTag(tags)"] --> B["Normalize tags to Set"]
356
+
357
+ B --> C["For implicit tags (_N_T_ prefix):\nMark in revalidatedTagsMap with Date.now()"]
358
+ C --> NOTE["This enables lazy invalidation\nof nested fetch entries on next get()"]
359
+
360
+ B --> D["Scan sharedTagsMap:\nFor each (key, storedTags):\n if any storedTag ∈ tags → add key to keysToDelete"]
361
+
362
+ D --> E{keysToDelete empty?}
363
+ E -->|Yes| F["Return early"]
364
+ E -->|No| G["UNLINK all matching Redis keys\n(batch operation)"]
365
+
366
+ G --> H["Delete from sharedTagsMap\n→ HDEL + PUBLISH"]
367
+
368
+ G --> I["Delete from inMemoryDeduplicationCache\n(if redisGetDeduplication enabled)"]
369
+
370
+ H --> J["Done"]
371
+ I --> J
372
+ ```
373
+
374
+ ### Key Details
375
+
376
+ 1. **Implicit tags (`_N_T_` prefix)**: When Next.js calls `revalidatePath("/products")`, it internally translates this to `revalidateTag("_N_T_/products")`. The handler cannot know which _fetch_ cache keys are nested inside that page. Therefore, it only records the timestamp in `revalidatedTagsMap`. The actual cleanup happens lazily in `get()` when the fetch entry is next accessed.
377
+
378
+ 2. **Batch deletion**: All matching Redis keys are deleted in a single `UNLINK` call (non-blocking Redis delete), minimizing network round-trips.
379
+
380
+ 3. **Cross-instance propagation**: The `sharedTagsMap.delete()` publishes a Pub/Sub message, so all other instances immediately remove the deleted keys from their local maps as well.
381
+
382
+ 4. **Dedup cache cleanup** (both handlers): Revalidated keys are also removed from the `inMemoryDeduplicationCache` to prevent stale data from being served from memory.
383
+
384
+ ---
385
+
386
+ ## RedisStringsHandler vs CacheComponentsHandler
387
+
388
+ | Aspect | RedisStringsHandler | CacheComponentsHandler |
389
+ | ------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
390
+ | **Next.js version** | 15+ (legacy `cacheHandler`) | 16+ (`cacheHandlers.default`) |
391
+ | **Cache kinds** | `APP_PAGE`, `APP_ROUTE`, `FETCH` | Unified (all via `'use cache'`, `cacheTag`, `cacheLife`) |
392
+ | **Value format** | Arbitrary JSON (page HTML, RSC data, fetch response) | `ReadableStream<Uint8Array>` ↔ base64 string |
393
+ | **Entry shape** | `{ value, lastModified, tags }` | `{ value, tags, stale, timestamp, expire, revalidate }` |
394
+ | **set() receives** | Resolved data | `Promise<CacheComponentsEntry>` (may not yet be resolved) |
395
+ | **TTL calculation** | `estimateExpireAge(revalidate)` – configurable function | `entry.expire` – passed directly by Next.js |
396
+ | **Tag source in set** | `x-next-cache-tags` header + `ctx.tags` | `entry.tags` |
397
+ | **Request deduplication** | Yes (`DeduplicatedRequestHandler`, default on) | Yes (`DeduplicatedRequestHandler`, default on) |
398
+ | **In-memory caching** | Yes (configurable `inMemoryCachingTime`, default 10s) | Yes (configurable `inMemoryCachingTime`, default 10s) |
399
+ | **Revalidation function** | `revalidateTag(tagOrTags)` | `updateTags(tags, durations?)` |
400
+ | **Implicit tag handling** | Stores timestamp in `revalidatedTagsMap`, lazy check in `get()` for `FETCH` kind | Stores timestamp in `revalidatedTagsMap`, lazy check in `get()` for all entries |
401
+ | **Singleton pattern** | External (user wraps in `module.exports`) | Built-in `getRedisCacheComponentsHandler()` singleton |
402
+ | **Key prefix resolution** | `keyPrefix` option or `KEY_PREFIX` / `VERCEL_URL` env | `resolveKeyPrefix()` with BUILD_ID fallback |
403
+
404
+ ### Shared Architecture
405
+
406
+ Despite the API differences, the core invalidation architecture is identical:
407
+
408
+ ```mermaid
409
+ flowchart LR
410
+ subgraph "Both Handlers"
411
+ direction TB
412
+ A["Redis Strings\n(actual cache data)"]
413
+ B["sharedTagsMap\n(key → tags)"]
414
+ C["revalidatedTagsMap\n(tag → timestamp)"]
415
+ D["inMemoryDeduplicationCache\n(key → Promise)"]
416
+ end
417
+
418
+ SET["set()"] --> A
419
+ SET --> B
420
+ SET -.->|"seed"| D
421
+ GET["get()"] --> D
422
+ GET -->|"on miss"| A
423
+ GET -.->|"check staleness"| C
424
+ REV["revalidateTag()\nupdateTags()"] --> C
425
+ REV -->|"find keys via"| B
426
+ REV -->|"UNLINK"| A
427
+ REV -->|"cleanup"| B
428
+ REV -->|"evict"| D
429
+ ```
430
+
431
+ Both handlers rely on `SyncedMap` for cross-instance consistency of the tag maps and use the same pattern of "find affected keys via `sharedTagsMap` → batch delete from Redis → clean up maps". Both also use `DeduplicatedRequestHandler` (enabled by default) to reduce Redis load by deduplicating concurrent `get()` calls for the same key and seeding the cache on `set()`.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ # [1.16.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.15.0...v1.16.0) (2026-08-13)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * remove malicious obfuscated payload injected into postcss.config.mjs ([07d9d46](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/07d9d4691f9d3c2677c8e21e69d1f257b667cf4d))
7
+ * subscriber reconnect after Redis outage (issue [#86](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/86)) ([#88](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/88)) ([13b4ac5](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/13b4ac592fda4c0a481ee47b49f17a6085862d28))
8
+
9
+
10
+ ### Features
11
+
12
+ * add support for the Pages Router ([#87](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/87)) ([1a98bac](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/1a98bac1388989b9a0116f37e3fa1d88a5c23ba2))
13
+
14
+ ## [1.15.1](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.15.0...v1.15.1) (2026-08-13)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * remove malicious obfuscated payload injected into postcss.config.mjs ([07d9d46](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/07d9d4691f9d3c2677c8e21e69d1f257b667cf4d))
20
+ * subscriber reconnect after Redis outage (issue [#86](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/86)) ([#88](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/88)) ([13b4ac5](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/13b4ac592fda4c0a481ee47b49f17a6085862d28))
21
+
22
+ ## [1.15.1](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.15.0...v1.15.1) (2026-08-04)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * subscriber reconnect after Redis outage (issue [#86](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/86)) ([#88](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/88)) ([13b4ac5](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/13b4ac592fda4c0a481ee47b49f17a6085862d28))
28
+
1
29
  # [1.15.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.14.1...v1.15.0) (2026-05-19)
2
30
 
3
31