mirror of
https://github.com/facebook/react.git
synced 2026-02-23 20:23:02 +00:00
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 `unstable_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.
142 lines
3.8 KiB
JavaScript
142 lines
3.8 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js';
|
|
|
|
import type {
|
|
DebugChannel,
|
|
FindSourceMapURLCallback,
|
|
Response,
|
|
} from 'react-client/src/ReactFlightClient';
|
|
|
|
import type {Readable} from 'stream';
|
|
|
|
import {
|
|
createResponse,
|
|
createStreamState,
|
|
getRoot,
|
|
reportGlobalError,
|
|
processStringChunk,
|
|
processBinaryChunk,
|
|
close,
|
|
} from 'react-client/src/ReactFlightClient';
|
|
|
|
import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient';
|
|
|
|
export {registerServerReference} from 'react-client/src/ReactFlightReplyClient';
|
|
|
|
function noServerCall() {
|
|
throw new Error(
|
|
'Server Functions cannot be called during initial render. ' +
|
|
'This would create a fetch waterfall. Try to use a Server Component ' +
|
|
'to pass data to Client Components instead.',
|
|
);
|
|
}
|
|
|
|
export function createServerReference<A: Iterable<any>, T>(
|
|
id: any,
|
|
callServer: any,
|
|
): (...A) => Promise<T> {
|
|
return createServerReferenceImpl(id, noServerCall);
|
|
}
|
|
|
|
type EncodeFormActionCallback = <A>(
|
|
id: any,
|
|
args: Promise<A>,
|
|
) => ReactCustomFormAction;
|
|
|
|
export type Options = {
|
|
nonce?: string,
|
|
encodeFormAction?: EncodeFormActionCallback,
|
|
unstable_allowPartialStream?: boolean,
|
|
findSourceMapURL?: FindSourceMapURLCallback,
|
|
replayConsoleLogs?: boolean,
|
|
environmentName?: string,
|
|
startTime?: number,
|
|
endTime?: number,
|
|
// For the Node.js client we only support a single-direction debug channel.
|
|
debugChannel?: Readable,
|
|
};
|
|
|
|
function startReadingFromStream(
|
|
response: Response,
|
|
stream: Readable,
|
|
onEnd: () => void,
|
|
): void {
|
|
const streamState = createStreamState(response, stream);
|
|
|
|
stream.on('data', chunk => {
|
|
if (typeof chunk === 'string') {
|
|
processStringChunk(response, streamState, chunk);
|
|
} else {
|
|
processBinaryChunk(response, streamState, chunk);
|
|
}
|
|
});
|
|
|
|
stream.on('error', error => {
|
|
reportGlobalError(response, error);
|
|
});
|
|
|
|
stream.on('end', onEnd);
|
|
}
|
|
|
|
function createFromNodeStream<T>(
|
|
stream: Readable,
|
|
moduleRootPath: string,
|
|
moduleBaseURL: string,
|
|
options?: Options,
|
|
): Thenable<T> {
|
|
const debugChannel: void | DebugChannel =
|
|
__DEV__ && options && options.debugChannel !== undefined
|
|
? {hasReadable: true, callback: null}
|
|
: undefined;
|
|
|
|
const response: Response = createResponse(
|
|
moduleRootPath,
|
|
null,
|
|
moduleBaseURL,
|
|
noServerCall,
|
|
options ? options.encodeFormAction : undefined,
|
|
options && typeof options.nonce === 'string' ? options.nonce : undefined,
|
|
undefined, // TODO: If encodeReply is supported, this should support temporaryReferences
|
|
options && options.unstable_allowPartialStream
|
|
? options.unstable_allowPartialStream
|
|
: false,
|
|
__DEV__ && options && options.findSourceMapURL
|
|
? options.findSourceMapURL
|
|
: undefined,
|
|
__DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false
|
|
__DEV__ && options && options.environmentName
|
|
? options.environmentName
|
|
: undefined,
|
|
__DEV__ && options && options.startTime != null
|
|
? options.startTime
|
|
: undefined,
|
|
__DEV__ && options && options.endTime != null ? options.endTime : undefined,
|
|
debugChannel,
|
|
);
|
|
|
|
if (__DEV__ && options && options.debugChannel) {
|
|
let streamEndedCount = 0;
|
|
const handleEnd = () => {
|
|
if (++streamEndedCount === 2) {
|
|
close(response);
|
|
}
|
|
};
|
|
startReadingFromStream(response, options.debugChannel, handleEnd);
|
|
startReadingFromStream(response, stream, handleEnd);
|
|
} else {
|
|
startReadingFromStream(response, stream, close.bind(null, response));
|
|
}
|
|
|
|
return getRoot(response);
|
|
}
|
|
|
|
export {createFromNodeStream};
|