For Edge Flight servers, that use Web Streams, we're defining the
`debugChannel` option as:
```
debugChannel?: {readable?: ReadableStream, writable?: WritableStream, ...}
```
Whereas for Node.js Flight servers, that use Node.js Streams, we're
defining it as:
```
debugChannel?: Readable | Writable | Duplex | WebSocket
```
For the Edge Flight clients, there is currently only one direction of
the debug channel supported, so we define the option as:
```
debugChannel?: {readable?: ReadableStream, ...}
```
Consequently, for the Node.js Flight clients, we define the option as:
```
debugChannel?: Readable
```
The presence of a readable debug channel is passed to the Flight client
internally via the `hasReadable` flag on the internal `debugChannel`
option. For the Node.js clients, that flag was accidentally derived from
the public option `debugChannel.readable`, which is conceptually
incorrect, because `debugChannel` is a `Readable` stream, not an options
object with a `readable` property. However, a `Readable` also has a
`readable` property, which is a boolean that indicates whether the
stream is in a readable state. This meant that the `hasReadable` flag
was incidentally still set correctly. Regardless, this was confusing and
unintentional, so we're now fixing it to always set `hasReadable` to
`true` when a `debugChannel` is provided to the Node.js clients. We'll
revisit this in case we ever add support for writable debug channels in
Node.js (and Edge) clients.
138 lines
3.7 KiB
JavaScript
138 lines
3.7 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,
|
|
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
|
|
__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};
|