Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Split out Edge and Node implementations of the Flight Client
  • Loading branch information
sebmarkbage committed Feb 20, 2023
commit 3cc0740cab967d689b95ad946a362f545ce9f1b3
34 changes: 34 additions & 0 deletions packages/react-client/src/ReactFlightClientHostConfigNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* 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 {TextDecoder} from 'util';

export type StringDecoder = TextDecoder;

export const supportsBinaryStreams = true;

export function createStringDecoder(): StringDecoder {
return new TextDecoder();
}

const decoderOptions = {stream: true};

export function readPartialStringChunk(
decoder: StringDecoder,
buffer: Uint8Array,
): string {
return decoder.decode(buffer, decoderOptions);
}

export function readFinalStringChunk(
decoder: StringDecoder,
buffer: Uint8Array,
): string {
return decoder.decode(buffer);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
* @flow
*/

export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
export * from 'react-client/src/ReactFlightClientHostConfigNode';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
* @flow
*/

export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
export * from 'react-client/src/ReactFlightClientHostConfigNode';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
export * from 'react-server-dom-webpack/src/ReactFlightClientNodeBundlerConfig';
2 changes: 1 addition & 1 deletion packages/react-server-dom-webpack/client.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
* @flow
*/

export * from './src/ReactFlightDOMClient';
export * from './src/ReactFlightDOMClientBrowser';
2 changes: 1 addition & 1 deletion packages/react-server-dom-webpack/client.edge.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
* @flow
*/

export * from './src/ReactFlightDOMClient';
export * from './src/ReactFlightDOMClientEdge';
2 changes: 1 addition & 1 deletion packages/react-server-dom-webpack/client.node.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
* @flow
*/

export * from './src/ReactFlightDOMClient';
export * from './src/ReactFlightDOMClientNode';
2 changes: 1 addition & 1 deletion packages/react-server-dom-webpack/client.node.unbundled.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
* @flow
*/

export * from './src/ReactFlightDOMClient';
export * from './src/ReactFlightDOMClientNode';
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* 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,
FulfilledThenable,
RejectedThenable,
} from 'shared/ReactTypes';

export type WebpackSSRMap = {
[clientId: string]: {
[clientExportName: string]: ClientReference<any>,
},
};

export type BundlerConfig = WebpackSSRMap;

export opaque type ClientReferenceMetadata = {
id: string,
chunks: Array<string>,
name: string,
async: boolean,
};

// eslint-disable-next-line no-unused-vars
export opaque type ClientReference<T> = {
specifier: string,
name: string,
};

export function resolveClientReference<T>(
bundlerConfig: BundlerConfig,
metadata: ClientReferenceMetadata,
): ClientReference<T> {
const resolvedModuleData = bundlerConfig[metadata.id][metadata.name];
return resolvedModuleData;
}

const asyncModuleCache: Map<string, Thenable<any>> = new Map();

