@trieb.work/nextjs-turbo-redis-cache 1.15.0 → 1.16.0
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 +199 -69
- package/.github/workflows/release.yml +9 -1
- package/ARCHITECTURE.md +431 -0
- package/CHANGELOG.md +28 -0
- package/README.md +77 -49
- package/dist/index.d.mts +71 -45
- package/dist/index.d.ts +71 -45
- package/dist/index.js +107 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +107 -21
- package/dist/index.mjs.map +1 -1
- package/docs/index.html +7 -6
- package/eslint.config.mjs +1 -1
- package/package.json +15 -16
- package/playwright.config.ts +5 -3
- package/src/CacheComponentsHandler.ts +57 -2
- package/src/RedisStringsHandler.ts +125 -66
- package/src/SyncedMap.ts +62 -13
- package/test/README.md +194 -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/test/nextjs-test-projects/next-pages-16-2-6/README.md +16 -0
- package/test/nextjs-test-projects/next-pages-16-2-6/eslint.config.mjs +18 -0
- package/test/nextjs-test-projects/next-pages-16-2-6/next.config.ts +7 -0
- package/test/nextjs-test-projects/next-pages-16-2-6/package.json +26 -0
- package/test/nextjs-test-projects/next-pages-16-2-6/pnpm-lock.yaml +3896 -0
- package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/api/revalidate.ts +24 -0
- package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/index.tsx +11 -0
- package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/isr/[slug].tsx +49 -0
- package/test/nextjs-test-projects/next-pages-16-2-6/src/pages/static-forever.tsx +20 -0
- package/test/nextjs-test-projects/next-pages-16-2-6/tsconfig.json +34 -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/integration/pages-router.integration.test.ts +420 -0
- package/test/vitest/unit/SyncedMap-reconnect-repro.test.ts +154 -0
- package/{src → test/vitest/unit}/index.test.ts +3 -3
- package/test/vitest/unit/pages-router-kinds.test.ts +292 -0
- 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
package/package.json
CHANGED
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trieb.work/nextjs-turbo-redis-cache",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"homepage": "https://trwk.de/case-studies/nextjs-turbo-redis-cache",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/trieb-work/nextjs-turbo-redis-cache.git"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"dev": "pnpm test",
|
|
11
|
-
"run-dev-server": "pnpm run-dev-server-15-4-7",
|
|
12
|
-
"run-dev-server-15-3-2": "cd ./test/integration/next-app-15-3-2 && pnpm dev",
|
|
13
|
-
"run-dev-server-15-0-3": "cd ./test/integration/next-app-15-0-3 && pnpm dev",
|
|
14
|
-
"run-dev-server-15-4-7": "cd ./test/integration/next-app-15-4-7 && pnpm dev",
|
|
10
|
+
"dev": "pnpm test:unit",
|
|
15
11
|
"build": "tsup",
|
|
16
12
|
"lint": "eslint -c eslint.config.mjs --fix",
|
|
17
13
|
"format": "prettier --write 'src/**/*.ts' 'src/*.ts'",
|
|
18
|
-
"test": "vitest --coverage --config vite.config.ts",
|
|
19
|
-
"test:ci": "vitest --run --coverage --config vite.config.ts src/**/*.test.ts src/**/*.test.tsx test/integration/nextjs-cache-handler.integration.test.ts",
|
|
20
|
-
"test:integration:build-id-prefix": "vitest --run --coverage --config vite.config.ts test/integration/build-id-prefix.integration.test.ts",
|
|
21
|
-
"test:ui": "vitest --ui --config vite.config.ts",
|
|
22
|
-
"test:unit": "vitest --config vite.config.ts src/**/*.test.ts src/**/*.test.tsx",
|
|
23
|
-
"test:integration": "vitest --config vite.config.ts ./test/integration/nextjs-cache-handler.integration.test.ts",
|
|
24
|
-
"test:cache-components": "vitest --run --config vitest.cache-components.config.ts",
|
|
25
14
|
"prepare": "./scripts/prepare.sh",
|
|
26
|
-
"test
|
|
27
|
-
"test:
|
|
15
|
+
"test": "vitest --coverage --config vite.config.ts",
|
|
16
|
+
"test:ui": "vitest --ui --config vite.config.ts",
|
|
17
|
+
"test:unit": "vitest --run --config vite.config.ts test/vitest/unit",
|
|
18
|
+
"test:unit:watch": "vitest --config vite.config.ts test/vitest/unit",
|
|
19
|
+
"test:unit:coverage": "vitest --run --coverage --config vite.config.ts test/vitest/unit",
|
|
20
|
+
"test:integration": "vitest --run --config vite.config.ts test/vitest/integration/nextjs-cache-handler.integration.test.ts",
|
|
21
|
+
"test:integration:build-id-prefix": "vitest --run --config vite.config.ts test/vitest/integration/build-id-prefix.integration.test.ts",
|
|
22
|
+
"test:integration:pages": "vitest --run --config vite.config.ts test/vitest/integration/pages-router.integration.test.ts",
|
|
23
|
+
"test:integration:cache-components": "vitest --run --config vitest.cache-components.config.ts",
|
|
24
|
+
"test:e2e": "playwright test",
|
|
25
|
+
"run-dev-server": "pnpm run-dev-server-15-4-11",
|
|
26
|
+
"run-dev-server-15-4-11": "cd ./test/nextjs-test-projects/next-app-15-4-11 && pnpm dev"
|
|
28
27
|
},
|
|
29
28
|
"main": "dist/index.js",
|
|
30
29
|
"module": "dist/index.mjs",
|
|
@@ -78,7 +77,7 @@
|
|
|
78
77
|
"devDependencies": {
|
|
79
78
|
"@commitlint/cli": "^19.6.0",
|
|
80
79
|
"@commitlint/config-conventional": "^19.6.0",
|
|
81
|
-
"@playwright/test": "^1.
|
|
80
|
+
"@playwright/test": "^1.62.0",
|
|
82
81
|
"@semantic-release/changelog": "^6.0.3",
|
|
83
82
|
"@semantic-release/git": "^10.0.1",
|
|
84
83
|
"@semantic-release/github": "^12.0.2",
|
package/playwright.config.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { defineConfig } from '@playwright/test';
|
|
2
2
|
|
|
3
|
+
const testApp =
|
|
4
|
+
process.env.PLAYWRIGHT_TEST_APP || 'next-app-16-2-6-cache-components';
|
|
5
|
+
|
|
3
6
|
export default defineConfig({
|
|
4
|
-
testDir: '
|
|
7
|
+
testDir: 'test/playwright',
|
|
5
8
|
use: {
|
|
6
9
|
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3101',
|
|
7
10
|
trace: 'on-first-retry',
|
|
8
11
|
},
|
|
9
12
|
webServer: {
|
|
10
|
-
command:
|
|
11
|
-
'pnpm -C test/integration/next-app-16-2-3-cache-components dev -p 3101',
|
|
13
|
+
command: `pnpm -C test/nextjs-test-projects/${testApp} dev -p 3101`,
|
|
12
14
|
url: 'http://localhost:3101',
|
|
13
15
|
reuseExistingServer: true,
|
|
14
16
|
timeout: 120_000,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
redisErrorHandler,
|
|
7
7
|
} from './RedisStringsHandler';
|
|
8
8
|
import { SyncedMap } from './SyncedMap';
|
|
9
|
+
import { DeduplicatedRequestHandler } from './DeduplicatedRequestHandler';
|
|
9
10
|
import { debug } from './utils/debug';
|
|
10
11
|
import { resolveKeyPrefix } from './utils/prefix';
|
|
11
12
|
|
|
@@ -87,8 +88,19 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
|
|
|
87
88
|
private client: Client;
|
|
88
89
|
private revalidatedTagsMap: SyncedMap<number>;
|
|
89
90
|
private sharedTagsMap: SyncedMap<string[]>;
|
|
91
|
+
private inMemoryDeduplicationCache: SyncedMap<
|
|
92
|
+
Promise<ReturnType<Client['get']>>
|
|
93
|
+
>;
|
|
90
94
|
private keyPrefix: string;
|
|
91
95
|
private getTimeoutMs: number;
|
|
96
|
+
private redisGet: Client['get'];
|
|
97
|
+
private redisDeduplicationHandler: DeduplicatedRequestHandler<
|
|
98
|
+
Client['get'],
|
|
99
|
+
string | Buffer | null
|
|
100
|
+
>;
|
|
101
|
+
private deduplicatedRedisGet: (key: string) => Client['get'];
|
|
102
|
+
private redisGetDeduplication: boolean;
|
|
103
|
+
private inMemoryCachingTime: number;
|
|
92
104
|
|
|
93
105
|
constructor({
|
|
94
106
|
redisUrl = process.env.REDIS_URL
|
|
@@ -103,6 +115,8 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
|
|
|
103
115
|
: 500,
|
|
104
116
|
revalidateTagQuerySize = 250,
|
|
105
117
|
avgResyncIntervalMs = 60 * 60 * 1_000,
|
|
118
|
+
redisGetDeduplication = true,
|
|
119
|
+
inMemoryCachingTime = 10_000,
|
|
106
120
|
socketOptions,
|
|
107
121
|
clientOptions,
|
|
108
122
|
killContainerOnErrorThreshold = process.env
|
|
@@ -118,6 +132,8 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
|
|
|
118
132
|
env: process.env,
|
|
119
133
|
});
|
|
120
134
|
this.getTimeoutMs = getTimeoutMs;
|
|
135
|
+
this.redisGetDeduplication = redisGetDeduplication;
|
|
136
|
+
this.inMemoryCachingTime = inMemoryCachingTime;
|
|
121
137
|
|
|
122
138
|
this.client = createClient({
|
|
123
139
|
url: redisUrl,
|
|
@@ -208,6 +224,29 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
|
|
|
208
224
|
avgResyncIntervalMs / 10 +
|
|
209
225
|
Math.random() * (avgResyncIntervalMs / 10),
|
|
210
226
|
});
|
|
227
|
+
|
|
228
|
+
this.inMemoryDeduplicationCache = new SyncedMap({
|
|
229
|
+
client: this.client,
|
|
230
|
+
keyPrefix: this.keyPrefix,
|
|
231
|
+
redisKey: '__cacheComponents_inMemoryDeduplicationCache__',
|
|
232
|
+
database,
|
|
233
|
+
querySize: revalidateTagQuerySize,
|
|
234
|
+
filterKeys,
|
|
235
|
+
customizedSync: {
|
|
236
|
+
withoutRedisHashmap: true,
|
|
237
|
+
withoutSetSync: true,
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const redisGet: Client['get'] = this.client.get.bind(this.client);
|
|
242
|
+
this.redisDeduplicationHandler = new DeduplicatedRequestHandler(
|
|
243
|
+
redisGet,
|
|
244
|
+
inMemoryCachingTime,
|
|
245
|
+
this.inMemoryDeduplicationCache,
|
|
246
|
+
);
|
|
247
|
+
this.redisGet = redisGet;
|
|
248
|
+
this.deduplicatedRedisGet =
|
|
249
|
+
this.redisDeduplicationHandler.deduplicatedFunction;
|
|
211
250
|
} catch (error) {
|
|
212
251
|
console.error('RedisCacheComponentsHandler constructor error', error);
|
|
213
252
|
throw error;
|
|
@@ -253,12 +292,18 @@ class RedisCacheComponentsHandler implements CacheComponentsHandler {
|
|
|
253
292
|
try {
|
|
254
293
|
await this.assertClientIsReady();
|
|
255
294
|
|
|
295
|
+
const clientGet = this.redisGetDeduplication
|
|
296
|
+
? this.deduplicatedRedisGet(cacheKey)
|
|
297
|
+
: this.redisGet;
|
|
298
|
+
|
|
256
299
|
const serialized = await redisErrorHandler(
|
|
257
|
-
'RedisCacheComponentsHandler.get(), operation: get
|
|
300
|
+
'RedisCacheComponentsHandler.get(), operation: get' +
|
|
301
|
+
(this.redisGetDeduplication ? ' deduplicated' : '') +
|
|
302
|
+
' ' +
|
|
258
303
|
this.getTimeoutMs +
|
|
259
304
|
'ms ' +
|
|
260
305
|
redisKey,
|
|
261
|
-
|
|
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;
|
|
@@ -13,6 +13,84 @@ export type CacheEntry = {
|
|
|
13
13
|
tags: string[];
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
/** Discriminated union of the `ctx` argument Next.js passes to
|
|
17
|
+
* {@link RedisStringsHandler.get}. The Pages Router (`PAGES`) has no PPR
|
|
18
|
+
* concept, so `isRoutePPREnabled` is optional for it.
|
|
19
|
+
*
|
|
20
|
+
* Note: `REDIRECT` is intentionally absent. Next.js always calls `get()` with
|
|
21
|
+
* `kind: 'PAGES'` for Pages Router routes, even when the stored value has
|
|
22
|
+
* `kind: 'REDIRECT'`. The handler returns whatever value is cached and Next.js
|
|
23
|
+
* interprets it accordingly. */
|
|
24
|
+
export type GetContext =
|
|
25
|
+
| {
|
|
26
|
+
kind: 'APP_ROUTE' | 'APP_PAGE';
|
|
27
|
+
isRoutePPREnabled: boolean;
|
|
28
|
+
isFallback: boolean;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
kind: 'PAGES';
|
|
32
|
+
isRoutePPREnabled?: boolean;
|
|
33
|
+
isFallback: boolean;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
kind: 'FETCH';
|
|
37
|
+
revalidate: number;
|
|
38
|
+
fetchUrl: string;
|
|
39
|
+
fetchIdx: number;
|
|
40
|
+
tags: string[];
|
|
41
|
+
softTags: string[];
|
|
42
|
+
isFallback: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Discriminated union of cache payloads accepted by
|
|
46
|
+
* {@link RedisStringsHandler.set}. `null` is a valid payload: the Pages Router
|
|
47
|
+
* stores `notFound: true` results as a cache entry with a null value. */
|
|
48
|
+
export type SetCacheValue =
|
|
49
|
+
| {
|
|
50
|
+
kind: 'APP_PAGE';
|
|
51
|
+
status?: number;
|
|
52
|
+
headers: {
|
|
53
|
+
'x-nextjs-stale-time': string; // timestamp in ms
|
|
54
|
+
'x-next-cache-tags': string; // comma separated paths (tags)
|
|
55
|
+
};
|
|
56
|
+
html: string;
|
|
57
|
+
rscData: Buffer;
|
|
58
|
+
segmentData: unknown;
|
|
59
|
+
postboned: unknown;
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
kind: 'APP_ROUTE';
|
|
63
|
+
status: number;
|
|
64
|
+
headers: {
|
|
65
|
+
'cache-control'?: string;
|
|
66
|
+
'x-nextjs-stale-time': string; // timestamp in ms
|
|
67
|
+
'x-next-cache-tags': string; // comma separated paths (tags)
|
|
68
|
+
};
|
|
69
|
+
body: Buffer;
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
kind: 'PAGES';
|
|
73
|
+
html: string;
|
|
74
|
+
pageData: Record<string, unknown>;
|
|
75
|
+
headers?: Record<string, number | string | string[] | undefined>;
|
|
76
|
+
status?: number;
|
|
77
|
+
}
|
|
78
|
+
| {
|
|
79
|
+
kind: 'REDIRECT';
|
|
80
|
+
props: Record<string, unknown>;
|
|
81
|
+
}
|
|
82
|
+
| {
|
|
83
|
+
kind: 'FETCH';
|
|
84
|
+
data: {
|
|
85
|
+
headers: Record<string, string>;
|
|
86
|
+
body: string; // base64 encoded
|
|
87
|
+
status: number;
|
|
88
|
+
url: string;
|
|
89
|
+
};
|
|
90
|
+
revalidate: number | false;
|
|
91
|
+
}
|
|
92
|
+
| null;
|
|
93
|
+
|
|
16
94
|
export function redisErrorHandler<T extends Promise<unknown>>(
|
|
17
95
|
debugInfo: string,
|
|
18
96
|
redisCommandResult: T,
|
|
@@ -144,6 +222,24 @@ const NEXT_CACHE_IMPLICIT_TAG_ID = '_N_T_';
|
|
|
144
222
|
// This helps track when specific tags were last invalidated
|
|
145
223
|
const REVALIDATED_TAGS_KEY = '__revalidated_tags__';
|
|
146
224
|
|
|
225
|
+
// Cache kinds this handler is designed and tested for. Kept as the single
|
|
226
|
+
// source of truth for both payload validation and the warning message, so
|
|
227
|
+
// adding a kind is a one-line change here instead of edits scattered across
|
|
228
|
+
// the type union, the guard chain, and the log string.
|
|
229
|
+
const SUPPORTED_GET_KINDS = [
|
|
230
|
+
'APP_ROUTE',
|
|
231
|
+
'APP_PAGE',
|
|
232
|
+
'PAGES',
|
|
233
|
+
'FETCH',
|
|
234
|
+
] as const;
|
|
235
|
+
const SUPPORTED_SET_KINDS = [
|
|
236
|
+
'APP_ROUTE',
|
|
237
|
+
'APP_PAGE',
|
|
238
|
+
'PAGES',
|
|
239
|
+
'REDIRECT',
|
|
240
|
+
'FETCH',
|
|
241
|
+
] as const;
|
|
242
|
+
|
|
147
243
|
let killContainerOnErrorCount: number = 0;
|
|
148
244
|
export default class RedisStringsHandler {
|
|
149
245
|
private client: Client;
|
|
@@ -371,35 +467,14 @@ export default class RedisStringsHandler {
|
|
|
371
467
|
}
|
|
372
468
|
}
|
|
373
469
|
|
|
374
|
-
public async get(
|
|
375
|
-
key: string,
|
|
376
|
-
ctx:
|
|
377
|
-
| {
|
|
378
|
-
kind: 'APP_ROUTE' | 'APP_PAGE';
|
|
379
|
-
isRoutePPREnabled: boolean;
|
|
380
|
-
isFallback: boolean;
|
|
381
|
-
}
|
|
382
|
-
| {
|
|
383
|
-
kind: 'FETCH';
|
|
384
|
-
revalidate: number;
|
|
385
|
-
fetchUrl: string;
|
|
386
|
-
fetchIdx: number;
|
|
387
|
-
tags: string[];
|
|
388
|
-
softTags: string[];
|
|
389
|
-
isFallback: boolean;
|
|
390
|
-
},
|
|
391
|
-
): Promise<CacheEntry | null> {
|
|
470
|
+
public async get(key: string, ctx: GetContext): Promise<CacheEntry | null> {
|
|
392
471
|
try {
|
|
393
|
-
if (
|
|
394
|
-
ctx.kind !== 'APP_ROUTE' &&
|
|
395
|
-
ctx.kind !== 'APP_PAGE' &&
|
|
396
|
-
ctx.kind !== 'FETCH'
|
|
397
|
-
) {
|
|
472
|
+
if (!(SUPPORTED_GET_KINDS as readonly string[]).includes(ctx.kind)) {
|
|
398
473
|
console.warn(
|
|
399
474
|
'RedisStringsHandler.get() called with',
|
|
400
475
|
key,
|
|
401
476
|
ctx,
|
|
402
|
-
|
|
477
|
+
`this cache handler is only designed and tested for kinds ${SUPPORTED_GET_KINDS.join(', ')} and not for kind`,
|
|
403
478
|
(ctx as { kind: string })?.kind,
|
|
404
479
|
);
|
|
405
480
|
}
|
|
@@ -479,7 +554,10 @@ export default class RedisStringsHandler {
|
|
|
479
554
|
'cacheEntry is mall formed (missing tags)',
|
|
480
555
|
);
|
|
481
556
|
}
|
|
482
|
-
|
|
557
|
+
// value === null is a legitimate entry: the Pages Router stores
|
|
558
|
+
// `notFound: true` results as a cache entry with a null value.
|
|
559
|
+
// Only an absent value indicates a malformed entry.
|
|
560
|
+
if (cacheEntry?.value === undefined) {
|
|
483
561
|
console.warn(
|
|
484
562
|
'RedisStringsHandler.get() called with',
|
|
485
563
|
key,
|
|
@@ -589,71 +667,49 @@ export default class RedisStringsHandler {
|
|
|
589
667
|
}
|
|
590
668
|
public async set(
|
|
591
669
|
key: string,
|
|
592
|
-
data:
|
|
593
|
-
| {
|
|
594
|
-
kind: 'APP_PAGE';
|
|
595
|
-
status?: number;
|
|
596
|
-
headers: {
|
|
597
|
-
'x-nextjs-stale-time': string; // timestamp in ms
|
|
598
|
-
'x-next-cache-tags': string; // comma separated paths (tags)
|
|
599
|
-
};
|
|
600
|
-
html: string;
|
|
601
|
-
rscData: Buffer;
|
|
602
|
-
segmentData: unknown;
|
|
603
|
-
postboned: unknown;
|
|
604
|
-
}
|
|
605
|
-
| {
|
|
606
|
-
kind: 'APP_ROUTE';
|
|
607
|
-
status: number;
|
|
608
|
-
headers: {
|
|
609
|
-
'cache-control'?: string;
|
|
610
|
-
'x-nextjs-stale-time': string; // timestamp in ms
|
|
611
|
-
'x-next-cache-tags': string; // comma separated paths (tags)
|
|
612
|
-
};
|
|
613
|
-
body: Buffer;
|
|
614
|
-
}
|
|
615
|
-
| {
|
|
616
|
-
kind: 'FETCH';
|
|
617
|
-
data: {
|
|
618
|
-
headers: Record<string, string>;
|
|
619
|
-
body: string; // base64 encoded
|
|
620
|
-
status: number;
|
|
621
|
-
url: string;
|
|
622
|
-
};
|
|
623
|
-
revalidate: number | false;
|
|
624
|
-
},
|
|
670
|
+
data: SetCacheValue,
|
|
625
671
|
ctx: {
|
|
626
672
|
isRoutePPREnabled: boolean;
|
|
627
673
|
isFallback: boolean;
|
|
628
674
|
tags?: string[];
|
|
629
675
|
// Different versions of Next.js use different arguments for the same functionality
|
|
630
676
|
revalidate?: number | false; // Version 15.0.3
|
|
631
|
-
cacheControl?: { revalidate:
|
|
677
|
+
cacheControl?: { revalidate: number | false; expire: number | undefined }; // Version 15.0.3+
|
|
632
678
|
},
|
|
633
679
|
) {
|
|
634
680
|
try {
|
|
635
681
|
if (
|
|
636
|
-
data
|
|
637
|
-
data.kind
|
|
638
|
-
data.kind !== 'FETCH'
|
|
682
|
+
data !== null &&
|
|
683
|
+
!(SUPPORTED_SET_KINDS as readonly string[]).includes(data.kind)
|
|
639
684
|
) {
|
|
640
685
|
console.warn(
|
|
641
686
|
'RedisStringsHandler.set() called with',
|
|
642
687
|
key,
|
|
643
688
|
ctx,
|
|
644
689
|
data,
|
|
645
|
-
|
|
690
|
+
`this cache handler is only designed and tested for kinds ${SUPPORTED_SET_KINDS.join(', ')} and not for kind`,
|
|
646
691
|
(data as { kind: string })?.kind,
|
|
647
692
|
);
|
|
648
693
|
}
|
|
649
694
|
|
|
650
695
|
await this.assertClientIsReady();
|
|
651
696
|
|
|
652
|
-
if (data
|
|
697
|
+
if (data?.kind === 'APP_PAGE' || data?.kind === 'APP_ROUTE') {
|
|
653
698
|
const tags = data.headers['x-next-cache-tags']?.split(',');
|
|
654
699
|
ctx.tags = [...(ctx.tags || []), ...(tags || [])];
|
|
655
700
|
}
|
|
656
701
|
|
|
702
|
+
// Pages Router entries (PAGES, REDIRECT and null/notFound results) do not
|
|
703
|
+
// carry an x-next-cache-tags header. Attach the implicit path tag
|
|
704
|
+
// (_N_T_/<path>) so that revalidatePath()/revalidateTag('_N_T_/<path>')
|
|
705
|
+
// invalidates them the same way as App Router entries.
|
|
706
|
+
if (data === null || data.kind === 'PAGES' || data.kind === 'REDIRECT') {
|
|
707
|
+
const implicitTag = NEXT_CACHE_IMPLICIT_TAG_ID + key;
|
|
708
|
+
if (!ctx.tags?.includes(implicitTag)) {
|
|
709
|
+
ctx.tags = [...(ctx.tags || []), implicitTag];
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
657
713
|
// Constructing and serializing the value for storing it in redis
|
|
658
714
|
const cacheEntry: CacheEntry = {
|
|
659
715
|
lastModified: Date.now(),
|
|
@@ -676,10 +732,13 @@ export default class RedisStringsHandler {
|
|
|
676
732
|
// Constructing the expire time for the cache entry
|
|
677
733
|
const revalidate =
|
|
678
734
|
// For fetch requests in newest versions, the revalidate context property is never used, and instead the revalidate property of the passed-in data is used
|
|
679
|
-
(data
|
|
735
|
+
(data?.kind === 'FETCH' && data.revalidate) ||
|
|
680
736
|
ctx.revalidate ||
|
|
681
737
|
ctx.cacheControl?.revalidate ||
|
|
682
|
-
|
|
738
|
+
// Legacy fallback: older Next.js versions attached `revalidate` directly
|
|
739
|
+
// to the data payload. FETCH is already handled above, so this only
|
|
740
|
+
// matters for those older, non-discriminated shapes.
|
|
741
|
+
(data as { revalidate?: number | false } | null)?.revalidate;
|
|
683
742
|
const expireAt =
|
|
684
743
|
revalidate && Number.isSafeInteger(revalidate) && revalidate > 0
|
|
685
744
|
? this.estimateExpireAge(revalidate)
|
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
|
|