Skip to content

Commit

Permalink
FB-specific builds of Flight Server, Flight Client, and React Shared …
Browse files Browse the repository at this point in the history
…Subset (facebook#27579)

This PR adds a new FB-specific configuration of Flight. We also need to
bundle a version of ReactSharedSubset that will be used for running
Flight on the server.

This initial implementation does not support server actions yet.

The FB-Flight still uses the text protocol on the server (the flag
`enableBinaryFlight` is set to false). It looks like we need some
changes in Hermes to properly support this binary format.
  • Loading branch information
alunyov authored and AndyPengc12 committed Apr 15, 2024
1 parent 035c9e7 commit 4794725
Show file tree
Hide file tree
Showing 18 changed files with 960 additions and 9 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ module.exports = {
'packages/react-server-dom-esm/**/*.js',
'packages/react-server-dom-webpack/**/*.js',
'packages/react-server-dom-turbopack/**/*.js',
'packages/react-server-dom-fb/**/*.js',
'packages/react-test-renderer/**/*.js',
'packages/react-debug-tools/**/*.js',
'packages/react-devtools-extensions/**/*.js',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* 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
*/

export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export * from 'react-server-dom-fb/src/ReactFlightClientConfigFBBundler';

export const usedWithSSR = false;
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

// This client file is in the shared folder because it applies to both SSR and browser contexts.
// It is the configuraiton of the FlightClient behavior which can run in either environment.
// It is the configuration of the FlightClient behavior which can run in either environment.

import type {HintCode, HintModel} from '../server/ReactFlightServerConfigDOM';

Expand Down Expand Up @@ -107,7 +107,7 @@ export function dispatchHint<Code: HintCode>(
}
}

// Flow is having troulbe refining the HintModels so we help it a bit.
// Flow is having trouble refining the HintModels so we help it a bit.
// This should be compiled out in the production build.
function refineModel<T>(code: T, model: HintModel<any>): HintModel<T> {
return model;
Expand Down
112 changes: 112 additions & 0 deletions packages/react-server-dom-fb/src/ReactFlightClientConfigFBBundler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* 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 ModuleLoading = mixed;

type ResolveClientReferenceFn<T> =
ClientReferenceMetadata => ClientReference<T>;

export type SSRModuleMap = {
resolveClientReference?: ResolveClientReferenceFn<any>,
};
export type ServerManifest = string;
export type {
ClientManifest,
ServerReferenceId,
ClientReferenceMetadata,
} from './ReactFlightReferencesFB';

import type {
ServerReferenceId,
ClientReferenceMetadata,
} from './ReactFlightReferencesFB';

export type ClientReference<T> = {
getModuleId: () => string,
load: () => Thenable<T>,
};

export function prepareDestinationForModule(
moduleLoading: ModuleLoading,
nonce: ?string,
metadata: ClientReferenceMetadata,
) {
return;
}

export function resolveClientReference<T>(
moduleMap: SSRModuleMap,
metadata: ClientReferenceMetadata,
): ClientReference<T> {
if (typeof moduleMap.resolveClientReference === 'function') {
return moduleMap.resolveClientReference(metadata);
} else {
throw new Error(
'Expected `resolveClientReference` to be defined on the moduleMap.',
);
}
}

export function resolveServerReference<T>(
config: ServerManifest,
id: ServerReferenceId,
): ClientReference<T> {
throw new Error('Not implemented');
}

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

export function preloadModule<T>(
clientReference: ClientReference<T>,
): null | Thenable<any> {
const existingPromise = asyncModuleCache.get(clientReference.getModuleId());
if (existingPromise) {
if (existingPromise.status === 'fulfilled') {
return null;
}
return existingPromise;
} else {
const modulePromise: Thenable<T> = clientReference.load();
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(clientReference.getModuleId(), modulePromise);
return modulePromise;
}
}

export function requireModule<T>(clientReference: ClientReference<T>): T {
let module;
// We assume that preloadModule has been called before, which
// should have added something to the module cache.
const promise: any = asyncModuleCache.get(clientReference.getModuleId());
if (promise.status === 'fulfilled') {
module = promise.value;
} else {
throw promise.reason;
}
// We are currently only support default exports for client components
return module;
}
91 changes: 91 additions & 0 deletions packages/react-server-dom-fb/src/ReactFlightDOMClientFB.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* 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 {enableBinaryFlight} from 'shared/ReactFeatureFlags';
import type {Thenable} from 'shared/ReactTypes';
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';

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

import type {SSRModuleMap} from './ReactFlightClientConfigFBBundler';

type Options = {
moduleMap: SSRModuleMap,
};

function createResponseFromOptions(options: void | Options) {
const moduleMap = options && options.moduleMap;
if (moduleMap == null) {
throw new Error('Expected `moduleMap` to be defined.');
}

return createResponse(moduleMap, null, undefined, undefined);
}

function processChunk(response: FlightResponse, chunk: string | Uint8Array) {
if (enableBinaryFlight) {
if (typeof chunk === 'string') {
throw new Error(
'`enableBinaryFlight` flag is enabled, expected a Uint8Array as input, got string.',
);
}
}
const buffer = typeof chunk !== 'string' ? chunk : encodeString(chunk);

processBinaryChunk(response, buffer);
}

function encodeString(string: string) {
const textEncoder = new TextEncoder();
return textEncoder.encode(string);
}

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);
processChunk(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);
}

export {createFromReadableStream};
68 changes: 68 additions & 0 deletions packages/react-server-dom-fb/src/ReactFlightDOMServerFB.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* 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 {ReactClientValue} from 'react-server/src/ReactFlightServer';
import type {
Destination,
Chunk,
PrecomputedChunk,
} from 'react-server/src/ReactServerStreamConfig';
import type {ClientManifest} from './ReactFlightReferencesFB';

import {
createRequest,
startWork,
startFlowing,
} from 'react-server/src/ReactFlightServer';

import {setByteLengthOfChunkImplementation} from 'react-server/src/ReactServerStreamConfig';

export {
registerClientReference,
registerServerReference,
getRequestedClientReferencesKeys,
clearRequestedClientReferencesKeysSet,
} from './ReactFlightReferencesFB';

type Options = {
onError?: (error: mixed) => void,
};

function renderToDestination(
destination: Destination,
model: ReactClientValue,
bundlerConfig: ClientManifest,
options?: Options,
): void {
if (!configured) {
throw new Error(
'Please make sure to call `setConfig(...)` before calling `renderToDestination`.',
);
}
const request = createRequest(
model,
bundlerConfig,
options ? options.onError : undefined,
);
startWork(request);
startFlowing(request, destination);
}

type Config = {
byteLength: (chunk: Chunk | PrecomputedChunk) => number,
};

let configured = false;

function setConfig(config: Config): void {
setByteLengthOfChunkImplementation(config.byteLength);
configured = true;
}

export {renderToDestination, setConfig};
Loading

0 comments on commit 4794725

Please sign in to comment.