export function preloadModule<T>(
metadata: ClientReference<T>,
): null | Thenable<any> {
const existingPromise = asyncModuleCache.get(metadata.specifier);
if (existingPromise) {
if (existingPromise.status === 'fulfilled') {
return null;
}
return existingPromise;
} else {
// $FlowFixMe[unsupported-syntax]
const modulePromise: Thenable<T> = import(metadata.specifier);
modulePromise.then(
value => {
const fulfilledThenable: FulfilledThenable<mixed> =
(modulePromise: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = value;
},
reason => {
const rejectedThenable: RejectedThenable<mixed> = (modulePromise: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = reason;
},
);
asyncModuleCache.set(metadata.specifier, modulePromise);
return modulePromise;
}
}

export function requireModule<T>(metadata: ClientReference<T>): T {
let moduleExports;
// We assume that preloadModule has been called before, which
// should have added something to the module cache.
const promise: any = asyncModuleCache.get(metadata.specifier);
if (promise.status === 'fulfilled') {
moduleExports = promise.value;
} else {
throw promise.reason;
}
if (metadata.name === '*') {
// This is a placeholder value that represents that the caller imported this
// as a CommonJS module as is.
return moduleExports;
}
if (metadata.name === '') {
// This is a placeholder value that represents that the caller accessed the
// default property of this if it was an ESM interop module.
return moduleExports.default;
}
return moduleExports[metadata.name];
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import type {Thenable} from 'shared/ReactTypes.js';

import type {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream';

import type {BundlerConfig} from './ReactFlightClientWebpackBundlerConfig';

import {
createResponse,
getRoot,
Expand All @@ -28,10 +26,16 @@ type CallServerCallback = <A, T>(
) => Promise<T>;

export type Options = {
moduleMap?: BundlerConfig,
callServer?: CallServerCallback,
};

function createResponseFromOptions(options: void | Options) {
return createResponse(
null,
options && options.callServer ? options.callServer : undefined,
);
}

function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
Expand Down Expand Up @@ -63,10 +67,7 @@ function createFromReadableStream<T>(
stream: ReadableStream,
options?: Options,
): Thenable<T> {
const response: FlightResponse = createResponse(
options && options.moduleMap ? options.moduleMap : null,
options && options.callServer ? options.callServer : undefined,
);
const response: FlightResponse = createResponseFromOptions(options);
startReadingFromStream(response, stream);
return getRoot(response);
}
Expand All @@ -75,10 +76,7 @@ function createFromFetch<T>(
promiseForResponse: Promise<Response>,
options?: Options,
): Thenable<T> {
const response: FlightResponse = createResponse(
options && options.moduleMap ? options.moduleMap : null,
options && options.callServer ? options.callServer : undefined,
);
const response: FlightResponse = createResponseFromOptions(options);
promiseForResponse.then(
function (r) {
startReadingFromStream(response, (r.body: any));
Expand All @@ -94,10 +92,7 @@ function createFromXHR<T>(
request: XMLHttpRequest,
options?: Options,
): Thenable<T> {
const response: FlightResponse = createResponse(
options && options.moduleMap ? options.moduleMap : null,
options && options.callServer ? options.callServer : undefined,
);
const response: FlightResponse = createResponseFromOptions(options);
let processedLength = 0;
function progress(e: ProgressEvent): void {
const chunk = request.responseText;
Expand Down
95 changes: 95 additions & 0 deletions packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* 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} from 'shared/ReactTypes.js';

import type {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream';

import type {BundlerConfig} from './ReactFlightClientWebpackBundlerConfig';

import {
createResponse,
getRoot,
reportGlobalError,
processBinaryChunk,
close,
} from 'react-client/src/ReactFlightClientStream';

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 type Options = {
moduleMap?: BundlerConfig,
};

function createResponseFromOptions(options: void | Options) {
return createResponse(
options && options.moduleMap ? options.moduleMap : null,
noServerCall,
);
}

function startReadingFromStream(
response: FlightResponse,
stream: ReadableStream,
): void {
const reader = stream.getReader();
function progress({
done,
value,
}: {
done: boolean,
value: ?any,
...
}): void | Promise<void> {
if (done) {
close(response);
return;
}
const buffer: Uint8Array = (value: any);
processBinaryChunk(response, buffer);
return reader.read().then(progress).catch(error);
}
function error(e: any) {
reportGlobalError(response, e);
}
reader.read().then(progress).catch(error);
}

function createFromReadableStream<T>(
stream: ReadableStream,
options?: Options,
): Thenable<T> {
const response: FlightResponse = createResponseFromOptions(options);
startReadingFromStream(response, stream);
return getRoot(response);
}

function createFromFetch<T>(
promiseForResponse: Promise<Response>,
options?: Options,
): Thenable<T> {
const response: FlightResponse = createResponseFromOptions(options);
promiseForResponse.then(
function (r) {
startReadingFromStream(response, (r.body: any));
},
function (e) {
reportGlobalError(response, e);
},
);
return getRoot(response);
}

export {createFromFetch, createFromReadableStream};
54 changes: 54 additions & 0 deletions packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* 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} from 'shared/ReactTypes.js';

import type {Response} from 'react-client/src/ReactFlightClientStream';

import type {BundlerConfig} from 'react-client/src/ReactFlightClientHostConfig';

import type {Readable} from 'stream';

import {
createResponse,
getRoot,
reportGlobalError,
processBinaryChunk,
close,
} from 'react-client/src/ReactFlightClientStream';
import {processStringChunk} from '../../react-client/src/ReactFlightClientStream';

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.',
);
}

function createFromNodeStream<T>(
stream: Readable,
moduleMap: $NonMaybeType<BundlerConfig>,
): Thenable<T> {
const response: Response = createResponse(moduleMap, noServerCall);
stream.on('data', chunk => {
if (typeof chunk === 'string') {
processStringChunk(response, chunk, 0);
} else {
processBinaryChunk(response, chunk);
}
});
stream.on('error', error => {
reportGlobalError(response, error);
});
stream.on('end', () => close(response));
return getRoot(response);
}

export {createFromNodeStream};
Loading