Skip to content
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

[Flight] Integrate Blocks into Flight #18371

Merged
merged 6 commits into from
Mar 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
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
147 changes: 114 additions & 33 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,25 @@
* @flow
*/

import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
import type {BlockComponent, BlockRenderFunction} from 'react/src/ReactBlock';
import type {LazyComponent} from 'react/src/ReactLazy';

// import type {
// ModuleReference,
// ModuleMetaData,
// } from './ReactFlightClientHostConfig';
import type {
ModuleReference,
ModuleMetaData,
} from './ReactFlightClientHostConfig';

// import {
// resolveModuleReference,
// preloadModule,
// requireModule,
// } from './ReactFlightClientHostConfig';
import {
resolveModuleReference,
preloadModule,
requireModule,
} from './ReactFlightClientHostConfig';

import {
REACT_LAZY_TYPE,
REACT_BLOCK_TYPE,
REACT_ELEMENT_TYPE,
} from 'shared/ReactSymbols';

export type ReactModelRoot<T> = {|
model: T,
Expand All @@ -32,40 +39,43 @@ export type JSONValue =
| {[key: string]: JSONValue}
| Array<JSONValue>;

const isArray = Array.isArray;

const PENDING = 0;
const RESOLVED = 1;
const ERRORED = 2;

const CHUNK_TYPE = Symbol('flight.chunk');

type PendingChunk = {|
$$typeof: Symbol,
status: 0,
value: Promise<void>,
resolve: () => void,
|};
type ResolvedChunk = {|
type ResolvedChunk<T> = {|
$$typeof: Symbol,
status: 1,
value: mixed,
value: T,
resolve: null,
|};
type ErroredChunk = {|
$$typeof: Symbol,
status: 2,
value: Error,
resolve: null,
|};
type Chunk = PendingChunk | ResolvedChunk | ErroredChunk;
type Chunk<T> = PendingChunk | ResolvedChunk<T> | ErroredChunk;

export type Response = {
partialRow: string,
modelRoot: ReactModelRoot<any>,
chunks: Map<number, Chunk>,
chunks: Map<number, Chunk<any>>,
};

export function createResponse(): Response {
let modelRoot: ReactModelRoot<any> = ({}: any);
let rootChunk: Chunk = createPendingChunk();
let rootChunk: Chunk<any> = createPendingChunk();
definePendingProperty(modelRoot, 'model', rootChunk);
let chunks: Map<number, Chunk> = new Map();
let chunks: Map<number, Chunk<any>> = new Map();
chunks.set(0, rootChunk);
let response = {
partialRow: '',
Expand All @@ -79,6 +89,7 @@ function createPendingChunk(): PendingChunk {
let resolve: () => void = (null: any);
let promise = new Promise(r => (resolve = r));
return {
$$typeof: CHUNK_TYPE,
status: PENDING,
value: promise,
resolve: resolve,
Expand All @@ -87,13 +98,14 @@ function createPendingChunk(): PendingChunk {

function createErrorChunk(error: Error): ErroredChunk {
return {
$$typeof: CHUNK_TYPE,
status: ERRORED,
value: error,
resolve: null,
};
}

function triggerErrorOnChunk(chunk: Chunk, error: Error): void {
function triggerErrorOnChunk<T>(chunk: Chunk<T>, error: Error): void {
if (chunk.status !== PENDING) {
// We already resolved. We didn't expect to see this.
return;
Expand All @@ -106,21 +118,22 @@ function triggerErrorOnChunk(chunk: Chunk, error: Error): void {
resolve();
}

function createResolvedChunk(value: mixed): ResolvedChunk {
function createResolvedChunk<T>(value: T): ResolvedChunk<T> {
return {
$$typeof: CHUNK_TYPE,
status: RESOLVED,
value: value,
resolve: null,
};
}

function resolveChunk(chunk: Chunk, value: mixed): void {
function resolveChunk<T>(chunk: Chunk<T>, value: T): void {
if (chunk.status !== PENDING) {
// We already resolved. We didn't expect to see this.
return;
}
let resolve = chunk.resolve;
let resolvedChunk: ResolvedChunk = (chunk: any);
let resolvedChunk: ResolvedChunk<T> = (chunk: any);
resolvedChunk.status = RESOLVED;
resolvedChunk.value = value;
resolvedChunk.resolve = null;
Expand All @@ -138,10 +151,23 @@ export function reportGlobalError(response: Response, error: Error): void {
});
}

function definePendingProperty(
function readMaybeChunk<T>(maybeChunk: Chunk<T> | T): T {
if ((maybeChunk: any).$$typeof !== CHUNK_TYPE) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've noticed maybeChunk is literally the User function in the Relay test. Is this intentional? We're missing some mocking that represents the module transport there?

Copy link
Collaborator Author

@sebmarkbage sebmarkbage Mar 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't intentional at first but I did notice it while writing the tests and decided to keep it. I think we should do some mocking here but we don't have it wired up yet in www so we're not sure what we're simulating yet. Once we have it set up we can simulate that here in the mocks.

Also, the tests are far from sufficient yet. Needs more async stuff. I need to write more but don't want to block on it.

// $FlowFixMe
return maybeChunk;
}
let chunk: Chunk<T> = (maybeChunk: any);
if (chunk.status === RESOLVED) {
return chunk.value;
} else {
throw chunk.value;
}
}

function definePendingProperty<T>(
object: Object,
key: string,
chunk: Chunk,
chunk: Chunk<T>,
): void {
Object.defineProperty(object, key, {
configurable: false,
Expand Down Expand Up @@ -197,6 +223,55 @@ function createElement(type, key, props): React$Element<any> {
return element;
}

type UninitializedBlockPayload<Data> = [
mixed,
ModuleMetaData | Chunk<ModuleMetaData>,
Data | Chunk<Data>,
];

type Thenable<T> = {
then(resolve: (T) => mixed, reject?: (mixed) => mixed): Thenable<any>,
};

function initializeBlock<Props, Data>(
tuple: UninitializedBlockPayload<Data>,
): BlockComponent<Props, Data> {
// Require module first and then data. The ordering matters.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we test correct ordering?

Copy link
Collaborator Author

@sebmarkbage sebmarkbage Mar 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. Yea. We could test a side-effect in the require somehow while data is still blocked. There's no module function that runs in the tests atm though so needs some infra or hook into the mocks somehow.

Copy link
Collaborator Author

@sebmarkbage sebmarkbage Mar 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also don't have any async tests yet for noop which is where we should be adding tests like this.

let moduleMetaData: ModuleMetaData = readMaybeChunk(tuple[1]);
let moduleReference: ModuleReference<
BlockRenderFunction<Props, Data>,
> = resolveModuleReference(moduleMetaData);
// TODO: Do this earlier, as the chunk is resolved.
preloadModule(moduleReference);
Copy link
Collaborator

@gaearon gaearon Mar 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment before implementation of preloadModule says

 Returning null means [...]

but retvalue is never used. Is it outdated?

Also, is it completely unobservable? I deleted this and nothing failed.

Copy link
Collaborator Author

@sebmarkbage sebmarkbage Mar 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is outdated.

It's not unobservable because of this: https://github.com/facebook/react/blob/master/packages/react-flight-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js#L60-L61

Lots more tests needs to be written at some point. None of the tests cover that chunks are being loaded.


let moduleExport = requireModule(moduleReference);

// The ordering here is important because this call might suspend.
// We don't want that to prevent the module graph for being initialized.
let data: Data = readMaybeChunk(tuple[2]);

return {
$$typeof: REACT_BLOCK_TYPE,
_status: -1,
_data: data,
_render: moduleExport,
};
}

function createLazyBlock<Props, Data>(
tuple: UninitializedBlockPayload<Data>,
): LazyComponent<BlockComponent<Props, Data>, UninitializedBlockPayload<Data>> {
let lazyType: LazyComponent<
BlockComponent<Props, Data>,
UninitializedBlockPayload<Data>,
> = {
$$typeof: REACT_LAZY_TYPE,
_payload: tuple,
_init: initializeBlock,
};
return lazyType;
}

export function parseModelFromJSON(
response: Response,
targetObj: Object,
Expand All @@ -217,20 +292,26 @@ export function parseModelFromJSON(
if (!chunk) {
chunk = createPendingChunk();
chunks.set(id, chunk);
} else if (chunk.status === RESOLVED) {
return chunk.value;
}
definePendingProperty(targetObj, key, chunk);
return undefined;
return chunk;
}
}
if (value === '@') {
return REACT_BLOCK_TYPE;
}
}
if (isArray(value)) {
if (typeof value === 'object' && value !== null) {
let tuple: [mixed, mixed, mixed, mixed] = (value: any);
if (tuple[0] === REACT_ELEMENT_TYPE) {
// TODO: Consider having React just directly accept these arrays as elements.
// Or even change the ReactElement type to be an array.
return createElement(tuple[1], tuple[2], tuple[3]);
switch (tuple[0]) {
case REACT_ELEMENT_TYPE: {
// TODO: Consider having React just directly accept these arrays as elements.
// Or even change the ReactElement type to be an array.
return createElement(tuple[1], tuple[2], tuple[3]);
}
case REACT_BLOCK_TYPE: {
// TODO: Consider having React just directly accept these arrays as blocks.
return createLazyBlock((tuple: any));
}
}
}
return value;
Expand Down
64 changes: 61 additions & 3 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@

'use strict';

const ReactFeatureFlags = require('shared/ReactFeatureFlags');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@acdlite Did you say we need to put these inline now? I don't remember why.

Copy link
Collaborator Author

@sebmarkbage sebmarkbage Mar 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copy-pasted this from elsewhere so if we do, we should codemod the rest of them too.


let act;
let React;
let ReactNoop;
let ReactNoopFlightServer;
let ReactNoopFlightClient;

Expand All @@ -19,24 +23,78 @@ describe('ReactFlight', () => {
jest.resetModules();

React = require('react');
ReactNoop = require('react-noop-renderer');
ReactNoopFlightServer = require('react-noop-renderer/flight-server');
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
act = ReactNoop.act;
});

it('can resolve a model', () => {
function block(query, render) {
return function(...args) {
let curriedQuery = () => {
return query(...args);
};
return [Symbol.for('react.server.block'), render, curriedQuery];
};
}

it('can render a server component', () => {
function Bar({text}) {
return text.toUpperCase();
}
function Foo() {
return {
bar: [<Bar text="a" />, <Bar text="b" />],
bar: (
<div>
<Bar text="a" />, <Bar text="b" />
</div>
),
};
}
let transport = ReactNoopFlightServer.render({
foo: <Foo />,
});
let root = ReactNoopFlightClient.read(transport);
let model = root.model;
expect(model).toEqual({foo: {bar: ['A', 'B']}});
expect(model).toEqual({
foo: {
bar: (
<div>
{'A'}
{', '}
{'B'}
</div>
),
},
});
});

if (ReactFeatureFlags.enableBlocksAPI) {
it('can transfer a Block to the client and render there', () => {
function Query(firstName, lastName) {
return {name: firstName + ' ' + lastName};
}
function User(props, data) {
return (
<span>
{props.greeting}, {data.name}
</span>
);
}
let loadUser = block(Query, User);
let model = {
User: loadUser('Seb', 'Smith'),
};

let transport = ReactNoopFlightServer.render(model);
let root = ReactNoopFlightClient.read(transport);

act(() => {
let UserClient = root.model.User;
ReactNoop.render(<UserClient greeting="Hello" />);
});

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