Skip to content

Commit

Permalink
use single data buffer and writer
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi committed Mar 16, 2022
1 parent 4dc9aed commit 8bd29de
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 58 deletions.
54 changes: 22 additions & 32 deletions packages/next/client/index.tsx
Expand Up @@ -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<string, string[]>()
const serverDataWriter = new Map<string, WritableStreamDefaultWriter>()
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
)
Expand All @@ -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
? (() => {
Expand Down Expand Up @@ -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
Expand All @@ -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(() => {
Expand All @@ -804,7 +794,7 @@ if (process.env.__NEXT_RSC) {
)

rscCache.set(currentCacheKey, response)
renrender()
rerender()
})
}

Expand Down
@@ -0,0 +1,23 @@
import Link from 'next/link'

export default function Nav() {
return (
<>
<div>
<Link href={'/next-api/link'}>
<a id="goto-next-link">next link</a>
</Link>
</div>
<div>
<Link href={'/streaming-rsc'}>
<a id="goto-streaming-rsc">streaming rsc</a>
</Link>
</div>
<div>
<Link href={'/'}>
<a id="goto-home">home</a>
</Link>
</div>
</>
)
}
@@ -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'
Expand All @@ -14,14 +14,7 @@ export default function Index({ header, router }) {
<div>
<Foo />
</div>
<div>
<Link href={'/next-api/link'}>
<a id="next-link">next link</a>
</Link>
</div>
<Link href={'/'}>
<a id="refresh">refresh</a>
</Link>
<Nav />
</div>
)
}
Expand Down
@@ -1,4 +1,5 @@
import Link from 'next/link'
import Nav from '../../components/nav.server'

export default function LinkPage({ router }) {
const { query } = router
Expand All @@ -11,11 +12,7 @@ export default function LinkPage({ router }) {
<a id="next_id">next id</a>
</Link>
</div>
<div>
<Link href={`/`}>
<a id="home">go home</a>
</Link>
</div>
<Nav />
</>
)
}
Expand Down
@@ -1,4 +1,5 @@
import { Suspense } from 'react'
import Nav from '../components/nav.server'

let result
let promise
Expand All @@ -16,9 +17,16 @@ function Data() {

export default function Page() {
return (
<Suspense fallback="next_streaming_fallback">
<Data />
</Suspense>
<div>
<div id="content">
<Suspense fallback="next_streaming_fallback">
<Data />
</Suspense>
</div>
<div>
<Nav />
</div>
</div>
)
}

Expand Down
29 changes: 20 additions & 9 deletions test/integration/react-streaming-and-server-components/test/rsc.js
Expand Up @@ -75,7 +75,7 @@ export default function (context, { runtime, env }) {
'#__next > div > a[href="/"]'
).text()

expect(linkText).toContain('go home')
expect(linkText).toContain('home')

const browser = await webdriver(context.appPort, '/next-api/link')

Expand All @@ -96,24 +96,35 @@ export default function (context, { runtime, env }) {
})

it('should be able to navigate between rsc pages', async () => {
let content
const browser = await webdriver(context.appPort, '/')

await browser.waitForElementByCss('#next-link').click()
await browser.waitForElementByCss('#goto-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 browser.waitForElementByCss('#goto-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')
content = await browser.elementByCss('#__next').text()
expect(content).toContain('component:index.server')

await browser.waitForElementByCss('#goto-streaming-rsc').click()
await new Promise((res) => setTimeout(res, 1500))
expect(await browser.url()).toBe(
`http://localhost:${context.appPort}/streaming-rsc`
)

content = await browser.elementByCss('#content').text()
expect(content).toContain('next_streaming_data')
})

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`)
const content = await browser.eval(
`document.querySelector('#content').innerText`
)
expect(content).toMatchInlineSnapshot('"next_streaming_data"')
})

Expand All @@ -132,7 +143,7 @@ export default function (context, { runtime, env }) {

it('should refresh correctly with next/link', async () => {
// Select the button which is not hidden but rendered
const selector = '#__next #refresh'
const selector = '#__next #goto-next-link'
let hasFlightRequest = false
const browser = await webdriver(context.appPort, '/', {
beforePageLoad(page) {
Expand All @@ -157,7 +168,7 @@ export default function (context, { runtime, env }) {
expect(hasFlightRequest).toBe(true)
}
const refreshText = await browser.elementByCss(selector).text()
expect(refreshText).toBe('refresh')
expect(refreshText).toBe('next link')
})

if (env === 'dev') {
Expand Down

0 comments on commit 8bd29de

Please sign in to comment.