[Flight] Add debugChannel option to Edge and Node clients (#34236)

When a debug channel is used between the Flight server and a browser
Flight client, we want to allow the same RSC stream to be used for
server-side rendering. To support this, the Edge and Node Flight clients
also need to accept a `debugChannel` option. Without it, debug
information would be missing (e.g. for SSR error stacks), and in some
cases this could result in `Connection closed` errors.

This PR adds support for the `debugChannel` option in the Edge and Node
clients for ESM, Parcel, Turbopack, and Webpack. Unlike the browser
clients, these clients only support a one-way channel, since the Flight
server’s return protocol is not designed for multiple clients.

The implementation follows the approach used in the browser clients, but
excludes the writable parts.
This commit is contained in:
Hendrik Liebau
2025-08-20 16:46:34 +02:00
committed by GitHub
parent 3e20dc8b9c
commit 0bc71e67ab
11 changed files with 635 additions and 66 deletions

View File

@@ -56,8 +56,38 @@ export type Options = {
findSourceMapURL?: FindSourceMapURLCallback,
replayConsoleLogs?: boolean,
environmentName?: string,
// For the Node.js client we only support a single-direction debug channel.
debugChannel?: Readable,
};
function startReadingFromStream(
response: Response,
stream: Readable,
isSecondaryStream: boolean,
): void {
const streamState = createStreamState();
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', () => {
// If we're the secondary stream, then we don't close the response until the
// debug channel closes.
if (!isSecondaryStream) {
close(response);
}
});
}
function createFromNodeStream<T>(
stream: Readable,
moduleRootPath: string,
@@ -80,18 +110,14 @@ function createFromNodeStream<T>(
? options.environmentName
: undefined,
);
const streamState = createStreamState();
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', () => close(response));
if (__DEV__ && options && options.debugChannel) {
startReadingFromStream(response, options.debugChannel, false);
startReadingFromStream(response, stream, true);
} else {
startReadingFromStream(response, stream, false);
}
return getRoot(response);
}