react/packages/react-server/src/ReactFlightServer.js

294 lines
7.3 KiB
JavaScript
Raw Normal View History

/**
* 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 {
Destination,
Chunk,
BundlerConfig,
// ModuleReference,
// ModuleMetaData,
} from './ReactFlightServerConfig';
import {
scheduleWork,
beginWriting,
writeChunk,
completeWriting,
flushBuffered,
close,
processModelChunk,
processErrorChunk,
// resolveModuleMetaData,
} from './ReactFlightServerConfig';
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
type ReactJSONValue =
| string
| boolean
| number
| null
| Array<ReactModel>
| ReactModelObject;
export type ReactModel =
| React$Element<any>
| string
| boolean
| number
| null
| Iterable<ReactModel>
| ReactModelObject;
type ReactModelObject = {+[key: string]: ReactModel};
type Segment = {
id: number,
model: ReactModel,
ping: () => void,
};
export type Request = {
destination: Destination,
bundlerConfig: BundlerConfig,
nextChunkId: number,
pendingChunks: number,
pingedSegments: Array<Segment>,
completedJSONChunks: Array<Chunk>,
completedErrorChunks: Array<Chunk>,
flowing: boolean,
toJSON: (key: string, value: ReactModel) => ReactJSONValue,
};
export function createRequest(
model: ReactModel,
destination: Destination,
bundlerConfig: BundlerConfig,
): Request {
let pingedSegments = [];
let request = {
destination,
bundlerConfig,
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;
}
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.
return [REACT_ELEMENT_TYPE, type, element.key, element.props];
} else {
throw new Error('Unsupported type.');
}
}
function pingSegment(request: Request, segment: Segment): void {
let pingedSegments = request.pingedSegments;
pingedSegments.push(segment);
if (pingedSegments.length === 1) {
scheduleWork(() => performWork(request));
}
}
function createSegment(request: Request, model: ReactModel): Segment {
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
// references to IDs and as a special symbol value.
return '$' + value;
} else {
return value;
}
}
export function resolveModelToJSON(
request: Request,
value: ReactModel,
): ReactJSONValue {
if (typeof value === 'string') {
return escapeStringValue(value);
}
if (value === REACT_ELEMENT_TYPE) {
return '$';
}
while (
typeof value === 'object' &&
value !== null &&
value.$$typeof === REACT_ELEMENT_TYPE
) {
let element: React$Element<any> = (value: any);
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);
}
}
}
return value;
}
function emitErrorChunk(request: Request, id: number, error: mixed): void {
// 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.';
}
let processedChunk = processErrorChunk(request, id, message, stack);
request.completedErrorChunks.push(processedChunk);
}
function retrySegment(request: Request, segment: Segment): void {
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);
}
let processedChunk = processModelChunk(request, segment.id, value);
request.completedJSONChunks.push(processedChunk);
} 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);
}
}
}
function performWork(request: Request): void {
let pingedSegments = request.pingedSegments;
request.pingedSegments = [];
for (let i = 0; i < pingedSegments.length; i++) {
let segment = pingedSegments[i];
retrySegment(request, segment);
}
if (request.flowing) {
flushCompletedChunks(request);
}
}
let reentrant = false;
function flushCompletedChunks(request: Request): void {
if (reentrant) {
return;
}
reentrant = true;
let destination = request.destination;
beginWriting(destination);
try {
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;
}
}
errorChunks.splice(0, i);
} finally {
reentrant = false;
completeWriting(destination);
}
flushBuffered(destination);
if (request.pendingChunks === 0) {
// We're done.
close(destination);
}
}
export function startWork(request: Request): void {
request.flowing = true;
scheduleWork(() => performWork(request));
}
export function startFlowing(request: Request): void {
request.flowing = true;
flushCompletedChunks(request);
}