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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/.cursor/skills/testing/SKILL.md +121 -0
  2. package/.github/workflows/ci.yml +155 -70
  3. package/ARCHITECTURE.md +431 -0
  4. package/CHANGELOG.md +7 -0
  5. package/README.md +72 -47
  6. package/dist/index.js +78 -14
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +78 -14
  9. package/dist/index.mjs.map +1 -1
  10. package/eslint.config.mjs +1 -1
  11. package/package.json +14 -16
  12. package/playwright.config.ts +5 -3
  13. package/src/CacheComponentsHandler.ts +57 -2
  14. package/src/SyncedMap.ts +62 -13
  15. package/test/README.md +179 -0
  16. package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/package.json +6 -6
  17. package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-15-4-11}/pnpm-lock.yaml +89 -105
  18. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/package.json +3 -3
  19. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/pnpm-lock.yaml +53 -53
  20. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/package.json +28 -0
  21. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/pnpm-lock.yaml +4206 -0
  22. package/test/nextjs-test-projects/next-app-16-2-6/eslint.config.mjs +18 -0
  23. package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/package.json +3 -3
  24. package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/pnpm-lock.yaml +54 -60
  25. package/test/nextjs-test-projects/next-app-16-2-6/postcss.config.mjs +7 -0
  26. package/test/nextjs-test-projects/next-app-16-2-6/src/app/favicon.ico +0 -0
  27. package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-2-6}/tsconfig.json +9 -2
  28. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/cache-handler.js +3 -0
  29. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/next.config.ts +13 -0
  30. package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6-cache-components}/package.json +3 -3
  31. package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6-cache-components}/pnpm-lock.yaml +54 -60
  32. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-static-fetch/route.ts +19 -0
  33. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-cachelife/route.ts +24 -0
  34. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-tag/route.ts +21 -0
  35. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidate-tag/route.ts +19 -0
  36. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidated-fetch/route.ts +19 -0
  37. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/cachelife-short/page.tsx +110 -0
  38. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/page.tsx +90 -0
  39. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +127 -0
  40. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +130 -0
  41. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +127 -0
  42. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +110 -0
  43. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/layout.tsx +0 -2
  44. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/update-tag-test/page.tsx +22 -0
  45. package/{tests → test/playwright}/update-tag.spec.ts +1 -1
  46. package/test/{integration → vitest/integration}/build-id-prefix.integration.test.ts +2 -2
  47. package/test/{cache-components/cache-components.integration.spec.ts → vitest/integration/cache-components/cache-components.integration.test.ts} +7 -3
  48. package/test/{cache-components/redis-kill-reconnect.spec.ts → vitest/integration/cache-components/redis-kill-reconnect.test.ts} +28 -7
  49. package/test/vitest/integration/cache-components/redis-quit-vs-disconnect.test.ts +59 -0
  50. package/test/vitest/integration/cache-components/redis-subscriber-outage.test.ts +100 -0
  51. package/test/{cache-components → vitest/integration/cache-components}/scripts/redis-kill-reconnect.ts +31 -8
  52. package/test/vitest/integration/cache-components/scripts/redis-quit-vs-disconnect.ts +113 -0
  53. package/test/vitest/integration/cache-components/scripts/redis-subscriber-outage.ts +379 -0
  54. package/test/vitest/integration/cache-components/scripts/redis-test-helpers.ts +155 -0
  55. package/test/{integration → vitest/integration}/nextjs-cache-handler.integration.test.ts +15 -9
  56. package/test/vitest/unit/SyncedMap-reconnect-repro.test.ts +154 -0
  57. package/{src → test/vitest/unit}/index.test.ts +3 -3
  58. package/test/{cache-components/reconnect-socket-already-opened.spec.ts → vitest/unit/reconnect-socket-already-opened.test.ts} +1 -1
  59. package/{src → test/vitest/unit}/serializer.test.ts +7 -3
  60. package/{src → test/vitest/unit}/utils/prefix.test.ts +1 -1
  61. package/vite.config.ts +4 -3
  62. package/vitest.cache-components.config.ts +1 -1
  63. package/test/browser/update-tag.browser.test.ts +0 -41
  64. package/test/integration/next-app-15-0-3/next.config.js +0 -6
  65. package/test/integration/next-app-15-0-3/package-lock.json +0 -5833
  66. package/test/integration/next-app-15-0-3/pnpm-lock.yaml +0 -3710
  67. package/test/integration/next-app-15-3-2/next.config.js +0 -6
  68. package/test/integration/next-app-15-3-2/package.json +0 -33
  69. package/test/integration/next-app-15-3-2/pnpm-lock.yaml +0 -3720
  70. package/test/integration/next-app-15-3-2/postcss.config.mjs +0 -5
  71. package/test/integration/next-app-15-3-2/src/app/api/revalidateTag/route.ts +0 -15
  72. package/test/integration/next-app-15-4-7/eslint.config.mjs +0 -16
  73. package/test/integration/next-app-15-4-7/package-lock.json +0 -5969
  74. package/test/integration/next-app-15-4-7/package.json +0 -33
  75. package/test/integration/next-app-15-4-7/postcss.config.mjs +0 -5
  76. package/test/integration/next-app-15-4-7/src/app/api/revalidateTag/route.ts +0 -15
  77. package/test/integration/next-app-15-4-7/tsconfig.json +0 -27
  78. package/test/integration/next-app-16-0-3/src/app/api/cached-static-fetch/route.ts +0 -18
  79. package/test/integration/next-app-16-0-3/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  80. package/test/integration/next-app-16-0-3/src/app/api/revalidatePath/route.ts +0 -15
  81. package/test/integration/next-app-16-0-3/src/app/api/revalidated-fetch/route.ts +0 -17
  82. package/test/integration/next-app-16-0-3/src/app/api/uncached-fetch/route.ts +0 -15
  83. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  84. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  85. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  86. package/test/integration/next-app-16-0-3/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  87. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  88. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  89. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  90. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  91. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  92. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  93. package/test/integration/next-app-16-2-3/src/app/api/cached-static-fetch/route.ts +0 -18
  94. package/test/integration/next-app-16-2-3/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  95. package/test/integration/next-app-16-2-3/src/app/api/revalidatePath/route.ts +0 -15
  96. package/test/integration/next-app-16-2-3/src/app/api/revalidated-fetch/route.ts +0 -17
  97. package/test/integration/next-app-16-2-3/src/app/api/uncached-fetch/route.ts +0 -15
  98. package/test/integration/next-app-16-2-3/src/app/globals.css +0 -26
  99. package/test/integration/next-app-16-2-3/src/app/layout.tsx +0 -59
  100. package/test/integration/next-app-16-2-3/src/app/page.tsx +0 -755
  101. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  102. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  103. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  104. package/test/integration/next-app-16-2-3/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  105. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  106. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  107. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  108. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  109. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  110. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  111. package/test/integration/next-app-16-2-3/src/app/revalidation-interface.tsx +0 -267
  112. package/test/integration/next-app-16-2-3-cache-components/public/file.svg +0 -1
  113. package/test/integration/next-app-16-2-3-cache-components/public/globe.svg +0 -1
  114. package/test/integration/next-app-16-2-3-cache-components/public/next.svg +0 -1
  115. package/test/integration/next-app-16-2-3-cache-components/public/vercel.svg +0 -1
  116. package/test/integration/next-app-16-2-3-cache-components/public/window.svg +0 -1
  117. package/test/integration/next-app-customized/README.md +0 -36
  118. package/test/integration/next-app-customized/eslint.config.mjs +0 -16
  119. package/vitest.browser.config.ts +0 -10
  120. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/README.md +0 -0
  121. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/eslint.config.mjs +0 -0
  122. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-15-4-11}/next.config.js +0 -0
  123. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-15-4-11}/package-lock.json +0 -0
  124. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/postcss.config.mjs +0 -0
  125. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/file.svg +0 -0
  126. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/globe.svg +0 -0
  127. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/next.svg +0 -0
  128. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/vercel.svg +0 -0
  129. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/window.svg +0 -0
  130. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/cached-static-fetch/route.ts +0 -0
  131. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -0
  132. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidatePath/route.ts +0 -0
  133. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidateTag/route.ts +0 -0
  134. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidated-fetch/route.ts +0 -0
  135. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/uncached-fetch/route.ts +0 -0
  136. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/globals.css +0 -0
  137. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/layout.tsx +0 -0
  138. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/page.tsx +0 -0
  139. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -0
  140. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -0
  141. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  142. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/no-fetch/default-page/page.tsx +0 -0
  143. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -0
  144. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -0
  145. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  146. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -0
  147. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -0
  148. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  149. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/revalidation-interface.tsx +0 -0
  150. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/tsconfig.json +0 -0
  151. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/README.md +0 -0
  152. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/eslint.config.mjs +0 -0
  153. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/next.config.ts +0 -0
  154. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/postcss.config.mjs +0 -0
  155. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/cached-static-fetch/route.ts +0 -0
  156. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -0
  157. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidatePath/route.ts +0 -0
  158. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidateTag/route.ts +0 -0
  159. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidated-fetch/route.ts +0 -0
  160. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/uncached-fetch/route.ts +0 -0
  161. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/src/app/favicon.ico +0 -0
  162. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/globals.css +0 -0
  163. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/layout.tsx +0 -0
  164. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/page.tsx +0 -0
  165. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -0
  166. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -0
  167. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  168. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/no-fetch/default-page/page.tsx +0 -0
  169. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -0
  170. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -0
  171. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  172. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -0
  173. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -0
  174. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  175. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/revalidation-interface.tsx +0 -0
  176. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/src/app/update-tag-test/page.tsx +0 -0
  177. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/tsconfig.json +0 -0
  178. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/README.md +0 -0
  179. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/cache-handler.js +0 -0
  180. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/eslint.config.mjs +0 -0
  181. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/next.config.ts +0 -0
  182. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/postcss.config.mjs +0 -0
  183. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/file.svg +0 -0
  184. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/globe.svg +0 -0
  185. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/next.svg +0 -0
  186. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/vercel.svg +0 -0
  187. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/window.svg +0 -0
  188. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/api/cached-static-fetch/route.ts +0 -0
  189. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/api/cached-with-cachelife/route.ts +0 -0
  190. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/api/cached-with-tag/route.ts +0 -0
  191. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/api/revalidate-tag/route.ts +0 -0
  192. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/api/revalidated-fetch/route.ts +0 -0
  193. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/cachelife-short/page.tsx +0 -0
  194. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/page.tsx +0 -0
  195. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/runtime-data-suspense/page.tsx +0 -0
  196. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/stale-while-revalidate/page.tsx +0 -0
  197. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/tag-invalidation/page.tsx +0 -0
  198. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/use-cache-nondeterministic/page.tsx +0 -0
  199. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/favicon.ico +0 -0
  200. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/globals.css +0 -0
  201. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/layout.tsx +0 -0
  202. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/page.tsx +0 -0
  203. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/revalidation-interface.tsx +0 -0
  204. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/update-tag-test/page.tsx +0 -0
  205. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/tsconfig.json +0 -0
  206. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/README.md +0 -0
  207. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/next.config.ts +0 -0
  208. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/cached-static-fetch/route.ts +0 -0
  209. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -0
  210. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidatePath/route.ts +0 -0
  211. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidateTag/route.ts +0 -0
  212. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidated-fetch/route.ts +0 -0
  213. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/uncached-fetch/route.ts +0 -0
  214. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/globals.css +0 -0
  215. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/layout.tsx +0 -0
  216. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/page.tsx +0 -0
  217. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -0
  218. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -0
  219. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  220. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/no-fetch/default-page/page.tsx +0 -0
  221. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -0
  222. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -0
  223. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  224. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -0
  225. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -0
  226. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  227. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/revalidation-interface.tsx +0 -0
  228. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/src/app/update-tag-test/page.tsx +0 -0
  229. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/README.md +0 -0
  230. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/eslint.config.mjs +0 -0
  231. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/postcss.config.mjs +0 -0
  232. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/file.svg +0 -0
  233. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/globe.svg +0 -0
  234. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/next.svg +0 -0
  235. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/vercel.svg +0 -0
  236. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/window.svg +0 -0
  237. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/favicon.ico +0 -0
  238. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/globals.css +0 -0
  239. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/page.tsx +0 -0
  240. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/revalidation-interface.tsx +0 -0
  241. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/tsconfig.json +0 -0
  242. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-customized}/README.md +0 -0
  243. /package/test/{integration → nextjs-test-projects}/next-app-customized/customized-cache-handler.js +0 -0
  244. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-customized}/eslint.config.mjs +0 -0
  245. /package/test/{integration → nextjs-test-projects}/next-app-customized/next.config.js +0 -0
  246. /package/{tests → test/playwright}/cache-lab.spec.ts +0 -0
