Files
react/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
T
Sebastian MarkbågeandGitHub 953cb02f6d [Fizz] Split createRequest into createRequest, createPrerenderRequest and resumeRequest (#27342)
Just moving some internal code around again.

I originally encoded what type of work using startRender vs
startPrerender. I had intended to do more forking of the work loop but
we've decided not to go with that strategy. It also turns out that
forking when we start working is actually too late because of a subtle
thing where you can call abort before work begins. Therefore it's
important that starting the work comes later.
2023-09-07 00:18:30 -04:00

207 lines
6.3 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 {PostponedState} from 'react-server/src/ReactFizzServer';
import type {ReactNodeList} from 'shared/ReactTypes';
import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
import type {ImportMap} from '../shared/ReactDOMTypes';
import ReactVersion from 'shared/ReactVersion';
import {
createRequest,
resumeRequest,
startWork,
startFlowing,
abort,
} from 'react-server/src/ReactFizzServer';
import {
createResumableState,
createRenderState,
createRootFormatContext,
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
type Options = {
identifierPrefix?: string,
namespaceURI?: string,
nonce?: string,
bootstrapScriptContent?: string,
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
progressiveChunkSize?: number,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
onPostpone?: (reason: string) => void,
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
importMap?: ImportMap,
};
type ResumeOptions = {
nonce?: string,
signal?: AbortSignal,
onError?: (error: mixed) => ?string,
onPostpone?: (reason: string) => void,
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
};
// TODO: Move to sub-classing ReadableStream.
type ReactDOMServerReadableStream = ReadableStream & {
allReady: Promise<void>,
};
function renderToReadableStream(
children: ReactNodeList,
options?: Options,
): Promise<ReactDOMServerReadableStream> {
return new Promise((resolve, reject) => {
let onFatalError;
let onAllReady;
const allReady = new Promise<void>((res, rej) => {
onAllReady = res;
onFatalError = rej;
});
function onShellReady() {
const stream: ReactDOMServerReadableStream = (new ReadableStream(
{
type: 'bytes',
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
abort(request);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
): any);
// TODO: Move to sub-classing ReadableStream.
stream.allReady = allReady;
resolve(stream);
}
function onShellError(error: mixed) {
// If the shell errors the caller of `renderToReadableStream` won't have access to `allReady`.
// However, `allReady` will be rejected by `onFatalError` as well.
// So we need to catch the duplicate, uncatchable fatal error in `allReady` to prevent a `UnhandledPromiseRejection`.
allReady.catch(() => {});
reject(error);
}
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
options ? options.bootstrapScriptContent : undefined,
options ? options.bootstrapScripts : undefined,
options ? options.bootstrapModules : undefined,
options ? options.unstable_externalRuntimeSrc : undefined,
);
const request = createRequest(
children,
resumableState,
createRenderState(
resumableState,
options ? options.nonce : undefined,
options ? options.importMap : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
onAllReady,
onShellReady,
onShellError,
onFatalError,
options ? options.onPostpone : undefined,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
} else {
const listener = () => {
abort(request, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
startWork(request);
});
}
function resume(
children: ReactNodeList,
postponedState: PostponedState,
options?: ResumeOptions,
): Promise<ReactDOMServerReadableStream> {
return new Promise((resolve, reject) => {
let onFatalError;
let onAllReady;
const allReady = new Promise<void>((res, rej) => {
onAllReady = res;
onFatalError = rej;
});
function onShellReady() {
const stream: ReactDOMServerReadableStream = (new ReadableStream(
{
type: 'bytes',
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
abort(request);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
): any);
// TODO: Move to sub-classing ReadableStream.
stream.allReady = allReady;
resolve(stream);
}
function onShellError(error: mixed) {
// If the shell errors the caller of `renderToReadableStream` won't have access to `allReady`.
// However, `allReady` will be rejected by `onFatalError` as well.
// So we need to catch the duplicate, uncatchable fatal error in `allReady` to prevent a `UnhandledPromiseRejection`.
allReady.catch(() => {});
reject(error);
}
const request = resumeRequest(
children,
postponedState,
createRenderState(
postponedState.resumableState,
options ? options.nonce : undefined,
undefined, // importMap
),
options ? options.onError : undefined,
onAllReady,
onShellReady,
onShellError,
onFatalError,
options ? options.onPostpone : undefined,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
} else {
const listener = () => {
abort(request, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
startWork(request);
});
}
export {renderToReadableStream, resume, ReactVersion as version};