Skip to content

Commit ffd8423

Browse files
authored
[Flight] Add support for Module References in transport protocol (#20121)
* Refactor Flight to require a module reference to be brand checked This exposes a host environment (bundler) specific hook to check if an object is a module reference. This will be used so that they can be passed directly into Flight without needing additional wrapper objects. * Emit module references as a special type of value We already have JSON and errors as special types of "rows". This encodes module references as a special type of row value. This was always the intention because it allows those values to be emitted first in the stream so that as a large models stream down, we can start preloading as early as possible. We preload the module when they resolve but we lazily require them as they are referenced. * Emit module references where ever they occur This emits module references where ever they occur. In blocks or even directly in elements. * Don't special case the root row I originally did this so that a simple stream is also just plain JSON. However, since we might want to emit things like modules before the root module in the stream, this gets unnecessarily complicated. We could add this back as a special case if it's the first byte written but meh. * Update the protocol * Add test for using a module reference as a client component * Relax element type check Since Flight now accepts a module reference as returned by any bundler system, depending on the renderer running. We need to drastically relax the check to include all of them. We can add more as we discover them. * Move flow annotation Seems like our compiler is not happy with stripping this. * Some bookkeeping bug * Can't use the private field to check
1 parent 343d7a4 commit ffd8423

20 files changed

+367
-65
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ export type JSONValue =
4141

4242
const PENDING = 0;
4343
const RESOLVED_MODEL = 1;
44-
const INITIALIZED = 2;
45-
const ERRORED = 3;
44+
const RESOLVED_MODULE = 2;
45+
const INITIALIZED = 3;
46+
const ERRORED = 4;
4647

4748
type PendingChunk = {
4849
_status: 0,
@@ -56,21 +57,28 @@ type ResolvedModelChunk = {
5657
_response: Response,
5758
then(resolve: () => mixed): void,
5859
};
59-
type InitializedChunk<T> = {
60+
type ResolvedModuleChunk<T> = {
6061
_status: 2,
62+
_value: ModuleReference<T>,
63+
_response: Response,
64+
then(resolve: () => mixed): void,
65+
};
66+
type InitializedChunk<T> = {
67+
_status: 3,
6168
_value: T,
6269
_response: Response,
6370
then(resolve: () => mixed): void,
6471
};
6572
type ErroredChunk = {
66-
_status: 3,
73+
_status: 4,
6774
_value: Error,
6875
_response: Response,
6976
then(resolve: () => mixed): void,
7077
};
7178
type SomeChunk<T> =
7279
| PendingChunk
7380
| ResolvedModelChunk
81+
| ResolvedModuleChunk<T>
7482
| InitializedChunk<T>
7583
| ErroredChunk;
7684

@@ -105,6 +113,8 @@ function readChunk<T>(chunk: SomeChunk<T>): T {
105113
return chunk._value;
106114
case RESOLVED_MODEL:
107115
return initializeModelChunk(chunk);
116+
case RESOLVED_MODULE:
117+
return initializeModuleChunk(chunk);
108118
case PENDING:
109119
// eslint-disable-next-line no-throw-literal
110120
throw (chunk: Wakeable);
@@ -155,6 +165,13 @@ function createResolvedModelChunk(
155165
return new Chunk(RESOLVED_MODEL, value, response);
156166
}
157167

168+
function createResolvedModuleChunk<T>(
169+
response: Response,
170+
value: ModuleReference<T>,
171+
): ResolvedModuleChunk<T> {
172+
return new Chunk(RESOLVED_MODULE, value, response);
173+
}
174+
158175
function resolveModelChunk<T>(
159176
chunk: SomeChunk<T>,
160177
value: UninitializedModel,
@@ -170,6 +187,21 @@ function resolveModelChunk<T>(
170187
wakeChunk(listeners);
171188
}
172189

190+
function resolveModuleChunk<T>(
191+
chunk: SomeChunk<T>,
192+
value: ModuleReference<T>,
193+
): void {
194+
if (chunk._status !== PENDING) {
195+
// We already resolved. We didn't expect to see this.
196+
return;
197+
}
198+
const listeners = chunk._value;
199+
const resolvedChunk: ResolvedModuleChunk<T> = (chunk: any);
200+
resolvedChunk._status = RESOLVED_MODULE;
201+
resolvedChunk._value = value;
202+
wakeChunk(listeners);
203+
}
204+
173205
function initializeModelChunk<T>(chunk: ResolvedModelChunk): T {
174206
const value: T = parseModel(chunk._response, chunk._value);
175207
const initializedChunk: InitializedChunk<T> = (chunk: any);
@@ -178,6 +210,14 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk): T {
178210
return value;
179211
}
180212

213+
function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): T {
214+
const value: T = requireModule(chunk._value);
215+
const initializedChunk: InitializedChunk<T> = (chunk: any);
216+
initializedChunk._status = INITIALIZED;
217+
initializedChunk._value = value;
218+
return value;
219+
}
220+
181221
// Report that any missing chunks in the model is now going to throw this
182222
// error upon read. Also notify any pending promises.
183223
export function reportGlobalError(response: Response, error: Error): void {
@@ -241,7 +281,7 @@ function createElement(type, key, props): React$Element<any> {
241281

242282
type UninitializedBlockPayload<Data> = [
243283
mixed,
244-
ModuleMetaData | SomeChunk<ModuleMetaData>,
284+
BlockRenderFunction<any, Data> | SomeChunk<BlockRenderFunction<any, Data>>,
245285
Data | SomeChunk<Data>,
246286
Response,
247287
];
@@ -250,14 +290,7 @@ function initializeBlock<Props, Data>(
250290
tuple: UninitializedBlockPayload<Data>,
251291
): BlockComponent<Props, Data> {
252292
// Require module first and then data. The ordering matters.
253-
const moduleMetaData: ModuleMetaData = readMaybeChunk(tuple[1]);
254-
const moduleReference: ModuleReference<
255-
BlockRenderFunction<Props, Data>,
256-
> = resolveModuleReference(moduleMetaData);
257-
// TODO: Do this earlier, as the chunk is resolved.
258-
preloadModule(moduleReference);
259-
260-
const moduleExport = requireModule(moduleReference);
293+
const moduleExport = readMaybeChunk(tuple[1]);
261294

262295
// The ordering here is important because this call might suspend.
263296
// We don't want that to prevent the module graph for being initialized.
@@ -363,6 +396,28 @@ export function resolveModel(
363396
}
364397
}
365398

399+
export function resolveModule(
400+
response: Response,
401+
id: number,
402+
model: UninitializedModel,
403+
): void {
404+
const chunks = response._chunks;
405+
const chunk = chunks.get(id);
406+
const moduleMetaData: ModuleMetaData = parseModel(response, model);
407+
const moduleReference = resolveModuleReference(moduleMetaData);
408+
409+
// TODO: Add an option to encode modules that are lazy loaded.
410+
// For now we preload all modules as early as possible since it's likely
411+
// that we'll need them.
412+
preloadModule(moduleReference);
413+
414+
if (!chunk) {
415+
chunks.set(id, createResolvedModuleChunk(response, moduleReference));
416+
} else {
417+
resolveModuleChunk(chunk, moduleReference);
418+
}
419+
}
420+
366421
export function resolveError(
367422
response: Response,
368423
id: number,

packages/react-client/src/ReactFlightClientStream.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import type {Response} from './ReactFlightClientHostConfigStream';
1111

1212
import {
13+
resolveModule,
1314
resolveModel,
1415
resolveError,
1516
createResponse as createResponseBase,
@@ -39,6 +40,13 @@ function processFullRow(response: Response, row: string): void {
3940
resolveModel(response, id, json);
4041
return;
4142
}
43+
case 'M': {
44+
const colon = row.indexOf(':', 1);
45+
const id = parseInt(row.substring(1, colon), 16);
46+
const json = row.substring(colon + 1);
47+
resolveModule(response, id, json);
48+
return;
49+
}
4250
case 'E': {
4351
const colon = row.indexOf(':', 1);
4452
const id = parseInt(row.substring(1, colon), 16);
@@ -48,9 +56,9 @@ function processFullRow(response: Response, row: string): void {
4856
return;
4957
}
5058
default: {
51-
// Assume this is the root model.
52-
resolveModel(response, 0, row);
53-
return;
59+
throw new Error(
60+
"Error parsing the data. It's probably an error code or network corruption.",
61+
);
5462
}
5563
}
5664
}

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,29 @@ describe('ReactFlight', () => {
5353
};
5454
});
5555

56+
function moduleReference(value) {
57+
return {
58+
$$typeof: Symbol.for('react.module.reference'),
59+
value: value,
60+
};
61+
}
62+
5663
function block(render, load) {
5764
if (load === undefined) {
5865
return () => {
59-
return ReactNoopFlightServerRuntime.serverBlockNoData(render);
66+
return ReactNoopFlightServerRuntime.serverBlockNoData(
67+
moduleReference(render),
68+
);
6069
};
6170
}
6271
return function(...args) {
6372
const curriedLoad = () => {
6473
return load(...args);
6574
};
66-
return ReactNoopFlightServerRuntime.serverBlock(render, curriedLoad);
75+
return ReactNoopFlightServerRuntime.serverBlock(
76+
moduleReference(render),
77+
curriedLoad,
78+
);
6779
};
6880
}
6981

@@ -97,6 +109,35 @@ describe('ReactFlight', () => {
97109
});
98110
});
99111

112+
it('can render a client component using a module reference and render there', () => {
113+
function UserClient(props) {
114+
return (
115+
<span>
116+
{props.greeting}, {props.name}
117+
</span>
118+
);
119+
}
120+
const User = moduleReference(UserClient);
121+
122+
function Greeting({firstName, lastName}) {
123+
return <User greeting="Hello" name={firstName + ' ' + lastName} />;
124+
}
125+
126+
const model = {
127+
greeting: <Greeting firstName="Seb" lastName="Smith" />,
128+
};
129+
130+
const transport = ReactNoopFlightServer.render(model);
131+
132+
act(() => {
133+
const rootModel = ReactNoopFlightClient.read(transport);
134+
const greeting = rootModel.greeting;
135+
ReactNoop.render(greeting);
136+
});
137+
138+
expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
139+
});
140+
100141
if (ReactFeatureFlags.enableBlocksAPI) {
101142
it('can transfer a Block to the client and render there, without data', () => {
102143
function User(props, data) {

packages/react-noop-renderer/src/ReactNoopFlightServer.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,14 @@ const ReactNoopFlightServer = ReactFlightServer({
4242
formatChunk(type: string, props: Object): Uint8Array {
4343
return Buffer.from(JSON.stringify({type, props}), 'utf8');
4444
},
45-
resolveModuleMetaData(config: void, renderFn: Function) {
46-
return saveModule(renderFn);
45+
isModuleReference(reference: Object): boolean {
46+
return reference.$$typeof === Symbol.for('react.module.reference');
47+
},
48+
resolveModuleMetaData(
49+
config: void,
50+
reference: {$$typeof: Symbol, value: any},
51+
) {
52+
return saveModule(reference.value);
4753
},
4854
});
4955

0 commit comments

Comments
 (0)