Files
react/packages/react-markup/src/ReactMarkupServer.js
Sebastian Markbåge eb7f8b42c9 [Flight] Add Separate Outgoing Debug Channel (#33754)
This lets us pass a writable on the server side and readable on the
client side to send debug info through a separate channel so that it
doesn't interfere with the main payload as much. The main payload refers
to chunks defined in the debug info which means it's still blocked on it
though. This ensures that the debug data has loaded by the time the
value is rendered so that the next step can forward the data.

This will be a bit fragile to race conditions until #33665 lands.
Another follow up needed is the ability to skip the debug channel on the
receiving side. Right now it'll block forever if you don't provide one
since we're blocking on the debug data.
2025-07-10 16:22:44 -04:00

238 lines
7.0 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 {ReactNodeList} from 'shared/ReactTypes';
import type {LazyComponent} from 'react/src/ReactLazy';
import type {ErrorInfo} from 'react-server/src/ReactFizzServer';
import ReactVersion from 'shared/ReactVersion';
import ReactSharedInternalsServer from 'react-server/src/ReactSharedInternalsServer';
import ReactSharedInternalsClient from 'shared/ReactSharedInternals';
import {
createRequest as createFlightRequest,
startWork as startFlightWork,
startFlowing as startFlightFlowing,
abort as abortFlight,
} from 'react-server/src/ReactFlightServer';
import {
createResponse as createFlightResponse,
createStreamState as createFlightStreamState,
getRoot as getFlightRoot,
processStringChunk as processFlightStringChunk,
close as closeFlight,
} from 'react-client/src/ReactFlightClient';
import {
createRequest as createFizzRequest,
startWork as startFizzWork,
startFlowing as startFizzFlowing,
abort as abortFizz,
} from 'react-server/src/ReactFizzServer';
import {
createResumableState,
createRenderState,
createRootFormatContext,
} from './ReactFizzConfigMarkup';
type ReactMarkupNodeList =
// This is the intersection of ReactNodeList and ReactClientValue minus
// Client/ServerReferences.
| React$Element<React$ComponentType<any>>
| LazyComponent<ReactMarkupNodeList, any>
| React$Element<string>
| string
| boolean
| number
| symbol
| null
| void
| bigint
| $AsyncIterable<ReactMarkupNodeList, ReactMarkupNodeList, void>
| $AsyncIterator<ReactMarkupNodeList, ReactMarkupNodeList, void>
| Iterable<ReactMarkupNodeList>
| Iterator<ReactMarkupNodeList>
| Array<ReactMarkupNodeList>
| Promise<ReactMarkupNodeList>; // Thenable<ReactMarkupNodeList>
type MarkupOptions = {
identifierPrefix?: string,
signal?: AbortSignal,
onError?: (error: mixed, errorInfo: ErrorInfo) => ?string,
};
function noServerCallOrFormAction() {
throw new Error(
'renderToHTML should not have emitted Server References. This is a bug in React.',
);
}
export function experimental_renderToHTML(
children: ReactMarkupNodeList,
options?: MarkupOptions,
): Promise<string> {
return new Promise((resolve, reject) => {
const streamState = createFlightStreamState();
const flightDestination = {
push(chunk: string | null): boolean {
if (chunk !== null) {
processFlightStringChunk(flightResponse, streamState, chunk);
} else {
closeFlight(flightResponse);
}
return true;
},
destroy(error: mixed): void {
abortFizz(fizzRequest, error);
reject(error);
},
};
let buffer = '';
const fizzDestination = {
// $FlowFixMe[missing-local-annot]
push(chunk) {
if (chunk !== null) {
buffer += chunk;
} else {
// null indicates that we finished
resolve(buffer);
}
return true;
},
// $FlowFixMe[missing-local-annot]
destroy(error) {
abortFlight(flightRequest, error);
reject(error);
},
};
let stashErrorIdx = 1;
const stashedErrors: Map<string, mixed> = new Map();
function handleFlightError(error: mixed): string {
// For Flight errors we don't immediately reject, because they might not matter
// to the output of the HTML. We stash the error with a digest in case we need
// to get to the original error from the Fizz render.
const id = '' + stashErrorIdx++;
stashedErrors.set(id, error);
return id;
}
function handleError(error: mixed, errorInfo: ErrorInfo) {
if (typeof error === 'object' && error !== null) {
const id = error.digest;
// Note that the original error might be `undefined` so we need a has check.
if (typeof id === 'string' && stashedErrors.has(id)) {
// Get the original error thrown inside Flight.
error = stashedErrors.get(id);
}
}
// Any error rejects the promise, regardless of where it happened.
// Unlike other React SSR we don't want to put Suspense boundaries into
// client rendering mode because there's no client rendering here.
reject(error);
const onError = options && options.onError;
if (onError) {
if (__DEV__) {
const prevGetCurrentStackImpl =
ReactSharedInternalsServer.getCurrentStack;
// We're inside a "client" callback from Fizz but we only have access to the
// "server" runtime so to get access to a stack trace within this callback we
// need to override it to get it from the client runtime.
ReactSharedInternalsServer.getCurrentStack =
ReactSharedInternalsClient.getCurrentStack;
try {
onError(error, errorInfo);
} finally {
ReactSharedInternalsServer.getCurrentStack =
prevGetCurrentStackImpl;
}
} else {
onError(error, errorInfo);
}
}
}
const flightRequest = createFlightRequest(
// $FlowFixMe: This should be a subtype but not everything is typed covariant.
children,
null,
handleFlightError,
options ? options.identifierPrefix : undefined,
undefined,
undefined,
'Markup',
undefined,
false,
);
const flightResponse = createFlightResponse(
null,
null,
null,
noServerCallOrFormAction,
noServerCallOrFormAction,
undefined,
undefined,
undefined,
false,
);
const resumableState = createResumableState(
options ? options.identifierPrefix : undefined,
undefined,
);
const root = getFlightRoot<ReactNodeList>(flightResponse);
const fizzRequest = createFizzRequest(
// $FlowFixMe: Thenables as children are supported.
root,
resumableState,
createRenderState(
resumableState,
undefined,
undefined,
undefined,
undefined,
undefined,
),
createRootFormatContext(),
Infinity,
handleError,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abortFlight(flightRequest, (signal: any).reason);
abortFizz(fizzRequest, (signal: any).reason);
} else {
const listener = () => {
abortFlight(flightRequest, (signal: any).reason);
abortFizz(fizzRequest, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
startFlightWork(flightRequest);
startFlightFlowing(flightRequest, flightDestination);
startFizzWork(fizzRequest);
startFizzFlowing(fizzRequest, fizzDestination);
});
}
export {ReactVersion as version};