Skip to content

[Flight] Add support for Module References in transport protocol #20121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Oct 30, 2020
81 changes: 68 additions & 13 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ export type JSONValue =

const PENDING = 0;
const RESOLVED_MODEL = 1;
const INITIALIZED = 2;
const ERRORED = 3;
const RESOLVED_MODULE = 2;
const INITIALIZED = 3;
const ERRORED = 4;

type PendingChunk = {
_status: 0,
Expand All @@ -56,21 +57,28 @@ type ResolvedModelChunk = {
_response: Response,
then(resolve: () => mixed): void,
};
type InitializedChunk<T> = {
type ResolvedModuleChunk<T> = {
_status: 2,
_value: ModuleReference<T>,
_response: Response,
then(resolve: () => mixed): void,
};
type InitializedChunk<T> = {
_status: 3,
_value: T,
_response: Response,
then(resolve: () => mixed): void,
};
type ErroredChunk = {
_status: 3,
_status: 4,
_value: Error,
_response: Response,
then(resolve: () => mixed): void,
};
type SomeChunk<T> =
| PendingChunk
| ResolvedModelChunk
| ResolvedModuleChunk<T>
| InitializedChunk<T>
| ErroredChunk;

Expand Down Expand Up @@ -105,6 +113,8 @@ function readChunk<T>(chunk: SomeChunk<T>): T {
return chunk._value;
case RESOLVED_MODEL:
return initializeModelChunk(chunk);
case RESOLVED_MODULE:
return initializeModuleChunk(chunk);
case PENDING:
// eslint-disable-next-line no-throw-literal
throw (chunk: Wakeable);
Expand Down Expand Up @@ -155,6 +165,13 @@ function createResolvedModelChunk(
return new Chunk(RESOLVED_MODEL, value, response);
}

function createResolvedModuleChunk<T>(
response: Response,
value: ModuleReference<T>,
): ResolvedModuleChunk<T> {
return new Chunk(RESOLVED_MODULE, value, response);
}

function resolveModelChunk<T>(
chunk: SomeChunk<T>,
value: UninitializedModel,
Expand All @@ -170,6 +187,21 @@ function resolveModelChunk<T>(
wakeChunk(listeners);
}

function resolveModuleChunk<T>(
chunk: SomeChunk<T>,
value: ModuleReference<T>,
): void {
if (chunk._status !== PENDING) {
// We already resolved. We didn't expect to see this.
return;
}
const listeners = chunk._value;
const resolvedChunk: ResolvedModuleChunk<T> = (chunk: any);
resolvedChunk._status = RESOLVED_MODULE;
resolvedChunk._value = value;
wakeChunk(listeners);
}

function initializeModelChunk<T>(chunk: ResolvedModelChunk): T {
const value: T = parseModel(chunk._response, chunk._value);
const initializedChunk: InitializedChunk<T> = (chunk: any);
Expand All @@ -178,6 +210,14 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk): T {
return value;
}

function initializeModuleChunk<T>(chunk: ResolvedModuleChunk<T>): T {
const value: T = requireModule(chunk._value);
const initializedChunk: InitializedChunk<T> = (chunk: any);
initializedChunk._status = INITIALIZED;
initializedChunk._value = value;
return value;
}

// Report that any missing chunks in the model is now going to throw this
// error upon read. Also notify any pending promises.
export function reportGlobalError(response: Response, error: Error): void {
Expand Down Expand Up @@ -241,7 +281,7 @@ function createElement(type, key, props): React$Element<any> {

type UninitializedBlockPayload<Data> = [
mixed,
ModuleMetaData | SomeChunk<ModuleMetaData>,
BlockRenderFunction<any, Data> | SomeChunk<BlockRenderFunction<any, Data>>,
Data | SomeChunk<Data>,
Response,
];
Expand All @@ -250,14 +290,7 @@ function initializeBlock<Props, Data>(
tuple: UninitializedBlockPayload<Data>,
): BlockComponent<Props, Data> {
// Require module first and then data. The ordering matters.
const moduleMetaData: ModuleMetaData = readMaybeChunk(tuple[1]);
const moduleReference: ModuleReference<
BlockRenderFunction<Props, Data>,
> = resolveModuleReference(moduleMetaData);
// TODO: Do this earlier, as the chunk is resolved.
preloadModule(moduleReference);

const moduleExport = requireModule(moduleReference);
const moduleExport = readMaybeChunk(tuple[1]);

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

export function resolveModule(
response: Response,
id: number,
model: UninitializedModel,
): void {
const chunks = response._chunks;
const chunk = chunks.get(id);
const moduleMetaData: ModuleMetaData = parseModel(response, model);
const moduleReference = resolveModuleReference(moduleMetaData);

// TODO: Add an option to encode modules that are lazy loaded.
// For now we preload all modules as early as possible since it's likely
// that we'll need them.
preloadModule(moduleReference);

if (!chunk) {
chunks.set(id, createResolvedModuleChunk(response, moduleReference));
} else {
resolveModuleChunk(chunk, moduleReference);
}
}

export function resolveError(
response: Response,
id: number,
Expand Down
14 changes: 11 additions & 3 deletions packages/react-client/src/ReactFlightClientStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import type {Response} from './ReactFlightClientHostConfigStream';

import {
resolveModule,
resolveModel,
resolveError,
createResponse as createResponseBase,
Expand Down Expand Up @@ -39,6 +40,13 @@ function processFullRow(response: Response, row: string): void {
resolveModel(response, id, json);
return;
}
case 'M': {
const colon = row.indexOf(':', 1);
const id = parseInt(row.substring(1, colon), 16);
const json = row.substring(colon + 1);
resolveModule(response, id, json);
return;
}
case 'E': {
const colon = row.indexOf(':', 1);
const id = parseInt(row.substring(1, colon), 16);
Expand All @@ -48,9 +56,9 @@ function processFullRow(response: Response, row: string): void {
return;
}
default: {
// Assume this is the root model.
resolveModel(response, 0, row);
return;
throw new Error(
"Error parsing the data. It's probably an error code or network corruption.",
);
}
}
}
Expand Down
45 changes: 43 additions & 2 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,29 @@ describe('ReactFlight', () => {
};
});

function moduleReference(value) {
return {
$$typeof: Symbol.for('react.module.reference'),
value: value,
};
}

function block(render, load) {
if (load === undefined) {
return () => {
return ReactNoopFlightServerRuntime.serverBlockNoData(render);
return ReactNoopFlightServerRuntime.serverBlockNoData(
moduleReference(render),
);
};
}
return function(...args) {
const curriedLoad = () => {
return load(...args);
};
return ReactNoopFlightServerRuntime.serverBlock(render, curriedLoad);
return ReactNoopFlightServerRuntime.serverBlock(
moduleReference(render),
curriedLoad,
);
};
}

Expand Down Expand Up @@ -97,6 +109,35 @@ describe('ReactFlight', () => {
});
});

it('can render a client component using a module reference and render there', () => {
function UserClient(props) {
return (
<span>
{props.greeting}, {props.name}
</span>
);
}
const User = moduleReference(UserClient);

function Greeting({firstName, lastName}) {
return <User greeting="Hello" name={firstName + ' ' + lastName} />;
}

const model = {
greeting: <Greeting firstName="Seb" lastName="Smith" />,
};

const transport = ReactNoopFlightServer.render(model);

act(() => {
const rootModel = ReactNoopFlightClient.read(transport);
const greeting = rootModel.greeting;
ReactNoop.render(greeting);
});

expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
});

if (ReactFeatureFlags.enableBlocksAPI) {
it('can transfer a Block to the client and render there, without data', () => {
function User(props, data) {
Expand Down
10 changes: 8 additions & 2 deletions packages/react-noop-renderer/src/ReactNoopFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,14 @@ const ReactNoopFlightServer = ReactFlightServer({
formatChunk(type: string, props: Object): Uint8Array {
return Buffer.from(JSON.stringify({type, props}), 'utf8');
},
resolveModuleMetaData(config: void, renderFn: Function) {
return saveModule(renderFn);
isModuleReference(reference: Object): boolean {
return reference.$$typeof === Symbol.for('react.module.reference');
},
resolveModuleMetaData(
config: void,
reference: {$$typeof: Symbol, value: any},
) {
return saveModule(reference.value);
},
});

Expand Down
Loading