package/README.md CHANGED
@@ -15,6 +15,8 @@ Key Features:
15
15
 
16
16
  This library offers you an easy and high performant caching solution for docker, Kubernetes or Google Cloud Run deployments of Next.js. Read more on how it originated at [TRWK> Case Study](https://trwk.de/case-studies/nextjs-turbo-redis-cache).
17
17
 
18
+ For a deep dive into the internal architecture (shared hash maps, SyncedMap, request deduplication, and how `get`/`set`/`revalidateTag` work under the hood), see [ARCHITECTURE.md](./ARCHITECTURE.md).
19
+
18
20
  ## Compatibility
19
21
 
20
22
  This package is compatible with Next.js 15.0.3 and above while using App Router. It is not compatible with Next.js 14.x. or 15-canary or if you are using Pages Router.
@@ -25,10 +27,10 @@ Tested versions are:
25
27
  - Nextjs 15.0.3 + redis client 4.7.0
26
28
  - Nextjs 15.2.4 + redis client 4.7.0
27
29
  - Nextjs 15.3.2 + redis client 4.7.0
28
- - Nextjs 15.4.7 + redis client 4.7.0
29
- - Nextjs 16.0.3 + redis client 4.7.0 (cacheComponents: false)
30
- - Nextjs 16.2.3 + redis client 4.7.0 (cacheComponents: false)
31
- - Nextjs 16.2.3 + redis client 4.7.0 (cacheComponents: true)
30
+ - Nextjs 15.4.11 + redis client 4.7.0
31
+ - Nextjs 16.0.11 + redis client 4.7.0 (cacheComponents: false)
32
+ - Nextjs 16.2.6 + redis client 4.7.0 (cacheComponents: false)
33
+ - Nextjs 16.2.6 + redis client 4.7.0 (cacheComponents: true)
32
34
 
33
35
  _Cache Components_ (Next.js 16+) are fully supported. Automated test coverage includes `'use cache'`, `cacheTag`, and `cacheLife` flows in the Cache Components integration suite.
34
36
 
@@ -133,27 +135,27 @@ const nextConfig = {
133
135
  }
134
136
  ```
135
137
 
136
- A working example of above can be found in the `test/integration/next-app-customized` folder.
138
+ A working example of above can be found in the `test/nextjs-test-projects/next-app-customized` folder.
137
139
 
138
140
  ## Available Options
139
141
 
140
- | Option | Description | Default Value |
141
- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
142
- | redisUrl | Redis connection url | `process.env.REDIS_URL? process.env.REDIS_URL : process.env.REDISHOST ? redis://${process.env.REDISHOST}:${process.env.REDISPORT} : 'redis://localhost:6379'` |
143
- | database | Redis database number to use. Uses DB 0 for production, DB 1 otherwise | `process.env.VERCEL_ENV === 'production' ? 0 : 1` |
144
- | keyPrefix | Prefix added to all Redis keys | `RedisStringsHandler` default: `process.env.KEY_PREFIX \|\| process.env.VERCEL_URL \|\| 'UNDEFINED_URL_'`<br> Next handlers resolve: `options.keyPrefix \|\| KEY_PREFIX \|\| VERCEL_URL \|\| BUILD_ID \|\| 'UNDEFINED_URL_'` |
145
- | sharedTagsKey | Key used to store shared tags hash map in Redis | `'__sharedTags__'` |
146
- | getTimeoutMs | Timeout in milliseconds for time critical Redis operations. If Redis get is not fulfilled within this time, returns null to avoid blocking site rendering. | `process.env.REDIS_COMMAND_TIMEOUT_MS ? (Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS) ?? 500) : 500` |
147
- | revalidateTagQuerySize | Number of entries to query in one batch during full sync of shared tags hash map | `250` |
148
- | avgResyncIntervalMs | Average interval in milliseconds between tag map full re-syncs | `3600000` (1 hour) |
149
- | redisGetDeduplication | Enable deduplication of Redis get requests via internal in-memory cache. | `true` |
150
- | inMemoryCachingTime | Time in milliseconds to cache Redis get results in memory. Set this to 0 to disable in-memory caching completely. | `10000` |
151
- | defaultStaleAge | Default stale age in seconds for cached items | `1209600` (14 days) |
152
- | estimateExpireAge | Function to calculate expire age (redis TTL value) from stale age | Production: `staleAge * 2`<br> Other: `staleAge * 1.2` |
153
- | socketOptions | Redis client socket options for TLS/SSL configuration (e.g., `{ tls: true, rejectUnauthorized: false }`) | `{ connectTimeout: timeoutMs }` |
154
- | clientOptions | Additional Redis client options (e.g., username, password) | `undefined` |
155
- | killContainerOnErrorThreshold | Number of consecutive errors before the container is killed. Set to 0 to disable. | `Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD) ?? 0 : 0` |
156
- | valueSerializer | Pluggable wire-format codec for Redis string values (compression, encryption, custom encoding). See [Custom value serializer](#custom-value-serializer-compression-encryption). | `jsonCacheValueSerializer` (`JSON.stringify` with built-in `Buffer` and `Map` encoding) |
142
+ | Option | Description | Default Value |
143
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
144
+ | redisUrl | Redis connection url | `process.env.REDIS_URL? process.env.REDIS_URL : process.env.REDISHOST ? redis://${process.env.REDISHOST}:${process.env.REDISPORT} : 'redis://localhost:6379'` |
145
+ | database | Redis database number to use. Uses DB 0 for production, DB 1 otherwise | `process.env.VERCEL_ENV === 'production' ? 0 : 1` |
146
+ | keyPrefix | Prefix added to all Redis keys | `RedisStringsHandler` default: `process.env.KEY_PREFIX \|\| process.env.VERCEL_URL \|\| 'UNDEFINED_URL_'`<br> Next handlers resolve: `options.keyPrefix \|\| KEY_PREFIX \|\| VERCEL_URL \|\| BUILD_ID \|\| 'UNDEFINED_URL_'` |
147
+ | sharedTagsKey | Key used to store shared tags hash map in Redis | `'__sharedTags__'` |
148
+ | getTimeoutMs | Timeout in milliseconds for time critical Redis operations. If Redis get is not fulfilled within this time, returns null to avoid blocking site rendering. | `process.env.REDIS_COMMAND_TIMEOUT_MS ? (Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS) ?? 500) : 500` |
149
+ | revalidateTagQuerySize | Number of entries to query in one batch during full sync of shared tags hash map | `250` |
150
+ | avgResyncIntervalMs | Average interval in milliseconds between tag map full re-syncs | `3600000` (1 hour) |
151
+ | redisGetDeduplication | Enable deduplication of Redis get requests via internal in-memory cache. | `true` |
152
+ | inMemoryCachingTime | Time in milliseconds to cache Redis get results in memory. Set this to 0 to disable in-memory caching completely. | `10000` |
153
+ | defaultStaleAge | Default stale age in seconds for cached items | `1209600` (14 days) |
154
+ | estimateExpireAge | Function to calculate expire age (redis TTL value) from stale age | Production: `staleAge * 2`<br> Other: `staleAge * 1.2` |
155
+ | socketOptions | Redis client socket options for TLS/SSL configuration (e.g., `{ tls: true, rejectUnauthorized: false }`) | `{ connectTimeout: timeoutMs }` |
156
+ | clientOptions | Additional Redis client options (e.g., username, password) | `undefined` |
157
+ | killContainerOnErrorThreshold | Number of consecutive errors before the container is killed. Set to 0 to disable. | `Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD) ?? 0 : 0` |
158
+ | valueSerializer | Pluggable wire-format codec for Redis string values (compression, encryption, custom encoding). See [Custom value serializer](#custom-value-serializer-compression-encryption). | `jsonCacheValueSerializer` (`JSON.stringify` with built-in `Buffer` and `Map` encoding) |
157
159
 
158
160
  ## Custom value serializer (compression, encryption)
159
161
 
@@ -300,7 +302,6 @@ export default class CustomizedCacheHandler {
300
302
  affects `RedisStringsHandler`. The Next.js 16+ `CacheComponentsHandler` does
301
303
  not currently route through `valueSerializer`; that's a candidate follow-up.
302
304
 
303
-
304
305
  ## TLS Configuration
305
306
 
306
307
  To connect to Redis using TLS/SSL (e.g., when using Redis over `rediss://` URLs), you can configure the socket options. Here's an example:
@@ -328,6 +329,8 @@ module.exports = class CustomizedCacheHandler {
328
329
 
329
330
  ## Consistency of Redis and this caching implementation
330
331
 
332
+ > For a detailed description of the shared hash maps (`sharedTagsMap`, `revalidatedTagsMap`), the `SyncedMap` Pub/Sub mechanism, and the `DeduplicatedRequestHandler`, see [ARCHITECTURE.md](./ARCHITECTURE.md).
333
+
331
334
  To understand consistency levels of this caching implementation we first have to understand the consistency of redis itself:
332
335
  Redis executes commands in a single-threaded manner. This ensures that all operations are processed sequentially, so clients always see a consistent view of the data.
333
336
  But depending on the setup of redis this can change:
@@ -336,23 +339,43 @@ But depending on the setup of redis this can change:
336
339
  - Eventual consistency: In a master-replica setup (strong consistency only while there is no failover)
337
340
  - Eventual consistency: In Redis Cluster mode
338
341
 
339
- Consistency levels of Caching Handler:
340
- If Redis is used in a single node setup and Request Deduplication is turned off, only then the caching handler will have strong consistency.
342
+ ### Consistency levels of the Caching Handler
343
+
344
+ Strong consistency is only achievable when **all** of the following conditions are met:
345
+
346
+ 1. Redis is used in a single node setup
347
+ 2. Only a **single application instance** is running (no cross-instance Pub/Sub propagation delay)
348
+ 3. Request Deduplication is turned off (`redisGetDeduplication: false`)
349
+
350
+ In practice, most deployments run multiple application instances, which introduces eventual consistency through two mechanisms:
351
+
352
+ #### Source 1: SyncedMap Pub/Sub propagation (both handlers)
353
+
354
+ Both `RedisStringsHandler` and `CacheComponentsHandler` maintain in-memory maps (`sharedTagsMap`, `revalidatedTagsMap`) that are synchronized across instances via Redis Pub/Sub (see [SyncedMap in ARCHITECTURE.md](./ARCHITECTURE.md#syncedmap--the-synchronization-primitive)). When a tag is revalidated on instance 1, the Pub/Sub message must propagate to instance 2 before its local maps reflect the change. Until the message arrives, `get()` on instance 2 may still consider a cache entry valid.
341
355
 
342
- If using Request Deduplication, the strong consistency is not guaranteed anymore. The following sequence can happen with request deduplication:
356
+ This is especially relevant for **implicit tags** (`_N_T_` prefix): when `revalidatePath("/products")` is called, the handler records a timestamp in `revalidatedTagsMap` and lazily invalidates nested fetch entries on the next `get()`. If another instance hasn't received the Pub/Sub update yet, it can serve a stale fetch result.
357
+
358
+ The propagation delay is typically in the order of a few milliseconds (Redis Pub/Sub round-trip).
359
+
360
+ #### Source 2: Request Deduplication (both handlers)
361
+
362
+ Both `RedisStringsHandler` and `CacheComponentsHandler` use `DeduplicatedRequestHandler` (enabled by default via `redisGetDeduplication: true`). It caches Redis GET results in memory for `inMemoryCachingTime` (default 10s). The following sequence can occur across instances:
363
+
364
+ ```
343
365
  Instance 1: call set A 1
344
366
  Instance 1: served set A 1
345
- Instance 2: call get A
346
- Instance 1: call delete A
347
- Instance 2: call get A
348
- Instance 2: served get A -> 1
349
- Instance 2: served get A -> 1 (served 1 but should already be deleted)
367
+ Instance 2: call get A → result cached in dedup cache
368
+ Instance 1: call delete A → revalidateTag deletes from Redis + publishes Pub/Sub delete
369
+ Instance 2: call get A → served from dedup cache (stale!) if Pub/Sub delete hasn't arrived
370
+ Instance 2: served get A 1 (should already be deleted)
350
371
  Instance 1: served delete A
372
+ ```
373
+
374
+ When `revalidateTag` / `updateTags` runs, it also deletes the affected keys from the `inMemoryDeduplicationCache` and broadcasts this deletion via Pub/Sub. So the consistency window is bounded by the Pub/Sub propagation time (typically 5–100ms), not the full `inMemoryCachingTime`. Only if the Pub/Sub message is lost would the stale entry persist for the full caching duration.
351
375
 
352
- The time window for this eventual consistency to occur is typically around the length of a single redis command. Depending on your load ranging from 5ms to max 100ms.
353
- If using local in-memory caching (Enabled by RedisStringsHandler option inMemoryCachingTime), the window for this eventual consistency to occur can be even higher because additionally also synchronization messages have to get delivered. Depending on your load typically ranging around 50ms to max of 120ms.
376
+ #### Practical impact
354
377
 
355
- Since all caching calls in one api/page/server action request is always served by the same instance this problem will not occur inside a single request but rather in a combination of multiple parallel requests. The probability that this will occur for a single user during a request sequence is very low, since typically a single user will not make the follow up request during this small time window of typically 50ms. To further mitigate the Problem and increase performance (increase local in-memory cache hit ratio) make sure that your load balancer will always serve one user to the same instance (sticky sessions).
378
+ Since all caching calls within one API/page/server action request are always served by the same instance, this problem will not occur inside a single request but rather in a combination of multiple parallel requests across instances. The probability that this will affect a single user during a request sequence is very low, since typically a single user will not make a follow-up request during this small time window of typically <100ms. To further mitigate the problem and increase performance (increase local in-memory cache hit ratio) make sure that your load balancer will always serve one user to the same instance (sticky sessions).
356
379
 
357
380
  By accepting and tolerating this eventual consistency, the performance of the caching handler is significantly increased.
358
381
 
@@ -382,9 +405,9 @@ pnpm test:integration:build-id-prefix
382
405
 
383
406
  Folder layout / runners:
384
407
 
385
- - **Vitest** (unit + integration) lives in `src/**/*.test.ts(x)` and `test/**`.
386
- - **Playwright** (E2E) lives in `tests/**` (see `playwright.config.ts`).
387
- - `test/browser/**` contains Vitest tests that hit a running Next.js app over HTTP. Despite the folder name, this is not Playwright and does not use Vitest browser mode.
408
+ - **Vitest** unit tests live in `test/vitest/unit/**`; integration tests in `test/vitest/integration/**`.
409
+ - **Playwright** (E2E) lives in `test/playwright/**` (see `playwright.config.ts`).
410
+ - **Test fixtures** (Next.js apps) live in `test/nextjs-test-projects/`.
388
411
 
389
412
  ### Unit tests
390
413
 
@@ -408,14 +431,6 @@ To run the BUILD_ID integration test independently:
408
431
  pnpm build && pnpm test:integration:build-id-prefix
409
432
  ```
410
433
 
411
- ### E2E tests (Playwright)
412
-
413
- To run Playwright tests (`tests/**`) you can use:
414
-
415
- ```bash
416
- pnpm test:e2e
417
- ```
418
-
419
434
  The integration tests will start a Next.js server and test the caching handler. You can modify testing behavior by setting the following environment variables:
420
435
 
421
436
  - SKIP_BUILD: If set to true, the integration tests will not build the Next.js app. Therefore the nextjs app needs to be built before running the tests. Or you execute the test once without skip build and the re-execute `pnpm test:integration` with skip build set to true.
@@ -424,6 +439,14 @@ The integration tests will start a Next.js server and test the caching handler.
424
439
 
425
440
  Integration tests may have dependencies between test cases, so individual test failures should be evaluated in the context of the full test suite rather than in isolation.
426
441
 
442
+ ### E2E tests (Playwright)
443
+
444
+ To run Playwright tests (`test/playwright/**`) you can use:
445
+
446
+ ```bash
447
+ pnpm test:e2e
448
+ ```
449
+
427
450
  ## Cache Components handler (Next.js 16+)
428
451
 
429
452
  This package can be used as a Cache Components handler (Node.js runtime) for Next.js apps that enable Cache Components.
@@ -487,8 +510,8 @@ This repo includes a dedicated Next.js Cache Components integration app with rea
487
510
  1. Install + start the Cache Components test app:
488
511
 
489
512
  ```bash
490
- pnpm -C test/integration/next-app-16-2-3-cache-components install
491
- pnpm -C test/integration/next-app-16-2-3-cache-components dev
513
+ pnpm -C test/nextjs-test-projects/next-app-16-2-6-cache-components install
514
+ pnpm -C test/nextjs-test-projects/next-app-16-2-6-cache-components dev
492
515
  ```
493
516
 
494
517
  Then open the Cache Lab pages:
@@ -516,6 +539,8 @@ app/<segment>/page.tsx files will request using the APP_PAGE kind.
516
539
 
517
540
  Fetch requests (inside app route or page) will request using the FETCH kind.
518
541
 
542
+ For details on how these kinds are handled internally (tag maps, deduplication, value transformation), see [ARCHITECTURE.md](./ARCHITECTURE.md).
543
+
519
544
  ## License
520
545
 
521
546
  This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
package/dist/index.js CHANGED
@@ -68,6 +68,7 @@ function debugVerbose(color, ...args) {
68
68
  var SYNC_CHANNEL_SUFFIX = ":sync-channel:";
69
69
  var SyncedMap = class {
70
70
  constructor(options) {
71
+ this.isReconnecting = false;
71
72
  this.client = options.client;
72
73
  this.keyPrefix = options.keyPrefix;
73
74
  this.redisKey = options.redisKey;
@@ -208,6 +209,10 @@ var SyncedMap = class {
208
209
  }
209
210
  };
210
211
  try {
212
+ this.subscriberClient.on("error", async (err) => {
213
+ console.error("Subscriber client error:", err);
214
+ await this.reconnectSubscriber();
215
+ });
211
216
  const connectIfNeeded = async () => {
212
217
  if (!this.subscriberClient.isOpen && !this.subscriberClient.isReady) {
213
218
  await this.subscriberClient.connect();
@@ -227,12 +232,12 @@ var SyncedMap = class {
227
232
  const keyspaceEventConfig = (await this.subscriberClient.configGet("notify-keyspace-events"))?.["notify-keyspace-events"];
228
233
  if (!keyspaceEventConfig.includes("E")) {
229
234
  throw new Error(
230
- 'Keyspace event configuration is set to "' + keyspaceEventConfig + "\" but has to include 'E' for Keyevent events, published with __keyevent@<db>__ prefix. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`"
235
+ 'Keyspace event configuration is set to "' + keyspaceEventConfig + " but has to include 'E' for Keyevent events, published with __keyevent@<db>__ prefix. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`"
231
236
  );
232
237
  }
233
238
  if (!keyspaceEventConfig.includes("A") && !(keyspaceEventConfig.includes("x") && keyspaceEventConfig.includes("e"))) {
234
239
  throw new Error(
235
- 'Keyspace event configuration is set to "' + keyspaceEventConfig + "\" but has to include 'A' or 'x' and 'e' for expired and evicted events. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`"
240
+ 'Keyspace event configuration is set to "' + keyspaceEventConfig + " but has to include 'A' or 'x' and 'e' for expired and evicted events. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`"
236
241
  );
237
242
  }
238
243
  }
@@ -252,22 +257,48 @@ var SyncedMap = class {
252
257
  keyEventHandler
253
258
  )
254
259
  ]);
255
- this.subscriberClient.on("error", async (err) => {
256
- console.error("Subscriber client error:", err);
260
+ } catch (error) {
261
+ console.error("Error setting up pub/sub client:", error);
262
+ throw error;
263
+ }
264
+ }
265
+ /**
266
+ * Reconnect the subscriber client with exponential backoff.
267
+ * Guarded by `isReconnecting` to prevent concurrent reconnection
268
+ * attempts when multiple error events fire during an outage.
269
+ * Retries indefinitely (with capped delay) so the subscriber
270
+ * eventually recovers once Redis is available again.
271
+ */
272
+ async reconnectSubscriber() {
273
+ if (this.isReconnecting) return;
274
+ this.isReconnecting = true;
275
+ try {
276
+ let attempt = 0;
277
+ while (true) {
257
278
  try {
258
- await this.subscriberClient.quit();
279
+ try {
280
+ this.subscriberClient.removeAllListeners("error");
281
+ await this.subscriberClient.disconnect();
282
+ } catch {
283
+ }
259
284
  this.subscriberClient = this.client.duplicate();
260
285
  await this.setupPubSub();
261
- } catch (reconnectError) {
286
+ return;
287
+ } catch (error) {
288
+ const delay = Math.min(
289
+ 500 * Math.pow(2, Math.min(attempt, 5)),
290
+ 1e4
291
+ );
292
+ attempt++;
262
293
  console.error(
263
- "Failed to reconnect subscriber client:",
264
- reconnectError
294
+ `Subscriber reconnect attempt ${attempt} failed, retrying in ${delay}ms`,
295
+ error
265
296
  );
297
+ await new Promise((r) => setTimeout(r, delay));
266
298
  }
267
- });
268
- } catch (error) {
269
- console.error("Error setting up pub/sub client:", error);
270
- throw error;
299
+ }
300
+ } finally {
301
+ this.isReconnecting = false;
271
302
  }
272
303
  }
273
304
  async waitUntilReady() {
@@ -1110,6 +1141,8 @@ var RedisCacheComponentsHandler = class {
1110
1141
  getTimeoutMs = process.env.REDIS_COMMAND_TIMEOUT_MS ? Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS) ?? 500 : 500,
1111
1142
  revalidateTagQuerySize = 250,
1112
1143
  avgResyncIntervalMs = 60 * 60 * 1e3,
1144
+ redisGetDeduplication = true,
1145
+ inMemoryCachingTime = 1e4,
1113
1146
  socketOptions,
1114
1147
  clientOptions,
1115
1148
  killContainerOnErrorThreshold = process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD ? Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD) ?? 0 : 0,
@@ -1122,6 +1155,8 @@ var RedisCacheComponentsHandler = class {
1122
1155
  env: process.env
1123
1156
  });
1124
1157
  this.getTimeoutMs = getTimeoutMs;
1158
+ this.redisGetDeduplication = redisGetDeduplication;
1159
+ this.inMemoryCachingTime = inMemoryCachingTime;
1125
1160
  this.client = (0, import_redis2.createClient)({
1126
1161
  url: redisUrl,
1127
1162
  pingInterval: 1e4,
@@ -1188,6 +1223,26 @@ var RedisCacheComponentsHandler = class {
1188
1223
  filterKeys,
1189
1224
  resyncIntervalMs: avgResyncIntervalMs - avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
1190
1225
  });
1226
+ this.inMemoryDeduplicationCache = new SyncedMap({
1227
+ client: this.client,
1228
+ keyPrefix: this.keyPrefix,
1229
+ redisKey: "__cacheComponents_inMemoryDeduplicationCache__",
1230
+ database,
1231
+ querySize: revalidateTagQuerySize,
1232
+ filterKeys,
1233
+ customizedSync: {
1234
+ withoutRedisHashmap: true,
1235
+ withoutSetSync: true
1236
+ }
1237
+ });
1238
+ const redisGet = this.client.get.bind(this.client);
1239
+ this.redisDeduplicationHandler = new DeduplicatedRequestHandler(
1240
+ redisGet,
1241
+ inMemoryCachingTime,
1242
+ this.inMemoryDeduplicationCache
1243
+ );
1244
+ this.redisGet = redisGet;
1245
+ this.deduplicatedRedisGet = this.redisDeduplicationHandler.deduplicatedFunction;
1191
1246
  } catch (error) {
1192
1247
  console.error("RedisCacheComponentsHandler constructor error", error);
1193
1248
  throw error;
@@ -1222,9 +1277,10 @@ var RedisCacheComponentsHandler = class {
1222
1277
  const redisKey = `${this.keyPrefix}${cacheKey}`;
1223
1278
  try {
1224
1279
  await this.assertClientIsReady();
1280
+ const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(cacheKey) : this.redisGet;
1225
1281
  const serialized = await redisErrorHandler(
1226
- "RedisCacheComponentsHandler.get(), operation: get " + this.getTimeoutMs + "ms " + redisKey,
1227
- this.client.get(
1282
+ "RedisCacheComponentsHandler.get(), operation: get" + (this.redisGetDeduplication ? " deduplicated" : "") + " " + this.getTimeoutMs + "ms " + redisKey,
1283
+ clientGet(
1228
1284
  (0, import_redis2.commandOptions)({ signal: AbortSignal.timeout(this.getTimeoutMs) }),
1229
1285
  redisKey
1230
1286
  )
@@ -1297,6 +1353,9 @@ var RedisCacheComponentsHandler = class {
1297
1353
  console.error("Stored object:", stored);
1298
1354
  throw jsonError;
1299
1355
  }
1356
+ if (this.redisGetDeduplication) {
1357
+ this.redisDeduplicationHandler.seedRequestReturn(cacheKey, serialized);
1358
+ }
1300
1359
  const ttlSeconds = Number.isFinite(stored.expire) && stored.expire > 0 ? Math.floor(stored.expire) : void 0;
1301
1360
  const redisKey = `${this.keyPrefix}${cacheKey}`;
1302
1361
  const setOperation = redisErrorHandler(
@@ -1363,6 +1422,11 @@ var RedisCacheComponentsHandler = class {
1363
1422
  "RedisCacheComponentsHandler.updateTags(), operation: unlink",
1364
1423
  this.client.unlink(fullRedisKeys)
1365
1424
  );
1425
+ if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
1426
+ for (const key of keysToDelete) {
1427
+ this.inMemoryDeduplicationCache.delete(key);
1428
+ }
1429
+ }
1366
1430
  const deleteTagsOperation = this.sharedTagsMap.delete(cacheKeys);
1367
1431
  await deleteTagsOperation;
1368
1432
  } catch (error) {