Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Node.js Stream support #65704

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
19a57c5
implement chainStreams
Ethan-Arrowood Apr 23, 2024
03dafeb
Implement createBufferedTransformStream
Ethan-Arrowood May 10, 2024
d2d4755
clean up implementation
Ethan-Arrowood May 10, 2024
133c1ef
fix up test
Ethan-Arrowood May 10, 2024
b29ee52
use uint8array over buffer
Ethan-Arrowood May 10, 2024
fb389c9
fix createBufferedTransformStream. add equivalent test for edge
Ethan-Arrowood May 11, 2024
9829431
Implement insertion stream utilities for Node.js Streams
Ethan-Arrowood May 10, 2024
9b1b1b3
rewrite without promises
Ethan-Arrowood May 11, 2024
733cf2e
temp
Ethan-Arrowood May 10, 2024
ff8e559
empty commit
Ethan-Arrowood May 13, 2024
b9602a4
Merge branch '04-23-implement_chainStreams' into implement-stream-utils
Ethan-Arrowood May 13, 2024
a5e6a19
Merge branch '05-10-Implement_createBufferedTransformStream' into imp…
Ethan-Arrowood May 13, 2024
b6b990e
Merge branch '05-10-Implement_insertion_stream_utilities_for_Node.js_…
Ethan-Arrowood May 13, 2024
a82a445
clean up insertion stream methods
Ethan-Arrowood May 13, 2024
9c50e87
finish sub methods
Ethan-Arrowood May 13, 2024
7f158e8
continueFizzStream
Ethan-Arrowood May 13, 2024
2aa40d4
getting there
Ethan-Arrowood May 13, 2024
ebe1d38
integrate new stream handling with app-render
Ethan-Arrowood May 13, 2024
3ee51ad
try to get inlinedDataStream working
Ethan-Arrowood May 13, 2024
7622989
Merge branch 'experimental-node-streams-support' into implement-strea…
Ethan-Arrowood May 14, 2024
0919c0b
add entry-base muxer
Ethan-Arrowood May 14, 2024
92e9d70
another day another muxing
Ethan-Arrowood May 14, 2024
cbf1da1
ughh
Ethan-Arrowood May 15, 2024
b210924
Merge branch 'experimental-node-streams-support' into implement-strea…
Ethan-Arrowood May 15, 2024
6bd5cf1
resolve inlinedDataStream issue
Ethan-Arrowood May 15, 2024
245cd8b
include new changes
Ethan-Arrowood May 16, 2024
e5e4925
commit before rebase
Ethan-Arrowood May 16, 2024
d725490
Merge branch 'experimental-node-streams-support' into implement-strea…
Ethan-Arrowood May 16, 2024
a248ecc
use uint8array
Ethan-Arrowood May 16, 2024
20ad2dd
fix imports in entry-base
Ethan-Arrowood May 16, 2024
7f6a264
temp
Ethan-Arrowood May 20, 2024
a32a1f2
Fix issues with buffer conversion
Ethan-Arrowood May 22, 2024
2412a0f
convert use-flight-response and try to tee Readable
Ethan-Arrowood May 23, 2024
821046b
Revert "Fix broken HTML inlining of non UTF-8 decodable binary data f…
Ethan-Arrowood May 28, 2024
0525daf
Merge branch 'experimental-node-streams-support' into implement-strea…
Ethan-Arrowood May 28, 2024
13ccc8d
fix types
Ethan-Arrowood May 28, 2024
193ff9a
Merge branch 'experimental-node-streams-support' into implement-strea…
Ethan-Arrowood May 28, 2024
6d02109
remove dead code
Ethan-Arrowood May 29, 2024
90544c7
Merge branch 'experimental-node-streams-support' into implement-strea…
Ethan-Arrowood May 29, 2024
513582e
fixes sorta
Ethan-Arrowood May 31, 2024
d700637
progress sorta
Ethan-Arrowood Jun 4, 2024
d0285d0
remove some unused code
Ethan-Arrowood Jun 5, 2024
44b6205
Merge branch 'experimental-node-streams-support' of github.com:vercel…
Ethan-Arrowood Jun 5, 2024
c25c7bd
fix build issue
Ethan-Arrowood Jun 5, 2024
fa718a7
Merge branch 'experimental-node-streams-support' of github.com:vercel…
Ethan-Arrowood Jun 10, 2024
de302bd
add to entrypoint
Ethan-Arrowood Jun 10, 2024
f74cfcb
webpack gr
Ethan-Arrowood Jun 11, 2024
d2d71d0
Merge branch 'experimental-node-streams-support' into implement-strea…
Ethan-Arrowood Jun 13, 2024
a021577
Merge branch 'experimental-node-streams-support' into implement-strea…
Ethan-Arrowood Jun 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/next/src/build/create-compiler-aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,11 @@ export function createRSCAliases(
'react-dom/server.browser$': `next/dist/build/webpack/alias/react-dom-server-browser${bundledReactChannel}.js`,
'react-dom/server.node$': `next/dist/build/webpack/alias/react-dom-server-browser${bundledReactChannel}.js`,
// react-server-dom-webpack alias
'react-server-dom-webpack/client$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/client`,
'react-server-dom-webpack/client.edge$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/client.edge`,
'react-server-dom-webpack/client.node$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/client.node`,
'react-server-dom-webpack/server.edge$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/server.edge`,
'react-server-dom-webpack/server.node$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/server.node`,
...createRSCRendererAliases(bundledReactChannel),
}

