2019-10-30 05:45:47 +08:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
|
|
|
|
|
2020-03-19 03:18:34 +08:00
|
|
|
import type {
|
|
|
|
|
Destination,
|
|
|
|
|
Chunk,
|
|
|
|
|
BundlerConfig,
|
|
|
|
|
// ModuleReference,
|
|
|
|
|
// ModuleMetaData,
|
|
|
|
|
} from './ReactFlightServerConfig';
|
2019-10-30 05:45:47 +08:00
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
scheduleWork,
|
|
|
|
|
beginWriting,
|
|
|
|
|
writeChunk,
|
|
|
|
|
completeWriting,
|
|
|
|
|
flushBuffered,
|
|
|
|
|
close,
|
2020-03-11 05:55:04 +08:00
|
|
|
processModelChunk,
|
|
|
|
|
processErrorChunk,
|
2020-03-19 03:18:34 +08:00
|
|
|
// resolveModuleMetaData,
|
2020-03-11 05:55:04 +08:00
|
|
|
} from './ReactFlightServerConfig';
|
2020-03-12 00:48:02 +08:00
|
|
|
|
2019-10-30 05:45:47 +08:00
|
|
|
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
|
|
|
|
|
|
2020-03-11 05:55:04 +08:00
|
|
|
type ReactJSONValue =
|
2019-10-30 05:45:47 +08:00
|
|
|
| string
|
|
|
|
|
| boolean
|
|
|
|
|
| number
|
|
|
|
|
| null
|
2020-03-11 05:55:04 +08:00
|
|
|
| Array<ReactModel>
|
2019-10-30 05:45:47 +08:00
|
|
|
| ReactModelObject;
|
|
|
|
|
|
2020-03-11 05:55:04 +08:00
|
|
|
export type ReactModel =
|
|
|
|
|
| React$Element<any>
|
2019-10-30 05:45:47 +08:00
|
|
|
| string
|
|
|
|
|
| boolean
|
|
|
|
|
| number
|
|
|
|
|
| null
|
2020-03-11 05:55:04 +08:00
|
|
|
| Iterable<ReactModel>
|
2019-10-30 05:45:47 +08:00
|
|
|
| ReactModelObject;
|
|
|
|
|
|
2020-03-11 05:55:04 +08:00
|
|
|
type ReactModelObject = {+[key: string]: ReactModel};
|
2019-10-30 05:45:47 +08:00
|
|
|
|
2019-11-07 01:48:34 +08:00
|
|
|
type Segment = {
|
|
|
|
|
id: number,
|
|
|
|
|
model: ReactModel,
|
|
|
|
|
ping: () => void,
|
|
|
|
|
};
|
|
|
|
|
|
2020-03-11 05:55:04 +08:00
|
|
|
export type Request = {
|
2019-10-30 05:45:47 +08:00
|
|
|
destination: Destination,
|
2020-03-19 03:18:34 +08:00
|
|
|
bundlerConfig: BundlerConfig,
|
2019-11-07 01:48:34 +08:00
|
|
|
nextChunkId: number,
|
|
|
|
|
pendingChunks: number,
|
|
|
|
|
pingedSegments: Array<Segment>,
|
2020-03-11 05:55:04 +08:00
|
|
|
completedJSONChunks: Array<Chunk>,
|
|
|
|
|
completedErrorChunks: Array<Chunk>,
|
2019-10-30 05:45:47 +08:00
|
|
|
flowing: boolean,
|
2019-11-07 01:48:34 +08:00
|
|
|
toJSON: (key: string, value: ReactModel) => ReactJSONValue,
|
2019-10-30 05:45:47 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function createRequest(
|
|
|
|
|
model: ReactModel,
|
|
|
|
|
destination: Destination,
|
2020-03-19 03:18:34 +08:00
|
|
|
bundlerConfig: BundlerConfig,
|
2020-03-11 05:55:04 +08:00
|
|
|
): Request {
|
2019-11-07 01:48:34 +08:00
|
|
|
let pingedSegments = [];
|
|
|
|
|
let request = {
|
|
|
|
|
destination,
|
2020-03-19 03:18:34 +08:00
|
|
|
bundlerConfig,
|
2019-11-07 01:48:34 +08:00
|
|
|
nextChunkId: 0,
|
|
|
|
|
pendingChunks: 0,
|
|
|
|
|
pingedSegments: pingedSegments,
|
|
|
|
|
completedJSONChunks: [],
|
|
|
|
|
completedErrorChunks: [],
|
|
|
|
|
flowing: false,
|
|
|
|
|
toJSON: (key: string, value: ReactModel) =>
|
|
|
|
|
resolveModelToJSON(request, value),
|
|
|
|
|
};
|
|
|
|
|
request.pendingChunks++;
|
|
|
|
|
let rootSegment = createSegment(request, model);
|
|
|
|
|
pingedSegments.push(rootSegment);
|
|
|
|
|
return request;
|
2019-10-30 05:45:47 +08:00
|
|
|
}
|
|
|
|
|
|
2019-11-07 01:48:34 +08:00
|
|
|
function attemptResolveModelComponent(element: React$Element<any>): ReactModel {
|
|
|
|
|
let type = element.type;
|
|
|
|
|
let props = element.props;
|
|
|
|
|
if (typeof type === 'function') {
|
|
|
|
|
// This is a nested view model.
|
|
|
|
|
return type(props);
|
|
|
|
|
} else if (typeof type === 'string') {
|
|
|
|
|
// This is a host element. E.g. HTML.
|
2020-03-12 00:48:02 +08:00
|
|
|
return [REACT_ELEMENT_TYPE, type, element.key, element.props];
|
2019-11-07 01:48:34 +08:00
|
|
|
} else {
|
|
|
|
|
throw new Error('Unsupported type.');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-11 05:55:04 +08:00
|
|
|
function pingSegment(request: Request, segment: Segment): void {
|
2019-11-07 01:48:34 +08:00
|
|
|
let pingedSegments = request.pingedSegments;
|
|
|
|
|
pingedSegments.push(segment);
|
|
|
|
|
if (pingedSegments.length === 1) {
|
|
|
|
|
scheduleWork(() => performWork(request));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-11 05:55:04 +08:00
|
|
|
function createSegment(request: Request, model: ReactModel): Segment {
|
2019-11-07 01:48:34 +08:00
|
|
|
let id = request.nextChunkId++;
|
|
|
|
|
let segment = {
|
|
|
|
|
id,
|
|
|
|
|
model,
|
|
|
|
|
ping: () => pingSegment(request, segment),
|
|
|
|
|
};
|
|
|
|
|
return segment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function serializeIDRef(id: number): string {
|
|
|
|
|
return '$' + id.toString(16);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escapeStringValue(value: string): string {
|
|
|
|
|
if (value[0] === '$') {
|
|
|
|
|
// We need to escape $ prefixed strings since we use that to encode
|
2020-03-12 00:48:02 +08:00
|
|
|
// references to IDs and as a special symbol value.
|
2019-11-07 01:48:34 +08:00
|
|
|
return '$' + value;
|
|
|
|
|
} else {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-11 05:55:04 +08:00
|
|
|
export function resolveModelToJSON(
|
|
|
|
|
request: Request,
|
2019-11-07 01:48:34 +08:00
|
|
|
value: ReactModel,
|
|
|
|
|
): ReactJSONValue {
|
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
|
return escapeStringValue(value);
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-12 00:48:02 +08:00
|
|
|
if (value === REACT_ELEMENT_TYPE) {
|
|
|
|
|
return '$';
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-07 01:48:34 +08:00
|
|
|
while (
|
|
|
|
|
typeof value === 'object' &&
|
|
|
|
|
value !== null &&
|
|
|
|
|
value.$$typeof === REACT_ELEMENT_TYPE
|
|
|
|
|
) {
|
2019-10-30 05:45:47 +08:00
|
|
|
let element: React$Element<any> = (value: any);
|
2019-11-07 01:48:34 +08:00
|
|
|
try {
|
|
|
|
|
value = attemptResolveModelComponent(element);
|
|
|
|
|
} catch (x) {
|
|
|
|
|
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
|
|
|
|
|
// Something suspended, we'll need to create a new segment and resolve it later.
|
|
|
|
|
request.pendingChunks++;
|
|
|
|
|
let newSegment = createSegment(request, element);
|
|
|
|
|
let ping = newSegment.ping;
|
|
|
|
|
x.then(ping, ping);
|
|
|
|
|
return serializeIDRef(newSegment.id);
|
|
|
|
|
} else {
|
|
|
|
|
request.pendingChunks++;
|
|
|
|
|
let errorId = request.nextChunkId++;
|
|
|
|
|
emitErrorChunk(request, errorId, x);
|
|
|
|
|
return serializeIDRef(errorId);
|
|
|
|
|
}
|
2019-10-30 05:45:47 +08:00
|
|
|
}
|
|
|
|
|
}
|
2019-11-07 01:48:34 +08:00
|
|
|
|
2019-10-30 05:45:47 +08:00
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-11 05:55:04 +08:00
|
|
|
function emitErrorChunk(request: Request, id: number, error: mixed): void {
|
2019-11-07 01:48:34 +08:00
|
|
|
// TODO: We should not leak error messages to the client in prod.
|
|
|
|
|
// Give this an error code instead and log on the server.
|
|
|
|
|
// We can serialize the error in DEV as a convenience.
|
|
|
|
|
let message;
|
|
|
|
|
let stack = '';
|
|
|
|
|
try {
|
|
|
|
|
if (error instanceof Error) {
|
|
|
|
|
message = '' + error.message;
|
|
|
|
|
stack = '' + error.stack;
|
|
|
|
|
} else {
|
|
|
|
|
message = 'Error: ' + (error: any);
|
|
|
|
|
}
|
|
|
|
|
} catch (x) {
|
|
|
|
|
message = 'An error occurred but serializing the error message failed.';
|
|
|
|
|
}
|
2020-03-11 05:55:04 +08:00
|
|
|
|
|
|
|
|
let processedChunk = processErrorChunk(request, id, message, stack);
|
|
|
|
|
request.completedErrorChunks.push(processedChunk);
|
2019-11-07 01:48:34 +08:00
|
|
|
}
|
|
|
|
|
|
2020-03-11 05:55:04 +08:00
|
|
|
function retrySegment(request: Request, segment: Segment): void {
|
2019-11-07 01:48:34 +08:00
|
|
|
let value = segment.model;
|
|
|
|
|
try {
|
|
|
|
|
while (
|
|
|
|
|
typeof value === 'object' &&
|
|
|
|
|
value !== null &&
|
|
|
|
|
value.$$typeof === REACT_ELEMENT_TYPE
|
|
|
|
|
) {
|
|
|
|
|
// If this is a nested model, there's no need to create another chunk,
|
|
|
|
|
// we can reuse the existing one and try again.
|
|
|
|
|
let element: React$Element<any> = (value: any);
|
|
|
|
|
segment.model = element;
|
|
|
|
|
value = attemptResolveModelComponent(element);
|
|
|
|
|
}
|
2020-03-11 05:55:04 +08:00
|
|
|
let processedChunk = processModelChunk(request, segment.id, value);
|
|
|
|
|
request.completedJSONChunks.push(processedChunk);
|
2019-11-07 01:48:34 +08:00
|
|
|
} catch (x) {
|
|
|
|
|
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
|
|
|
|
|
// Something suspended again, let's pick it back up later.
|
|
|
|
|
let ping = segment.ping;
|
|
|
|
|
x.then(ping, ping);
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
// This errored, we need to serialize this error to the
|
|
|
|
|
emitErrorChunk(request, segment.id, x);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-11 05:55:04 +08:00
|
|
|
function performWork(request: Request): void {
|
2019-11-07 01:48:34 +08:00
|
|
|
let pingedSegments = request.pingedSegments;
|
|
|
|
|
request.pingedSegments = [];
|
|
|
|
|
for (let i = 0; i < pingedSegments.length; i++) {
|
|
|
|
|
let segment = pingedSegments[i];
|
|
|
|
|
retrySegment(request, segment);
|
|
|
|
|
}
|
2019-10-30 05:45:47 +08:00
|
|
|
if (request.flowing) {
|
|
|
|
|
flushCompletedChunks(request);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-07 01:48:34 +08:00
|
|
|
let reentrant = false;
|
2020-03-11 05:55:04 +08:00
|
|
|
function flushCompletedChunks(request: Request): void {
|
2019-11-07 01:48:34 +08:00
|
|
|
if (reentrant) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
reentrant = true;
|
2019-10-30 05:45:47 +08:00
|
|
|
let destination = request.destination;
|
|
|
|
|
beginWriting(destination);
|
|
|
|
|
try {
|
2019-11-07 01:48:34 +08:00
|
|
|
let jsonChunks = request.completedJSONChunks;
|
|
|
|
|
let i = 0;
|
|
|
|
|
for (; i < jsonChunks.length; i++) {
|
|
|
|
|
request.pendingChunks--;
|
|
|
|
|
let chunk = jsonChunks[i];
|
|
|
|
|
if (!writeChunk(destination, chunk)) {
|
|
|
|
|
request.flowing = false;
|
|
|
|
|
i++;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
jsonChunks.splice(0, i);
|
|
|
|
|
let errorChunks = request.completedErrorChunks;
|
|
|
|
|
i = 0;
|
|
|
|
|
for (; i < errorChunks.length; i++) {
|
|
|
|
|
request.pendingChunks--;
|
|
|
|
|
let chunk = errorChunks[i];
|
|
|
|
|
if (!writeChunk(destination, chunk)) {
|
|
|
|
|
request.flowing = false;
|
|
|
|
|
i++;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2019-10-30 05:45:47 +08:00
|
|
|
}
|
2019-11-07 01:48:34 +08:00
|
|
|
errorChunks.splice(0, i);
|
2019-10-30 05:45:47 +08:00
|
|
|
} finally {
|
2019-11-07 01:48:34 +08:00
|
|
|
reentrant = false;
|
2019-10-30 05:45:47 +08:00
|
|
|
completeWriting(destination);
|
|
|
|
|
}
|
2019-11-07 01:48:34 +08:00
|
|
|
flushBuffered(destination);
|
|
|
|
|
if (request.pendingChunks === 0) {
|
|
|
|
|
// We're done.
|
|
|
|
|
close(destination);
|
|
|
|
|
}
|
2019-10-30 05:45:47 +08:00
|
|
|
}
|
|
|
|
|
|
2020-03-11 05:55:04 +08:00
|
|
|
export function startWork(request: Request): void {
|
2019-10-30 05:45:47 +08:00
|
|
|
request.flowing = true;
|
|
|
|
|
scheduleWork(() => performWork(request));
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-11 05:55:04 +08:00
|
|
|
export function startFlowing(request: Request): void {
|
2019-11-07 01:48:34 +08:00
|
|
|
request.flowing = true;
|
2019-10-30 05:45:47 +08:00
|
|
|
flushCompletedChunks(request);
|
|
|
|
|
}
|