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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/.cursor/skills/testing/SKILL.md +121 -0
  2. package/.github/workflows/ci.yml +155 -70
  3. package/.github/workflows/pages.yml +38 -0
  4. package/ARCHITECTURE.md +431 -0
  5. package/CHANGELOG.md +19 -0
  6. package/README.md +220 -48
  7. package/dist/index.d.mts +77 -2
  8. package/dist/index.d.ts +77 -2
  9. package/dist/index.js +109 -23
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +106 -23
  12. package/dist/index.mjs.map +1 -1
  13. package/docs/index.html +139 -0
  14. package/docs/robots.txt +4 -0
  15. package/docs/sitemap.xml +8 -0
  16. package/docs/styles.css +141 -0
  17. package/eslint.config.mjs +1 -1
  18. package/package.json +16 -18
  19. package/playwright.config.ts +5 -3
  20. package/src/CacheComponentsHandler.ts +57 -2
  21. package/src/RedisStringsHandler.ts +48 -11
  22. package/src/SyncedMap.ts +62 -13
  23. package/src/index.ts +4 -0
  24. package/src/serializer.ts +59 -0
  25. package/test/README.md +179 -0
  26. package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/package.json +6 -6
  27. package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-15-4-11}/pnpm-lock.yaml +89 -105
  28. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/package.json +3 -3
  29. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/pnpm-lock.yaml +53 -53
  30. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/package.json +28 -0
  31. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/pnpm-lock.yaml +4206 -0
  32. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/cached-with-cachelife/route.ts +24 -0
  33. package/test/nextjs-test-projects/next-app-16-2-6/eslint.config.mjs +18 -0
  34. package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/package.json +3 -3
  35. package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/pnpm-lock.yaml +54 -60
  36. package/test/nextjs-test-projects/next-app-16-2-6/postcss.config.mjs +7 -0
  37. package/test/nextjs-test-projects/next-app-16-2-6/src/app/favicon.ico +0 -0
  38. package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-2-6}/tsconfig.json +9 -2
  39. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/cache-handler.js +3 -0
  40. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/next.config.ts +13 -0
  41. package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6-cache-components}/package.json +3 -3
  42. package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6-cache-components}/pnpm-lock.yaml +54 -60
  43. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-static-fetch/route.ts +19 -0
  44. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-cachelife/route.ts +24 -0
  45. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-tag/route.ts +21 -0
  46. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidate-tag/route.ts +19 -0
  47. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidated-fetch/route.ts +19 -0
  48. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/cachelife-short/page.tsx +110 -0
  49. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/page.tsx +90 -0
  50. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +127 -0
  51. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +130 -0
  52. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +127 -0
  53. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +110 -0
  54. package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/layout.tsx +0 -2
  55. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/update-tag-test/page.tsx +22 -0
  56. package/{tests → test/playwright}/update-tag.spec.ts +1 -1
  57. package/test/{integration → vitest/integration}/build-id-prefix.integration.test.ts +2 -2
  58. package/test/{cache-components/cache-components.integration.spec.ts → vitest/integration/cache-components/cache-components.integration.test.ts} +46 -3
  59. package/test/{cache-components/redis-kill-reconnect.spec.ts → vitest/integration/cache-components/redis-kill-reconnect.test.ts} +28 -7
  60. package/test/vitest/integration/cache-components/redis-quit-vs-disconnect.test.ts +59 -0
  61. package/test/vitest/integration/cache-components/redis-subscriber-outage.test.ts +100 -0
  62. package/test/{cache-components → vitest/integration/cache-components}/scripts/redis-kill-reconnect.ts +31 -8
  63. package/test/vitest/integration/cache-components/scripts/redis-quit-vs-disconnect.ts +113 -0
  64. package/test/vitest/integration/cache-components/scripts/redis-subscriber-outage.ts +379 -0
  65. package/test/vitest/integration/cache-components/scripts/redis-test-helpers.ts +155 -0
  66. package/test/{integration → vitest/integration}/nextjs-cache-handler.integration.test.ts +17 -10
  67. package/test/vitest/unit/SyncedMap-reconnect-repro.test.ts +154 -0
  68. package/{src → test/vitest/unit}/index.test.ts +3 -3
  69. package/test/{cache-components/reconnect-socket-already-opened.spec.ts → vitest/unit/reconnect-socket-already-opened.test.ts} +1 -1
  70. package/test/vitest/unit/serializer.test.ts +182 -0
  71. package/{src → test/vitest/unit}/utils/prefix.test.ts +1 -1
  72. package/vite.config.ts +4 -3
  73. package/vitest.cache-components.config.ts +1 -1
  74. package/test/browser/update-tag.browser.test.ts +0 -41
  75. package/test/integration/next-app-15-0-3/next.config.js +0 -6
  76. package/test/integration/next-app-15-0-3/package-lock.json +0 -5833
  77. package/test/integration/next-app-15-0-3/pnpm-lock.yaml +0 -3710
  78. package/test/integration/next-app-15-3-2/next.config.js +0 -6
  79. package/test/integration/next-app-15-3-2/package.json +0 -33
  80. package/test/integration/next-app-15-3-2/pnpm-lock.yaml +0 -3720
  81. package/test/integration/next-app-15-3-2/postcss.config.mjs +0 -5
  82. package/test/integration/next-app-15-3-2/src/app/api/revalidateTag/route.ts +0 -15
  83. package/test/integration/next-app-15-4-7/eslint.config.mjs +0 -16
  84. package/test/integration/next-app-15-4-7/package-lock.json +0 -5969
  85. package/test/integration/next-app-15-4-7/package.json +0 -33
  86. package/test/integration/next-app-15-4-7/postcss.config.mjs +0 -5
  87. package/test/integration/next-app-15-4-7/src/app/api/revalidateTag/route.ts +0 -15
  88. package/test/integration/next-app-15-4-7/tsconfig.json +0 -27
  89. package/test/integration/next-app-16-0-3/src/app/api/cached-static-fetch/route.ts +0 -18
  90. package/test/integration/next-app-16-0-3/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  91. package/test/integration/next-app-16-0-3/src/app/api/revalidatePath/route.ts +0 -15
  92. package/test/integration/next-app-16-0-3/src/app/api/revalidated-fetch/route.ts +0 -17
  93. package/test/integration/next-app-16-0-3/src/app/api/uncached-fetch/route.ts +0 -15
  94. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  95. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  96. package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  97. package/test/integration/next-app-16-0-3/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  98. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  99. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  100. package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  101. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  102. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  103. package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  104. package/test/integration/next-app-16-2-3/src/app/api/cached-static-fetch/route.ts +0 -18
  105. package/test/integration/next-app-16-2-3/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
  106. package/test/integration/next-app-16-2-3/src/app/api/revalidatePath/route.ts +0 -15
  107. package/test/integration/next-app-16-2-3/src/app/api/revalidated-fetch/route.ts +0 -17
  108. package/test/integration/next-app-16-2-3/src/app/api/uncached-fetch/route.ts +0 -15
  109. package/test/integration/next-app-16-2-3/src/app/globals.css +0 -26
  110. package/test/integration/next-app-16-2-3/src/app/layout.tsx +0 -59
  111. package/test/integration/next-app-16-2-3/src/app/page.tsx +0 -755
  112. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
  113. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
  114. package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  115. package/test/integration/next-app-16-2-3/src/app/pages/no-fetch/default-page/page.tsx +0 -55
  116. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
  117. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
  118. package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  119. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
  120. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
  121. package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
  122. package/test/integration/next-app-16-2-3/src/app/revalidation-interface.tsx +0 -267
  123. package/test/integration/next-app-16-2-3-cache-components/public/file.svg +0 -1
  124. package/test/integration/next-app-16-2-3-cache-components/public/globe.svg +0 -1
  125. package/test/integration/next-app-16-2-3-cache-components/public/next.svg +0 -1
  126. package/test/integration/next-app-16-2-3-cache-components/public/vercel.svg +0 -1
  127. package/test/integration/next-app-16-2-3-cache-components/public/window.svg +0 -1
  128. package/test/integration/next-app-customized/README.md +0 -36
  129. package/test/integration/next-app-customized/eslint.config.mjs +0 -16
  130. package/vitest.browser.config.ts +0 -10
  131. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/README.md +0 -0
  132. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/eslint.config.mjs +0 -0
  133. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-15-4-11}/next.config.js +0 -0
  134. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-15-4-11}/package-lock.json +0 -0
  135. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/postcss.config.mjs +0 -0
  136. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/file.svg +0 -0
  137. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/globe.svg +0 -0
  138. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/next.svg +0 -0
  139. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/vercel.svg +0 -0
  140. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/window.svg +0 -0
  141. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/cached-static-fetch/route.ts +0 -0
  142. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -0
  143. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidatePath/route.ts +0 -0
  144. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidateTag/route.ts +0 -0
  145. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidated-fetch/route.ts +0 -0
  146. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/uncached-fetch/route.ts +0 -0
  147. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/globals.css +0 -0
  148. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/layout.tsx +0 -0
  149. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/page.tsx +0 -0
  150. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -0
  151. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -0
  152. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  153. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/no-fetch/default-page/page.tsx +0 -0
  154. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -0
  155. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -0
  156. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  157. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -0
  158. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -0
  159. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  160. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/revalidation-interface.tsx +0 -0
  161. /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/tsconfig.json +0 -0
  162. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/README.md +0 -0
  163. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/eslint.config.mjs +0 -0
  164. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/next.config.ts +0 -0
  165. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/postcss.config.mjs +0 -0
  166. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/cached-static-fetch/route.ts +0 -0
  167. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -0
  168. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidatePath/route.ts +0 -0
  169. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidateTag/route.ts +0 -0
  170. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidated-fetch/route.ts +0 -0
  171. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/uncached-fetch/route.ts +0 -0
  172. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/src/app/favicon.ico +0 -0
  173. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/globals.css +0 -0
  174. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/layout.tsx +0 -0
  175. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/page.tsx +0 -0
  176. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -0
  177. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -0
  178. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  179. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/no-fetch/default-page/page.tsx +0 -0
  180. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -0
  181. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -0
  182. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  183. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -0
  184. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -0
  185. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  186. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/revalidation-interface.tsx +0 -0
  187. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/src/app/update-tag-test/page.tsx +0 -0
  188. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/tsconfig.json +0 -0
  189. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/README.md +0 -0
  190. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/cache-handler.js +0 -0
  191. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/eslint.config.mjs +0 -0
  192. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/next.config.ts +0 -0
  193. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/postcss.config.mjs +0 -0
  194. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/file.svg +0 -0
  195. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/globe.svg +0 -0
  196. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/next.svg +0 -0
  197. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/vercel.svg +0 -0
  198. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/window.svg +0 -0
  199. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/api/cached-static-fetch/route.ts +0 -0
  200. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/api/cached-with-tag/route.ts +0 -0
  201. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/api/revalidate-tag/route.ts +0 -0
  202. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/api/revalidated-fetch/route.ts +0 -0
  203. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/cachelife-short/page.tsx +0 -0
  204. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/page.tsx +0 -0
  205. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/runtime-data-suspense/page.tsx +0 -0
  206. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/stale-while-revalidate/page.tsx +0 -0
  207. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/tag-invalidation/page.tsx +0 -0
  208. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/cache-lab/use-cache-nondeterministic/page.tsx +0 -0
  209. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/favicon.ico +0 -0
  210. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/globals.css +0 -0
  211. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/layout.tsx +0 -0
  212. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/page.tsx +0 -0
  213. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/revalidation-interface.tsx +0 -0
  214. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/update-tag-test/page.tsx +0 -0
  215. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/tsconfig.json +0 -0
  216. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/README.md +0 -0
  217. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/next.config.ts +0 -0
  218. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/cached-static-fetch/route.ts +0 -0
  219. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -0
  220. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidatePath/route.ts +0 -0
  221. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidateTag/route.ts +0 -0
  222. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidated-fetch/route.ts +0 -0
  223. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/uncached-fetch/route.ts +0 -0
  224. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/globals.css +0 -0
  225. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/layout.tsx +0 -0
  226. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/page.tsx +0 -0
  227. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -0
  228. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -0
  229. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  230. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/no-fetch/default-page/page.tsx +0 -0
  231. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -0
  232. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -0
  233. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  234. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -0
  235. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -0
  236. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -0
  237. /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/revalidation-interface.tsx +0 -0
  238. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/src/app/update-tag-test/page.tsx +0 -0
  239. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/README.md +0 -0
  240. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/eslint.config.mjs +0 -0
  241. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/postcss.config.mjs +0 -0
  242. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/file.svg +0 -0
  243. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/globe.svg +0 -0
  244. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/next.svg +0 -0
  245. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/vercel.svg +0 -0
  246. /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/window.svg +0 -0
  247. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/favicon.ico +0 -0
  248. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/globals.css +0 -0
  249. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/page.tsx +0 -0
  250. /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/revalidation-interface.tsx +0 -0
  251. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/tsconfig.json +0 -0
  252. /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-customized}/README.md +0 -0
  253. /package/test/{integration → nextjs-test-projects}/next-app-customized/customized-cache-handler.js +0 -0
  254. /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-customized}/eslint.config.mjs +0 -0
  255. /package/test/{integration → nextjs-test-projects}/next-app-customized/next.config.js +0 -0
  256. /package/{tests → test/playwright}/cache-lab.spec.ts +0 -0
