Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a6269c9
Node.js streams: First pass
timneutkens Feb 24, 2026
94530e9
Simplify types
timneutkens Feb 25, 2026
74647cb
Update render-result.ts
timneutkens Mar 3, 2026
940102f
Update render-result.ts
timneutkens Mar 3, 2026
5bd027f
Fix readable bug
timneutkens Mar 3, 2026
f1f23ef
Update app-render-prerender-utils.ts
timneutkens Mar 3, 2026
f7a1b78
Fix bugs
timneutkens Mar 3, 2026
5d89a32
Try fix
timneutkens Mar 3, 2026
ab2def8
Bugfixes
timneutkens Mar 3, 2026
5b239b8
Add debug channel
timneutkens Mar 4, 2026
ba8c944
Update
timneutkens Mar 4, 2026
b21cb4c
Update debug-channel-server.node.ts
timneutkens Mar 4, 2026
581ab9e
Make errorsRscStream take anystream
timneutkens Mar 4, 2026
b2ba285
Update
timneutkens Mar 4, 2026
af79cc4
Remove unused code
timneutkens Mar 4, 2026
f7222d6
Update stream-ops.node.ts
timneutkens Mar 4, 2026
91c1a30
Simplify require
timneutkens Mar 4, 2026
cb0656a
Remove confusion by removing "StreamLike" in favor of "AnyStream"
timneutkens Mar 4, 2026
d888ddf
Fix
timneutkens Mar 4, 2026
b013715
Update app-render-prerender-utils.ts
timneutkens Mar 4, 2026
8386014
Update flight-render-result.ts
timneutkens Mar 5, 2026
fa32864
Support AnyStream for debugChannel
timneutkens Mar 5, 2026
5dcaafe
Update debug-channel-server.web.ts
timneutkens Mar 5, 2026
0cab211
Update debugChannel
timneutkens Mar 5, 2026
6fa9470
Update instant-validation.tsx
timneutkens Mar 5, 2026
ec19548
Revert "Update instant-validation.tsx"
timneutkens Mar 5, 2026
5c6c498
Update debug-channel-server.web.ts
timneutkens Mar 5, 2026
e54067a
Use renderToFlightStream from stream-ops
timneutkens Mar 5, 2026
be62063
Remove typecast
timneutkens Mar 5, 2026
6ce7b91
Update instant-validation.tsx
timneutkens Mar 5, 2026
68b520a
Update app-render.tsx
timneutkens Mar 5, 2026
dfa4738
Fix bundling issue
timneutkens Mar 5, 2026
f3500db
Revert "Update app-render.tsx"
timneutkens Mar 5, 2026
dbcf50d
Update app-render.tsx
timneutkens Mar 6, 2026
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
63 changes: 48 additions & 15 deletions packages/next/src/server/app-render/app-render-prerender-utils.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,53 @@
import type { Readable } from 'node:stream'
import { InvariantError } from '../../shared/lib/invariant-error'

export type AnyStream = ReadableStream<Uint8Array> | Readable

function isWebStream(stream: AnyStream): stream is ReadableStream<Uint8Array> {
return typeof (stream as ReadableStream).tee === 'function'
}

// React's RSC prerender function will emit an incomplete flight stream when using `prerender`. If the connection
// closes then whatever hanging chunks exist will be errored. This is because prerender (an experimental feature)
// has not yet implemented a concept of resume. For now we will simulate a paused connection by wrapping the stream
// in one that doesn't close even when the underlying is complete.
export class ReactServerResult {
private _stream: null | ReadableStream<Uint8Array>
private _stream: null | AnyStream

constructor(stream: ReadableStream<Uint8Array>) {
constructor(stream: AnyStream) {
this._stream = stream
}

tee() {
tee(): AnyStream {
if (this._stream === null) {
throw new Error(
'Cannot tee a ReactServerResult that has already been consumed'
)
}
const tee = this._stream.tee()
this._stream = tee[0]
return tee[1]
if (isWebStream(this._stream)) {
const tee = this._stream.tee()
this._stream = tee[0]
return tee[1]
}

let Readable: typeof import('node:stream').Readable
if (process.env.TURBOPACK) {
Readable = (require('node:stream') as typeof import('node:stream'))
.Readable
} else {
Readable = (
__non_webpack_require__('node:stream') as typeof import('node:stream')
).Readable
}
const webStream = Readable.toWeb(this._stream) as ReadableStream<Uint8Array>
const tee = webStream.tee()
this._stream = Readable.fromWeb(
tee[0] as import('stream/web').ReadableStream
)
return Readable.fromWeb(tee[1] as import('stream/web').ReadableStream)
}

consume() {
consume(): AnyStream {
if (this._stream === null) {
throw new Error(
'Cannot consume a ReactServerResult that has already been consumed'
Expand Down Expand Up @@ -55,18 +80,26 @@ export async function createReactServerPrerenderResult(
}

export async function createReactServerPrerenderResultFromRender(
underlying: ReadableStream<Uint8Array>
underlying: AnyStream
): Promise<ReactServerPrerenderResult> {
const chunks: Array<Uint8Array> = []
const reader = underlying.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) {
break
} else {
chunks.push(value)

if (isWebStream(underlying)) {
const reader = underlying.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) {
break
} else {
chunks.push(value)
}
}
} else {
for await (const chunk of underlying) {
chunks.push(chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk))
}
}

return new ReactServerPrerenderResult(chunks)
}
export class ReactServerPrerenderResult {
Expand Down
Loading
Loading