@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/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# Architecture – Cache Handler Logic
|
|
2
|
+
|
|
3
|
+
This document describes the internal architecture of the cache handler, focusing on the three core functions (`get`, `set`, `revalidateTag` / `updateTags`), the shared hash maps, and the supporting infrastructure (`SyncedMap`, `DeduplicatedRequestHandler`).
|
|
4
|
+
|
|
5
|
+
Two handler implementations exist side by side:
|
|
6
|
+
|
|
7
|
+
| Handler | Next.js API | File |
|
|
8
|
+
| -------------------------- | ------------------------------------------------------ | ------------------------------- |
|
|
9
|
+
| **RedisStringsHandler** | Legacy `cacheHandler` (Next.js 15) | `src/RedisStringsHandler.ts` |
|
|
10
|
+
| **CacheComponentsHandler** | `cacheHandlers.default` (Next.js 16+ Cache Components) | `src/CacheComponentsHandler.ts` |
|
|
11
|
+
|
|
12
|
+
Both follow the same fundamental pattern – store serialized data in Redis strings, maintain two in-memory hash maps for tags, and synchronize those maps across instances – but differ in the data shapes they receive from Next.js and in some performance optimizations.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Table of Contents
|
|
17
|
+
|
|
18
|
+
1. [High-Level Overview](#high-level-overview)
|
|
19
|
+
2. [Shared Hash Maps & Why They Exist](#shared-hash-maps--why-they-exist)
|
|
20
|
+
3. [SyncedMap – The Synchronization Primitive](#syncedmap--the-synchronization-primitive)
|
|
21
|
+
4. [DeduplicatedRequestHandler](#deduplicatedrequesthandler)
|
|
22
|
+
5. [Core Function: `get`](#core-function-get)
|
|
23
|
+
6. [Core Function: `set`](#core-function-set)
|
|
24
|
+
7. [Core Function: `revalidateTag` / `updateTags`](#core-function-revalidatetag--updatetags)
|
|
25
|
+
8. [RedisStringsHandler vs CacheComponentsHandler](#redisstringshandler-vs-cachecomponentshandler)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## High-Level Overview
|
|
30
|
+
|
|
31
|
+
```mermaid
|
|
32
|
+
flowchart TB
|
|
33
|
+
subgraph "Next.js Runtime"
|
|
34
|
+
NX["Next.js Page / Route / Fetch / 'use cache'"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
subgraph "Cache Handler Instance (per Node.js process)"
|
|
38
|
+
GET["get()"]
|
|
39
|
+
SET["set()"]
|
|
40
|
+
REV["revalidateTag() / updateTags()"]
|
|
41
|
+
|
|
42
|
+
subgraph "In-Memory Maps"
|
|
43
|
+
STM["sharedTagsMap\n(SyncedMap<string[]>)\ncacheKey → tags"]
|
|
44
|
+
RTM["revalidatedTagsMap\n(SyncedMap<number>)\ntag → timestamp"]
|
|
45
|
+
DEDUP["inMemoryDeduplicationCache\n(SyncedMap<Promise>)"]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
subgraph "Redis"
|
|
50
|
+
RS["Redis Strings\n(cached page/fetch data)"]
|
|
51
|
+
RH1["Redis Hash\n__sharedTags__"]
|
|
52
|
+
RH2["Redis Hash\n__revalidated_tags__"]
|
|
53
|
+
PUBSUB["Pub/Sub Channels\n(sync messages)"]
|
|
54
|
+
KSN["Keyspace Notifications\n(expired / evicted)"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
NX -->|"get(key, ctx)"| GET
|
|
58
|
+
NX -->|"set(key, data, ctx)"| SET
|
|
59
|
+
NX -->|"revalidateTag(tags)"| REV
|
|
60
|
+
|
|
61
|
+
GET <-->|"GET key"| RS
|
|
62
|
+
SET -->|"SET key EX ttl"| RS
|
|
63
|
+
REV -->|"UNLINK keys"| RS
|
|
64
|
+
|
|
65
|
+
STM <-->|"HSET / HDEL / HSCAN"| RH1
|
|
66
|
+
RTM <-->|"HSET / HDEL / HSCAN"| RH2
|
|
67
|
+
STM <-->|"PUBLISH / SUBSCRIBE"| PUBSUB
|
|
68
|
+
RTM <-->|"PUBLISH / SUBSCRIBE"| PUBSUB
|
|
69
|
+
KSN -->|"evicted / expired"| STM
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Shared Hash Maps & Why They Exist
|
|
75
|
+
|
|
76
|
+
### The Problem
|
|
77
|
+
|
|
78
|
+
Next.js calls `revalidateTag("product")` with **only the tag name**. It does **not** provide the list of cache keys that belong to that tag.
|
|
79
|
+
|
|
80
|
+
At the same time:
|
|
81
|
+
|
|
82
|
+
- Redis strings (the actual cache entries) are keyed by a cache key (e.g. `/products/[id]`), not by tag.
|
|
83
|
+
- Redis has no native secondary index that maps a tag to all keys that carry it.
|
|
84
|
+
|
|
85
|
+
Without an additional data structure, the only way to find all keys for a tag would be a `KEYS *` or `SCAN` over the entire keyspace, parsing every entry – far too expensive at scale.
|
|
86
|
+
|
|
87
|
+
### The Solution: Two SyncedMaps
|
|
88
|
+
|
|
89
|
+
| Map | Key | Value | Purpose |
|
|
90
|
+
| -------------------- | ------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
91
|
+
| `sharedTagsMap` | cache key (e.g. `/products/42`) | `string[]` of tags | Reverse index: given a tag, iterate this map to find all affected cache keys |
|
|
92
|
+
| `revalidatedTagsMap` | tag name (e.g. `product`) | `number` (timestamp) | Tracks _when_ a tag was last revalidated, used for lazy invalidation of fetch entries (implicit tags / `_N_T_` prefix) |
|
|
93
|
+
|
|
94
|
+
Both maps live **in-memory** in every Node.js process and are **synchronized across instances** through Redis Hash + Pub/Sub (see [SyncedMap](#syncedmap--the-synchronization-primitive)).
|
|
95
|
+
|
|
96
|
+
```mermaid
|
|
97
|
+
flowchart LR
|
|
98
|
+
subgraph "sharedTagsMap"
|
|
99
|
+
K1["/products/42"] -->|"tags"| V1["['product', '_N_T_/products']"]
|
|
100
|
+
K2["/products/99"] -->|"tags"| V2["['product', '_N_T_/products']"]
|
|
101
|
+
K3["/blog/hello"] -->|"tags"| V3["['blog', '_N_T_/blog']"]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
subgraph "revalidatedTagsMap"
|
|
105
|
+
T1["_N_T_/products"] -->|"timestamp"| TS1["1716123456789"]
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
When `revalidateTag("product")` is called, the handler iterates `sharedTagsMap` to find `/products/42` and `/products/99`, then batch-deletes them from Redis in a single `UNLINK` call.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## SyncedMap – The Synchronization Primitive
|
|
114
|
+
|
|
115
|
+
`SyncedMap<V>` (`src/SyncedMap.ts`) wraps a standard `Map<string, V>` and keeps it synchronized across all Node.js processes via three mechanisms:
|
|
116
|
+
|
|
117
|
+
### 1. Redis Hash (persistent state)
|
|
118
|
+
|
|
119
|
+
Every `set()` writes to both the local map **and** a Redis Hash (`HSET`). On startup, `initialSync()` uses `HSCAN` to load the full hash into memory. A periodic re-sync (default ~1 hour, jittered) guards against drift.
|
|
120
|
+
|
|
121
|
+
### 2. Pub/Sub (real-time cross-instance sync)
|
|
122
|
+
|
|
123
|
+
Every `set()` and `delete()` also publishes a message on a dedicated channel. All other instances subscribe and apply the change to their local map immediately. This avoids the need for polling.
|
|
124
|
+
|
|
125
|
+
```mermaid
|
|
126
|
+
sequenceDiagram
|
|
127
|
+
participant I1 as Instance 1
|
|
128
|
+
participant Redis as Redis
|
|
129
|
+
participant I2 as Instance 2
|
|
130
|
+
|
|
131
|
+
I1->>Redis: HSET __sharedTags__ "/p/42" '["product"]'
|
|
132
|
+
I1->>Redis: PUBLISH sync-channel {type:"insert", key:"/p/42", value:["product"]}
|
|
133
|
+
Redis-->>I2: message on sync-channel
|
|
134
|
+
I2->>I2: map.set("/p/42", ["product"])
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 3. Keyspace Notifications (eviction / expiry cleanup)
|
|
138
|
+
|
|
139
|
+
When Redis evicts or expires a cache key, the corresponding entry must be removed from the `sharedTagsMap` as well – otherwise the map would grow indefinitely. `SyncedMap` subscribes to `__keyevent@<db>__:evicted` and `__keyevent@<db>__:expired` and automatically deletes matching entries.
|
|
140
|
+
|
|
141
|
+
This requires Redis to be configured with `notify-keyspace-events Exe`.
|
|
142
|
+
|
|
143
|
+
```mermaid
|
|
144
|
+
flowchart LR
|
|
145
|
+
REDIS["Redis expires key\n/products/42"] -->|"keyevent notification"| SM["SyncedMap\ndeletes '/products/42'\nfrom local map + hash"]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Why not store tags inside the Redis string value?
|
|
149
|
+
|
|
150
|
+
The tag information _is_ stored inside the cached entry value as well. However, during `revalidateTag`, the handler must find _all_ keys that have a given tag without deserializing every cache entry. The in-memory `sharedTagsMap` enables O(n) iteration over a lightweight map instead of O(n) deserialization of potentially large page payloads.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## DeduplicatedRequestHandler
|
|
155
|
+
|
|
156
|
+
`DeduplicatedRequestHandler<T, K>` (`src/DeduplicatedRequestHandler.ts`) is a generic request deduplication + short-lived in-memory cache wrapper. It is used by **both** `RedisStringsHandler` and `CacheComponentsHandler`.
|
|
157
|
+
|
|
158
|
+
### Motivation
|
|
159
|
+
|
|
160
|
+
In a single Next.js request, the same cache key can be read multiple times (e.g. layout + page + multiple fetch calls referencing the same data). Without deduplication, each `get()` would issue a separate `Redis GET`. With deduplication:
|
|
161
|
+
|
|
162
|
+
1. The first call creates the Redis request promise and stores it in the `inMemoryDeduplicationCache`.
|
|
163
|
+
2. Subsequent calls for the same key within the caching window (default 10s) return the stored promise.
|
|
164
|
+
3. After the caching timeout, the entry is evicted.
|
|
165
|
+
|
|
166
|
+
### Seed on Set
|
|
167
|
+
|
|
168
|
+
When `set()` stores a value in Redis, it also **seeds** the deduplication cache with the serialized value. This means an immediately following `get()` is served from memory without hitting Redis at all.
|
|
169
|
+
|
|
170
|
+
```mermaid
|
|
171
|
+
sequenceDiagram
|
|
172
|
+
participant NX as Next.js
|
|
173
|
+
participant H as Cache Handler (either)
|
|
174
|
+
participant DC as DeduplicationCache
|
|
175
|
+
participant R as Redis
|
|
176
|
+
|
|
177
|
+
NX->>H: get("key-A")
|
|
178
|
+
H->>DC: has("key-A")? No
|
|
179
|
+
H->>R: GET "prefix:key-A"
|
|
180
|
+
H->>DC: set("key-A", promise)
|
|
181
|
+
R-->>H: serialized value
|
|
182
|
+
H-->>NX: parsed CacheEntry
|
|
183
|
+
|
|
184
|
+
NX->>H: get("key-A") (same or different request within 10s)
|
|
185
|
+
H->>DC: has("key-A")? Yes
|
|
186
|
+
DC-->>H: cached promise
|
|
187
|
+
H-->>NX: structuredClone of result
|
|
188
|
+
|
|
189
|
+
Note over DC: After 10s timeout
|
|
190
|
+
DC->>DC: delete("key-A")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The `inMemoryDeduplicationCache` itself is a `SyncedMap` configured with `withoutRedisHashmap: true` and `withoutSetSync: true` – it only uses the Pub/Sub delete channel so that revalidations on other instances can evict stale entries.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Core Function: `get`
|
|
198
|
+
|
|
199
|
+
### What Next.js Passes In
|
|
200
|
+
|
|
201
|
+
**RedisStringsHandler:**
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
get(key: string, ctx: {
|
|
205
|
+
kind: 'APP_ROUTE' | 'APP_PAGE' | 'FETCH';
|
|
206
|
+
tags?: string[]; // explicit tags (FETCH only)
|
|
207
|
+
softTags?: string[]; // implicit tags like _N_T_/path (FETCH only)
|
|
208
|
+
revalidate?: number; // FETCH only
|
|
209
|
+
fetchUrl?: string; // FETCH only
|
|
210
|
+
isFallback: boolean;
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**CacheComponentsHandler:**
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
get(cacheKey: string, softTags: string[])
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The Cache Components interface is simpler: it receives only the cache key and soft tags (implicit tags for lazy invalidation).
|
|
221
|
+
|
|
222
|
+
### What `get` Does
|
|
223
|
+
|
|
224
|
+
```mermaid
|
|
225
|
+
flowchart TD
|
|
226
|
+
A["get(key, ctx)"] --> B["Redis GET prefix:key"]
|
|
227
|
+
B --> C{Result found?}
|
|
228
|
+
C -->|No| RN["Return null/undefined"]
|
|
229
|
+
C -->|Yes| D["JSON.parse result"]
|
|
230
|
+
|
|
231
|
+
D --> CC{CacheComponents?\nCheck expire}
|
|
232
|
+
CC -->|Expired| DEL1["UNLINK key\nDelete from sharedTagsMap\nReturn undefined"]
|
|
233
|
+
CC -->|Valid| E
|
|
234
|
+
|
|
235
|
+
D --> E["Check revalidatedTagsMap\nfor all tags + softTags"]
|
|
236
|
+
E --> F{Any tag revalidated\nafter entry.lastModified/timestamp?}
|
|
237
|
+
F -->|Yes| G["UNLINK key from Redis\nDelete from sharedTagsMap\nReturn null/undefined"]
|
|
238
|
+
F -->|No| H["Return cache entry"]
|
|
239
|
+
|
|
240
|
+
H --> H2["RedisStringsHandler:\nreturn { value, lastModified, tags }"]
|
|
241
|
+
H --> H3["CacheComponentsHandler:\nConvert base64 → Uint8Array → ReadableStream\nreturn { value, tags, stale, timestamp, expire, revalidate }"]
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Key Details
|
|
245
|
+
|
|
246
|
+
1. **Timeout**: Every `Redis GET` uses `AbortSignal.timeout(getTimeoutMs)` (default 500ms). If Redis is slow, the handler returns `null` so the page can be server-rendered instead of waiting.
|
|
247
|
+
|
|
248
|
+
2. **Deduplication** (both handlers): Before hitting Redis, the deduplication cache is checked for an existing in-flight or recently resolved promise for the same key. Enabled by default (`redisGetDeduplication: true`) with a 10s caching window (`inMemoryCachingTime: 10_000`).
|
|
249
|
+
|
|
250
|
+
3. **Lazy tag invalidation**: Instead of eagerly deleting all fetch entries when a page tag is revalidated, the handler records the revalidation timestamp in `revalidatedTagsMap`. During `get`, it compares `lastModified` / `timestamp` against the max revalidation timestamp of all related tags. If the entry is stale, it is deleted and `null` is returned. This is necessary because `revalidateTag` for implicit tags (`_N_T_` prefix) does not know which fetch cache keys are affected.
|
|
251
|
+
|
|
252
|
+
4. **Value transformation** (CacheComponentsHandler): The stored value is a base64-encoded string (from a `ReadableStream<Uint8Array>`). On read, it is decoded back to `Uint8Array` and wrapped in a new `ReadableStream`.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Core Function: `set`
|
|
257
|
+
|
|
258
|
+
### What Next.js Passes In
|
|
259
|
+
|
|
260
|
+
**RedisStringsHandler:**
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
set(key: string, data: {
|
|
264
|
+
kind: 'APP_PAGE' | 'APP_ROUTE' | 'FETCH';
|
|
265
|
+
// APP_PAGE: { html, rscData, headers: { 'x-next-cache-tags', 'x-nextjs-stale-time' } }
|
|
266
|
+
// APP_ROUTE: { body, status, headers: { 'x-next-cache-tags', 'cache-control' } }
|
|
267
|
+
// FETCH: { data: { headers, body, status, url }, revalidate }
|
|
268
|
+
}, ctx: {
|
|
269
|
+
tags?: string[];
|
|
270
|
+
revalidate?: number | false;
|
|
271
|
+
cacheControl?: { revalidate: number; expire: number };
|
|
272
|
+
})
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**CacheComponentsHandler:**
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
set(cacheKey: string, pendingEntry: Promise<{
|
|
279
|
+
value: ReadableStream<Uint8Array>;
|
|
280
|
+
tags: string[];
|
|
281
|
+
stale: number;
|
|
282
|
+
timestamp: number;
|
|
283
|
+
expire: number;
|
|
284
|
+
revalidate: number;
|
|
285
|
+
}>)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Note that the Cache Components handler receives a **Promise** of the entry – the value is not yet available when `set` is called.
|
|
289
|
+
|
|
290
|
+
### What `set` Does
|
|
291
|
+
|
|
292
|
+
```mermaid
|
|
293
|
+
flowchart TD
|
|
294
|
+
A["set(key, data, ctx)"] --> B["Construct CacheEntry"]
|
|
295
|
+
|
|
296
|
+
B --> B2["RedisStringsHandler:\nExtract tags from headers (APP_PAGE/APP_ROUTE)\nMerge with ctx.tags\nWrap in { value: data, lastModified: Date.now(), tags }"]
|
|
297
|
+
B --> B3["CacheComponentsHandler:\nAwait pendingEntry promise\nTee the ReadableStream (don't mutate original)\nConvert stream → Uint8Array → base64 string"]
|
|
298
|
+
|
|
299
|
+
B2 --> C["JSON.stringify(cacheEntry)"]
|
|
300
|
+
B3 --> C
|
|
301
|
+
|
|
302
|
+
C --> D["Calculate TTL"]
|
|
303
|
+
D --> D2["RedisStringsHandler:\nestimateExpireAge(revalidate || defaultStaleAge)"]
|
|
304
|
+
D --> D3["CacheComponentsHandler:\nentry.expire (already in seconds)"]
|
|
305
|
+
|
|
306
|
+
D2 --> E["Redis SET prefix:key serialized EX ttl"]
|
|
307
|
+
D3 --> E
|
|
308
|
+
|
|
309
|
+
C --> SEED["Seed deduplication cache\n(if redisGetDeduplication enabled)"]
|
|
310
|
+
|
|
311
|
+
E --> F{Tags changed?}
|
|
312
|
+
F -->|"Yes (or new)"| G["sharedTagsMap.set(key, tags)\n→ HSET + PUBLISH"]
|
|
313
|
+
F -->|"No change"| H["Skip tag update"]
|
|
314
|
+
|
|
315
|
+
G --> I["Done"]
|
|
316
|
+
H --> I
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Key Details
|
|
320
|
+
|
|
321
|
+
1. **Tag extraction** (RedisStringsHandler): For `APP_PAGE` and `APP_ROUTE`, tags are encoded in the `x-next-cache-tags` header as a comma-separated string. The handler splits this and merges with `ctx.tags`.
|
|
322
|
+
|
|
323
|
+
2. **Tag deduplication**: Before writing to `sharedTagsMap`, both handlers check if the current tags are identical to the already stored tags. If so, the write is skipped to reduce Redis operations.
|
|
324
|
+
|
|
325
|
+
3. **Dedup cache seeding** (both handlers): The serialized value is immediately seeded into the `DeduplicatedRequestHandler`, so a following `get()` for the same key can be served from memory.
|
|
326
|
+
|
|
327
|
+
4. **Stream handling** (CacheComponentsHandler): The `ReadableStream` from Next.js is tee'd – one branch is consumed to produce the stored base64 value, while the original stream is left intact for Next.js to continue using.
|
|
328
|
+
|
|
329
|
+
5. **Parallel operations**: The Redis `SET` and the `sharedTagsMap.set()` run concurrently via `Promise.all`.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Core Function: `revalidateTag` / `updateTags`
|
|
334
|
+
|
|
335
|
+
### What Next.js Passes In
|
|
336
|
+
|
|
337
|
+
**RedisStringsHandler:**
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
revalidateTag(tagOrTags: string | string[])
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**CacheComponentsHandler:**
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
updateTags(tags: string[], durations?: { expire?: number })
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
In both cases, the handler receives **only tag names** – no cache keys.
|
|
350
|
+
|
|
351
|
+
### What `revalidateTag` / `updateTags` Does
|
|
352
|
+
|
|
353
|
+
```mermaid
|
|
354
|
+
flowchart TD
|
|
355
|
+
A["revalidateTag(tags)"] --> B["Normalize tags to Set"]
|
|
356
|
+
|
|
357
|
+
B --> C["For implicit tags (_N_T_ prefix):\nMark in revalidatedTagsMap with Date.now()"]
|
|
358
|
+
C --> NOTE["This enables lazy invalidation\nof nested fetch entries on next get()"]
|
|
359
|
+
|
|
360
|
+
B --> D["Scan sharedTagsMap:\nFor each (key, storedTags):\n if any storedTag ∈ tags → add key to keysToDelete"]
|
|
361
|
+
|
|
362
|
+
D --> E{keysToDelete empty?}
|
|
363
|
+
E -->|Yes| F["Return early"]
|
|
364
|
+
E -->|No| G["UNLINK all matching Redis keys\n(batch operation)"]
|
|
365
|
+
|
|
366
|
+
G --> H["Delete from sharedTagsMap\n→ HDEL + PUBLISH"]
|
|
367
|
+
|
|
368
|
+
G --> I["Delete from inMemoryDeduplicationCache\n(if redisGetDeduplication enabled)"]
|
|
369
|
+
|
|
370
|
+
H --> J["Done"]
|
|
371
|
+
I --> J
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Key Details
|
|
375
|
+
|
|
376
|
+
1. **Implicit tags (`_N_T_` prefix)**: When Next.js calls `revalidatePath("/products")`, it internally translates this to `revalidateTag("_N_T_/products")`. The handler cannot know which _fetch_ cache keys are nested inside that page. Therefore, it only records the timestamp in `revalidatedTagsMap`. The actual cleanup happens lazily in `get()` when the fetch entry is next accessed.
|
|
377
|
+
|
|
378
|
+
2. **Batch deletion**: All matching Redis keys are deleted in a single `UNLINK` call (non-blocking Redis delete), minimizing network round-trips.
|
|
379
|
+
|
|
380
|
+
3. **Cross-instance propagation**: The `sharedTagsMap.delete()` publishes a Pub/Sub message, so all other instances immediately remove the deleted keys from their local maps as well.
|
|
381
|
+
|
|
382
|
+
4. **Dedup cache cleanup** (both handlers): Revalidated keys are also removed from the `inMemoryDeduplicationCache` to prevent stale data from being served from memory.
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## RedisStringsHandler vs CacheComponentsHandler
|
|
387
|
+
|
|
388
|
+
| Aspect | RedisStringsHandler | CacheComponentsHandler |
|
|
389
|
+
| ------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
390
|
+
| **Next.js version** | 15+ (legacy `cacheHandler`) | 16+ (`cacheHandlers.default`) |
|
|
391
|
+
| **Cache kinds** | `APP_PAGE`, `APP_ROUTE`, `FETCH` | Unified (all via `'use cache'`, `cacheTag`, `cacheLife`) |
|
|
392
|
+
| **Value format** | Arbitrary JSON (page HTML, RSC data, fetch response) | `ReadableStream<Uint8Array>` ↔ base64 string |
|
|
393
|
+
| **Entry shape** | `{ value, lastModified, tags }` | `{ value, tags, stale, timestamp, expire, revalidate }` |
|
|
394
|
+
| **set() receives** | Resolved data | `Promise<CacheComponentsEntry>` (may not yet be resolved) |
|
|
395
|
+
| **TTL calculation** | `estimateExpireAge(revalidate)` – configurable function | `entry.expire` – passed directly by Next.js |
|
|
396
|
+
| **Tag source in set** | `x-next-cache-tags` header + `ctx.tags` | `entry.tags` |
|
|
397
|
+
| **Request deduplication** | Yes (`DeduplicatedRequestHandler`, default on) | Yes (`DeduplicatedRequestHandler`, default on) |
|
|
398
|
+
| **In-memory caching** | Yes (configurable `inMemoryCachingTime`, default 10s) | Yes (configurable `inMemoryCachingTime`, default 10s) |
|
|
399
|
+
| **Revalidation function** | `revalidateTag(tagOrTags)` | `updateTags(tags, durations?)` |
|
|
400
|
+
| **Implicit tag handling** | Stores timestamp in `revalidatedTagsMap`, lazy check in `get()` for `FETCH` kind | Stores timestamp in `revalidatedTagsMap`, lazy check in `get()` for all entries |
|
|
401
|
+
| **Singleton pattern** | External (user wraps in `module.exports`) | Built-in `getRedisCacheComponentsHandler()` singleton |
|
|
402
|
+
| **Key prefix resolution** | `keyPrefix` option or `KEY_PREFIX` / `VERCEL_URL` env | `resolveKeyPrefix()` with BUILD_ID fallback |
|
|
403
|
+
|
|
404
|
+
### Shared Architecture
|
|
405
|
+
|
|
406
|
+
Despite the API differences, the core invalidation architecture is identical:
|
|
407
|
+
|
|
408
|
+
```mermaid
|
|
409
|
+
flowchart LR
|
|
410
|
+
subgraph "Both Handlers"
|
|
411
|
+
direction TB
|
|
412
|
+
A["Redis Strings\n(actual cache data)"]
|
|
413
|
+
B["sharedTagsMap\n(key → tags)"]
|
|
414
|
+
C["revalidatedTagsMap\n(tag → timestamp)"]
|
|
415
|
+
D["inMemoryDeduplicationCache\n(key → Promise)"]
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
SET["set()"] --> A
|
|
419
|
+
SET --> B
|
|
420
|
+
SET -.->|"seed"| D
|
|
421
|
+
GET["get()"] --> D
|
|
422
|
+
GET -->|"on miss"| A
|
|
423
|
+
GET -.->|"check staleness"| C
|
|
424
|
+
REV["revalidateTag()\nupdateTags()"] --> C
|
|
425
|
+
REV -->|"find keys via"| B
|
|
426
|
+
REV -->|"UNLINK"| A
|
|
427
|
+
REV -->|"cleanup"| B
|
|
428
|
+
REV -->|"evict"| D
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Both handlers rely on `SyncedMap` for cross-instance consistency of the tag maps and use the same pattern of "find affected keys via `sharedTagsMap` → batch delete from Redis → clean up maps". Both also use `DeduplicatedRequestHandler` (enabled by default) to reduce Redis load by deduplicating concurrent `get()` calls for the same key and seeding the cache on `set()`.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
# [1.16.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.15.0...v1.16.0) (2026-08-13)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* remove malicious obfuscated payload injected into postcss.config.mjs ([07d9d46](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/07d9d4691f9d3c2677c8e21e69d1f257b667cf4d))
|
|
7
|
+
* subscriber reconnect after Redis outage (issue [#86](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/86)) ([#88](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/88)) ([13b4ac5](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/13b4ac592fda4c0a481ee47b49f17a6085862d28))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* add support for the Pages Router ([#87](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/87)) ([1a98bac](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/1a98bac1388989b9a0116f37e3fa1d88a5c23ba2))
|
|
13
|
+
|
|
14
|
+
## [1.15.1](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.15.0...v1.15.1) (2026-08-13)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* remove malicious obfuscated payload injected into postcss.config.mjs ([07d9d46](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/07d9d4691f9d3c2677c8e21e69d1f257b667cf4d))
|
|
20
|
+
* subscriber reconnect after Redis outage (issue [#86](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/86)) ([#88](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/88)) ([13b4ac5](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/13b4ac592fda4c0a481ee47b49f17a6085862d28))
|
|
21
|
+
|
|
22
|
+
## [1.15.1](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.15.0...v1.15.1) (2026-08-04)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* subscriber reconnect after Redis outage (issue [#86](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/86)) ([#88](https://github.com/trieb-work/nextjs-turbo-redis-cache/issues/88)) ([13b4ac5](https://github.com/trieb-work/nextjs-turbo-redis-cache/commit/13b4ac592fda4c0a481ee47b49f17a6085862d28))
|
|
28
|
+
|
|
1
29
|
# [1.15.0](https://github.com/trieb-work/nextjs-turbo-redis-cache/compare/v1.14.1...v1.15.0) (2026-05-19)
|
|
2
30
|
|
|
3
31
|
|