@@ -0,0 +1,139 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>nextjs-turbo-redis-cache | High-Performance Redis Cache Handler for Next.js</title>
7
+ <meta
8
+ name="description"
9
+ content="High-performance Redis cache handler for Next.js 15/16 App Router. Get request deduplication, batch tag invalidation, in-memory caching, and production-ready scalability."
10
+ />
11
+ <meta
12
+ name="keywords"
13
+ content="Next.js cache handler, Redis cache, App Router cache, Next.js performance, cache components, batch tag invalidation"
14
+ />
15
+ <meta name="robots" content="index, follow" />
16
+ <meta name="author" content="TRWK" />
17
+ <link rel="canonical" href="https://trieb-work.github.io/nextjs-turbo-redis-cache/" />
18
+
19
+ <meta property="og:type" content="website" />
20
+ <meta property="og:title" content="nextjs-turbo-redis-cache | High-Performance Redis Cache Handler for Next.js" />
21
+ <meta
22
+ property="og:description"
23
+ content="The production-ready Redis cache handler for Next.js 15/16 with request deduplication, in-memory caching, and efficient tag invalidation."
24
+ />
25
+ <meta property="og:url" content="https://trieb-work.github.io/nextjs-turbo-redis-cache/" />
26
+ <meta
27
+ property="og:image"
28
+ content="https://github.com/user-attachments/assets/4103191e-4f4d-4139-a519-0b5bfab3e8b4"
29
+ />
30
+
31
+ <meta name="twitter:card" content="summary_large_image" />
32
+ <meta name="twitter:title" content="nextjs-turbo-redis-cache" />
33
+ <meta
34
+ name="twitter:description"
35
+ content="Production-ready Redis cache handler for Next.js 15/16 App Router workloads."
36
+ />
37
+ <meta
38
+ name="twitter:image"
39
+ content="https://github.com/user-attachments/assets/4103191e-4f4d-4139-a519-0b5bfab3e8b4"
40
+ />
41
+
42
+ <link rel="stylesheet" href="./styles.css" />
43
+
44
+ <script type="application/ld+json">
45
+ {
46
+ "@context": "https://schema.org",
47
+ "@type": "SoftwareSourceCode",
48
+ "name": "nextjs-turbo-redis-cache",
49
+ "description": "High-performance Redis cache handler for Next.js 15/16 App Router with request deduplication, in-memory caching, and batch tag invalidation.",
50
+ "codeRepository": "https://github.com/trieb-work/nextjs-turbo-redis-cache",
51
+ "programmingLanguage": "TypeScript",
52
+ "runtimePlatform": "Node.js",
53
+ "license": "https://opensource.org/license/mit",
54
+ "author": {
55
+ "@type": "Organization",
56
+ "name": "TRWK",
57
+ "url": "https://trwk.de"
58
+ },
59
+ "keywords": [
60
+ "Next.js",
61
+ "Redis",
62
+ "Cache Handler",
63
+ "App Router",
64
+ "Performance"
65
+ ]
66
+ }
67
+ </script>
68
+ </head>
69
+ <body>
70
+ <header class="hero">
71
+ <p class="eyebrow">Open-source · MIT Licensed</p>
72
+ <h1>Redis Cache Handler Built for High-Traffic Next.js Apps</h1>
73
+ <p class="lead">
74
+ nextjs-turbo-redis-cache is a production-ready cache handler for Next.js 15/16 App Router, designed for
75
+ speed, consistency tradeoff awareness, and scale.
76
+ </p>
77
+ <div class="actions">
78
+ <a class="button primary" href="https://www.npmjs.com/package/@trieb.work/nextjs-turbo-redis-cache">Install from npm</a>
79
+ <a class="button" href="https://github.com/trieb-work/nextjs-turbo-redis-cache">View on GitHub</a>
80
+ </div>
81
+ </header>
82
+
83
+ <main>
84
+ <section aria-labelledby="features-title">
85
+ <h2 id="features-title">Why teams use nextjs-turbo-redis-cache</h2>
86
+ <ul class="feature-list">
87
+ <li>
88
+ <h3>Batch tag invalidation</h3>
89
+ <p>Groups and optimizes delete operations to reduce Redis load during revalidation spikes.</p>
90
+ </li>
91
+ <li>
92
+ <h3>Request deduplication</h3>
93
+ <p>Prevents duplicate Redis reads in hot paths to lower latency and improve throughput.</p>
94
+ </li>
95
+ <li>
96
+ <h3>In-memory acceleration</h3>
97
+ <p>Local memory caching reduces repetitive lookups and keeps frequent reads fast.</p>
98
+ </li>
99
+ <li>
100
+ <h3>Cache Components support</h3>
101
+ <p>Supports Next.js 16+ Cache Components flows including use cache, cacheTag, and cacheLife.</p>
102
+ </li>
103
+ </ul>
104
+ </section>
105
+
106
+ <section aria-labelledby="quickstart-title">
107
+ <h2 id="quickstart-title">Quick start</h2>
108
+ <p>Install the package and wire it as your cache handler in a Next.js App Router project.</p>
109
+ <pre><code>pnpm add @trieb.work/nextjs-turbo-redis-cache</code></pre>
110
+ <p>Then follow the setup guide in the README for configuration, Redis environment variables, and advanced options.</p>
111
+ </section>
112
+
113
+ <section aria-labelledby="links-title" class="links">
114
+ <h2 id="links-title">Resources</h2>
115
+ <ul>
116
+ <li><a href="https://github.com/trieb-work/nextjs-turbo-redis-cache">GitHub Repository</a></li>
117
+ <li><a href="https://www.npmjs.com/package/@trieb.work/nextjs-turbo-redis-cache">npm Package</a></li>
118
+ <li><a href="https://trwk.de/case-studies/nextjs-turbo-redis-cache">TRWK Case Study</a></li>
119
+ </ul>
120
+ </section>
121
+
122
+ <section aria-labelledby="alternatives-title" class="links">
123
+ <h2 id="alternatives-title">Other cache handler projects</h2>
124
+ <p>Compare this project with other open-source handlers used in the Next.js ecosystem:</p>
125
+ <ul>
126
+ <li><a href="https://github.com/fortedigital/nextjs-cache-handler">fortedigital/nextjs-cache-handler</a></li>
127
+ <li><a href="https://github.com/mrjasonroy/cache-components-cache-handler">mrjasonroy/cache-components-cache-handler</a></li>
128
+ <li><a href="https://github.com/leejpsd/nextjs-cache-handler">leejpsd/nextjs-cache-handler</a></li>
129
+ <li><a href="https://github.com/caching-tools/next-shared-cache">caching-tools/next-shared-cache (@neshca/cache-handler)</a></li>
130
+ <li><a href="https://github.com/vercel/next.js/tree/canary/examples/cache-handler-redis">vercel/next.js cache-handler-redis example</a></li>
131
+ </ul>
132
+ </section>
133
+ </main>
134
+
135
+ <footer>
136
+ <p>Maintained by <a href="https://trwk.de">TRWK</a>.</p>
137
+ </footer>
138
+ </body>
139
+ </html>
@@ -0,0 +1,4 @@
1
+ User-agent: *
2
+ Allow: /
3
+
4
+ Sitemap: https://trieb-work.github.io/nextjs-turbo-redis-cache/sitemap.xml
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
+ <url>
4
+ <loc>https://trieb-work.github.io/nextjs-turbo-redis-cache/</loc>
5
+ <changefreq>weekly</changefreq>
6
+ <priority>1.0</priority>
7
+ </url>
8
+ </urlset>
@@ -0,0 +1,141 @@
1
+ :root {
2
+ --bg: #0a1628;
3
+ --bg-alt: #0f2139;
4
+ --card: #112a49;
5
+ --text: #e8f1ff;
6
+ --muted: #b8c8de;
7
+ --accent: #56c2ff;
8
+ --accent-strong: #1fa6f3;
9
+ --border: #2a476b;
10
+ }
11
+
12
+ * {
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ body {
17
+ margin: 0;
18
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
19
+ background: radial-gradient(circle at top, var(--bg-alt), var(--bg));
20
+ color: var(--text);
21
+ line-height: 1.6;
22
+ }
23
+
24
+ .hero,
25
+ main,
26
+ footer {
27
+ width: min(1080px, 92vw);
28
+ margin: 0 auto;
29
+ }
30
+
31
+ .hero {
32
+ padding: 4.5rem 0 2.2rem;
33
+ }
34
+
35
+ .eyebrow {
36
+ color: var(--accent);
37
+ font-weight: 700;
38
+ text-transform: uppercase;
39
+ letter-spacing: 0.06em;
40
+ font-size: 0.82rem;
41
+ }
42
+
43
+ h1 {
44
+ font-size: clamp(2rem, 4vw, 3.2rem);
45
+ line-height: 1.15;
46
+ margin: 0.5rem 0 1rem;
47
+ max-width: 18ch;
48
+ }
49
+
50
+ .lead {
51
+ color: var(--muted);
52
+ font-size: 1.1rem;
53
+ max-width: 64ch;
54
+ }
55
+
56
+ .actions {
57
+ display: flex;
58
+ gap: 0.8rem;
59
+ flex-wrap: wrap;
60
+ margin-top: 1.5rem;
61
+ }
62
+
63
+ .button {
64
+ display: inline-block;
65
+ text-decoration: none;
66
+ color: var(--text);
67
+ border: 1px solid var(--border);
68
+ padding: 0.75rem 1rem;
69
+ border-radius: 0.7rem;
70
+ font-weight: 600;
71
+ }
72
+
73
+ .button.primary {
74
+ background: var(--accent-strong);
75
+ border-color: var(--accent-strong);
76
+ }
77
+
78
+ main {
79
+ padding: 1.5rem 0 3rem;
80
+ display: grid;
81
+ gap: 1.2rem;
82
+ }
83
+
84
+ section {
85
+ background: color-mix(in srgb, var(--card) 82%, black);
86
+ border: 1px solid var(--border);
87
+ border-radius: 1rem;
88
+ padding: 1.2rem;
89
+ }
90
+
91
+ h2 {
92
+ margin-top: 0;
93
+ }
94
+
95
+ .feature-list {
96
+ margin: 0;
97
+ padding-left: 1rem;
98
+ display: grid;
99
+ gap: 0.75rem;
100
+ }
101
+
102
+ .feature-list li {
103
+ list-style: square;
104
+ }
105
+
106
+ .feature-list h3 {
107
+ margin-bottom: 0.2rem;
108
+ }
109
+
110
+ .feature-list p,
111
+ .links li,
112
+ footer p {
113
+ color: var(--muted);
114
+ }
115
+
116
+ pre {
117
+ overflow-x: auto;
118
+ background: #081323;
119
+ border: 1px solid var(--border);
120
+ border-radius: 0.7rem;
121
+ padding: 0.8rem;
122
+ }
123
+
124
+ a {
125
+ color: var(--accent);
126
+ }
127
+
128
+ footer {
129
+ padding: 0 0 2rem;
130
+ }
131
+
132
+ @media (max-width: 700px) {
133
+ .hero {
134
+ padding-top: 2.8rem;
135
+ }
136
+
137
+ .button {
138
+ width: 100%;
139
+ text-align: center;
140
+ }
141
+ }
package/eslint.config.mjs CHANGED
@@ -3,7 +3,7 @@ import tsParser from '@typescript-eslint/parser';
3
3
 
4
4
  const config = [
5
5
  {
6
- ignores: ['dist', 'node_modules'],
6
+ ignores: ['**/dist/**', '**/node_modules/**', '**/.next/**'],
7
7
  },
8
8
  {
9
9
  files: ['src/**/*.ts', 'src/**/*.tsx'],
package/package.json CHANGED
@@ -1,30 +1,28 @@
1
1
  {
2
2
  "name": "@trieb.work/nextjs-turbo-redis-cache",
3
- "version": "1.14.1",
3
+ "version": "1.15.1",
4
4
  "homepage": "https://trwk.de/case-studies/nextjs-turbo-redis-cache",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/trieb-work/nextjs-turbo-redis-cache.git"
8
8
  },
9
9
  "scripts": {
10
- "dev": "pnpm test",
11
- "run-dev-server": "pnpm run-dev-server-15-4-7",
12
- "run-dev-server-15-3-2": "cd ./test/integration/next-app-15-3-2 && pnpm dev",
13
- "run-dev-server-15-0-3": "cd ./test/integration/next-app-15-0-3 && pnpm dev",
14
- "run-dev-server-15-4-7": "cd ./test/integration/next-app-15-4-7 && pnpm dev",
10
+ "dev": "pnpm test:unit",
15
11
  "build": "tsup",
16
12
  "lint": "eslint -c eslint.config.mjs --fix",
17
13
  "format": "prettier --write 'src/**/*.ts' 'src/*.ts'",
18
- "test": "vitest --coverage --config vite.config.ts",
19
- "test:ci": "vitest --run --coverage --config vite.config.ts src/**/*.test.ts src/**/*.test.tsx test/integration/nextjs-cache-handler.integration.test.ts",
20
- "test:integration:build-id-prefix": "vitest --run --coverage --config vite.config.ts test/integration/build-id-prefix.integration.test.ts",
21
- "test:ui": "vitest --ui --config vite.config.ts",
22
- "test:unit": "vitest --config vite.config.ts src/**/*.test.ts src/**/*.test.tsx",
23
- "test:integration": "vitest --config vite.config.ts ./test/integration/nextjs-cache-handler.integration.test.ts",
24
- "test:cache-components": "vitest --run --config vitest.cache-components.config.ts",
25
14
  "prepare": "./scripts/prepare.sh",
26
- "test:browser": "vitest --config=vitest.browser.config.ts",
27
- "test:e2e": "playwright test"
15
+ "test": "vitest --coverage --config vite.config.ts",
16
+ "test:ui": "vitest --ui --config vite.config.ts",
17
+ "test:unit": "vitest --run --config vite.config.ts test/vitest/unit",
18
+ "test:unit:watch": "vitest --config vite.config.ts test/vitest/unit",
19
+ "test:unit:coverage": "vitest --run --coverage --config vite.config.ts test/vitest/unit",
20
+ "test:integration": "vitest --run --config vite.config.ts test/vitest/integration/nextjs-cache-handler.integration.test.ts",
21
+ "test:integration:build-id-prefix": "vitest --run --config vite.config.ts test/vitest/integration/build-id-prefix.integration.test.ts",
22
+ "test:integration:cache-components": "vitest --run --config vitest.cache-components.config.ts",
23
+ "test:e2e": "playwright test",
24
+ "run-dev-server": "pnpm run-dev-server-15-4-11",
25
+ "run-dev-server-15-4-11": "cd ./test/nextjs-test-projects/next-app-15-4-11 && pnpm dev"
28
26
  },
29
27
  "main": "dist/index.js",
30
28
  "module": "dist/index.mjs",
@@ -56,9 +54,9 @@
56
54
  "tags-cache",
57
55
  "nextjs-turbo-redis-cache"
58
56
  ],
59
- "author": "Designed for speed, scalability, and optimized performance, nextjs-turbo-redis-cache is your go-to solution for Next.js caching in demanding production environments.",
57
+ "author": "TRWK <info@trwk.de>",
60
58
  "license": "ISC",
61
- "description": "Next.js redis cache handler",
59
+ "description": "Designed for speed, scalability, and optimized performance, nextjs-turbo-redis-cache is your custom cache handler for demanding production environments.",
62
60
  "publishConfig": {
63
61
  "access": "public",
64
62
  "provenance": true
@@ -78,7 +76,7 @@
78
76
  "devDependencies": {
79
77
  "@commitlint/cli": "^19.6.0",
80
78
  "@commitlint/config-conventional": "^19.6.0",
81
- "@playwright/test": "^1.56.1",
79
+ "@playwright/test": "^1.62.0",
82
80
  "@semantic-release/changelog": "^6.0.3",
83
81
  "@semantic-release/git": "^10.0.1",
84
82
  "@semantic-release/github": "^12.0.2",
@@ -1,14 +1,16 @@
1
1
  import { defineConfig } from '@playwright/test';
2
2
 
3
+ const testApp =
4
+ process.env.PLAYWRIGHT_TEST_APP || 'next-app-16-2-6-cache-components';
5
+
3
6
  export default defineConfig({
4
- testDir: 'tests',
7
+ testDir: 'test/playwright',
5
8
  use: {
6
9
  baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3101',
7
10
  trace: 'on-first-retry',
8
11
  },
9
12
  webServer: {
10
- command:
11
- 'pnpm -C test/integration/next-app-16-2-3-cache-components dev -p 3101',
13
+ command: `pnpm -C test/nextjs-test-projects/${testApp} dev -p 3101`,
12
14
  url: 'http://localhost:3101',
13
15
  reuseExistingServer: true,
14
16
  timeout: 120_000,
@@ -6,6 +6,7 @@ import {
6
6
  redisErrorHandler,
7
7
  } from './RedisStringsHandler';
8
8
  import { SyncedMap } from './SyncedMap';
9
+ import { DeduplicatedRequestHandler } from './DeduplicatedRequestHandler';
9
10
  import { debug } from './utils/debug';
10
11
  import { resolveKeyPrefix } from './utils/prefix';
11
12
 
@@ -87,8 +88,19 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
87
88
  private client: Client;
88
89
  private revalidatedTagsMap: SyncedMap<number>;
89
90
  private sharedTagsMap: SyncedMap<string[]>;
91
+ private inMemoryDeduplicationCache: SyncedMap<
92
+ Promise<ReturnType<Client['get']>>
93
+ >;
90
94
  private keyPrefix: string;
91
95
  private getTimeoutMs: number;
96
+ private redisGet: Client['get'];
97
+ private redisDeduplicationHandler: DeduplicatedRequestHandler<
98
+ Client['get'],
99
+ string | Buffer | null
100
+ >;
101
+ private deduplicatedRedisGet: (key: string) => Client['get'];
102
+ private redisGetDeduplication: boolean;
103
+ private inMemoryCachingTime: number;
92
104
 
93
105
  constructor({
94
106
  redisUrl = process.env.REDIS_URL
@@ -103,6 +115,8 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
103
115
  : 500,
104
116
  revalidateTagQuerySize = 250,
105
117
  avgResyncIntervalMs = 60 * 60 * 1_000,
118
+ redisGetDeduplication = true,
119
+ inMemoryCachingTime = 10_000,
106
120
  socketOptions,
107
121
  clientOptions,
108
122
  killContainerOnErrorThreshold = process.env
@@ -118,6 +132,8 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
118
132
  env: process.env,
119
133
  });
120
134
  this.getTimeoutMs = getTimeoutMs;
135
+ this.redisGetDeduplication = redisGetDeduplication;
136
+ this.inMemoryCachingTime = inMemoryCachingTime;
121
137
 
122
138
  this.client = createClient({
123
139
  url: redisUrl,
@@ -208,6 +224,29 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
208
224
  avgResyncIntervalMs / 10 +
209
225
  Math.random() * (avgResyncIntervalMs / 10),
210
226
  });
227
+
228
+ this.inMemoryDeduplicationCache = new SyncedMap({
229
+ client: this.client,
230
+ keyPrefix: this.keyPrefix,
231
+ redisKey: '__cacheComponents_inMemoryDeduplicationCache__',
232
+ database,
233
+ querySize: revalidateTagQuerySize,
234
+ filterKeys,
235
+ customizedSync: {
236
+ withoutRedisHashmap: true,
237
+ withoutSetSync: true,
238
+ },
239
+ });
240
+
241
+ const redisGet: Client['get'] = this.client.get.bind(this.client);
242
+ this.redisDeduplicationHandler = new DeduplicatedRequestHandler(
243
+ redisGet,
244
+ inMemoryCachingTime,
245
+ this.inMemoryDeduplicationCache,
246
+ );
247
+ this.redisGet = redisGet;
248
+ this.deduplicatedRedisGet =
249
+ this.redisDeduplicationHandler.deduplicatedFunction;
211
250
  } catch (error) {
212
251
  console.error('RedisCacheComponentsHandler constructor error', error);
213
252
  throw error;
@@ -253,12 +292,18 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
253
292
  try {
254
293
  await this.assertClientIsReady();
255
294
 
295
+ const clientGet = this.redisGetDeduplication
296
+ ? this.deduplicatedRedisGet(cacheKey)
297
+ : this.redisGet;
298
+
256
299
  const serialized = await redisErrorHandler(
257
- 'RedisCacheComponentsHandler.get(), operation: get ' +
300
+ 'RedisCacheComponentsHandler.get(), operation: get' +
301
+ (this.redisGetDeduplication ? ' deduplicated' : '') +
302
+ ' ' +
258
303
  this.getTimeoutMs +
259
304
  'ms ' +
260
305
  redisKey,
261
- this.client.get(
306
+ clientGet(
262
307
  commandOptions({ signal: AbortSignal.timeout(this.getTimeoutMs) }),
263
308
  redisKey,
264
309
  ),
@@ -355,6 +400,10 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
355
400
  throw jsonError;
356
401
  }
357
402
 
403
+ if (this.redisGetDeduplication) {
404
+ this.redisDeduplicationHandler.seedRequestReturn(cacheKey, serialized);
405
+ }
406
+
358
407
  // expire is already a duration in seconds, use it directly
359
408
  const ttlSeconds =
360
409
  Number.isFinite(stored.expire) && stored.expire > 0
@@ -450,6 +499,12 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
450
499
  this.client.unlink(fullRedisKeys),
451
500
  );
452
501
 
502
+ if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
503
+ for (const key of keysToDelete) {
504
+ this.inMemoryDeduplicationCache.delete(key);
505
+ }
506
+ }
507
+
453
508
  // Delete from sharedTagsMap
454
509
  const deleteTagsOperation = this.sharedTagsMap.delete(cacheKeys);
455
510
  await deleteTagsOperation;
@@ -2,7 +2,7 @@ import { commandOptions, createClient, RedisClientOptions } from 'redis';
2
2
  import { SyncedMap } from './SyncedMap';
3
3
  import { DeduplicatedRequestHandler } from './DeduplicatedRequestHandler';
4
4
  import { debug } from './utils/debug';
5
- import { bufferAndMapReviver, bufferAndMapReplacer } from './utils/json';
5
+ import { CacheValueSerializer, jsonCacheValueSerializer } from './serializer';
6
6
 
7
7
  export type CommandOptions = ReturnType<typeof commandOptions>;
8
8
  export type Client = ReturnType<typeof createClient>;
@@ -113,6 +113,27 @@ export type CreateRedisStringsHandlerOptions = {
113
113
  * @example { username: 'user', password: 'pass' }
114
114
  */
115
115
  clientOptions?: Omit<RedisClientOptions, 'url' | 'database' | 'socket'>;
116
+ /** Pluggable wire-format codec for Redis string values. Lets you plug in
117
+ * compression (gzip/brotli), encryption, or any other custom encoding without
118
+ * forking this package or losing the existing dedup / batch / keyspace features.
119
+ *
120
+ * Both `serialize` and `deserialize` may return a `Promise`, enabling
121
+ * non-blocking async codecs (e.g. `zlib.brotliCompress`) that don't block the
122
+ * Node.js event loop. Synchronous implementations continue to work unchanged.
123
+ *
124
+ * Only the main cache-entry storage path is routed through the serializer.
125
+ * The shared-tags map and the revalidated-tags map are not. The in-memory
126
+ * deduplication cache stores the wire-format string verbatim - its contents
127
+ * change with the serializer, but the cache itself is not re-encoded.
128
+ *
129
+ * Operational note: changing the serializer (or any of its parameters such as
130
+ * a compression level or encryption key) makes existing Redis keys unreadable.
131
+ * Either flush the affected keys or bump `keyPrefix` before deploying.
132
+ *
133
+ * @default jsonCacheValueSerializer (JSON.stringify with bufferAndMapReplacer
134
+ * so native Buffer and Map values inside a CacheEntry round-trip transparently)
135
+ */
136
+ valueSerializer?: CacheValueSerializer;
116
137
  };
117
138
 
118
139
  // Identifier prefix used by Next.js to mark automatically generated cache tags
@@ -144,6 +165,7 @@ export default class RedisStringsHandler {
144
165
  private defaultStaleAge: number;
145
166
  private estimateExpireAge: (staleAge: number) => number;
146
167
  private killContainerOnErrorThreshold: number;
168
+ private valueSerializer: CacheValueSerializer;
147
169
 
148
170
  constructor({
149
171
  redisUrl = process.env.REDIS_URL
@@ -172,6 +194,7 @@ export default class RedisStringsHandler {
172
194
  : 0,
173
195
  socketOptions,
174
196
  clientOptions,
197
+ valueSerializer = jsonCacheValueSerializer,
175
198
  }: CreateRedisStringsHandlerOptions) {
176
199
  try {
177
200
  this.keyPrefix = keyPrefix;
@@ -181,6 +204,7 @@ export default class RedisStringsHandler {
181
204
  this.estimateExpireAge = estimateExpireAge;
182
205
  this.killContainerOnErrorThreshold = killContainerOnErrorThreshold;
183
206
  this.getTimeoutMs = getTimeoutMs;
207
+ this.valueSerializer = valueSerializer;
184
208
 
185
209
  try {
186
210
  // Create Redis client with properly typed configuration
@@ -418,10 +442,24 @@ export default class RedisStringsHandler {
418
442
  return null;
419
443
  }
420
444
 
421
- const cacheEntry: CacheEntry | null = JSON.parse(
422
- serializedCacheEntry,
423
- bufferAndMapReviver,
424
- );
445
+ let cacheEntry: CacheEntry | null;
446
+ try {
447
+ cacheEntry =
448
+ await this.valueSerializer.deserialize(serializedCacheEntry);
449
+ } catch (err) {
450
+ // A decode failure (e.g. legacy/corrupted entry after swapping codecs,
451
+ // bumping a compression level or rotating an encryption key) is treated
452
+ // as a cache miss rather than a hard error - this matches the
453
+ // null-from-deserialize semantics in the CacheValueSerializer contract
454
+ // and prevents a single unreadable entry from incrementing
455
+ // killContainerOnErrorCount on every read.
456
+ console.warn(
457
+ 'RedisStringsHandler.get() valueSerializer.deserialize failed, treating as cache miss',
458
+ this.keyPrefix + key,
459
+ err,
460
+ );
461
+ return null;
462
+ }
425
463
 
426
464
  debug(
427
465
  'green',
@@ -520,8 +558,9 @@ export default class RedisStringsHandler {
520
558
  } catch (error) {
521
559
  // This catch block is necessary to handle any errors that may occur during:
522
560
  // 1. Redis operations (get, unlink)
523
- // 2. JSON parsing of cache entries
524
- // 3. Tag validation and cleanup
561
+ // 2. Tag validation and cleanup
562
+ // (Deserialization errors are handled locally above and treated as cache misses,
563
+ // so they do not reach this branch and do not count toward the kill threshold.)
525
564
  // If any error occurs, we return null to indicate no valid cache entry was found,
526
565
  // allowing the application to regenerate the content rather than crash
527
566
  console.error(
@@ -621,10 +660,8 @@ export default class RedisStringsHandler {
621
660
  tags: ctx?.tags || [],
622
661
  value: data,
623
662
  };
624
- const serializedCacheEntry = JSON.stringify(
625
- cacheEntry,
626
- bufferAndMapReplacer,
627
- );
663
+ const serializedCacheEntry =
664
+ await this.valueSerializer.serialize(cacheEntry);
628
665
 
629
666
  // pre seed data into deduplicated get client. This will reduce redis load by not requesting
630
667
  // the same value from redis which was just set.