@trieb.work/nextjs-turbo-redis-cache 1.17.0 → 1.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/README.md +87 -24
  2. package/dist/index.d.mts +40 -1
  3. package/dist/index.d.ts +40 -1
  4. package/dist/index.js +291 -116
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +288 -115
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +9 -6
  9. package/.cursor/skills/testing/SKILL.md +0 -121
  10. package/.github/workflows/ci.yml +0 -309
  11. package/.github/workflows/pages.yml +0 -38
  12. package/.github/workflows/release.yml +0 -207
  13. package/.prettierrc.json +0 -6
  14. package/.vscode/settings.json +0 -10
  15. package/ARCHITECTURE.md +0 -431
  16. package/CHANGELOG.md +0 -411
  17. package/commitlint.config.cjs +0 -4
  18. package/docker/redis.conf +0 -1
  19. package/docs/index.html +0 -140
  20. package/docs/robots.txt +0 -4
  21. package/docs/sitemap.xml +0 -8
  22. package/docs/styles.css +0 -141
  23. package/eslint.config.mjs +0 -26
  24. package/hooks/commit-msg +0 -10
  25. package/hooks/pre-commit +0 -11
  26. package/playwright.config.ts +0 -18
  27. package/release.config.cjs +0 -31
  28. package/scripts/prepare.sh +0 -7
  29. package/scripts/setup-git.sh +0 -8
  30. package/scripts/setup-repo-name.sh +0 -17
  31. package/scripts/vitest-run-staged.cjs +0 -44
  32. package/src/CacheComponentsHandler.ts +0 -567
  33. package/src/CachedHandler.ts +0 -53
  34. package/src/DeduplicatedRequestHandler.ts +0 -110
  35. package/src/RedisStringsHandler.ts +0 -937
  36. package/src/SyncedMap.ts +0 -482
  37. package/src/ZodHandler.ts +0 -45
  38. package/src/index.ts +0 -16
  39. package/src/serializer.ts +0 -59
  40. package/src/utils/debug.ts +0 -30
  41. package/src/utils/json.ts +0 -49
  42. package/src/utils/prefix.ts +0 -44
  43. package/test/README.md +0 -194
  44. package/test/nextjs-test-projects/next-app-15-4-11/README.md +0 -36
  45. package/test/nextjs-test-projects/next-app-15-4-11/eslint.config.mjs +0 -16
  46. package/test/nextjs-test-projects/next-app-15-4-11/next.config.js +0 -9
  47. package/test/nextjs-test-projects/next-app-15-4-11/package-lock.json +0 -5969
  48. package/test/nextjs-test-projects/next-app-15-4-11/package.json +0 -29
  49. package/test/nextjs-test-projects/next-app-15-4-11/pnpm-lock.yaml +0 -3726
  50. package/test/nextjs-test-projects/next-app-15-4-11/postcss.config.mjs +0 -5
  51. package/test/nextjs-test-projects/next-app-15-4-11/public/file.svg +0 -1
  52. package/test/nextjs-test-projects/next-app-15-4-11/public/globe.svg +0 -1
  53. package/test/nextjs-test-projects/next-app-15-4-11/public/next.svg +0 -1
  54. package/test/nextjs-test-projects/next-app-15-4-11/public/vercel.svg +0 -1
  55. package/test/nextjs-test-projects/next-app-15-4-11/public/window.svg +0 -1
  56. package/test/nextjs-test-projects/next-app-15-4-11/src/app/api/cached-static-fetch/route.ts +0 -18
  57. package/test/nextjs-test-projects/next-app-15-4-11/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  58. package/test/nextjs-test-projects/next-app-15-4-11/src/app/api/revalidatePath/route.ts +0 -15
  59. package/test/nextjs-test-projects/next-app-15-4-11/src/app/api/revalidateTag/route.ts +0 -15
  60. package/test/nextjs-test-projects/next-app-15-4-11/src/app/api/revalidated-fetch/route.ts +0 -17
  61. package/test/nextjs-test-projects/next-app-15-4-11/src/app/api/uncached-fetch/route.ts +0 -15
  62. package/test/nextjs-test-projects/next-app-15-4-11/src/app/globals.css +0 -26
  63. package/test/nextjs-test-projects/next-app-15-4-11/src/app/layout.tsx +0 -59
  64. package/test/nextjs-test-projects/next-app-15-4-11/src/app/page.tsx +0 -755
  65. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  66. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  67. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  68. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  69. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  70. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  71. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  72. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  73. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  74. package/test/nextjs-test-projects/next-app-15-4-11/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  75. package/test/nextjs-test-projects/next-app-15-4-11/src/app/revalidation-interface.tsx +0 -267
  76. package/test/nextjs-test-projects/next-app-15-4-11/tsconfig.json +0 -27
  77. package/test/nextjs-test-projects/next-app-16-0-11/README.md +0 -36
  78. package/test/nextjs-test-projects/next-app-16-0-11/eslint.config.mjs +0 -18
  79. package/test/nextjs-test-projects/next-app-16-0-11/next.config.ts +0 -10
  80. package/test/nextjs-test-projects/next-app-16-0-11/package.json +0 -28
  81. package/test/nextjs-test-projects/next-app-16-0-11/pnpm-lock.yaml +0 -4163
  82. package/test/nextjs-test-projects/next-app-16-0-11/postcss.config.mjs +0 -7
  83. package/test/nextjs-test-projects/next-app-16-0-11/src/app/api/cached-static-fetch/route.ts +0 -18
  84. package/test/nextjs-test-projects/next-app-16-0-11/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  85. package/test/nextjs-test-projects/next-app-16-0-11/src/app/api/revalidatePath/route.ts +0 -15
  86. package/test/nextjs-test-projects/next-app-16-0-11/src/app/api/revalidateTag/route.ts +0 -20
  87. package/test/nextjs-test-projects/next-app-16-0-11/src/app/api/revalidated-fetch/route.ts +0 -17
  88. package/test/nextjs-test-projects/next-app-16-0-11/src/app/api/uncached-fetch/route.ts +0 -15
  89. package/test/nextjs-test-projects/next-app-16-0-11/src/app/favicon.ico +0 -0
  90. package/test/nextjs-test-projects/next-app-16-0-11/src/app/globals.css +0 -26
  91. package/test/nextjs-test-projects/next-app-16-0-11/src/app/layout.tsx +0 -59
  92. package/test/nextjs-test-projects/next-app-16-0-11/src/app/page.tsx +0 -755
  93. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  94. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  95. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  96. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  97. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  98. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  99. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  100. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  101. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  102. package/test/nextjs-test-projects/next-app-16-0-11/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  103. package/test/nextjs-test-projects/next-app-16-0-11/src/app/revalidation-interface.tsx +0 -267
  104. package/test/nextjs-test-projects/next-app-16-0-11/src/app/update-tag-test/page.tsx +0 -25
  105. package/test/nextjs-test-projects/next-app-16-0-11/tsconfig.json +0 -34
  106. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/README.md +0 -36
  107. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/cache-handler.js +0 -3
  108. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/eslint.config.mjs +0 -18
  109. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/next.config.ts +0 -13
  110. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/package.json +0 -28
  111. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/pnpm-lock.yaml +0 -4206
  112. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/postcss.config.mjs +0 -7
  113. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/public/file.svg +0 -1
  114. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/public/globe.svg +0 -1
  115. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/public/next.svg +0 -1
  116. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/public/vercel.svg +0 -1
  117. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/public/window.svg +0 -1
  118. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/cached-static-fetch/route.ts +0 -19
  119. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/cached-with-cachelife/route.ts +0 -24
  120. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/cached-with-tag/route.ts +0 -21
  121. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/revalidate-tag/route.ts +0 -19
  122. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/revalidated-fetch/route.ts +0 -19
  123. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/cachelife-short/page.tsx +0 -110
  124. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/page.tsx +0 -90
  125. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +0 -127
  126. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +0 -130
  127. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +0 -127
  128. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +0 -110
  129. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/favicon.ico +0 -0
  130. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/globals.css +0 -26
  131. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/layout.tsx +0 -57
  132. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/page.tsx +0 -755
  133. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/revalidation-interface.tsx +0 -267
  134. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/update-tag-test/page.tsx +0 -22
  135. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/tsconfig.json +0 -34
  136. package/test/nextjs-test-projects/next-app-16-2-6/README.md +0 -36
  137. package/test/nextjs-test-projects/next-app-16-2-6/eslint.config.mjs +0 -18
  138. package/test/nextjs-test-projects/next-app-16-2-6/next.config.ts +0 -7
  139. package/test/nextjs-test-projects/next-app-16-2-6/package.json +0 -28
  140. package/test/nextjs-test-projects/next-app-16-2-6/pnpm-lock.yaml +0 -4165
  141. package/test/nextjs-test-projects/next-app-16-2-6/postcss.config.mjs +0 -7
  142. package/test/nextjs-test-projects/next-app-16-2-6/src/app/api/cached-static-fetch/route.ts +0 -18
  143. package/test/nextjs-test-projects/next-app-16-2-6/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  144. package/test/nextjs-test-projects/next-app-16-2-6/src/app/api/revalidatePath/route.ts +0 -15
  145. package/test/nextjs-test-projects/next-app-16-2-6/src/app/api/revalidateTag/route.ts +0 -20
  146. package/test/nextjs-test-projects/next-app-16-2-6/src/app/api/revalidated-fetch/route.ts +0 -17
  147. package/test/nextjs-test-projects/next-app-16-2-6/src/app/api/uncached-fetch/route.ts +0 -15
  148. package/test/nextjs-test-projects/next-app-16-2-6/src/app/favicon.ico +0 -0
  149. package/test/nextjs-test-projects/next-app-16-2-6/src/app/globals.css +0 -26
  150. package/test/nextjs-test-projects/next-app-16-2-6/src/app/layout.tsx +0 -59
  151. package/test/nextjs-test-projects/next-app-16-2-6/src/app/page.tsx +0 -755
  152. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  153. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  154. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  155. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  156. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  157. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  158. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  159. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  160. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  161. package/test/nextjs-test-projects/next-app-16-2-6/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  162. package/test/nextjs-test-projects/next-app-16-2-6/src/app/revalidation-interface.tsx +0 -267
  163. package/test/nextjs-test-projects/next-app-16-2-6/src/app/update-tag-test/page.tsx +0 -25
  164. package/test/nextjs-test-projects/next-app-16-2-6/tsconfig.json +0 -34
  165. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/README.md +0 -36
  166. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/cache-handler.js +0 -3
  167. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/eslint.config.mjs +0 -18
  168. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/next.config.ts +0 -13
  169. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/package.json +0 -28
  170. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/pnpm-lock.yaml +0 -4165
  171. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/postcss.config.mjs +0 -7
  172. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/public/file.svg +0 -1
  173. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/public/globe.svg +0 -1
  174. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/public/next.svg +0 -1
  175. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/public/vercel.svg +0 -1
  176. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/public/window.svg +0 -1
  177. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-static-fetch/route.ts +0 -19
  178. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-cachelife/route.ts +0 -24
  179. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-tag/route.ts +0 -21
  180. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidate-tag/route.ts +0 -19
  181. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidated-fetch/route.ts +0 -19
  182. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/cachelife-short/page.tsx +0 -110
  183. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/page.tsx +0 -90
  184. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +0 -127
  185. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +0 -130
  186. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +0 -127
  187. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +0 -110
  188. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/favicon.ico +0 -0
  189. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/globals.css +0 -26
  190. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/layout.tsx +0 -57
  191. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/page.tsx +0 -755
  192. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/revalidation-interface.tsx +0 -267
  193. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/update-tag-test/page.tsx +0 -22
  194. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/tsconfig.json +0 -34
  195. package/test/nextjs-test-projects/next-app-16-3-0/README.md +0 -36
  196. package/test/nextjs-test-projects/next-app-16-3-0/eslint.config.mjs +0 -18
  197. package/test/nextjs-test-projects/next-app-16-3-0/next.config.ts +0 -7
  198. package/test/nextjs-test-projects/next-app-16-3-0/package.json +0 -28
  199. package/test/nextjs-test-projects/next-app-16-3-0/pnpm-lock.yaml +0 -4284
  200. package/test/nextjs-test-projects/next-app-16-3-0/postcss.config.mjs +0 -7
  201. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/cached-static-fetch/route.ts +0 -18
  202. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  203. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidatePath/route.ts +0 -15
  204. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidateTag/route.ts +0 -20
  205. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidated-fetch/route.ts +0 -17
  206. package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/uncached-fetch/route.ts +0 -15
  207. package/test/nextjs-test-projects/next-app-16-3-0/src/app/favicon.ico +0 -0
  208. package/test/nextjs-test-projects/next-app-16-3-0/src/app/globals.css +0 -26
  209. package/test/nextjs-test-projects/next-app-16-3-0/src/app/layout.tsx +0 -59
  210. package/test/nextjs-test-projects/next-app-16-3-0/src/app/page.tsx +0 -755
  211. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  212. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  213. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  214. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  215. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  216. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  217. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  218. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  219. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  220. package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  221. package/test/nextjs-test-projects/next-app-16-3-0/src/app/revalidation-interface.tsx +0 -267
  222. package/test/nextjs-test-projects/next-app-16-3-0/src/app/update-tag-test/page.tsx +0 -25
  223. package/test/nextjs-test-projects/next-app-16-3-0/tsconfig.json +0 -34
  224. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/README.md +0 -36
  225. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/cache-handler.js +0 -3
  226. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/eslint.config.mjs +0 -18
  227. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/next.config.ts +0 -13
  228. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/package.json +0 -28
  229. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/pnpm-lock.yaml +0 -4284
  230. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/postcss.config.mjs +0 -7
  231. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/file.svg +0 -1
  232. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/globe.svg +0 -1
  233. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/next.svg +0 -1
  234. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/vercel.svg +0 -1
  235. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/window.svg +0 -1
  236. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-static-fetch/route.ts +0 -19
  237. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-with-cachelife/route.ts +0 -24
  238. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-with-tag/route.ts +0 -21
  239. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/revalidate-tag/route.ts +0 -19
  240. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/revalidated-fetch/route.ts +0 -19
  241. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/cachelife-short/page.tsx +0 -110
  242. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/page.tsx +0 -90
  243. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +0 -127
  244. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +0 -130
  245. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +0 -127
  246. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +0 -110
  247. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/favicon.ico +0 -0
  248. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/globals.css +0 -26
  249. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/layout.tsx +0 -57
  250. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/page.tsx +0 -755
  251. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/revalidation-interface.tsx +0 -267
  252. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/update-tag-test/page.tsx +0 -22
  253. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/tsconfig.json +0 -34
  254. package/test/nextjs-test-projects/next-app-customized/README.md +0 -36
  255. package/test/nextjs-test-projects/next-app-customized/customized-cache-handler.js +0 -34
  256. package/test/nextjs-test-projects/next-app-customized/eslint.config.mjs +0 -16
  257. package/test/nextjs-test-projects/next-app-customized/next.config.js +0 -6
  258. package/test/nextjs-test-projects/next-pages-16-2-6/README.md +0 -16
  259. package/test/nextjs-test-projects/next-pages-16-2-6/eslint.config.mjs +0 -18
  260. package/test/nextjs-test-projects/next-pages-16-2-6/next.config.ts +0 -7
  261. package/test/nextjs-test-projects/next-pages-16-2-6/package.json +0 -26
  262. package/test/nextjs-test-projects/next-pages-16-2-6/pnpm-lock.yaml +0 -3896
  263. package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/api/revalidate.ts +0 -24
  264. package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/index.tsx +0 -11
  265. package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/isr/[slug].tsx +0 -49
  266. package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/static-forever.tsx +0 -20
  267. package/test/nextjs-test-projects/next-pages-16-2-6/tsconfig.json +0 -34
  268. package/test/nextjs-test-projects/next-pages-16-3-0/README.md +0 -16
  269. package/test/nextjs-test-projects/next-pages-16-3-0/eslint.config.mjs +0 -18
  270. package/test/nextjs-test-projects/next-pages-16-3-0/next.config.ts +0 -7
  271. package/test/nextjs-test-projects/next-pages-16-3-0/package.json +0 -26
  272. package/test/nextjs-test-projects/next-pages-16-3-0/pnpm-lock.yaml +0 -3939
  273. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/api/revalidate.ts +0 -24
  274. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/index.tsx +0 -11
  275. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/isr/[slug].tsx +0 -49
  276. package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/static-forever.tsx +0 -20
  277. package/test/nextjs-test-projects/next-pages-16-3-0/tsconfig.json +0 -34
  278. package/test/playwright/cache-lab.spec.ts +0 -157
  279. package/test/playwright/update-tag.spec.ts +0 -33
  280. package/test/vitest/integration/build-id-prefix.integration.test.ts +0 -102
  281. package/test/vitest/integration/cache-components/cache-components.integration.test.ts +0 -231
  282. package/test/vitest/integration/cache-components/redis-kill-reconnect.test.ts +0 -65
  283. package/test/vitest/integration/cache-components/redis-quit-vs-disconnect.test.ts +0 -59
  284. package/test/vitest/integration/cache-components/redis-subscriber-outage.test.ts +0 -100
  285. package/test/vitest/integration/cache-components/scripts/redis-kill-reconnect.ts +0 -243
  286. package/test/vitest/integration/cache-components/scripts/redis-quit-vs-disconnect.ts +0 -113
  287. package/test/vitest/integration/cache-components/scripts/redis-subscriber-outage.ts +0 -379
  288. package/test/vitest/integration/cache-components/scripts/redis-test-helpers.ts +0 -155
  289. package/test/vitest/integration/nextjs-cache-handler.integration.test.ts +0 -945
  290. package/test/vitest/integration/pages-router.integration.test.ts +0 -420
  291. package/test/vitest/unit/SyncedMap-reconnect-repro.test.ts +0 -154
  292. package/test/vitest/unit/index.test.ts +0 -173
  293. package/test/vitest/unit/pages-router-kinds.test.ts +0 -292
  294. package/test/vitest/unit/reconnect-socket-already-opened.test.ts +0 -117
  295. package/test/vitest/unit/serializer.test.ts +0 -182
  296. package/test/vitest/unit/utils/prefix.test.ts +0 -115
  297. package/tsconfig.json +0 -18
  298. package/tsup.config.ts +0 -14
  299. package/vite.config.ts +0 -34
  300. package/vitest.cache-components.config.ts +0 -16
