Skip to content

Commit c17a27e

Browse files
authored
FB-specific builds of Flight Server, Flight Client, and React Shared Subset (#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.
1 parent 6c7b41d commit c17a27e

18 files changed

+960
-9
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ module.exports = {
327327
'packages/react-server-dom-esm/**/*.js',
328328
'packages/react-server-dom-webpack/**/*.js',
329329
'packages/react-server-dom-turbopack/**/*.js',
330+
'packages/react-server-dom-fb/**/*.js',
330331
'packages/react-test-renderer/**/*.js',
331332
'packages/react-debug-tools/**/*.js',
332333
'packages/react-devtools-extensions/**/*.js',
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
export * from 'react-client/src/ReactFlightClientConfigBrowser';
11+
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
12+
export * from 'react-server-dom-fb/src/ReactFlightClientConfigFBBundler';
13+
14+
export const usedWithSSR = false;

packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

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

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

@@ -107,7 +107,7 @@ export function dispatchHint<Code: HintCode>(
107107
}
108108
}
109109

110-
// Flow is having troulbe refining the HintModels so we help it a bit.
110+
// Flow is having trouble refining the HintModels so we help it a bit.
111111
// This should be compiled out in the production build.
112112
function refineModel<T>(code: T, model: HintModel<any>): HintModel<T> {
113113
return model;
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {
11+
Thenable,
12+
FulfilledThenable,
13+
RejectedThenable,
14+
} from 'shared/ReactTypes';
15+
16+
export type ModuleLoading = mixed;
17+
18+
type ResolveClientReferenceFn<T> =
19+
ClientReferenceMetadata => ClientReference<T>;
20+
21+
export type SSRModuleMap = {
22+
resolveClientReference?: ResolveClientReferenceFn<any>,
23+
};
24+
export type ServerManifest = string;
25+
export type {
26+
ClientManifest,
27+
ServerReferenceId,
28+
ClientReferenceMetadata,
29+
} from './ReactFlightReferencesFB';
30+
31+
import type {
32+
ServerReferenceId,
33+
ClientReferenceMetadata,
34+
} from './ReactFlightReferencesFB';
35+
36+
export type ClientReference<T> = {
37+
getModuleId: () => string,
38+
load: () => Thenable<T>,
39+
};
40+
41+
export function prepareDestinationForModule(
42+
moduleLoading: ModuleLoading,
43+
nonce: ?string,
44+
metadata: ClientReferenceMetadata,
45+
) {
46+
return;
47+
}
48+
49+
export function resolveClientReference<T>(
50+
moduleMap: SSRModuleMap,
51+
metadata: ClientReferenceMetadata,
52+
): ClientReference<T> {
53+
if (typeof moduleMap.resolveClientReference === 'function') {
54+
return moduleMap.resolveClientReference(metadata);
55+
} else {
56+
throw new Error(
57+
'Expected `resolveClientReference` to be defined on the moduleMap.',
58+
);
59+
}
60+
}
61+
62+
export function resolveServerReference<T>(
63+
config: ServerManifest,
64+
id: ServerReferenceId,
65+
): ClientReference<T> {
66+
throw new Error('Not implemented');
67+
}
68+
69+
const asyncModuleCache: Map<string, Thenable<any>> = new Map();
70+
71+
export function preloadModule<T>(
72+
clientReference: ClientReference<T>,
73+
): null | Thenable<any> {
74+
const existingPromise = asyncModuleCache.get(clientReference.getModuleId());
75+
if (existingPromise) {
76+
if (existingPromise.status === 'fulfilled') {
77+
return null;
78+
}
79+
return existingPromise;
80+
} else {
81+
const modulePromise: Thenable<T> = clientReference.load();
82+
modulePromise.then(
83+
value => {
84+
const fulfilledThenable: FulfilledThenable<mixed> =
85+
(modulePromise: any);
86+
fulfilledThenable.status = 'fulfilled';
87+
fulfilledThenable.value = value;
88+
},
89+
reason => {
90+
const rejectedThenable: RejectedThenable<mixed> = (modulePromise: any);
91+
rejectedThenable.status = 'rejected';
92+
rejectedThenable.reason = reason;
93+
},
94+
);
95+
asyncModuleCache.set(clientReference.getModuleId(), modulePromise);
96+
return modulePromise;
97+
}
98+
}
99+
100+
export function requireModule<T>(clientReference: ClientReference<T>): T {
101+
let module;
102+
// We assume that preloadModule has been called before, which
103+
// should have added something to the module cache.
104+
const promise: any = asyncModuleCache.get(clientReference.getModuleId());
105+
if (promise.status === 'fulfilled') {
106+
module = promise.value;
107+
} else {
108+
throw promise.reason;
109+
}
110+
// We are currently only support default exports for client components
111+
return module;
112+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import {enableBinaryFlight} from 'shared/ReactFeatureFlags';
11+
import type {Thenable} from 'shared/ReactTypes';
12+
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
13+
14+
import {
15+
createResponse,
16+
getRoot,
17+
reportGlobalError,
18+
processBinaryChunk,
19+
close,
20+
} from 'react-client/src/ReactFlightClient';
21+
22+
import type {SSRModuleMap} from './ReactFlightClientConfigFBBundler';
23+
24+
type Options = {
25+
moduleMap: SSRModuleMap,
26+
};
27+
28+
function createResponseFromOptions(options: void | Options) {
29+
const moduleMap = options && options.moduleMap;
30+
if (moduleMap == null) {
31+
throw new Error('Expected `moduleMap` to be defined.');
32+
}
33+
34+
return createResponse(moduleMap, null, undefined, undefined);
35+
}
36+
37+
function processChunk(response: FlightResponse, chunk: string | Uint8Array) {
38+
if (enableBinaryFlight) {
39+
if (typeof chunk === 'string') {
40+
throw new Error(
41+
'`enableBinaryFlight` flag is enabled, expected a Uint8Array as input, got string.',
42+
);
43+
}
44+
}
45+
const buffer = typeof chunk !== 'string' ? chunk : encodeString(chunk);
46+
47+
processBinaryChunk(response, buffer);
48+
}
49+
50+
function encodeString(string: string) {
51+
const textEncoder = new TextEncoder();
52+
return textEncoder.encode(string);
53+
}
54+
55+
function startReadingFromStream(
56+
response: FlightResponse,
57+
stream: ReadableStream,
58+
): void {
59+
const reader = stream.getReader();
60+
function progress({
61+
done,
62+
value,
63+
}: {
64+
done: boolean,
65+
value: ?any,
66+
...
67+
}): void | Promise<void> {
68+
if (done) {
69+
close(response);
70+
return;
71+
}
72+
const buffer: Uint8Array = (value: any);
73+
processChunk(response, buffer);
74+
return reader.read().then(progress).catch(error);
75+
}
76+
function error(e: any) {
77+
reportGlobalError(response, e);
78+
}
79+
reader.read().then(progress).catch(error);
80+
}
81+
82+
function createFromReadableStream<T>(
83+
stream: ReadableStream,
84+
options?: Options,
85+
): Thenable<T> {
86+
const response: FlightResponse = createResponseFromOptions(options);
87+
startReadingFromStream(response, stream);
88+
return getRoot(response);
89+
}
90+
91+
export {createFromReadableStream};
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
11+
import type {
12+
Destination,
13+
Chunk,
14+
PrecomputedChunk,
15+
} from 'react-server/src/ReactServerStreamConfig';
16+
import type {ClientManifest} from './ReactFlightReferencesFB';
17+
18+
import {
19+
createRequest,
20+
startWork,
21+
startFlowing,
22+
} from 'react-server/src/ReactFlightServer';
23+
24+
import {setByteLengthOfChunkImplementation} from 'react-server/src/ReactServerStreamConfig';
25+
26+
export {
27+
registerClientReference,
28+
registerServerReference,
29+
getRequestedClientReferencesKeys,
30+
clearRequestedClientReferencesKeysSet,
31+
} from './ReactFlightReferencesFB';
32+
33+
type Options = {
34+
onError?: (error: mixed) => void,
35+
};
36+
37+
function renderToDestination(
38+
destination: Destination,
39+
model: ReactClientValue,
40+
bundlerConfig: ClientManifest,
41+
options?: Options,
42+
): void {
43+
if (!configured) {
44+
throw new Error(
45+
'Please make sure to call `setConfig(...)` before calling `renderToDestination`.',
46+
);
47+
}
48+
const request = createRequest(
49+
model,
50+
bundlerConfig,
51+
options ? options.onError : undefined,
52+
);
53+
startWork(request);
54+
startFlowing(request, destination);
55+
}
56+
57+
type Config = {
58+
byteLength: (chunk: Chunk | PrecomputedChunk) => number,
59+
};
60+
61+
let configured = false;
62+
63+
function setConfig(config: Config): void {
64+
setByteLengthOfChunkImplementation(config.byteLength);
65+
configured = true;
66+
}
67+
68+
export {renderToDestination, setConfig};

0 commit comments

Comments
 (0)