Skip to content

Commit

Permalink
convert use-flight-response and try to tee Readable
Browse files Browse the repository at this point in the history
  • Loading branch information
Ethan-Arrowood committed May 23, 2024
1 parent a32a1f2 commit 5c92958
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 18 deletions.
2 changes: 1 addition & 1 deletion packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ function ReactServerEntrypoint<T>({
nonce?: string
}): T {
preinitScripts()
const response = useFlightStream(
const response = useFlightStream<T>(
reactServerStream,
clientReferenceManifest,
nonce
Expand Down
29 changes: 29 additions & 0 deletions packages/next/src/server/app-render/use-flight-response/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Readable } from 'node:stream'
import type { DeepReadonly } from '../../../shared/lib/deep-readonly'
import type { ClientReferenceManifest } from '../../../build/webpack/plugins/flight-manifest-plugin'

export function useFlightStream<T>(
flightStream: Readable | ReadableStream<Uint8Array>,
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>,
nonce?: string
): Promise<T>

export function flightRenderComplete(
flightStream: Readable | ReadableStream<Uint8Array>
): Promise<void>

export function createInlinedDataReadableStream(
flightStream: Readable,
nonce: string | undefined,
formState: unknown | null
): Readable
export function createInlinedDataReadableStream(
flightStream: ReadableStream<Uint8Array>,
nonce: string | undefined,
formState: unknown | null
): ReadableStream<Uint8Array>
export function createInlinedDataReadableStream(
flightStream: Readable | ReadableStream<Uint8Array>,
nonce: string | undefined,
formState: unknown | null
): Readable | ReadableStream<Uint8Array>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
if (
process.env.NEXT_RUNTIME === 'nodejs' &&
process.env.EXPERIMENTAL_NODE_STREAMS_SUPPORT === '1'
) {
module.exports = require('./use-flight-response.node.js')
} else {
module.exports = require('./use-flight-response.edge.js')
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin'
import type { BinaryStreamOf } from './app-render'
import type { ClientReferenceManifest } from '../../../build/webpack/plugins/flight-manifest-plugin'
import type { BinaryStreamOf } from '../app-render'

import { htmlEscapeJsonString } from '../htmlescape'
import type { DeepReadonly } from '../../shared/lib/deep-readonly'
import { htmlEscapeJsonString } from '../../htmlescape'
import type { DeepReadonly } from '../../../shared/lib/deep-readonly'
import type { Readable } from 'node:stream'

const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'
Expand All @@ -23,7 +23,7 @@ const encoder = new TextEncoder()
* This is only used for renderToHTML, the Flight response does not need additional wrappers.
*/
export function useFlightStream<T>(
flightStream: Readable | BinaryStreamOf<T>,
flightStream: BinaryStreamOf<T>,
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>,
nonce?: string
): Promise<T> {
Expand All @@ -38,20 +38,12 @@ export function useFlightStream<T>(
// @TODO: investigate why the aliasing for turbopack doesn't pick this up, requiring this runtime check
if (process.env.TURBOPACK) {
createFromStream =
flightStream instanceof ReadableStream
? // eslint-disable-next-line import/no-extraneous-dependencies
require('react-server-dom-turbopack/client.edge')
.createFromReadableStream
: // eslint-disable-next-line import/no-extraneous-dependencies
require('react-server-dom-turbopack/client.node').createFromNodeStream
// eslint-disable-next-line import/no-extraneous-dependencies
require('react-server-dom-turbopack/client.edge').createFromReadableStream
} else {
createFromStream =
flightStream instanceof ReadableStream
? // eslint-disable-next-line import/no-extraneous-dependencies
require('react-server-dom-webpack/client.edge')
.createFromReadableStream
: // eslint-disable-next-line import/no-extraneous-dependencies
require('react-server-dom-webpack/client.node').createFromNodeStream
// eslint-disable-next-line import/no-extraneous-dependencies
require('react-server-dom-webpack/client.edge').createFromReadableStream
}

const newResponse = createFromStream(flightStream, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { Readable } from 'node:stream'
import type { ClientReferenceManifest } from '../../../build/webpack/plugins/flight-manifest-plugin'
import type { DeepReadonly } from '../../../shared/lib/deep-readonly'
import { htmlEscapeJsonString } from '../../htmlescape'
import isError from '../../../lib/is-error'

const flightResponses = new WeakMap<Readable, Promise<any>>()
const encoder = new TextEncoder()

export function useFlightStream<T>(
flightStream: Readable,
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>,
nonce?: string
): Promise<T> {
const response = flightResponses.get(flightStream)

if (response) {
return response
}

// react-server-dom-webpack/client.edge must not be hoisted for require cache clearing to work correctly
let createFromStream
// @TODO: investigate why the aliasing for turbopack doesn't pick this up, requiring this runtime check
if (process.env.TURBOPACK) {
createFromStream =
// eslint-disable-next-line import/no-extraneous-dependencies
require('react-server-dom-turbopack/client.node').createFromNodeStream
} else {
createFromStream =
// eslint-disable-next-line import/no-extraneous-dependencies
require('react-server-dom-webpack/client.node').createFromNodeStream
}

const newResponse = createFromStream(flightStream, {
ssrManifest: {
moduleLoading: clientReferenceManifest.moduleLoading,
moduleMap: clientReferenceManifest.ssrModuleMapping,
},
nonce,
})

flightResponses.set(flightStream, newResponse)

return newResponse
}

export function flightRenderComplete(flightStream: Readable): Promise<void> {
return new Promise((resolve, reject) => {
flightStream
.resume()
.on('end', () => {
resolve()
})
.on('error', (error) => {
reject(error)
})
})
}

const INLINE_FLIGHT_PAYLOAD_BOOTSTRAP = 0
const INLINE_FLIGHT_PAYLOAD_DATA = 1
const INLINE_FLIGHT_PAYLOAD_FORM_STATE = 2
const INLINE_FLIGHT_PAYLOAD_BINARY = 3

export function createInlinedDataReadableStream(
flightStream: Readable,
nonce: string | undefined,
formState: unknown | null
): Readable {
const startScriptTag = nonce
? `<script nonce=${JSON.stringify(nonce)}>`
: '<script>'

const decoder = new TextDecoder('utf-8', { fatal: true })

if (flightStream.readableFlowing) {
flightStream.pause()
}

return new Readable({
construct(callback) {
try {
const chunk = encoder.encode(
`${startScriptTag}(self.__next_f=self.__next_f||[]).push(${htmlEscapeJsonString(
JSON.stringify([INLINE_FLIGHT_PAYLOAD_BOOTSTRAP])
)});self.__next_f.push(${htmlEscapeJsonString(
JSON.stringify([INLINE_FLIGHT_PAYLOAD_FORM_STATE, formState])
)})</script>`
)
this.push(chunk)
return callback(null)
} catch (error) {
return isError(error) ? callback(error) : callback()
}
},
read() {
try {
const chunk = flightStream.read()
if (chunk) {
try {
const decodedString = decoder.decode(chunk, { stream: true })
writeFlightDataInstruction(this, startScriptTag, decodedString)
} catch {
writeFlightDataInstruction(this, startScriptTag, chunk)
}
} else {
try {
const decodedString = decoder.decode()
if (decodedString) {
writeFlightDataInstruction(this, startScriptTag, decodedString)
}
} catch {}

this.push(null)
}
} catch (error) {
if (isError(error)) {
this.destroy(error)
}
}
},
})
}

function writeFlightDataInstruction(
readable: Readable,
scriptStart: string,
chunk: string | Uint8Array
) {
let htmlInlinedData: string

if (typeof chunk === 'string') {
htmlInlinedData = htmlEscapeJsonString(
JSON.stringify([INLINE_FLIGHT_PAYLOAD_DATA, chunk])
)
} else {
// The chunk cannot be embedded as a UTF-8 string in the script tag.
// Instead let's inline it in base64.
// Credits to Devon Govett (devongovett) for the technique.
// https://github.com/devongovett/rsc-html-stream
const base64 = btoa(String.fromCodePoint(...chunk))
htmlInlinedData = htmlEscapeJsonString(
JSON.stringify([INLINE_FLIGHT_PAYLOAD_BINARY, base64])
)
}

readable.push(
encoder.encode(
`${scriptStart}self.__next_f.push(${htmlInlinedData})</script>`
)
)
}

0 comments on commit 5c92958

Please sign in to comment.