mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
a317bd033f
* Flip the arguments of Blocks and make the query optional * Rename Query to Load
362 lines
9.0 KiB
JavaScript
362 lines
9.0 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its 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 {BlockComponent, BlockRenderFunction} from 'react/src/ReactBlock';
|
|
import type {LazyComponent} from 'react/src/ReactLazy';
|
|
|
|
import type {
|
|
ModuleReference,
|
|
ModuleMetaData,
|
|
} 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,
|
|
|};
|
|
|
|
export type JSONValue =
|
|
| number
|
|
| null
|
|
| boolean
|
|
| string
|
|
| {[key: string]: JSONValue}
|
|
| Array<JSONValue>;
|
|
|
|
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<T> = {|
|
|
$$typeof: Symbol,
|
|
status: 1,
|
|
value: T,
|
|
resolve: null,
|
|
|};
|
|
type ErroredChunk = {|
|
|
$$typeof: Symbol,
|
|
status: 2,
|
|
value: Error,
|
|
resolve: null,
|
|
|};
|
|
type Chunk<T> = PendingChunk | ResolvedChunk<T> | ErroredChunk;
|
|
|
|
export type Response = {
|
|
partialRow: string,
|
|
modelRoot: ReactModelRoot<any>,
|
|
chunks: Map<number, Chunk<any>>,
|
|
};
|
|
|
|
export function createResponse(): Response {
|
|
let modelRoot: ReactModelRoot<any> = ({}: any);
|
|
let rootChunk: Chunk<any> = createPendingChunk();
|
|
definePendingProperty(modelRoot, 'model', rootChunk);
|
|
let chunks: Map<number, Chunk<any>> = new Map();
|
|
chunks.set(0, rootChunk);
|
|
let response = {
|
|
partialRow: '',
|
|
modelRoot,
|
|
chunks: chunks,
|
|
};
|
|
return response;
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
function createErrorChunk(error: Error): ErroredChunk {
|
|
return {
|
|
$$typeof: CHUNK_TYPE,
|
|
status: ERRORED,
|
|
value: error,
|
|
resolve: null,
|
|
};
|
|
}
|
|
|
|
function triggerErrorOnChunk<T>(chunk: Chunk<T>, error: Error): void {
|
|
if (chunk.status !== PENDING) {
|
|
// We already resolved. We didn't expect to see this.
|
|
return;
|
|
}
|
|
let resolve = chunk.resolve;
|
|
let erroredChunk: ErroredChunk = (chunk: any);
|
|
erroredChunk.status = ERRORED;
|
|
erroredChunk.value = error;
|
|
erroredChunk.resolve = null;
|
|
resolve();
|
|
}
|
|
|
|
function createResolvedChunk<T>(value: T): ResolvedChunk<T> {
|
|
return {
|
|
$$typeof: CHUNK_TYPE,
|
|
status: RESOLVED,
|
|
value: value,
|
|
resolve: null,
|
|
};
|
|
}
|
|
|
|
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<T> = (chunk: any);
|
|
resolvedChunk.status = RESOLVED;
|
|
resolvedChunk.value = value;
|
|
resolvedChunk.resolve = null;
|
|
resolve();
|
|
}
|
|
|
|
// 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 {
|
|
response.chunks.forEach(chunk => {
|
|
// If this chunk was already resolved or errored, it won't
|
|
// trigger an error but if it wasn't then we need to
|
|
// because we won't be getting any new data to resolve it.
|
|
triggerErrorOnChunk(chunk, error);
|
|
});
|
|
}
|
|
|
|
function readMaybeChunk<T>(maybeChunk: Chunk<T> | T): T {
|
|
if (maybeChunk == null || (maybeChunk: any).$$typeof !== CHUNK_TYPE) {
|
|
// $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<T>,
|
|
): void {
|
|
Object.defineProperty(object, key, {
|
|
configurable: false,
|
|
enumerable: true,
|
|
get() {
|
|
if (chunk.status === RESOLVED) {
|
|
return chunk.value;
|
|
} else {
|
|
throw chunk.value;
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
function createElement(type, key, props): React$Element<any> {
|
|
const element: any = {
|
|
// This tag allows us to uniquely identify this as a React Element
|
|
$$typeof: REACT_ELEMENT_TYPE,
|
|
|
|
// Built-in properties that belong on the element
|
|
type: type,
|
|
key: key,
|
|
ref: null,
|
|
props: props,
|
|
|
|
// Record the component responsible for creating this element.
|
|
_owner: null,
|
|
};
|
|
if (__DEV__) {
|
|
// We don't really need to add any of these but keeping them for good measure.
|
|
// Unfortunately, _store is enumerable in jest matchers so for equality to
|
|
// work, I need to keep it or make _store non-enumerable in the other file.
|
|
element._store = {};
|
|
Object.defineProperty(element._store, 'validated', {
|
|
configurable: false,
|
|
enumerable: false,
|
|
writable: true,
|
|
value: true, // This element has already been validated on the server.
|
|
});
|
|
Object.defineProperty(element, '_self', {
|
|
configurable: false,
|
|
enumerable: false,
|
|
writable: false,
|
|
value: null,
|
|
});
|
|
Object.defineProperty(element, '_source', {
|
|
configurable: false,
|
|
enumerable: false,
|
|
writable: false,
|
|
value: null,
|
|
});
|
|
}
|
|
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.
|
|
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);
|
|
|
|
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,
|
|
key: string,
|
|
value: JSONValue,
|
|
): mixed {
|
|
if (typeof value === 'string') {
|
|
if (value[0] === '$') {
|
|
if (value === '$') {
|
|
return REACT_ELEMENT_TYPE;
|
|
} else if (value[1] === '$' || value[1] === '@') {
|
|
// This was an escaped string value.
|
|
return value.substring(1);
|
|
} else {
|
|
let id = parseInt(value.substring(1), 16);
|
|
let chunks = response.chunks;
|
|
let chunk = chunks.get(id);
|
|
if (!chunk) {
|
|
chunk = createPendingChunk();
|
|
chunks.set(id, chunk);
|
|
}
|
|
return chunk;
|
|
}
|
|
}
|
|
if (value === '@') {
|
|
return REACT_BLOCK_TYPE;
|
|
}
|
|
}
|
|
if (typeof value === 'object' && value !== null) {
|
|
let tuple: [mixed, mixed, mixed, mixed] = (value: any);
|
|
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;
|
|
}
|
|
|
|
export function resolveModelChunk<T>(
|
|
response: Response,
|
|
id: number,
|
|
model: T,
|
|
): void {
|
|
let chunks = response.chunks;
|
|
let chunk = chunks.get(id);
|
|
if (!chunk) {
|
|
chunks.set(id, createResolvedChunk(model));
|
|
} else {
|
|
resolveChunk(chunk, model);
|
|
}
|
|
}
|
|
|
|
export function resolveErrorChunk(
|
|
response: Response,
|
|
id: number,
|
|
message: string,
|
|
stack: string,
|
|
): void {
|
|
let error = new Error(message);
|
|
error.stack = stack;
|
|
let chunks = response.chunks;
|
|
let chunk = chunks.get(id);
|
|
if (!chunk) {
|
|
chunks.set(id, createErrorChunk(error));
|
|
} else {
|
|
triggerErrorOnChunk(chunk, error);
|
|
}
|
|
}
|
|
|
|
export function close(response: Response): void {
|
|
// In case there are any remaining unresolved chunks, they won't
|
|
// be resolved now. So we need to issue an error to those.
|
|
// Ideally we should be able to early bail out if we kept a
|
|
// ref count of pending chunks.
|
|
reportGlobalError(response, new Error('Connection closed.'));
|
|
}
|
|
|
|
export function getModelRoot<T>(response: Response): ReactModelRoot<T> {
|
|
return response.modelRoot;
|
|
}
|