@@ -1,207 +0,0 @@
1
- name: Release
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
- - beta
8
-
9
- # Minimal permissions:
10
- # contents: semantic-release pushes the release commit + tag to the branch
11
- # issues: @semantic-release/github comments on referenced issues
12
- # pull-requests: @semantic-release/github comments on referenced PRs
13
- # id-token: npm Trusted Publishing OIDC token exchange
14
- permissions:
15
- contents: write
16
- issues: write
17
- pull-requests: write
18
- id-token: write
19
-
20
- jobs:
21
- release:
22
- runs-on: ubuntu-latest
23
-
24
- steps:
25
- - name: Create GitHub App token
26
- id: app-token
27
- uses: actions/create-github-app-token@v1
28
- with:
29
- app-id: ${{ vars.RELEASE_APP_ID }}
30
- private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
31
-
32
- - name: Checkout code
33
- uses: actions/checkout@v6
34
- with:
35
- # Need full history so the post-release audit can diff against pre-release SHA.
36
- fetch-depth: 0
37
- token: ${{ steps.app-token.outputs.token }}
38
-
39
- - name: Capture pre-release SHA
40
- id: pre
41
- run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
42
-
43
- - name: Install pnpm
44
- run: corepack enable
45
-
46
- - name: Setup Node.js
47
- uses: actions/setup-node@v6
48
- with:
49
- node-version: '22'
50
- cache: 'pnpm'
51
-
52
- - name: Install dependencies
53
- # --ignore-scripts blocks lifecycle scripts (preinstall/install/postinstall/prepare)
54
- # of all dependencies. This is the primary defence against supply-chain attacks
55
- # injected via a compromised transitive dep's postinstall hook.
56
- run: pnpm install --frozen-lockfile --ignore-scripts
57
- - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
58
- run: npm audit signatures
59
-
60
- - name: Build project
61
- run: pnpm build
62
-
63
- - name: Verify working tree is clean before release
64
- # If install/build silently modified tracked files, abort before semantic-release
65
- # can include them in the release commit.
66
- run: |
67
- if [ -n "$(git status --porcelain)" ]; then
68
- echo "::error::Working tree is dirty before semantic-release. Aborting."
69
- git status --short
70
- git diff --stat
71
- exit 1
72
- fi
73
-
74
- - name: Run Semantic Release
75
- env:
76
- GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} # GitHub App token (bypasses required_signatures ruleset)
77
- NPM_CONFIG_PROVENANCE: 'true'
78
- run: pnpm exec semantic-release
79
-
80
- - name: Audit semantic-release commit — fail if files outside allowlist were touched
81
- env:
82
- PRE_SHA: ${{ steps.pre.outputs.sha }}
83
- run: |
84
- # Allowlist must mirror the `assets` array in release.config.cjs.
85
- # Any file outside this list being modified by the release run is suspicious
86
- # and blocks the npm publish step below.
87
- ALLOWED="package.json pnpm-lock.yaml CHANGELOG.md"
88
-
89
- if [ "$(git rev-parse HEAD)" = "$PRE_SHA" ]; then
90
- echo "semantic-release made no commit; nothing to audit."
91
- exit 0
92
- fi
93
-
94
- echo "Auditing commits ${PRE_SHA}..HEAD"
95
- CHANGED=$(git diff --name-only "${PRE_SHA}..HEAD")
96
- echo "Files changed:"
97
- printf '%s\n' "$CHANGED"
98
-
99
- UNEXPECTED=""
100
- while IFS= read -r file; do
101
- [ -z "$file" ] && continue
102
- match=false
103
- for allowed in $ALLOWED; do
104
- if [ "$file" = "$allowed" ]; then
105
- match=true
106
- break
107
- fi
108
- done
109
- [ "$match" = false ] && UNEXPECTED="${UNEXPECTED}\n ${file}"
110
- done <<< "$CHANGED"
111
-
112
- if [ -n "$UNEXPECTED" ]; then
113
- printf '::error::semantic-release touched files outside the allowlist:%b\n' "$UNEXPECTED"
114
- echo "::error::This may indicate a compromised dependency. Aborting publish."
115
- exit 1
116
- fi
117
-
118
- echo "All changed files are within the allowlist."
119
-
120
- - name: Check if current version is already published
121
- id: version-check
122
- env:
123
- NPM_PACKAGE_NAME: '@trieb.work/nextjs-turbo-redis-cache'
124
- run: |
125
- VERSION=$(node -p "require('./package.json').version")
126
- echo "version=$VERSION" >> "$GITHUB_OUTPUT"
127
- if npm view "$NPM_PACKAGE_NAME@$VERSION" version --registry https://registry.npmjs.org/ >/dev/null 2>&1; then
128
- echo "should_publish=false" >> "$GITHUB_OUTPUT"
129
- else
130
- echo "should_publish=true" >> "$GITHUB_OUTPUT"
131
- fi
132
-
133
- - name: Exchange GitHub OIDC token for npm token
134
- id: npm-oidc
135
- env:
136
- NPM_PACKAGE_NAME: '@trieb.work/nextjs-turbo-redis-cache'
137
- run: |
138
- node <<'NODE'
139
- const fs = require('node:fs');
140
-
141
- const pkg = process.env.NPM_PACKAGE_NAME;
142
- const reqUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
143
- const reqToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
144
-
145
- if (!pkg || !reqUrl || !reqToken) {
146
- console.error('Missing required env for OIDC token retrieval');
147
- process.exit(1);
148
- }
149
-
150
- const audience = 'npm:registry.npmjs.org';
151
- const url = reqUrl + (reqUrl.includes('?') ? '&' : '?') + 'audience=' + encodeURIComponent(audience);
152
-
153
- (async () => {
154
- const idRes = await fetch(url, { headers: { Authorization: 'Bearer ' + reqToken } });
155
- if (!idRes.ok) {
156
- console.error('Failed to fetch GitHub OIDC token:', idRes.status, await idRes.text());
157
- process.exit(1);
158
- }
159
-
160
- const idBody = await idRes.json();
161
- const idToken = idBody.value;
162
-
163
- const exUrl =
164
- 'https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/' + encodeURIComponent(pkg);
165
- const exRes = await fetch(exUrl, {
166
- method: 'POST',
167
- headers: { Authorization: 'Bearer ' + idToken },
168
- });
169
-
170
- const exText = await exRes.text();
171
- if (!exRes.ok) {
172
- console.error('OIDC token exchange with npm failed:', exRes.status, exText);
173
- process.exit(1);
174
- }
175
-
176
- const exBody = JSON.parse(exText);
177
- const npmToken = exBody.token;
178
- if (!npmToken) {
179
- console.error('npm exchange response missing token');
180
- process.exit(1);
181
- }
182
-
183
- fs.appendFileSync(process.env.GITHUB_OUTPUT, `node_auth_token=${npmToken}\n`);
184
- const npmrcPath = `${process.env.RUNNER_TEMP}/npmrc`;
185
- const npmrc = [
186
- 'registry=https://registry.npmjs.org/',
187
- 'always-auth=true',
188
- '//registry.npmjs.org/:_authToken=' + npmToken,
189
- '',
190
- ].join('\n');
191
- fs.writeFileSync(npmrcPath, npmrc, { encoding: 'utf8' });
192
- fs.appendFileSync(process.env.GITHUB_OUTPUT, `npmrc_path=${npmrcPath}\n`);
193
- console.log('OIDC token exchange with npm registry succeeded');
194
- })().catch((e) => {
195
- console.error(e);
196
- process.exit(1);
197
- });
198
- NODE
199
-
200
- - name: Publish to npm (Trusted Publishing)
201
- if: steps.version-check.outputs.should_publish == 'true'
202
- env:
203
- NPM_CONFIG_PROVENANCE: 'true'
204
- NPM_DIST_TAG: ${{ github.ref_name == 'beta' && 'beta' || 'latest' }}
205
- NPM_CONFIG_USERCONFIG: ${{ steps.npm-oidc.outputs.npmrc_path }}
206
- run: npm publish --provenance --access public --tag $NPM_DIST_TAG --registry https://registry.npmjs.org/
207
-
package/.prettierrc.json DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "semi": true,
3
- "singleQuote": true,
4
- "printWidth": 80,
5
- "trailingComma": "all"
6
- }
@@ -1,10 +0,0 @@
1
- {
2
- "cSpell.words": [
3
- "EJSON",
4
- "Keyevent",
5
- "keyspace",
6
- "postboned",
7
- "REDISHOST",
8
- "REDISPORT"
9
- ]
10
- }
package/ARCHITECTURE.md DELETED
@@ -1,431 +0,0 @@
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&lt;string[]&gt;)\ncacheKey → tags"]
44
- RTM["revalidatedTagsMap\n(SyncedMap&lt;number&gt;)\ntag → timestamp"]
45
- DEDUP["inMemoryDeduplicationCache\n(SyncedMap&lt;Promise&gt;)"]
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()`.