@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.
- package/.cursor/skills/testing/SKILL.md +121 -0
- package/.github/workflows/ci.yml +155 -70
- package/ARCHITECTURE.md +431 -0
- package/CHANGELOG.md +7 -0
- package/README.md +72 -47
- package/dist/index.js +78 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +78 -14
- package/dist/index.mjs.map +1 -1
- package/eslint.config.mjs +1 -1
- package/package.json +14 -16
- package/playwright.config.ts +5 -3
- package/src/CacheComponentsHandler.ts +57 -2
- package/src/SyncedMap.ts +62 -13
- package/test/README.md +179 -0
- package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/package.json +6 -6
- package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-15-4-11}/pnpm-lock.yaml +89 -105
- package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/package.json +3 -3
- package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/pnpm-lock.yaml +53 -53
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/package.json +28 -0
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/pnpm-lock.yaml +4206 -0
- package/test/nextjs-test-projects/next-app-16-2-6/eslint.config.mjs +18 -0
- package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/package.json +3 -3
- package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/pnpm-lock.yaml +54 -60
- package/test/nextjs-test-projects/next-app-16-2-6/postcss.config.mjs +7 -0
- package/test/nextjs-test-projects/next-app-16-2-6/src/app/favicon.ico +0 -0
- package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-2-6}/tsconfig.json +9 -2
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/cache-handler.js +3 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/next.config.ts +13 -0
- package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6-cache-components}/package.json +3 -3
- 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
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-static-fetch/route.ts +19 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-cachelife/route.ts +24 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/cached-with-tag/route.ts +21 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidate-tag/route.ts +19 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/revalidated-fetch/route.ts +19 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/cachelife-short/page.tsx +110 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/page.tsx +90 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +127 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +130 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +127 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +110 -0
- package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/layout.tsx +0 -2
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/update-tag-test/page.tsx +22 -0
- package/{tests → test/playwright}/update-tag.spec.ts +1 -1
- package/test/{integration → vitest/integration}/build-id-prefix.integration.test.ts +2 -2
- package/test/{cache-components/cache-components.integration.spec.ts → vitest/integration/cache-components/cache-components.integration.test.ts} +7 -3
- package/test/{cache-components/redis-kill-reconnect.spec.ts → vitest/integration/cache-components/redis-kill-reconnect.test.ts} +28 -7
- package/test/vitest/integration/cache-components/redis-quit-vs-disconnect.test.ts +59 -0
- package/test/vitest/integration/cache-components/redis-subscriber-outage.test.ts +100 -0
- package/test/{cache-components → vitest/integration/cache-components}/scripts/redis-kill-reconnect.ts +31 -8
- package/test/vitest/integration/cache-components/scripts/redis-quit-vs-disconnect.ts +113 -0
- package/test/vitest/integration/cache-components/scripts/redis-subscriber-outage.ts +379 -0
- package/test/vitest/integration/cache-components/scripts/redis-test-helpers.ts +155 -0
- package/test/{integration → vitest/integration}/nextjs-cache-handler.integration.test.ts +15 -9
- package/test/vitest/unit/SyncedMap-reconnect-repro.test.ts +154 -0
- package/{src → test/vitest/unit}/index.test.ts +3 -3
- package/test/{cache-components/reconnect-socket-already-opened.spec.ts → vitest/unit/reconnect-socket-already-opened.test.ts} +1 -1
- package/{src → test/vitest/unit}/serializer.test.ts +7 -3
- package/{src → test/vitest/unit}/utils/prefix.test.ts +1 -1
- package/vite.config.ts +4 -3
- package/vitest.cache-components.config.ts +1 -1
- package/test/browser/update-tag.browser.test.ts +0 -41
- package/test/integration/next-app-15-0-3/next.config.js +0 -6
- package/test/integration/next-app-15-0-3/package-lock.json +0 -5833
- package/test/integration/next-app-15-0-3/pnpm-lock.yaml +0 -3710
- package/test/integration/next-app-15-3-2/next.config.js +0 -6
- package/test/integration/next-app-15-3-2/package.json +0 -33
- package/test/integration/next-app-15-3-2/pnpm-lock.yaml +0 -3720
- package/test/integration/next-app-15-3-2/postcss.config.mjs +0 -5
- package/test/integration/next-app-15-3-2/src/app/api/revalidateTag/route.ts +0 -15
- package/test/integration/next-app-15-4-7/eslint.config.mjs +0 -16
- package/test/integration/next-app-15-4-7/package-lock.json +0 -5969
- package/test/integration/next-app-15-4-7/package.json +0 -33
- package/test/integration/next-app-15-4-7/postcss.config.mjs +0 -5
- package/test/integration/next-app-15-4-7/src/app/api/revalidateTag/route.ts +0 -15
- package/test/integration/next-app-15-4-7/tsconfig.json +0 -27
- package/test/integration/next-app-16-0-3/src/app/api/cached-static-fetch/route.ts +0 -18
- package/test/integration/next-app-16-0-3/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
- package/test/integration/next-app-16-0-3/src/app/api/revalidatePath/route.ts +0 -15
- package/test/integration/next-app-16-0-3/src/app/api/revalidated-fetch/route.ts +0 -17
- package/test/integration/next-app-16-0-3/src/app/api/uncached-fetch/route.ts +0 -15
- package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
- package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
- package/test/integration/next-app-16-0-3/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
- package/test/integration/next-app-16-0-3/src/app/pages/no-fetch/default-page/page.tsx +0 -55
- package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
- package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
- package/test/integration/next-app-16-0-3/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
- package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
- package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
- package/test/integration/next-app-16-0-3/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
- package/test/integration/next-app-16-2-3/src/app/api/cached-static-fetch/route.ts +0 -18
- package/test/integration/next-app-16-2-3/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +0 -27
- package/test/integration/next-app-16-2-3/src/app/api/revalidatePath/route.ts +0 -15
- package/test/integration/next-app-16-2-3/src/app/api/revalidated-fetch/route.ts +0 -17
- package/test/integration/next-app-16-2-3/src/app/api/uncached-fetch/route.ts +0 -15
- package/test/integration/next-app-16-2-3/src/app/globals.css +0 -26
- package/test/integration/next-app-16-2-3/src/app/layout.tsx +0 -59
- package/test/integration/next-app-16-2-3/src/app/page.tsx +0 -755
- package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +0 -19
- package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +0 -34
- package/test/integration/next-app-16-2-3/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
- package/test/integration/next-app-16-2-3/src/app/pages/no-fetch/default-page/page.tsx +0 -55
- package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +0 -19
- package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +0 -35
- package/test/integration/next-app-16-2-3/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
- package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +0 -19
- package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +0 -32
- package/test/integration/next-app-16-2-3/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +0 -25
- package/test/integration/next-app-16-2-3/src/app/revalidation-interface.tsx +0 -267
- package/test/integration/next-app-16-2-3-cache-components/public/file.svg +0 -1
- package/test/integration/next-app-16-2-3-cache-components/public/globe.svg +0 -1
- package/test/integration/next-app-16-2-3-cache-components/public/next.svg +0 -1
- package/test/integration/next-app-16-2-3-cache-components/public/vercel.svg +0 -1
- package/test/integration/next-app-16-2-3-cache-components/public/window.svg +0 -1
- package/test/integration/next-app-customized/README.md +0 -36
- package/test/integration/next-app-customized/eslint.config.mjs +0 -16
- package/vitest.browser.config.ts +0 -10
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/README.md +0 -0
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/eslint.config.mjs +0 -0
- /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-15-4-11}/next.config.js +0 -0
- /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-15-4-11}/package-lock.json +0 -0
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/postcss.config.mjs +0 -0
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/file.svg +0 -0
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/globe.svg +0 -0
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/next.svg +0 -0
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/vercel.svg +0 -0
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/public/window.svg +0 -0
- /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
- /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
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidatePath/route.ts +0 -0
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/api/revalidateTag/route.ts +0 -0
- /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
- /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
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/globals.css +0 -0
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/layout.tsx +0 -0
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/page.tsx +0 -0
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/src/app/revalidation-interface.tsx +0 -0
- /package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/tsconfig.json +0 -0
- /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/README.md +0 -0
- /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/eslint.config.mjs +0 -0
- /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/next.config.ts +0 -0
- /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/postcss.config.mjs +0 -0
- /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
- /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
- /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidatePath/route.ts +0 -0
- /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11}/src/app/api/revalidateTag/route.ts +0 -0
- /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
- /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
- /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/src/app/favicon.ico +0 -0
- /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/globals.css +0 -0
- /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11}/src/app/layout.tsx +0 -0
- /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/page.tsx +0 -0
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-0-11}/src/app/revalidation-interface.tsx +0 -0
- /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
- /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-0-11}/tsconfig.json +0 -0
- /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/README.md +0 -0
- /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
- /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/eslint.config.mjs +0 -0
- /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
- /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/postcss.config.mjs +0 -0
- /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/file.svg +0 -0
- /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/globe.svg +0 -0
- /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/next.svg +0 -0
- /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/vercel.svg +0 -0
- /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/public/window.svg +0 -0
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/favicon.ico +0 -0
- /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/globals.css +0 -0
- /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
- /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-16-0-11-cache-components}/src/app/page.tsx +0 -0
- /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
- /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
- /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-0-11-cache-components}/tsconfig.json +0 -0
- /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/README.md +0 -0
- /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/next.config.ts +0 -0
- /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
- /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
- /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidatePath/route.ts +0 -0
- /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6}/src/app/api/revalidateTag/route.ts +0 -0
- /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
- /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
- /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/globals.css +0 -0
- /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6}/src/app/layout.tsx +0 -0
- /package/test/{integration/next-app-16-2-3-cache-components → nextjs-test-projects/next-app-16-2-6}/src/app/page.tsx +0 -0
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/README.md +0 -0
- /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/eslint.config.mjs +0 -0
- /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/postcss.config.mjs +0 -0
- /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/file.svg +0 -0
- /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/globe.svg +0 -0
- /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/next.svg +0 -0
- /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/vercel.svg +0 -0
- /package/test/{integration/next-app-15-4-7 → nextjs-test-projects/next-app-16-2-6-cache-components}/public/window.svg +0 -0
- /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/favicon.ico +0 -0
- /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/globals.css +0 -0
- /package/test/{integration/next-app-16-0-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/src/app/page.tsx +0 -0
- /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
- /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-16-2-6-cache-components}/tsconfig.json +0 -0
- /package/test/{integration/next-app-16-2-3 → nextjs-test-projects/next-app-customized}/README.md +0 -0
- /package/test/{integration → nextjs-test-projects}/next-app-customized/customized-cache-handler.js +0 -0
- /package/test/{integration/next-app-15-3-2 → nextjs-test-projects/next-app-customized}/eslint.config.mjs +0 -0
- /package/test/{integration → nextjs-test-projects}/next-app-customized/next.config.js +0 -0
- /package/{tests → test/playwright}/cache-lab.spec.ts +0 -0
|
@@ -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
|
-
|
|
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;
|
package/src/SyncedMap.ts
CHANGED
|
@@ -41,6 +41,7 @@ export class SyncedMap<V> {
|
|
|
41
41
|
|
|
42
42
|
private setupLock: Promise<void>;
|
|
43
43
|
private setupLockResolve!: () => void;
|
|
44
|
+
private isReconnecting = false;
|
|
44
45
|
|
|
45
46
|
constructor(options: SyncedMapOptions) {
|
|
46
47
|
this.client = options.client;
|
|
@@ -209,6 +210,16 @@ export class SyncedMap<V> {
|
|
|
209
210
|
};
|
|
210
211
|
|
|
211
212
|
try {
|
|
213
|
+
// Attach error handler BEFORE subscribing so that errors during
|
|
214
|
+
// subscribe or after connection loss are always caught and trigger
|
|
215
|
+
// a reconnection. Previously this was attached after the subscribe()
|
|
216
|
+
// calls, which meant if subscribe threw during an outage the handler
|
|
217
|
+
// was never attached and the subscriber was permanently dead.
|
|
218
|
+
this.subscriberClient.on('error', async (err) => {
|
|
219
|
+
console.error('Subscriber client error:', err);
|
|
220
|
+
await this.reconnectSubscriber();
|
|
221
|
+
});
|
|
222
|
+
|
|
212
223
|
const connectIfNeeded = async () => {
|
|
213
224
|
// Avoid overlapping connect() calls which can lead to
|
|
214
225
|
// "Socket already opened" errors when a connect is already in progress
|
|
@@ -241,7 +252,9 @@ export class SyncedMap<V> {
|
|
|
241
252
|
throw new Error(
|
|
242
253
|
'Keyspace event configuration is set to "' +
|
|
243
254
|
keyspaceEventConfig +
|
|
244
|
-
"
|
|
255
|
+
" but has to include 'E' for Keyevent events, " +
|
|
256
|
+
'published with __keyevent@<db>__ prefix. We recommend to set it to ' +
|
|
257
|
+
"'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`",
|
|
245
258
|
);
|
|
246
259
|
}
|
|
247
260
|
if (
|
|
@@ -254,7 +267,9 @@ export class SyncedMap<V> {
|
|
|
254
267
|
throw new Error(
|
|
255
268
|
'Keyspace event configuration is set to "' +
|
|
256
269
|
keyspaceEventConfig +
|
|
257
|
-
"
|
|
270
|
+
" but has to include 'A' or 'x' and 'e' for expired and " +
|
|
271
|
+
'evicted events. We recommend to set it to ' +
|
|
272
|
+
"'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`",
|
|
258
273
|
);
|
|
259
274
|
}
|
|
260
275
|
}
|
|
@@ -275,24 +290,58 @@ export class SyncedMap<V> {
|
|
|
275
290
|
keyEventHandler,
|
|
276
291
|
),
|
|
277
292
|
]);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error('Error setting up pub/sub client:', error);
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
278
298
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Reconnect the subscriber client with exponential backoff.
|
|
301
|
+
* Guarded by `isReconnecting` to prevent concurrent reconnection
|
|
302
|
+
* attempts when multiple error events fire during an outage.
|
|
303
|
+
* Retries indefinitely (with capped delay) so the subscriber
|
|
304
|
+
* eventually recovers once Redis is available again.
|
|
305
|
+
*/
|
|
306
|
+
private async reconnectSubscriber(): Promise<void> {
|
|
307
|
+
if (this.isReconnecting) return;
|
|
308
|
+
this.isReconnecting = true;
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
let attempt = 0;
|
|
312
|
+
while (true) {
|
|
282
313
|
try {
|
|
283
|
-
|
|
314
|
+
// Forcibly tear down the old subscriber so its internal reconnect
|
|
315
|
+
// loop cannot come back to life once Redis recovers. `quit()` waits
|
|
316
|
+
// for the server and can silently fail during an outage, leaving a
|
|
317
|
+
// zombie client that still fires our error handler. Remove that
|
|
318
|
+
// handler first so a delayed error from the old socket cannot tear
|
|
319
|
+
// down the replacement subscriber we are about to create.
|
|
320
|
+
try {
|
|
321
|
+
this.subscriberClient.removeAllListeners('error');
|
|
322
|
+
await this.subscriberClient.disconnect();
|
|
323
|
+
} catch {
|
|
324
|
+
// expected during outage
|
|
325
|
+
}
|
|
326
|
+
// Create a fresh subscriber client
|
|
284
327
|
this.subscriberClient = this.client.duplicate();
|
|
285
328
|
await this.setupPubSub();
|
|
286
|
-
|
|
329
|
+
return; // success
|
|
330
|
+
} catch (error) {
|
|
331
|
+
const delay = Math.min(
|
|
332
|
+
500 * Math.pow(2, Math.min(attempt, 5)),
|
|
333
|
+
10_000,
|
|
334
|
+
);
|
|
335
|
+
attempt++;
|
|
287
336
|
console.error(
|
|
288
|
-
|
|
289
|
-
|
|
337
|
+
`Subscriber reconnect attempt ${attempt} failed, retrying in ${delay}ms`,
|
|
338
|
+
error,
|
|
290
339
|
);
|
|
340
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
291
341
|
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
throw error;
|
|
342
|
+
}
|
|
343
|
+
} finally {
|
|
344
|
+
this.isReconnecting = false;
|
|
296
345
|
}
|
|
297
346
|
}
|
|
298
347
|
|
package/test/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Test Structure
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
test/
|
|
5
|
+
├── playwright/ # E2E tests — browser automation (Playwright)
|
|
6
|
+
├── vitest/ # Unit + Integration tests (Vitest)
|
|
7
|
+
│ ├── unit/ # Pure unit tests (no external deps)
|
|
8
|
+
│ └── integration/ # Integration tests (Redis + Next.js server)
|
|
9
|
+
│ └── cache-components/ # Cache Components integration (Next.js 16+)
|
|
10
|
+
└── nextjs-test-projects/ # Next.js app fixtures (shared across test types)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
| Layer | Runner | What it validates | Needs Redis? | Needs Next.js app? |
|
|
16
|
+
| --------------- | ---------- | ----------------------------------------------------------------------- | ------------ | ------------------ |
|
|
17
|
+
| **Unit** | Vitest | Logic in isolation (serializer, prefix resolution, reconnect handling) | No | No |
|
|
18
|
+
| **Integration** | Vitest | Cache handler ↔ Redis ↔ Next.js HTTP responses (server-side plumbing) | Yes | Yes |
|
|
19
|
+
| **E2E** | Playwright | User-facing behavior in a real browser (Cache Components / `use cache`) | Yes | Yes |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Unit Tests (`test/vitest/unit/`)
|
|
24
|
+
|
|
25
|
+
**Runner:** Vitest
|
|
26
|
+
**Command:** `pnpm test:unit`
|
|
27
|
+
**Config:** `vite.config.ts`
|
|
28
|
+
|
|
29
|
+
Fast tests with no external dependencies. Mocks are used where needed.
|
|
30
|
+
|
|
31
|
+
| File | What it tests |
|
|
32
|
+
| ----------------------------------------- | -------------------------------------------------------------------------------- |
|
|
33
|
+
| `serializer.test.ts` | `CacheValueSerializer` interface, JSON round-trips, singleton stability |
|
|
34
|
+
| `index.test.ts` | `RedisStringsHandler` constructor options, default behaviors |
|
|
35
|
+
| `utils/prefix.test.ts` | `resolveKeyPrefix` logic (BUILD_ID fallback, env var precedence) |
|
|
36
|
+
| `reconnect-socket-already-opened.test.ts` | Regression: reconnect logic doesn't call `connect()` when socket is already open |
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm test:unit # single run
|
|
40
|
+
pnpm test:unit:watch # watch mode
|
|
41
|
+
pnpm test:unit:coverage # with coverage report (used in CI)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Integration Tests (`test/vitest/integration/`)
|
|
47
|
+
|
|
48
|
+
**Runner:** Vitest
|
|
49
|
+
**Command:** `pnpm test:integration`
|
|
50
|
+
**Config:** `vite.config.ts`
|
|
51
|
+
**Requires:** Redis on localhost:6379, pre-built Next.js test app
|
|
52
|
+
|
|
53
|
+
These tests spawn a real Next.js server as a child process, make `fetch()` requests against it, and verify both HTTP responses and Redis state directly.
|
|
54
|
+
|
|
55
|
+
### Standard Integration (`nextjs-cache-handler.integration.test.ts`)
|
|
56
|
+
|
|
57
|
+
Full cache lifecycle: static pages, fetch caching, revalidation, tag invalidation, TTL behavior. In CI this runs against a matrix of Next.js versions (15.4–16.2).
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pnpm test:integration
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### BUILD_ID Prefix (`build-id-prefix.integration.test.ts`)
|
|
64
|
+
|
|
65
|
+
Verifies that when neither `KEY_PREFIX` nor `VERCEL_URL` is set, the handler falls back to `.next/BUILD_ID` as the Redis key prefix. Runs in its own CI job because it needs a clean environment without those env vars.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pnpm test:integration:build-id-prefix
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Cache Components (`cache-components/`)
|
|
72
|
+
|
|
73
|
+
Integration tests specific to Next.js 16 Cache Components (`use cache`). Uses `next-app-16-2-6-cache-components` as the test app.
|
|
74
|
+
|
|
75
|
+
| File | What it tests |
|
|
76
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
77
|
+
| `cache-components.integration.test.ts` | `use cache` lifecycle: store, retrieve, tag invalidation, `cacheLife` expiry |
|
|
78
|
+
| `redis-kill-reconnect.test.ts` | Graceful recovery when Redis drops and reconnects (main client ping only) |
|
|
79
|
+
| `redis-subscriber-outage.test.ts` | Issue #86: subscriber PubSub does not recover after Redis outage (expected to fail until bug is fixed) |
|
|
80
|
+
| `redis-quit-vs-disconnect.test.ts` | `quit()` does not reliably close a subscriber during an outage; `disconnect()` does |
|
|
81
|
+
|
|
82
|
+
`redis-kill-reconnect.test.ts` uses a container runtime and auto-detects `podman` first, then `docker`. You can force runtime selection with `CONTAINER_RUNTIME=podman` or `CONTAINER_RUNTIME=docker`.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pnpm test:integration:cache-components
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Prerequisites for all integration tests:**
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cd test/nextjs-test-projects/<app-name>
|
|
92
|
+
pnpm install && pnpm build
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## E2E Tests (`test/playwright/`)
|
|
98
|
+
|
|
99
|
+
**Runner:** Playwright
|
|
100
|
+
**Command:** `pnpm test:e2e`
|
|
101
|
+
**Config:** `playwright.config.ts`
|
|
102
|
+
**Requires:** Redis on localhost:6379 (Playwright auto-starts `next-app-16-2-6-cache-components` via `webServer`)
|
|
103
|
+
|
|
104
|
+
Browser-based tests that validate Cache Components behavior from the user's perspective. Playwright was introduced because the Cache Components (`use cache`) feature in Next.js 16 relies on interactions that `fetch()` alone cannot reproduce:
|
|
105
|
+
|
|
106
|
+
- **Server Actions** are triggered by form submissions / button clicks — requires a real browser context
|
|
107
|
+
- `**updateTag`\*\* must be called from within a Server Action — needs actual UI interaction to verify
|
|
108
|
+
- **Cookie-based cache keys** depend on the browser sending cookies during navigation
|
|
109
|
+
- **Stale-while-revalidate** effects are only observable through page reloads and DOM diffing
|
|
110
|
+
|
|
111
|
+
The Vitest cache-components integration tests verify the server-side plumbing (Redis state, HTTP responses). Playwright closes the gap by testing the full user-facing flow.
|
|
112
|
+
|
|
113
|
+
| File | What it tests |
|
|
114
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
115
|
+
| `cache-lab.spec.ts` | `use cache` stability, tag invalidation (`updateTag`/`revalidateTag`), runtime cookie-based cache keys, SWR behavior |
|
|
116
|
+
| `update-tag.spec.ts` | `updateTag` via Server Actions (button click → action → UI update) |
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pnpm test:e2e
|
|
120
|
+
|
|
121
|
+
# Test against a specific Next.js version:
|
|
122
|
+
PLAYWRIGHT_TEST_APP=next-app-16-0-11-cache-components pnpm test:e2e
|
|
123
|
+
|
|
124
|
+
# Or point at an already-running server:
|
|
125
|
+
PLAYWRIGHT_BASE_URL=http://localhost:3001 pnpm test:e2e
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Next.js Test Projects (`test/nextjs-test-projects/`)
|
|
131
|
+
|
|
132
|
+
Minimal Next.js applications used as fixtures. They are not test runners — they provide the server that tests run against.
|
|
133
|
+
|
|
134
|
+
| App | Next.js | Used by |
|
|
135
|
+
| ----------------------------------- | ------- | -------------------------------------------------------------- |
|
|
136
|
+
| `next-app-15-4-11` | 15.4.11 | Integration (matrix, default for local), build-id-prefix |
|
|
137
|
+
| `next-app-16-0-11` | 16.0.11 | Integration (matrix) |
|
|
138
|
+
| `next-app-16-2-6` | 16.2.6 | Integration (matrix) |
|
|
139
|
+
| `next-app-16-0-11-cache-components` | 16.0.11 | Integration (cache-components matrix), E2E (Playwright matrix) |
|
|
140
|
+
| `next-app-16-2-6-cache-components` | 16.2.6 | Integration (cache-components matrix), E2E (Playwright matrix) |
|
|
141
|
+
| `next-app-customized` | — | Example of custom config (referenced in project README) |
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## CI Jobs
|
|
146
|
+
|
|
147
|
+
The CI workflow (`.github/workflows/ci.yml`) is structured as:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
lint-and-unit → Lint + Unit Tests + Coverage
|
|
151
|
+
├── integration → Matrix: 3 Next.js versions (15.4–16.2)
|
|
152
|
+
├── integration-build-id-prefix → Isolated BUILD_ID prefix test
|
|
153
|
+
├── integration-cache-components → Matrix: 16.0.11 + 16.2.6 cache-components
|
|
154
|
+
└── e2e → Matrix: Playwright against 16.0.11 + 16.2.6
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`lint-and-unit` runs first as a gate. All other jobs run in parallel after it passes.
|
|
158
|
+
|
|
159
|
+
| CI Job | Test App(s) | What runs |
|
|
160
|
+
| ------------------------------ | --------------------------------------------------------- | --------------------------------------------------------------- |
|
|
161
|
+
| `lint-and-unit` | — | `pnpm lint` + `pnpm test:unit:coverage` |
|
|
162
|
+
| `integration` | `next-app-15-4-11`, `next-app-16-0-11`, `next-app-16-2-6` | `pnpm test:integration` (per matrix entry) |
|
|
163
|
+
| `integration-build-id-prefix` | `next-app-15-4-11` | `pnpm test:integration:build-id-prefix` |
|
|
164
|
+
| `integration-cache-components` | `next-app-16-{0-3,2-3}-cache-components` | `pnpm test:integration:cache-components` + Redis kill/reconnect |
|
|
165
|
+
| `e2e` | `next-app-16-{0-3,2-3}-cache-components` | `pnpm test:e2e` (Playwright) |
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Environment Variables
|
|
170
|
+
|
|
171
|
+
| Variable | Used by | Description |
|
|
172
|
+
| ----------------------- | ------------------------------ | ------------------------------------------------------------------------- |
|
|
173
|
+
| `NEXT_TEST_APP` | Integration | Which test app to use (default: `next-app-15-4-11`) |
|
|
174
|
+
| `CACHE_COMPONENTS_APP` | Integration (cache-components) | Which cache-components app (default: `next-app-16-2-6-cache-components`) |
|
|
175
|
+
| `PLAYWRIGHT_TEST_APP` | E2E | Which app Playwright starts (default: `next-app-16-2-6-cache-components`) |
|
|
176
|
+
| `PLAYWRIGHT_BASE_URL` | E2E | Override base URL (skips `webServer` auto-start) |
|
|
177
|
+
| `SKIP_BUILD` | Integration | Skip Next.js build if app is pre-built |
|
|
178
|
+
| `DEBUG_INTEGRATION` | Integration | Print child process stdout/stderr |
|
|
179
|
+
| `CACHE_COMPONENTS_PORT` | Integration (cache-components) | Port for the server (default: 3065) |
|
package/test/{integration/next-app-15-0-3 → nextjs-test-projects/next-app-15-4-11}/package.json
RENAMED
|
@@ -3,27 +3,27 @@
|
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
|
-
"dev": "export VERCEL_URL=\"dev-test-$RANDOM\" && echo $VERCEL_URL && cd ../../.. && pnpm build && cd test/
|
|
6
|
+
"dev": "export VERCEL_URL=\"dev-test-$RANDOM\" && echo $VERCEL_URL && cd ../../.. && pnpm build && cd test/nextjs-test-projects/next-app-15-4-11 && pnpm i && pnpm build && VERCEL_ENV=production REDISHOST=localhost REDISPORT=6379 DEBUG_CACHE_HANDLER=true pnpm start",
|
|
7
7
|
"build": "next build",
|
|
8
8
|
"start": "next start",
|
|
9
9
|
"lint": "next lint"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"react": "19.
|
|
13
|
-
"react-dom": "19.
|
|
14
|
-
"next": "15.
|
|
12
|
+
"react": "19.2.0",
|
|
13
|
+
"react-dom": "19.2.0",
|
|
14
|
+
"next": "15.4.11",
|
|
15
15
|
"redis": "4.7.0",
|
|
16
16
|
"@trieb.work/nextjs-turbo-redis-cache": "file:../../../"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"typescript": "^5",
|
|
20
|
-
"@types/node": "^
|
|
20
|
+
"@types/node": "^22",
|
|
21
21
|
"@types/react": "^19",
|
|
22
22
|
"@types/react-dom": "^19",
|
|
23
23
|
"@tailwindcss/postcss": "^4",
|
|
24
24
|
"tailwindcss": "^4",
|
|
25
25
|
"eslint": "^9",
|
|
26
|
-
"eslint-config-next": "15.
|
|
26
|
+
"eslint-config-next": "15.4.11",
|
|
27
27
|
"@eslint/eslintrc": "^3"
|
|
28
28
|
}
|
|
29
29
|
}
|