From 186b3ff433e48261d667de0a0c944a4fa5d46dc2 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Wed, 25 Sep 2024 14:06:21 +0530 Subject: [PATCH 1/8] revalidate tags in case of rsc payload response from server --- packages/next/src/server/app-render/app-render.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 740444cd7cbfb..090142457c058 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -512,6 +512,20 @@ async function generateDynamicFlightRenderResult( onError, } ) + await waitAtLeastOneReactRenderTask() + + if ( + ctx.staticGenerationStore.pendingRevalidates || + ctx.staticGenerationStore.revalidatedTags + ) { + const promises = Promise.all([ + ctx.staticGenerationStore.incrementalCache?.revalidateTag( + ctx.staticGenerationStore.revalidatedTags || [] + ), + ...Object.values(ctx.staticGenerationStore.pendingRevalidates || {}), + ]) + ctx.renderOpts.waitUntil = (p) => promises.then(() => p) + } return new FlightRenderResult(flightReadableStream, { fetchMetrics: ctx.staticGenerationStore.fetchMetrics, From 035a389c044fedb8e9f99f8c3c7ce9c1aa695d63 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Wed, 25 Sep 2024 14:07:37 +0530 Subject: [PATCH 2/8] add checks for revalidatedTags along with pendingRevalidates --- .../next/src/server/app-render/app-render.tsx | 88 ++++++++++--------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 090142457c058..bc3934957256f 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -227,8 +227,8 @@ function parseRequestHeaders( const flightRouterState = shouldProvideFlightRouterState ? parseAndValidateFlightRouterState( - headers[NEXT_ROUTER_STATE_TREE_HEADER.toLowerCase()] - ) + headers[NEXT_ROUTER_STATE_TREE_HEADER.toLowerCase()] + ) : undefined const csp = @@ -942,7 +942,7 @@ async function renderToHTMLOrFlightImpl( }) .end( metrics.clientComponentLoadStart + - metrics.clientComponentLoadTimes + metrics.clientComponentLoadTimes ) } } @@ -1098,7 +1098,8 @@ async function renderToHTMLOrFlightImpl( // If we have pending revalidates, wait until they are all resolved. if ( staticGenerationStore.pendingRevalidates || - staticGenerationStore.pendingRevalidateWrites + staticGenerationStore.pendingRevalidateWrites || + staticGenerationStore.revalidatedTags ) { options.waitUntil = Promise.all([ staticGenerationStore.incrementalCache?.revalidateTag( @@ -1207,7 +1208,8 @@ async function renderToHTMLOrFlightImpl( // If we have pending revalidates, wait until they are all resolved. if ( staticGenerationStore.pendingRevalidates || - staticGenerationStore.pendingRevalidateWrites + staticGenerationStore.pendingRevalidateWrites || + staticGenerationStore.revalidatedTags ) { options.waitUntil = Promise.all([ staticGenerationStore.incrementalCache?.revalidateTag( @@ -1865,25 +1867,25 @@ async function prerenderToStream( res.statusCode === 404 ) - ;( - prerenderAsyncStorage.run( - // The store to scope - prospectiveRenderPrerenderStore, - // The function to run - ComponentMod.prerender, - // ... the arguments for the function to run - firstAttemptRSCPayload, - clientReferenceManifest.clientModules, - { - // This render will be thrown away so we don't need to track errors or postpones - onError: undefined, - onPostpone: undefined, - // we don't care to track postpones during the prospective render because we need - // to always do a final render anyway - signal: flightController.signal, - } - ) as Promise - ).catch(() => {}) + ; ( + prerenderAsyncStorage.run( + // The store to scope + prospectiveRenderPrerenderStore, + // The function to run + ComponentMod.prerender, + // ... the arguments for the function to run + firstAttemptRSCPayload, + clientReferenceManifest.clientModules, + { + // This render will be thrown away so we don't need to track errors or postpones + onError: undefined, + onPostpone: undefined, + // we don't care to track postpones during the prospective render because we need + // to always do a final render anyway + signal: flightController.signal, + } + ) as Promise + ).catch(() => { }) // When this resolves the cache has no inflight reads and we can ascertain the dynamic outcome await cacheSignal.cacheReady() @@ -2079,7 +2081,7 @@ async function prerenderToStream( const resumeStream = await resume( {}} + preinitScripts={() => { }} clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} nonce={ctx.nonce} @@ -2174,23 +2176,23 @@ async function prerenderToStream( ctx, res.statusCode === 404 ) - // We're not going to use the result of this render because the only time it could be used - // is if it completes in a microtask and that's likely very rare for any non-trivial app - ;( - prerenderAsyncStorage.run( - // The store to scope - prospectiveRenderPrerenderStore, - // The function to run - ComponentMod.prerender, - // ... the arguments for the function to run - firstAttemptRSCPayload, - clientReferenceManifest.clientModules, - { - onError, - signal: flightController.signal, - } - ) as Promise - ).catch(() => {}) + // We're not going to use the result of this render because the only time it could be used + // is if it completes in a microtask and that's likely very rare for any non-trivial app + ; ( + prerenderAsyncStorage.run( + // The store to scope + prospectiveRenderPrerenderStore, + // The function to run + ComponentMod.prerender, + // ... the arguments for the function to run + firstAttemptRSCPayload, + clientReferenceManifest.clientModules, + { + onError, + signal: flightController.signal, + } + ) as Promise + ).catch(() => { }) // When this resolves the cache has no inflight reads and we can ascertain the dynamic outcome await cacheSignal.cacheReady() @@ -2535,7 +2537,7 @@ async function prerenderToStream( const resumeStream = await resume( {}} + preinitScripts={() => { }} clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} nonce={ctx.nonce} From 301575ab3a76491b7ee91d7ae66b74aa4fa5011b Mon Sep 17 00:00:00 2001 From: Abhishek Date: Wed, 25 Sep 2024 14:08:17 +0530 Subject: [PATCH 3/8] add tests --- .../app/RevalidateViaForm.tsx | 17 +++++++++ .../app/actions/revalidate.ts | 11 ++++++ .../app-dir/revalidatetag-rsc/app/layout.tsx | 9 +++++ .../app-dir/revalidatetag-rsc/app/page.tsx | 23 ++++++++++++ .../app/revalidate_via_page/page.tsx | 24 +++++++++++++ .../app-dir/revalidatetag-rsc/next.config.js | 6 ++++ .../revalidatetag-rsc.test.ts | 35 +++++++++++++++++++ 7 files changed, 125 insertions(+) create mode 100644 test/e2e/app-dir/revalidatetag-rsc/app/RevalidateViaForm.tsx create mode 100644 test/e2e/app-dir/revalidatetag-rsc/app/actions/revalidate.ts create mode 100644 test/e2e/app-dir/revalidatetag-rsc/app/layout.tsx create mode 100644 test/e2e/app-dir/revalidatetag-rsc/app/page.tsx create mode 100644 test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_page/page.tsx create mode 100644 test/e2e/app-dir/revalidatetag-rsc/next.config.js create mode 100644 test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts diff --git a/test/e2e/app-dir/revalidatetag-rsc/app/RevalidateViaForm.tsx b/test/e2e/app-dir/revalidatetag-rsc/app/RevalidateViaForm.tsx new file mode 100644 index 0000000000000..762d46859ca11 --- /dev/null +++ b/test/e2e/app-dir/revalidatetag-rsc/app/RevalidateViaForm.tsx @@ -0,0 +1,17 @@ +'use client' + +import { revalidate } from './actions/revalidate' + +export default function RevalidateViaForm({ tag }: { tag: string }) { + const handleRevalidate = async () => { + await revalidate(tag) + } + + return ( +
+ +
+ ) +} diff --git a/test/e2e/app-dir/revalidatetag-rsc/app/actions/revalidate.ts b/test/e2e/app-dir/revalidatetag-rsc/app/actions/revalidate.ts new file mode 100644 index 0000000000000..2fba1ff18f207 --- /dev/null +++ b/test/e2e/app-dir/revalidatetag-rsc/app/actions/revalidate.ts @@ -0,0 +1,11 @@ +'use server' + +import { revalidateTag } from 'next/cache' + +export const revalidate = async ( + tag: string +): Promise<{ revalidated: boolean }> => { + revalidateTag(tag) + + return { revalidated: true } +} diff --git a/test/e2e/app-dir/revalidatetag-rsc/app/layout.tsx b/test/e2e/app-dir/revalidatetag-rsc/app/layout.tsx new file mode 100644 index 0000000000000..716a8db36f52c --- /dev/null +++ b/test/e2e/app-dir/revalidatetag-rsc/app/layout.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from 'react' + +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/revalidatetag-rsc/app/page.tsx b/test/e2e/app-dir/revalidatetag-rsc/app/page.tsx new file mode 100644 index 0000000000000..0c07e4da8cb14 --- /dev/null +++ b/test/e2e/app-dir/revalidatetag-rsc/app/page.tsx @@ -0,0 +1,23 @@ +import RevalidateViaForm from './RevalidateViaForm' +import Link from 'next/link' + +export default async function Page() { + const data = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random', + { + next: { + tags: ['data'], + }, + } + ).then((res) => res.text()) + + return ( +
+ {data} + + + Revalidate via page + +
+ ) +} diff --git a/test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_page/page.tsx b/test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_page/page.tsx new file mode 100644 index 0000000000000..5bef033f211f3 --- /dev/null +++ b/test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_page/page.tsx @@ -0,0 +1,24 @@ +'use server' + +import Link from 'next/link' +import { revalidateTag } from 'next/cache' + +const RevalidateViaPage = async ({ + searchParams, +}: { + searchParams: { tag: string } +}) => { + const { tag } = searchParams + revalidateTag(tag) + + return ( +
+
Tag [{tag}] has been revalidated
+ + To Home + +
+ ) +} + +export default RevalidateViaPage diff --git a/test/e2e/app-dir/revalidatetag-rsc/next.config.js b/test/e2e/app-dir/revalidatetag-rsc/next.config.js new file mode 100644 index 0000000000000..807126e4cf0bf --- /dev/null +++ b/test/e2e/app-dir/revalidatetag-rsc/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts b/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts new file mode 100644 index 0000000000000..e69efc1bbcd0b --- /dev/null +++ b/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts @@ -0,0 +1,35 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('revalidateTag-rsc', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should revalidate fetch cache if revalidateTag invoked via server action', async () => { + const browser = await next.browser('/') + const randomNumber = await browser.elementById('data').text() + await browser.refresh() + const randomNumber2 = await browser.elementById('data').text() + expect(randomNumber).toEqual(randomNumber2) + + await browser.elementByCss('#submit-form').click() + await browser.waitForIdleNetwork() + const randomNumber3 = await browser.elementById('data').text() + expect(randomNumber3).not.toEqual(randomNumber) + }) + + it('should revalidate fetch cache if revalidateTag invoked via server component', async () => { + const browser = await next.browser('/') + const randomNumber = await browser.elementById('data').text() + await browser.refresh() + const randomNumber2 = await browser.elementById('data').text() + expect(randomNumber).toEqual(randomNumber2) + + await browser.elementByCss('#revalidate-via-page').click() + await browser.waitForElementByCss('#home') + await browser.elementByCss('#home').click() + await browser.waitForElementByCss('#data') + const randomNumber3 = await browser.elementById('data').text() + expect(randomNumber3).not.toEqual(randomNumber) + }) +}) From 7eb14d7f0777f3678a46a50d5ccdf000d3423822 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Sun, 29 Sep 2024 19:30:15 +0530 Subject: [PATCH 4/8] undo formatting --- .../next/src/server/app-render/app-render.tsx | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index bc3934957256f..dfa17f1193261 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -227,8 +227,8 @@ function parseRequestHeaders( const flightRouterState = shouldProvideFlightRouterState ? parseAndValidateFlightRouterState( - headers[NEXT_ROUTER_STATE_TREE_HEADER.toLowerCase()] - ) + headers[NEXT_ROUTER_STATE_TREE_HEADER.toLowerCase()] + ) : undefined const csp = @@ -942,7 +942,7 @@ async function renderToHTMLOrFlightImpl( }) .end( metrics.clientComponentLoadStart + - metrics.clientComponentLoadTimes + metrics.clientComponentLoadTimes ) } } @@ -1867,25 +1867,25 @@ async function prerenderToStream( res.statusCode === 404 ) - ; ( - prerenderAsyncStorage.run( - // The store to scope - prospectiveRenderPrerenderStore, - // The function to run - ComponentMod.prerender, - // ... the arguments for the function to run - firstAttemptRSCPayload, - clientReferenceManifest.clientModules, - { - // This render will be thrown away so we don't need to track errors or postpones - onError: undefined, - onPostpone: undefined, - // we don't care to track postpones during the prospective render because we need - // to always do a final render anyway - signal: flightController.signal, - } - ) as Promise - ).catch(() => { }) + ;( + prerenderAsyncStorage.run( + // The store to scope + prospectiveRenderPrerenderStore, + // The function to run + ComponentMod.prerender, + // ... the arguments for the function to run + firstAttemptRSCPayload, + clientReferenceManifest.clientModules, + { + // This render will be thrown away so we don't need to track errors or postpones + onError: undefined, + onPostpone: undefined, + // we don't care to track postpones during the prospective render because we need + // to always do a final render anyway + signal: flightController.signal, + } + ) as Promise + ).catch(() => {}) // When this resolves the cache has no inflight reads and we can ascertain the dynamic outcome await cacheSignal.cacheReady() @@ -2081,7 +2081,7 @@ async function prerenderToStream( const resumeStream = await resume( { }} + preinitScripts={() => {}} clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} nonce={ctx.nonce} @@ -2176,23 +2176,23 @@ async function prerenderToStream( ctx, res.statusCode === 404 ) - // We're not going to use the result of this render because the only time it could be used - // is if it completes in a microtask and that's likely very rare for any non-trivial app - ; ( - prerenderAsyncStorage.run( - // The store to scope - prospectiveRenderPrerenderStore, - // The function to run - ComponentMod.prerender, - // ... the arguments for the function to run - firstAttemptRSCPayload, - clientReferenceManifest.clientModules, - { - onError, - signal: flightController.signal, - } - ) as Promise - ).catch(() => { }) + // We're not going to use the result of this render because the only time it could be used + // is if it completes in a microtask and that's likely very rare for any non-trivial app + ;( + prerenderAsyncStorage.run( + // The store to scope + prospectiveRenderPrerenderStore, + // The function to run + ComponentMod.prerender, + // ... the arguments for the function to run + firstAttemptRSCPayload, + clientReferenceManifest.clientModules, + { + onError, + signal: flightController.signal, + } + ) as Promise + ).catch(() => {}) // When this resolves the cache has no inflight reads and we can ascertain the dynamic outcome await cacheSignal.cacheReady() @@ -2537,7 +2537,7 @@ async function prerenderToStream( const resumeStream = await resume( { }} + preinitScripts={() => {}} clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} nonce={ctx.nonce} From 7bc0b9d9ceca6d44e01bfebfd1d4097d0cc97d03 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Sun, 29 Sep 2024 19:48:21 +0530 Subject: [PATCH 5/8] add check for pendingRevalidates in generateDynamicFlightRenderResult --- packages/next/src/server/app-render/app-render.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index dfa17f1193261..37160306a8b2f 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -516,13 +516,15 @@ async function generateDynamicFlightRenderResult( if ( ctx.staticGenerationStore.pendingRevalidates || - ctx.staticGenerationStore.revalidatedTags + ctx.staticGenerationStore.revalidatedTags || + ctx.staticGenerationStore.pendingRevalidateWrites ) { const promises = Promise.all([ ctx.staticGenerationStore.incrementalCache?.revalidateTag( ctx.staticGenerationStore.revalidatedTags || [] ), ...Object.values(ctx.staticGenerationStore.pendingRevalidates || {}), + ...(ctx.staticGenerationStore.pendingRevalidateWrites || []), ]) ctx.renderOpts.waitUntil = (p) => promises.then(() => p) } From 2f55634c12ca39cd59c9fc080b2f276dce2f6cc2 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Sun, 29 Sep 2024 19:48:42 +0530 Subject: [PATCH 6/8] fix search params typing in test --- .../revalidatetag-rsc/app/revalidate_via_page/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_page/page.tsx b/test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_page/page.tsx index 5bef033f211f3..319a31582f655 100644 --- a/test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_page/page.tsx +++ b/test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_page/page.tsx @@ -6,9 +6,9 @@ import { revalidateTag } from 'next/cache' const RevalidateViaPage = async ({ searchParams, }: { - searchParams: { tag: string } + searchParams: Promise<{ tag: string }> }) => { - const { tag } = searchParams + const { tag } = await searchParams revalidateTag(tag) return ( From 02cd7406ddd07236141695c2942e20ad002500c7 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Mon, 30 Sep 2024 21:01:22 +0530 Subject: [PATCH 7/8] add revalidate: false in test to have predictable behaviour during testing --- test/e2e/app-dir/revalidatetag-rsc/app/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/app-dir/revalidatetag-rsc/app/page.tsx b/test/e2e/app-dir/revalidatetag-rsc/app/page.tsx index 0c07e4da8cb14..e4ced2724de32 100644 --- a/test/e2e/app-dir/revalidatetag-rsc/app/page.tsx +++ b/test/e2e/app-dir/revalidatetag-rsc/app/page.tsx @@ -7,6 +7,7 @@ export default async function Page() { { next: { tags: ['data'], + revalidate: false, }, } ).then((res) => res.text()) From 7f223778fe8a62d40201a63fd58b0cdd7be98288 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Mon, 30 Sep 2024 11:34:16 -0700 Subject: [PATCH 8/8] fix flakey assertion --- .../app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts b/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts index e69efc1bbcd0b..5355fb7d1c602 100644 --- a/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts +++ b/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts @@ -1,4 +1,5 @@ import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' describe('revalidateTag-rsc', () => { const { next } = nextTestSetup({ @@ -13,9 +14,11 @@ describe('revalidateTag-rsc', () => { expect(randomNumber).toEqual(randomNumber2) await browser.elementByCss('#submit-form').click() - await browser.waitForIdleNetwork() - const randomNumber3 = await browser.elementById('data').text() - expect(randomNumber3).not.toEqual(randomNumber) + + await retry(async () => { + const randomNumber3 = await browser.elementById('data').text() + expect(randomNumber3).not.toEqual(randomNumber) + }) }) it('should revalidate fetch cache if revalidateTag invoked via server component', async () => {