From 2337ad056bf4eff422c8e7e5790b24e583af4831 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 6 Feb 2026 16:30:26 +0100 Subject: [PATCH 1/2] [Flight] Add `allowPartialStream` option to Flight Client When using a partial prerender stream, i.e. a prerender that is intentionally aborted before all I/O has resolved, consumers of `createFromReadableStream` would need to keep the stream unclosed to prevent React Flight from erroring on unresolved chunks. However, some browsers (e.g. Chrome, Firefox) keep unclosed ReadableStreams with pending reads as native GC roots, retaining the entire Flight response. With this PR we're adding an `allowPartialStream` option, that allows consumers to close the stream normally. The Flight Client's `close()` function then transitions pending chunks to halted instead of erroring them. Halted chunks keep Suspense fallbacks showing (i.e. they never resolve), and their `.then()` is a no-op so no new listeners accumulate. Inner stream chunks (ReadableStream/AsyncIterable) are closed gracefully, and `getChunk()` returns halted chunks for new IDs that are accessed after closing the response. Blocked chunks are left alone because they may be waiting on client-side async operations like module loading, or on forward references to chunks that appeared later in the stream, both of which resolve independently of closing. --- .../react-client/src/ReactFlightClient.js | 65 ++++++- .../react-markup/src/ReactMarkupServer.js | 1 + .../src/ReactNoopFlightClient.js | 1 + .../src/client/ReactFlightDOMClientBrowser.js | 2 + .../src/client/ReactFlightDOMClientNode.js | 2 + .../src/client/ReactFlightDOMClientBrowser.js | 2 + .../src/client/ReactFlightDOMClientEdge.js | 2 + .../src/client/ReactFlightDOMClientNode.js | 2 + .../src/client/ReactFlightDOMClientBrowser.js | 2 + .../src/client/ReactFlightDOMClientEdge.js | 2 + .../src/client/ReactFlightDOMClientNode.js | 2 + .../src/client/ReactFlightDOMClientEdge.js | 2 + .../src/client/ReactFlightDOMClientNode.js | 2 + .../__tests__/ReactFlightDOMBrowser-test.js | 169 +++++++++++++++++- .../src/client/ReactFlightDOMClientBrowser.js | 2 + .../src/client/ReactFlightDOMClientEdge.js | 2 + .../src/client/ReactFlightDOMClientNode.js | 2 + 17 files changed, 252 insertions(+), 10 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 80673006eadb..20aa8ce8f9a3 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -359,6 +359,7 @@ type Response = { _stringDecoder: StringDecoder, _closed: boolean, _closedReason: mixed, + _allowPartialStream: boolean, _tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from _timeOrigin: number, // Profiling-only _pendingInitialRender: null | TimeoutID, // Profiling-only, @@ -1456,9 +1457,19 @@ function getChunk(response: Response, id: number): SomeChunk { let chunk = chunks.get(id); if (!chunk) { if (response._closed) { - // We have already errored the response and we're not going to get - // anything more streaming in so this will immediately error. - chunk = createErrorChunk(response, response._closedReason); + if (response._allowPartialStream) { + // For partial streams, chunks accessed after close should be HALTED + // (never resolve). + chunk = createPendingChunk(response); + const haltedChunk: HaltedChunk = (chunk: any); + haltedChunk.status = HALTED; + haltedChunk.value = null; + haltedChunk.reason = null; + } else { + // We have already errored the response and we're not going to get + // anything more streaming in so this will immediately error. + chunk = createErrorChunk(response, response._closedReason); + } } else { chunk = createPendingChunk(response); } @@ -2655,6 +2666,7 @@ function ResponseInstance( encodeFormAction: void | EncodeFormActionCallback, nonce: void | string, temporaryReferences: void | TemporaryReferenceSet, + allowPartialStream: boolean, findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only replayConsole: boolean, // DEV-only environmentName: void | string, // DEV-only @@ -2674,6 +2686,7 @@ function ResponseInstance( this._fromJSON = (null: any); this._closed = false; this._closedReason = null; + this._allowPartialStream = allowPartialStream; this._tempRefs = temporaryReferences; if (enableProfilerTimer && enableComponentPerformanceTrack) { this._timeOrigin = 0; @@ -2767,6 +2780,7 @@ export function createResponse( encodeFormAction: void | EncodeFormActionCallback, nonce: void | string, temporaryReferences: void | TemporaryReferenceSet, + allowPartialStream: boolean, findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only replayConsole: boolean, // DEV-only environmentName: void | string, // DEV-only @@ -2792,6 +2806,7 @@ export function createResponse( encodeFormAction, nonce, temporaryReferences, + allowPartialStream, findSourceMapURL, replayConsole, environmentName, @@ -5243,11 +5258,45 @@ function createFromJSONCallback(response: Response) { } export function close(weakResponse: WeakResponse): void { - // In case there are any remaining unresolved chunks, they won't - // be resolved now. So we need to issue an error to those. - // Ideally we should be able to early bail out if we kept a - // ref count of pending chunks. - reportGlobalError(weakResponse, new Error('Connection closed.')); + // In case there are any remaining unresolved chunks, they won't be resolved + // now. So we either error or halt them depending on whether partial streams + // are allowed. + // TODO: Ideally we should be able to bail out early if we kept a ref count of + // pending chunks. + if (hasGCedResponse(weakResponse)) { + return; + } + const response = unwrapWeakResponse(weakResponse); + if (response._allowPartialStream) { + // For partial streams, we halt pending chunks instead of erroring them. + response._closed = true; + response._chunks.forEach(chunk => { + if (chunk.status === PENDING) { + // Clear listeners to release closures and transition to HALTED. + // Future .then() calls on HALTED chunks are no-ops. + releasePendingChunk(response, chunk); + const haltedChunk: HaltedChunk = (chunk: any); + haltedChunk.status = HALTED; + haltedChunk.value = null; + haltedChunk.reason = null; + } else if (chunk.status === INITIALIZED && chunk.reason !== null) { + // Stream chunk - close gracefully instead of erroring. + chunk.reason.close('"$undefined"'); + } + }); + if (__DEV__) { + const debugChannel = response._debugChannel; + if (debugChannel !== undefined) { + closeDebugChannel(debugChannel); + response._debugChannel = undefined; + if (debugChannelRegistry !== null) { + debugChannelRegistry.unregister(response); + } + } + } + } else { + reportGlobalError(weakResponse, new Error('Connection closed.')); + } } function getCurrentOwnerInDEV(): null | ReactComponentInfo { diff --git a/packages/react-markup/src/ReactMarkupServer.js b/packages/react-markup/src/ReactMarkupServer.js index 95a5ce51c3e1..43e258bf13ef 100644 --- a/packages/react-markup/src/ReactMarkupServer.js +++ b/packages/react-markup/src/ReactMarkupServer.js @@ -89,6 +89,7 @@ export function experimental_renderToHTML( noServerCallOrFormAction, undefined, undefined, + false, undefined, false, undefined, diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js index 45edbd6f0030..a5c43bd65259 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightClient.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js @@ -71,6 +71,7 @@ function read(source: Source, options: ReadOptions): Thenable { undefined, undefined, undefined, + false, options !== undefined ? options.findSourceMapURL : undefined, true, undefined, diff --git a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js index ee2475287d03..315dae150496 100644 --- a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js @@ -49,6 +49,7 @@ export type Options = { callServer?: CallServerCallback, debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...}, temporaryReferences?: TemporaryReferenceSet, + allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -98,6 +99,7 @@ function createResponseFromOptions(options: void | Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, + options && options.allowPartialStream ? options.allowPartialStream : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js index ae11dc29bfff..b7e4ff9990cd 100644 --- a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js @@ -54,6 +54,7 @@ type EncodeFormActionCallback = ( export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, + allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -104,6 +105,7 @@ function createFromNodeStream( options ? options.encodeFormAction : undefined, options && typeof options.nonce === 'string' ? options.nonce : undefined, undefined, // TODO: If encodeReply is supported, this should support temporaryReferences + options && options.allowPartialStream ? options.allowPartialStream : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js index b304d442046d..22d287acb863 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js @@ -124,6 +124,7 @@ function createResponseFromOptions(options: void | Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, + options && options.allowPartialStream ? options.allowPartialStream : false, __DEV__ ? findSourceMapURL : undefined, __DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true __DEV__ && options && options.environmentName @@ -207,6 +208,7 @@ function startReadingFromStream( export type Options = { debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...}, temporaryReferences?: TemporaryReferenceSet, + allowPartialStream?: boolean, replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js index f58f8534348e..d3cbab637585 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js @@ -77,6 +77,7 @@ export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, temporaryReferences?: TemporaryReferenceSet, + allowPartialStream?: boolean, replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, @@ -104,6 +105,7 @@ function createResponseFromOptions(options?: Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, + options && options.allowPartialStream ? options.allowPartialStream : false, __DEV__ ? findSourceMapURL : undefined, __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false __DEV__ && options && options.environmentName diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js index e8716bdc6bb9..db93c1b72180 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js @@ -50,6 +50,7 @@ type EncodeFormActionCallback = ( export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, + allowPartialStream?: boolean, replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, @@ -97,6 +98,7 @@ export function createFromNodeStream( options ? options.encodeFormAction : undefined, options && typeof options.nonce === 'string' ? options.nonce : undefined, undefined, // TODO: If encodeReply is supported, this should support temporaryReferences + options && options.allowPartialStream ? options.allowPartialStream : false, __DEV__ ? findSourceMapURL : undefined, __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false __DEV__ && options && options.environmentName diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js index 0bf615001982..7781f62e4194 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js @@ -48,6 +48,7 @@ export type Options = { callServer?: CallServerCallback, debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...}, temporaryReferences?: TemporaryReferenceSet, + allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -97,6 +98,7 @@ function createResponseFromOptions(options: void | Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, + options && options.allowPartialStream ? options.allowPartialStream : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js index c6dd4ee94ad1..fad07766de2d 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js @@ -76,6 +76,7 @@ export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, temporaryReferences?: TemporaryReferenceSet, + allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -104,6 +105,7 @@ function createResponseFromOptions(options: Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, + options && options.allowPartialStream ? options.allowPartialStream : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js index 8863b1bf1c66..5a1c3445c576 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js @@ -57,6 +57,7 @@ type EncodeFormActionCallback = ( export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, + allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -106,6 +107,7 @@ function createFromNodeStream( options ? options.encodeFormAction : undefined, options && typeof options.nonce === 'string' ? options.nonce : undefined, undefined, // TODO: If encodeReply is supported, this should support temporaryReferences + options && options.allowPartialStream ? options.allowPartialStream : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js index 2cf668f679d3..580e2a983a07 100644 --- a/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js @@ -76,6 +76,7 @@ export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, temporaryReferences?: TemporaryReferenceSet, + allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -104,6 +105,7 @@ function createResponseFromOptions(options: Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, + options && options.allowPartialStream ? options.allowPartialStream : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js index 8863b1bf1c66..5a1c3445c576 100644 --- a/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js @@ -57,6 +57,7 @@ type EncodeFormActionCallback = ( export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, + allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -106,6 +107,7 @@ function createFromNodeStream( options ? options.encodeFormAction : undefined, options && typeof options.nonce === 'string' ? options.nonce : undefined, undefined, // TODO: If encodeReply is supported, this should support temporaryReferences + options && options.allowPartialStream ? options.allowPartialStream : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index 1399effbc1ec..6698c61a307a 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -2504,6 +2504,171 @@ describe('ReactFlightDOMBrowser', () => { expect(container.innerHTML).toBe(''); }); + it('renders Suspense fallback for unresolved promises with allowPartialStream', async () => { + let resolveGreeting; + const greetingPromise = new Promise(resolve => { + resolveGreeting = resolve; + }); + + function App() { + return ( + + + + ); + } + + async function Greeting() { + const greeting = await greetingPromise; + return greeting; + } + + const controller = new AbortController(); + const {pendingResult} = await serverAct(async () => { + return { + pendingResult: ReactServerDOMStaticServer.prerender( + , + webpackMap, + { + signal: controller.signal, + }, + ), + }; + }); + + controller.abort(); + resolveGreeting('Hello, World!'); + const {prelude} = await serverAct(() => pendingResult); + + function ClientRoot({response}) { + return use(response); + } + + const response = ReactServerDOMClient.createFromReadableStream( + passThrough(prelude), + { + allowPartialStream: true, + }, + ); + const container = document.createElement('div'); + const errors = []; + const root = ReactDOMClient.createRoot(container, { + onUncaughtError(err) { + errors.push(err); + }, + }); + + await act(() => { + root.render(); + }); + + // With `allowPartialStream`, we should see the fallback instead of a + // 'Connection closed.' error + expect(errors).toEqual([]); + expect(container.innerHTML).toBe('loading...'); + }); + + it('renders client components that are blocked on chunks with allowPartialStream', async () => { + let resolveClientComponentChunk; + + const ClientComponent = clientExports( + function ClientComponent({children}) { + return
{children}
; + }, + '42', + '/test.js', + new Promise(resolve => (resolveClientComponentChunk = resolve)), + ); + + function App() { + return Hello, World!; + } + + const controller = new AbortController(); + const {pendingResult} = await serverAct(async () => { + return { + pendingResult: ReactServerDOMStaticServer.prerender( + , + webpackMap, + { + signal: controller.signal, + }, + ), + }; + }); + + controller.abort(); + const {prelude} = await serverAct(() => pendingResult); + + function ClientRoot({response}) { + return use(response); + } + + const response = ReactServerDOMClient.createFromReadableStream( + passThrough(prelude), + { + allowPartialStream: true, + }, + ); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render(); + }); + + expect(container.innerHTML).toBe(''); + + await act(() => { + resolveClientComponentChunk(); + }); + + expect(container.innerHTML).toBe('
Hello, World!
'); + }); + + it('closes inner ReadableStreams gracefully with allowPartialStream', async () => { + let streamController; + const innerStream = new ReadableStream({ + start(c) { + streamController = c; + }, + }); + + const abortController = new AbortController(); + const {pendingResult} = await serverAct(async () => { + streamController.enqueue({hello: 'world'}); + return { + pendingResult: ReactServerDOMStaticServer.prerender( + {stream: innerStream}, + webpackMap, + { + signal: abortController.signal, + }, + ), + }; + }); + + abortController.abort(); + const {prelude} = await serverAct(() => pendingResult); + + const response = await ReactServerDOMClient.createFromReadableStream( + passThrough(prelude), + { + allowPartialStream: true, + }, + ); + + // The inner stream should be readable up to what was enqueued. + const reader = response.stream.getReader(); + const {value, done} = await reader.read(); + expect(value).toEqual({hello: 'world'}); + expect(done).toBe(false); + + // The next read should signal the stream is done (closed, not errored). + const final = await reader.read(); + expect(final.done).toBe(true); + }); + it('can dedupe references inside promises', async () => { const foo = {}; const bar = { @@ -2902,9 +3067,9 @@ describe('ReactFlightDOMBrowser', () => { [ "Object.", "/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js", - 2824, + 2989, 19, - 2808, + 2973, 89, ], ], diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js index 0bf615001982..7781f62e4194 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js @@ -48,6 +48,7 @@ export type Options = { callServer?: CallServerCallback, debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...}, temporaryReferences?: TemporaryReferenceSet, + allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -97,6 +98,7 @@ function createResponseFromOptions(options: void | Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, + options && options.allowPartialStream ? options.allowPartialStream : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js index 2cf668f679d3..580e2a983a07 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js @@ -76,6 +76,7 @@ export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, temporaryReferences?: TemporaryReferenceSet, + allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -104,6 +105,7 @@ function createResponseFromOptions(options: Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, + options && options.allowPartialStream ? options.allowPartialStream : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js index 8863b1bf1c66..5a1c3445c576 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js @@ -57,6 +57,7 @@ type EncodeFormActionCallback =
( export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, + allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -106,6 +107,7 @@ function createFromNodeStream( options ? options.encodeFormAction : undefined, options && typeof options.nonce === 'string' ? options.nonce : undefined, undefined, // TODO: If encodeReply is supported, this should support temporaryReferences + options && options.allowPartialStream ? options.allowPartialStream : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, From f6e060c1b26ddce8ab604ca53307838a08c943ac Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Mon, 9 Feb 2026 18:20:07 +0100 Subject: [PATCH 2/2] Prefix option with `unstable_` --- .../src/client/ReactFlightDOMClientBrowser.js | 6 ++++-- .../src/client/ReactFlightDOMClientNode.js | 6 ++++-- .../src/client/ReactFlightDOMClientBrowser.js | 6 ++++-- .../src/client/ReactFlightDOMClientEdge.js | 6 ++++-- .../src/client/ReactFlightDOMClientNode.js | 6 ++++-- .../src/client/ReactFlightDOMClientBrowser.js | 6 ++++-- .../src/client/ReactFlightDOMClientEdge.js | 6 ++++-- .../src/client/ReactFlightDOMClientNode.js | 6 ++++-- .../src/client/ReactFlightDOMClientEdge.js | 6 ++++-- .../src/client/ReactFlightDOMClientNode.js | 6 ++++-- .../src/__tests__/ReactFlightDOMBrowser-test.js | 14 +++++++------- .../src/client/ReactFlightDOMClientBrowser.js | 6 ++++-- .../src/client/ReactFlightDOMClientEdge.js | 6 ++++-- .../src/client/ReactFlightDOMClientNode.js | 6 ++++-- 14 files changed, 59 insertions(+), 33 deletions(-) diff --git a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js index 315dae150496..1c07d4369b85 100644 --- a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js @@ -49,7 +49,7 @@ export type Options = { callServer?: CallServerCallback, debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...}, temporaryReferences?: TemporaryReferenceSet, - allowPartialStream?: boolean, + unstable_allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -99,7 +99,9 @@ function createResponseFromOptions(options: void | Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, - options && options.allowPartialStream ? options.allowPartialStream : false, + options && options.unstable_allowPartialStream + ? options.unstable_allowPartialStream + : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js index b7e4ff9990cd..e9692997dd7c 100644 --- a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js @@ -54,7 +54,7 @@ type EncodeFormActionCallback = ( export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, - allowPartialStream?: boolean, + unstable_allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -105,7 +105,9 @@ function createFromNodeStream( options ? options.encodeFormAction : undefined, options && typeof options.nonce === 'string' ? options.nonce : undefined, undefined, // TODO: If encodeReply is supported, this should support temporaryReferences - options && options.allowPartialStream ? options.allowPartialStream : false, + options && options.unstable_allowPartialStream + ? options.unstable_allowPartialStream + : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js index 22d287acb863..a034a460f809 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js @@ -124,7 +124,9 @@ function createResponseFromOptions(options: void | Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, - options && options.allowPartialStream ? options.allowPartialStream : false, + options && options.unstable_allowPartialStream + ? options.unstable_allowPartialStream + : false, __DEV__ ? findSourceMapURL : undefined, __DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true __DEV__ && options && options.environmentName @@ -208,7 +210,7 @@ function startReadingFromStream( export type Options = { debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...}, temporaryReferences?: TemporaryReferenceSet, - allowPartialStream?: boolean, + unstable_allowPartialStream?: boolean, replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js index d3cbab637585..57afee2e914c 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js @@ -77,7 +77,7 @@ export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, temporaryReferences?: TemporaryReferenceSet, - allowPartialStream?: boolean, + unstable_allowPartialStream?: boolean, replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, @@ -105,7 +105,9 @@ function createResponseFromOptions(options?: Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, - options && options.allowPartialStream ? options.allowPartialStream : false, + options && options.unstable_allowPartialStream + ? options.unstable_allowPartialStream + : false, __DEV__ ? findSourceMapURL : undefined, __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false __DEV__ && options && options.environmentName diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js index db93c1b72180..941ca67dcfa1 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js @@ -50,7 +50,7 @@ type EncodeFormActionCallback = ( export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, - allowPartialStream?: boolean, + unstable_allowPartialStream?: boolean, replayConsoleLogs?: boolean, environmentName?: string, startTime?: number, @@ -98,7 +98,9 @@ export function createFromNodeStream( options ? options.encodeFormAction : undefined, options && typeof options.nonce === 'string' ? options.nonce : undefined, undefined, // TODO: If encodeReply is supported, this should support temporaryReferences - options && options.allowPartialStream ? options.allowPartialStream : false, + options && options.unstable_allowPartialStream + ? options.unstable_allowPartialStream + : false, __DEV__ ? findSourceMapURL : undefined, __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false __DEV__ && options && options.environmentName diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js index 7781f62e4194..c38d5fd05133 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js @@ -48,7 +48,7 @@ export type Options = { callServer?: CallServerCallback, debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...}, temporaryReferences?: TemporaryReferenceSet, - allowPartialStream?: boolean, + unstable_allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -98,7 +98,9 @@ function createResponseFromOptions(options: void | Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, - options && options.allowPartialStream ? options.allowPartialStream : false, + options && options.unstable_allowPartialStream + ? options.unstable_allowPartialStream + : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js index fad07766de2d..6b781f897fc1 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js @@ -76,7 +76,7 @@ export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, temporaryReferences?: TemporaryReferenceSet, - allowPartialStream?: boolean, + unstable_allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -105,7 +105,9 @@ function createResponseFromOptions(options: Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, - options && options.allowPartialStream ? options.allowPartialStream : false, + options && options.unstable_allowPartialStream + ? options.unstable_allowPartialStream + : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js index 5a1c3445c576..7aff35e85da5 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js @@ -57,7 +57,7 @@ type EncodeFormActionCallback = ( export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, - allowPartialStream?: boolean, + unstable_allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -107,7 +107,9 @@ function createFromNodeStream( options ? options.encodeFormAction : undefined, options && typeof options.nonce === 'string' ? options.nonce : undefined, undefined, // TODO: If encodeReply is supported, this should support temporaryReferences - options && options.allowPartialStream ? options.allowPartialStream : false, + options && options.unstable_allowPartialStream + ? options.unstable_allowPartialStream + : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js index 580e2a983a07..8bcdcdbfe0d2 100644 --- a/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientEdge.js @@ -76,7 +76,7 @@ export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, temporaryReferences?: TemporaryReferenceSet, - allowPartialStream?: boolean, + unstable_allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -105,7 +105,9 @@ function createResponseFromOptions(options: Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, - options && options.allowPartialStream ? options.allowPartialStream : false, + options && options.unstable_allowPartialStream + ? options.unstable_allowPartialStream + : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js index 5a1c3445c576..7aff35e85da5 100644 --- a/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-unbundled/src/client/ReactFlightDOMClientNode.js @@ -57,7 +57,7 @@ type EncodeFormActionCallback = ( export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, - allowPartialStream?: boolean, + unstable_allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -107,7 +107,9 @@ function createFromNodeStream( options ? options.encodeFormAction : undefined, options && typeof options.nonce === 'string' ? options.nonce : undefined, undefined, // TODO: If encodeReply is supported, this should support temporaryReferences - options && options.allowPartialStream ? options.allowPartialStream : false, + options && options.unstable_allowPartialStream + ? options.unstable_allowPartialStream + : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index 6698c61a307a..d7ec51780a19 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -2504,7 +2504,7 @@ describe('ReactFlightDOMBrowser', () => { expect(container.innerHTML).toBe(''); }); - it('renders Suspense fallback for unresolved promises with allowPartialStream', async () => { + it('renders Suspense fallback for unresolved promises with unstable_allowPartialStream', async () => { let resolveGreeting; const greetingPromise = new Promise(resolve => { resolveGreeting = resolve; @@ -2547,7 +2547,7 @@ describe('ReactFlightDOMBrowser', () => { const response = ReactServerDOMClient.createFromReadableStream( passThrough(prelude), { - allowPartialStream: true, + unstable_allowPartialStream: true, }, ); const container = document.createElement('div'); @@ -2562,13 +2562,13 @@ describe('ReactFlightDOMBrowser', () => { root.render(); }); - // With `allowPartialStream`, we should see the fallback instead of a + // With `unstable_allowPartialStream`, we should see the fallback instead of a // 'Connection closed.' error expect(errors).toEqual([]); expect(container.innerHTML).toBe('loading...'); }); - it('renders client components that are blocked on chunks with allowPartialStream', async () => { + it('renders client components that are blocked on chunks with unstable_allowPartialStream', async () => { let resolveClientComponentChunk; const ClientComponent = clientExports( @@ -2607,7 +2607,7 @@ describe('ReactFlightDOMBrowser', () => { const response = ReactServerDOMClient.createFromReadableStream( passThrough(prelude), { - allowPartialStream: true, + unstable_allowPartialStream: true, }, ); const container = document.createElement('div'); @@ -2626,7 +2626,7 @@ describe('ReactFlightDOMBrowser', () => { expect(container.innerHTML).toBe('
Hello, World!
'); }); - it('closes inner ReadableStreams gracefully with allowPartialStream', async () => { + it('closes inner ReadableStreams gracefully with unstable_allowPartialStream', async () => { let streamController; const innerStream = new ReadableStream({ start(c) { @@ -2654,7 +2654,7 @@ describe('ReactFlightDOMBrowser', () => { const response = await ReactServerDOMClient.createFromReadableStream( passThrough(prelude), { - allowPartialStream: true, + unstable_allowPartialStream: true, }, ); diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js index 7781f62e4194..c38d5fd05133 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js @@ -48,7 +48,7 @@ export type Options = { callServer?: CallServerCallback, debugChannel?: {writable?: WritableStream, readable?: ReadableStream, ...}, temporaryReferences?: TemporaryReferenceSet, - allowPartialStream?: boolean, + unstable_allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -98,7 +98,9 @@ function createResponseFromOptions(options: void | Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, - options && options.allowPartialStream ? options.allowPartialStream : false, + options && options.unstable_allowPartialStream + ? options.unstable_allowPartialStream + : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js index 580e2a983a07..8bcdcdbfe0d2 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js @@ -76,7 +76,7 @@ export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, temporaryReferences?: TemporaryReferenceSet, - allowPartialStream?: boolean, + unstable_allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -105,7 +105,9 @@ function createResponseFromOptions(options: Options) { options && options.temporaryReferences ? options.temporaryReferences : undefined, - options && options.allowPartialStream ? options.allowPartialStream : false, + options && options.unstable_allowPartialStream + ? options.unstable_allowPartialStream + : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js index 5a1c3445c576..7aff35e85da5 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js @@ -57,7 +57,7 @@ type EncodeFormActionCallback =
( export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, - allowPartialStream?: boolean, + unstable_allowPartialStream?: boolean, findSourceMapURL?: FindSourceMapURLCallback, replayConsoleLogs?: boolean, environmentName?: string, @@ -107,7 +107,9 @@ function createFromNodeStream( options ? options.encodeFormAction : undefined, options && typeof options.nonce === 'string' ? options.nonce : undefined, undefined, // TODO: If encodeReply is supported, this should support temporaryReferences - options && options.allowPartialStream ? options.allowPartialStream : false, + options && options.unstable_allowPartialStream + ? options.unstable_allowPartialStream + : false, __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined,