@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.
- package/.cursor/skills/testing/SKILL.md +121 -0
- package/.github/workflows/ci.yml +155 -70
- package/.github/workflows/pages.yml +38 -0
- package/ARCHITECTURE.md +431 -0
- package/CHANGELOG.md +19 -0
- package/README.md +220 -48
- package/dist/index.d.mts +77 -2
- package/dist/index.d.ts +77 -2
- package/dist/index.js +109 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +106 -23
- package/dist/index.mjs.map +1 -1
- package/docs/index.html +139 -0
- package/docs/robots.txt +4 -0
- package/docs/sitemap.xml +8 -0
- package/docs/styles.css +141 -0
- package/eslint.config.mjs +1 -1
- package/package.json +16 -18
- package/playwright.config.ts +5 -3
- package/src/CacheComponentsHandler.ts +57 -2
- package/src/RedisStringsHandler.ts +48 -11
- package/src/SyncedMap.ts +62 -13
- package/src/index.ts +4 -0
- package/src/serializer.ts +59 -0
- 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-0-11-cache-components/src/app/api/cached-with-cachelife/route.ts +24 -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} +46 -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 +17 -10
- 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/test/vitest/unit/serializer.test.ts +182 -0
- 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-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
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/src/index.ts
CHANGED
|
@@ -5,6 +5,10 @@ import RedisStringsHandler from './RedisStringsHandler';
|
|
|
5
5
|
export { RedisStringsHandler };
|
|
6
6
|
export type { CreateRedisStringsHandlerOptions } from './RedisStringsHandler';
|
|
7
7
|
|
|
8
|
+
export { jsonCacheValueSerializer } from './serializer';
|
|
9
|
+
export type { CacheValueSerializer } from './serializer';
|
|
10
|
+
export { bufferAndMapReplacer, bufferAndMapReviver } from './utils/json';
|
|
11
|
+
|
|
8
12
|
import {
|
|
9
13
|
redisCacheHandler,
|
|
10
14
|
getRedisCacheComponentsHandler,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable wire-format codec for Redis string values used by `RedisStringsHandler`.
|
|
3
|
+
*
|
|
4
|
+
* The serializer is the single point at which an in-memory `CacheEntry` becomes the
|
|
5
|
+
* string written to Redis (and back). Plugging in a custom serializer lets you add
|
|
6
|
+
* compression (gzip/brotli), encryption, or any other custom encoding without forking
|
|
7
|
+
* this package or losing the existing dedup / batch / keyspace features.
|
|
8
|
+
*
|
|
9
|
+
* Both `serialize` and `deserialize` may return either a value directly or a `Promise`,
|
|
10
|
+
* which enables non-blocking async codecs such as stream-based compression
|
|
11
|
+
* (`zlib.brotliCompress`) or encryption (`crypto.subtle`). Synchronous implementations
|
|
12
|
+
* continue to work unchanged - awaiting a plain value is a no-op.
|
|
13
|
+
*
|
|
14
|
+
* The default export {@link jsonCacheValueSerializer} is `JSON.stringify` /
|
|
15
|
+
* `JSON.parse` paired with {@link bufferAndMapReplacer} / {@link bufferAndMapReviver},
|
|
16
|
+
* so native `Buffer` and `Map` values inside a `CacheEntry` round-trip transparently
|
|
17
|
+
* (this is required for Next.js RSC payloads).
|
|
18
|
+
*
|
|
19
|
+
* Operational note: changing the serializer (or any of its parameters such as a
|
|
20
|
+
* compression level or encryption key) makes existing Redis keys unreadable, because
|
|
21
|
+
* the deserializer will fail or return `null` for entries written by the previous
|
|
22
|
+
* format. Either flush the affected keys, bump `keyPrefix`, or migrate values
|
|
23
|
+
* out-of-band before deploying a new serializer.
|
|
24
|
+
*/
|
|
25
|
+
import type { CacheEntry } from './RedisStringsHandler';
|
|
26
|
+
import { bufferAndMapReplacer, bufferAndMapReviver } from './utils/json';
|
|
27
|
+
|
|
28
|
+
export type CacheValueSerializer = {
|
|
29
|
+
/**
|
|
30
|
+
* Encode an in-memory `CacheEntry` into the string written to Redis.
|
|
31
|
+
* May return a `Promise` for async codecs (e.g. compression, encryption).
|
|
32
|
+
*/
|
|
33
|
+
serialize(value: CacheEntry): string | Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* Decode a string read from Redis back into a `CacheEntry`.
|
|
36
|
+
* Returning `null` (or a `Promise<null>`) signals "treat as cache miss" -
|
|
37
|
+
* the handler will return `null` from `get()` without surfacing an error.
|
|
38
|
+
* May return a `Promise` for async codecs.
|
|
39
|
+
*/
|
|
40
|
+
deserialize(stored: string): CacheEntry | null | Promise<CacheEntry | null>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Default serializer used by `RedisStringsHandler` when no `valueSerializer` is
|
|
45
|
+
* configured. Wraps `JSON.stringify` / `JSON.parse` with this package's
|
|
46
|
+
* {@link bufferAndMapReplacer} / {@link bufferAndMapReviver} so native `Buffer`
|
|
47
|
+
* and `Map` values inside a `CacheEntry` survive the round-trip.
|
|
48
|
+
*
|
|
49
|
+
* Exported as a singleton so consumers can compare against the default by
|
|
50
|
+
* reference (e.g. to detect that no custom serializer was configured).
|
|
51
|
+
*/
|
|
52
|
+
export const jsonCacheValueSerializer: CacheValueSerializer = {
|
|
53
|
+
serialize(value) {
|
|
54
|
+
return JSON.stringify(value, bufferAndMapReplacer);
|
|
55
|
+
},
|
|
56
|
+
deserialize(stored) {
|
|
57
|
+
return JSON.parse(stored, bufferAndMapReviver) as CacheEntry | null;
|
|
58
|
+
},
|
|
59
|
+
};
|
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
|
}
|