Expand Down
8 changes: 4 additions & 4 deletions packages/next/src/server/app-render/action-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ function limitUntrustedHeaderValueForLogs(value: string) {
return value.length > 100 ? value.slice(0, 100) + '...' : value
}

type ServerModuleMap = Record<
export type ServerModuleMap = Record<
string,
| {
id: string
Expand Down Expand Up @@ -609,7 +609,7 @@ export async function handleAction({
// TODO-APP: Add streaming support
const formData = await req.request.formData()
if (isFetchAction) {
bound = await decodeReply(formData, serverModuleMap)
bound = await decodeReply<any[]>(formData, serverModuleMap)
} else {
const action = await decodeAction(formData, serverModuleMap)
if (typeof action === 'function') {
Expand Down Expand Up @@ -648,9 +648,9 @@ export async function handleAction({

if (isURLEncodedAction) {
const formData = formDataFromSearchQueryString(actionData)
bound = await decodeReply(formData, serverModuleMap)
bound = await decodeReply<any[]>(formData, serverModuleMap)
} else {
bound = await decodeReply(actionData, serverModuleMap)
bound = await decodeReply<any[]>(actionData, serverModuleMap)
}
}
} else if (
Expand Down
150 changes: 97 additions & 53 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ import { isNodeNextRequest } from '../base-http/helpers'
import { HeadersAdapter } from '../web/spec-extension/adapters/headers'
import { parseParameter } from '../../shared/lib/router/utils/route-regex'
import { parseRelativeUrl } from '../../shared/lib/router/utils/parse-relative-url'
import type { Readable } from 'stream'

export type GetDynamicParamFromSegment = (
// [slug] / [[slug]] / [...slug]
Expand Down Expand Up @@ -315,7 +316,7 @@ async function generateFlight(
const {
componentMod: {
tree: loaderTree,
renderToReadableStream,
renderToStream,
createDynamicallyTrackedSearchParams,
},
getDynamicParamFromSegment,
Expand Down Expand Up @@ -362,18 +363,35 @@ async function generateFlight(

// For app dir, use the bundled version of Flight server renderer (renderToReadableStream)
// which contains the subset React.
const flightReadableStream = renderToReadableStream(
const flightReadableStream = renderToStream(
options
? [options.actionResult, buildIdFlightDataPair]
: buildIdFlightDataPair,
ctx.clientReferenceManifest.clientModules,
{
onError: ctx.flightDataRendererErrorHandler,
// @ts-expect-error This `renderToStream` wraps the `renderToReadableStream` or `renderToPipeableStream` from `react-server-dom-webpack` which doesn't specify a `nonce` prop on either options object. Leaving it in in case some other method is being used here.
nonce: ctx.nonce,
}
)

return new FlightRenderResult(flightReadableStream)
let resultStream: Readable | ReadableStream<Uint8Array>
if (
process.env.NEXT_RUNTIME === 'nodejs' &&
!(flightReadableStream instanceof ReadableStream)
) {
const { PassThrough } =
require('node:stream') as typeof import('node:stream')
resultStream = flightReadableStream.pipe(new PassThrough())
} else if (!(flightReadableStream instanceof ReadableStream)) {
throw new Error(
'Invariant. Stream is not a ReadableStream in non-Node.js runtime.'
)
} else {
resultStream = flightReadableStream
}

return new FlightRenderResult(resultStream)
}

type RenderToStreamResult = {
Expand Down Expand Up @@ -598,13 +616,13 @@ function ReactServerEntrypoint<T>({
clientReferenceManifest,
nonce,
}: {
reactServerStream: BinaryStreamOf<T>
reactServerStream: Readable | BinaryStreamOf<T>
preinitScripts: () => void
clientReferenceManifest: NonNullable<RenderOpts['clientReferenceManifest']>
nonce?: string
}): T {
preinitScripts()
const response = useFlightStream(
const response = useFlightStream<T>(
reactServerStream,
clientReferenceManifest,
nonce
Expand Down Expand Up @@ -954,17 +972,29 @@ async function renderToHTMLOrFlightImpl(
// We kick off the Flight Request (render) here. It is ok to initiate the render in an arbitrary
// place however it is critical that we only construct the Flight Response inside the SSR
// render so that directives like preloads are correctly piped through
const serverStream = ComponentMod.renderToReadableStream(
const serverStream = ComponentMod.renderToStream(
<ReactServerApp tree={tree} ctx={ctx} asNotFound={asNotFound} />,
clientReferenceManifest.clientModules,
{
onError: serverComponentsErrorHandler,
// @ts-expect-error This `renderToStream` wraps the `renderToReadableStream` or `renderToPipeableStream` from `react-server-dom-webpack` which doesn't specify a `nonce` prop on either options object. Leaving it in in case some other method is being used here.
nonce,
}
)

// We are going to consume this render both for SSR and for inlining the flight data
let [renderStream, dataStream] = serverStream.tee()
let renderStream, dataStream

if (
process.env.NEXT_RUNTIME === 'nodejs' &&
!(serverStream instanceof ReadableStream)
) {
const { teeReadable } = require('../stream-utils')
;[renderStream, dataStream] = teeReadable(serverStream)
} else {
// We are going to consume this render both for SSR and for inlining the flight data
// @ts-ignore
;[renderStream, dataStream] = serverStream.tee()
}

const children = (
<HeadManagerContext.Provider
Expand Down Expand Up @@ -1037,19 +1067,6 @@ async function renderToHTMLOrFlightImpl(
try {
let { stream, postponed, resumed } = await renderer.render(children)

if (
process.env.NEXT_RUNTIME === 'nodejs' &&
!(stream instanceof ReadableStream)
) {
const { Readable } = require('node:stream')
stream = Readable.toWeb(stream) as ReadableStream<Uint8Array>
}

// TODO (@Ethan-Arrowood): Remove this when stream utilities support both stream types.
if (!(stream instanceof ReadableStream)) {
throw new Error("Invariant: stream isn't a ReadableStream")
}

const prerenderState = staticGenerationStore.prerenderState
if (prerenderState) {
/**
Expand Down Expand Up @@ -1085,14 +1102,28 @@ async function renderToHTMLOrFlightImpl(
// It is possible in the set of stream transforms for Dynamic HTML vs Dynamic Data may differ but currently both states
// require the same set so we unify the code path here
return {
// @ts-ignore
stream: await continueDynamicPrerender(stream, {
getServerInsertedHTML,
}),
}
} else {
// We may still be rendering the RSC stream even though the HTML is finished.
// We wait for the RSC stream to complete and check again if dynamic was used
const [original, flightSpy] = dataStream.tee()
let original, flightSpy

if (
process.env.NEXT_RUNTIME === 'nodejs' &&
!(dataStream instanceof ReadableStream)
) {
const { teeReadable } = require('../stream-utils')
;[original, flightSpy] = teeReadable(dataStream)
} else {
// We are going to consume this render both for SSR and for inlining the flight data
// @ts-ignore
;[original, flightSpy] = dataStream.tee()
}

dataStream = original

await flightRenderComplete(flightSpy)
Expand All @@ -1115,6 +1146,7 @@ async function renderToHTMLOrFlightImpl(
// It is possible in the set of stream transforms for Dynamic HTML vs Dynamic Data may differ but currently both states
// require the same set so we unify the code path here
return {
// @ts-ignore
stream: await continueDynamicPrerender(stream, {
getServerInsertedHTML,
}),
Expand Down Expand Up @@ -1148,7 +1180,14 @@ async function renderToHTMLOrFlightImpl(

// We don't actually want to render anything so we just pass a stream
// that never resolves. The resume call is going to abort immediately anyway
const foreverStream = new ReadableStream<Uint8Array>()
let foreverStream

if (process.env.NEXT_RUNTIME === 'nodejs') {
const { Readable } = require('node:stream')
foreverStream = new Readable()
} else {
foreverStream = new ReadableStream<Uint8Array>()
}

const resumeChildren = (
<HeadManagerContext.Provider
Expand All @@ -1171,37 +1210,42 @@ async function renderToHTMLOrFlightImpl(
const { stream: resumeStream } =
await resumeRenderer.render(resumeChildren)

// FIXME: shouldn't need this when chainStreams supports ReadableStream | Readable
if (!(resumeStream instanceof ReadableStream)) {
throw new Error("Invariant: stream wasn't a ReadableStream")
}
// First we write everything from the prerender, then we write everything from the aborted resume render
renderedHTMLStream = chainStreams(stream, resumeStream)
renderedHTMLStream = chainStreams(
// @ts-ignore
stream,
resumeStream
)
}

let inlinedDataStream = createInlinedDataReadableStream(
dataStream,
nonce,
formState
)

return {
stream: await continueStaticPrerender(renderedHTMLStream, {
inlinedDataStream: createInlinedDataReadableStream(
dataStream,
nonce,
formState
),
// @ts-ignore
inlinedDataStream,
getServerInsertedHTML,
}),
}
}
}
} else if (renderOpts.postponed) {
// This is a continuation of either an Incomplete or Dynamic Data Prerender.
const inlinedDataStream = createInlinedDataReadableStream(
let inlinedDataStream = createInlinedDataReadableStream(
dataStream,
nonce,
formState
)

if (resumed) {
// We have new HTML to stream and we also need to include server inserted HTML
return {
stream: await continueDynamicHTMLResume(stream, {
// @ts-ignore
inlinedDataStream,
getServerInsertedHTML,
}),
Expand All @@ -1210,6 +1254,7 @@ async function renderToHTMLOrFlightImpl(
// We are continuing a Dynamic Data Prerender and simply need to append new inlined flight data
return {
stream: await continueDynamicDataResume(stream, {
// @ts-ignore
inlinedDataStream,
}),
}
Expand Down Expand Up @@ -1308,20 +1353,35 @@ async function renderToHTMLOrFlightImpl(
nonce
)

const errorServerStream = ComponentMod.renderToReadableStream(
const errorServerStream = ComponentMod.renderToStream(
<ReactServerError tree={tree} ctx={ctx} errorType={errorType} />,
clientReferenceManifest.clientModules,
{
onError: serverComponentsErrorHandler,
// @ts-expect-error This `renderToStream` wraps the `renderToReadableStream` or `renderToPipeableStream` from `react-server-dom-webpack` which doesn't specify a `nonce` prop on either options object. Leaving it in in case some other method is being used here.
nonce,
}
)

let resultStream2: Readable | ReadableStream<Uint8Array>
if (
process.env.NEXT_RUNTIME === 'nodejs' &&
!(errorServerStream instanceof ReadableStream)
) {
const { PassThrough } =
require('node:stream') as typeof import('node:stream')
resultStream2 = errorServerStream.pipe(new PassThrough())
} else if (!(errorServerStream instanceof ReadableStream)) {
throw new Error('Invariant. Stream is not ReadableStream')
} else {
resultStream2 = errorServerStream
}

try {
let fizzStream = await renderToInitialFizzStream({
element: (
<ReactServerEntrypoint
reactServerStream={errorServerStream}
reactServerStream={resultStream2}
preinitScripts={errorPreinitScripts}
clientReferenceManifest={clientReferenceManifest}
nonce={nonce}
Expand All @@ -1335,22 +1395,6 @@ async function renderToHTMLOrFlightImpl(
},
})

if (
process.env.NEXT_RUNTIME === 'nodejs' &&
!(fizzStream instanceof ReadableStream)
) {
const { Readable } = require('node:stream')

fizzStream = Readable.toWeb(
fizzStream
) as ReadableStream<Uint8Array>
}

// TODO (@Ethan-Arrowood): Remove this when stream utilities support both stream types.
if (!(fizzStream instanceof ReadableStream)) {
throw new Error("Invariant: stream isn't a ReadableStream")
}

return {
// Returning the error that was thrown so it can be used to handle
// the response in the caller.
Expand All @@ -1369,8 +1413,8 @@ async function renderToHTMLOrFlightImpl(
polyfills,
renderServerInsertedHTML,
serverCapturedErrors: [],
tracingMetadata: undefined,
basePath: renderOpts.basePath,
tracingMetadata: tracingMetadata,
}),
serverInsertedHTMLToHead: true,
validateRootLayout,
Expand Down
7 changes: 3 additions & 4 deletions packages/next/src/server/app-render/entry-base.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// eslint-disable-next-line import/no-extraneous-dependencies
export {
renderToReadableStream,
decodeReply,
renderToStream,
decodeAction,
decodeFormState,
} from 'react-server-dom-webpack/server.edge'
decodeReply,
} from './react-server-dom-webpack'

import AppRouter from '../../client/components/app-router'
import LayoutRouter from '../../client/components/layout-router'
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/server/app-render/flight-render-result.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { Readable } from 'node:stream'
import { RSC_CONTENT_TYPE_HEADER } from '../../client/components/app-router-headers'
import RenderResult from '../render-result'

/**
* Flight Response is always set to RSC_CONTENT_TYPE_HEADER to ensure it does not get interpreted as HTML.
*/
export class FlightRenderResult extends RenderResult {
constructor(response: string | ReadableStream<Uint8Array>) {
constructor(response: string | ReadableStream<Uint8Array> | Readable) {
super(response, { contentType: RSC_CONTENT_TYPE_HEADER, metadata: {} })
}
}
Loading
Loading