Files
react/packages/react-server/src/ReactFlightServer.js
T
Sebastian Markbåge dc7eedae3c Encode server rendered host components as array tuples (#18273)
This replaces the HTML renderer with instead resolving host elements into
arrays tagged with the react.element symbol. These turn into proper
React Elements on the client.

The symbol is encoded as the magical value "$". This has security implications
so this special value needs to remain escaped for other strings.

We could just encode the element as {$$typeof: "$", key: key props: props}
but that's a lot more bytes. So instead I encode it as:
["$", key, props] and then convert it back.

It would be nicer if React's reconciler could just accept these tuples.
2020-03-11 09:48:02 -07:00

284 lines
7.1 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 {Destination, Chunk} from './ReactFlightServerConfig';
import {
scheduleWork,
beginWriting,
writeChunk,
completeWriting,
flushBuffered,
close,
processModelChunk,
processErrorChunk,
} 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,
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,
): Request {
let pingedSegments = [];
let request = {
destination,
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);
}