From 4dc9aed6698238ee8d9984ebef7d2a42c4a1909c Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 15 Mar 2022 22:57:08 +0100 Subject: [PATCH 1/2] Make router able to navigate between rsc pages --- packages/next/client/index.tsx | 37 ++++++++----------- .../app/pages/index.server.js | 5 +++ .../app/pages/next-api/link.server.js | 16 +++++--- .../test/rsc.js | 21 ++++++++++- 4 files changed, 50 insertions(+), 29 deletions(-) diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 59a4b4fa10e8..f7a37e8e3263 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -707,10 +707,6 @@ if (process.env.__NEXT_RSC) { buffer.forEach((val) => { writer.write(encoder.encode(val)) }) - buffer.length = 0 - // Clean buffer but not deleting the key to mark bootstrap as complete. - // Then `nextServerDataCallback` will be safely skipped in the future renders. - serverDataBuffer.set(key, []) } serverDataWriter.set(key, writer) } @@ -754,21 +750,24 @@ if (process.env.__NEXT_RSC) { if (response) return response const bufferCacheKey = cacheKey + ',' + router.route + ',' + id - if (serverDataBuffer.has(bufferCacheKey)) { + const hasKey = serverDataBuffer.has(bufferCacheKey) + if (hasKey) { const t = new TransformStream() const writer = t.writable.getWriter() response = createFromFetch(Promise.resolve({ body: t.readable })) nextServerDataRegisterWriter(bufferCacheKey, writer) } else { - response = createFromFetch( - serialized - ? (() => { - const t = new TransformStream() - t.writable.getWriter().write(new TextEncoder().encode(serialized)) - return Promise.resolve({ body: t.readable }) - })() - : fetchFlight(getCacheKey()) - ) + const fetchPromise = serialized + ? (() => { + const t = new TransformStream() + const writer = t.writable.getWriter() + writer.ready.then(() => { + writer.write(new TextEncoder().encode(serialized)) + }) + return Promise.resolve({ body: t.readable }) + })() + : fetchFlight(getCacheKey()) + response = createFromFetch(fetchPromise) } rscCache.set(cacheKey, response) @@ -778,11 +777,9 @@ if (process.env.__NEXT_RSC) { const ServerRoot = ({ cacheKey, serialized, - _fresh, }: { cacheKey: string serialized?: string - _fresh?: boolean }) => { React.useEffect(() => { rscCache.delete(cacheKey) @@ -794,7 +791,7 @@ if (process.env.__NEXT_RSC) { RSCComponent = (props: any) => { const cacheKey = getCacheKey() - const { __flight_serialized__, __flight_fresh__ } = props + const { __flight_serialized__ } = props const [, dispatch] = useState({}) const startTransition = (React as any).startTransition const renrender = () => dispatch({}) @@ -813,11 +810,7 @@ if (process.env.__NEXT_RSC) { return ( - + ) } diff --git a/test/integration/react-streaming-and-server-components/app/pages/index.server.js b/test/integration/react-streaming-and-server-components/app/pages/index.server.js index 929f47615a2b..2ccb8a8fde27 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/index.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/index.server.js @@ -14,6 +14,11 @@ export default function Index({ header, router }) {
+
+ + next link + +
refresh diff --git a/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js b/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js index c5fdc1f1d40f..7c171c8ad4ce 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/next-api/link.server.js @@ -6,12 +6,16 @@ export default function LinkPage({ router }) { return ( <>

query:{id}

- - next id - - - go home - +
+ + next id + +
+
+ + go home + +
) } diff --git a/test/integration/react-streaming-and-server-components/test/rsc.js b/test/integration/react-streaming-and-server-components/test/rsc.js index 8b4a8185022c..cb983fa948cf 100644 --- a/test/integration/react-streaming-and-server-components/test/rsc.js +++ b/test/integration/react-streaming-and-server-components/test/rsc.js @@ -70,7 +70,10 @@ export default function (context, { runtime, env }) { it('should support next/link in server components', async () => { const linkHTML = await renderViaHTTP(context.appPort, '/next-api/link') - const linkText = getNodeBySelector(linkHTML, '#__next > a[href="/"]').text() + const linkText = getNodeBySelector( + linkHTML, + '#__next > div > a[href="/"]' + ).text() expect(linkText).toContain('go home') @@ -92,6 +95,22 @@ export default function (context, { runtime, env }) { expect(await browser.eval('window.beforeNav')).toBe(1) }) + it('should be able to navigate between rsc pages', async () => { + const browser = await webdriver(context.appPort, '/') + + await browser.waitForElementByCss('#next-link').click() + await new Promise((res) => setTimeout(res, 1000)) + expect(await browser.url()).toBe( + `http://localhost:${context.appPort}/next-api/link` + ) + await browser.waitForElementByCss('#home').click() + await new Promise((res) => setTimeout(res, 1000)) + expect(await browser.url()).toBe(`http://localhost:${context.appPort}/`) + const homeContent = await browser.elementByCss('#__next').text() + console.log('homeContent', homeContent) + expect(homeContent).toContain('component:index.server') + }) + it('should handle streaming server components correctly', async () => { const browser = await webdriver(context.appPort, '/streaming-rsc') const content = await browser.eval(`window.document.body.innerText`) From 8bd29de09883655af0a0651c773fa84d2d431b0f Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 16 Mar 2022 16:21:42 +0100 Subject: [PATCH 2/2] use single data buffer and writer --- packages/next/client/index.tsx | 54 ++++++++----------- .../app/components/nav.server.js | 23 ++++++++ .../app/pages/index.server.js | 11 +--- .../app/pages/next-api/link.server.js | 7 +-- .../app/pages/streaming-rsc.server.js | 14 +++-- .../test/rsc.js | 29 ++++++---- 6 files changed, 80 insertions(+), 58 deletions(-) create mode 100644 test/integration/react-streaming-and-server-components/app/components/nav.server.js diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index f7a37e8e3263..c00d17f7276c 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -678,47 +678,38 @@ if (process.env.__NEXT_RSC) { } = require('next/dist/compiled/react-server-dom-webpack') const encoder = new TextEncoder() - const serverDataBuffer = new Map() - const serverDataWriter = new Map() - const serverDataCacheKey = getCacheKey() + let initialServerDataBuffer: string[] | undefined = undefined + let initialServerDataWriter: WritableStreamDefaultWriter | undefined = + undefined function nextServerDataCallback(seg: [number, string, string]) { - const key = serverDataCacheKey + ',' + seg[1] if (seg[0] === 0) { - serverDataBuffer.set(key, []) + initialServerDataBuffer = [] } else { - const buffer = serverDataBuffer.get(key) - if (!buffer) + if (!initialServerDataBuffer) throw new Error('Unexpected server data: missing bootstrap script.') - const writer = serverDataWriter.get(key) - if (writer) { - writer.write(encoder.encode(seg[2])) + if (initialServerDataWriter) { + initialServerDataWriter.write(encoder.encode(seg[2])) } else { - buffer.push(seg[2]) + initialServerDataBuffer.push(seg[2]) } } } - function nextServerDataRegisterWriter( - key: string, - writer: WritableStreamDefaultWriter - ) { - const buffer = serverDataBuffer.get(key) - if (buffer) { - buffer.forEach((val) => { + function nextServerDataRegisterWriter(writer: WritableStreamDefaultWriter) { + if (initialServerDataBuffer) { + initialServerDataBuffer.forEach((val) => { writer.write(encoder.encode(val)) }) } - serverDataWriter.set(key, writer) + initialServerDataWriter = writer } // When `DOMContentLoaded`, we can close all pending writers to finish hydration. document.addEventListener( 'DOMContentLoaded', function () { - serverDataWriter.forEach((writer) => { - if (!writer.closed) { - writer.close() - } - }) + if (initialServerDataWriter && !initialServerDataWriter.closed) { + initialServerDataWriter.close() + } }, false ) @@ -744,18 +735,14 @@ if (process.env.__NEXT_RSC) { } function useServerResponse(cacheKey: string, serialized?: string) { - const id = (React as any).useId() - let response = rscCache.get(cacheKey) if (response) return response - const bufferCacheKey = cacheKey + ',' + router.route + ',' + id - const hasKey = serverDataBuffer.has(bufferCacheKey) - if (hasKey) { + if (initialServerDataBuffer) { const t = new TransformStream() const writer = t.writable.getWriter() response = createFromFetch(Promise.resolve({ body: t.readable })) - nextServerDataRegisterWriter(bufferCacheKey, writer) + nextServerDataRegisterWriter(writer) } else { const fetchPromise = serialized ? (() => { @@ -784,6 +771,9 @@ if (process.env.__NEXT_RSC) { React.useEffect(() => { rscCache.delete(cacheKey) }) + React.useEffect(() => { + initialServerDataBuffer = undefined + }, []) const response = useServerResponse(cacheKey, serialized) const root = response.readRoot() return root @@ -794,7 +784,7 @@ if (process.env.__NEXT_RSC) { const { __flight_serialized__ } = props const [, dispatch] = useState({}) const startTransition = (React as any).startTransition - const renrender = () => dispatch({}) + const rerender = () => dispatch({}) // If there is no cache, or there is serialized data already function refreshCache(nextProps: any) { startTransition(() => { @@ -804,7 +794,7 @@ if (process.env.__NEXT_RSC) { ) rscCache.set(currentCacheKey, response) - renrender() + rerender() }) } diff --git a/test/integration/react-streaming-and-server-components/app/components/nav.server.js b/test/integration/react-streaming-and-server-components/app/components/nav.server.js new file mode 100644 index 000000000000..23fb83d933a3 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/app/components/nav.server.js @@ -0,0 +1,23 @@ +import Link from 'next/link' + +export default function Nav() { + return ( + <> +
+ + next link + +
+
+ + streaming rsc + +
+
+ + home + +
+ + ) +} diff --git a/test/integration/react-streaming-and-server-components/app/pages/index.server.js b/test/integration/react-streaming-and-server-components/app/pages/index.server.js index 2ccb8a8fde27..4bbf05c55236 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/index.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/index.server.js @@ -1,5 +1,5 @@ import Foo from '../components/foo.client' -import Link from 'next/link' +import Nav from '../components/nav.server' const envVar = process.env.ENV_VAR_TEST const headerKey = 'x-next-test-client' @@ -14,14 +14,7 @@ export default function Index({ header, router }) {
-
- - next link - -
- - refresh - +