Files
react/packages/react-server/src/ReactFlightServer.js
T
Dan Abramov c1ac052158 [Flight] Support more element types and Hooks for Server and Hybrid Components (#19711)
* Shim support for more element types

* Shim commonly used Hooks that are safe

* Flow

* Oopsie
2020-08-27 20:19:13 +01:00

452 lines
12 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 {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes';
import type {
Destination,
Chunk,
BundlerConfig,
ModuleMetaData,
ModuleReference,
} from './ReactFlightServerConfig';
import {
scheduleWork,
beginWriting,
writeChunk,
completeWriting,
flushBuffered,
close,
processModelChunk,
processErrorChunk,
resolveModuleMetaData,
} from './ReactFlightServerConfig';
import {
REACT_BLOCK_TYPE,
REACT_ELEMENT_TYPE,
REACT_DEBUG_TRACING_MODE_TYPE,
REACT_FORWARD_REF_TYPE,
REACT_FRAGMENT_TYPE,
REACT_LAZY_TYPE,
REACT_LEGACY_HIDDEN_TYPE,
REACT_MEMO_TYPE,
REACT_OFFSCREEN_TYPE,
REACT_PROFILER_TYPE,
REACT_SCOPE_TYPE,
REACT_SERVER_BLOCK_TYPE,
REACT_STRICT_MODE_TYPE,
REACT_SUSPENSE_TYPE,
REACT_SUSPENSE_LIST_TYPE,
} from 'shared/ReactSymbols';
import * as React from 'react';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import invariant from 'shared/invariant';
type ReactJSONValue =
| string
| boolean
| number
| null
| $ReadOnlyArray<ReactJSONValue>
| ReactModelObject;
export type ReactModel =
| React$Element<any>
| string
| boolean
| number
| null
| Iterable<ReactModel>
| ReactModelObject;
type ReactModelObject = {+[key: string]: ReactModel};
type Segment = {
id: number,
query: () => 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,
};
const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
export function createRequest(
model: ReactModel,
destination: Destination,
bundlerConfig: BundlerConfig,
): Request {
const pingedSegments = [];
const request = {
destination,
bundlerConfig,
nextChunkId: 0,
pendingChunks: 0,
pingedSegments: pingedSegments,
completedJSONChunks: [],
completedErrorChunks: [],
flowing: false,
toJSON: function(key: string, value: ReactModel): ReactJSONValue {
return resolveModelToJSON(request, this, key, value);
},
};
request.pendingChunks++;
const rootSegment = createSegment(request, () => model);
pingedSegments.push(rootSegment);
return request;
}
function attemptResolveElement(element: React$Element<any>): ReactModel {
const type = element.type;
const props = element.props;
if (typeof type === 'function') {
// This is a server-side component.
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 if (type[0] === REACT_SERVER_BLOCK_TYPE) {
return [REACT_ELEMENT_TYPE, type, element.key, element.props];
} else if (
type === REACT_FRAGMENT_TYPE ||
type === REACT_STRICT_MODE_TYPE ||
type === REACT_PROFILER_TYPE ||
type === REACT_SCOPE_TYPE ||
type === REACT_DEBUG_TRACING_MODE_TYPE ||
type === REACT_LEGACY_HIDDEN_TYPE ||
type === REACT_OFFSCREEN_TYPE ||
// TODO: These are temporary shims
// and we'll want a different behavior.
type === REACT_SUSPENSE_TYPE ||
type === REACT_SUSPENSE_LIST_TYPE
) {
return element.props.children;
} else if (type != null && typeof type === 'object') {
switch (type.$$typeof) {
case REACT_FORWARD_REF_TYPE: {
const render = type.render;
return render(props, undefined);
}
case REACT_MEMO_TYPE: {
const nextChildren = React.createElement(type.type, element.props);
return attemptResolveElement(nextChildren);
}
}
}
invariant(false, 'Unsupported type.');
}
function pingSegment(request: Request, segment: Segment): void {
const pingedSegments = request.pingedSegments;
pingedSegments.push(segment);
if (pingedSegments.length === 1) {
scheduleWork(() => performWork(request));
}
}
function createSegment(request: Request, query: () => ReactModel): Segment {
const id = request.nextChunkId++;
const segment = {
id,
query,
ping: () => pingSegment(request, segment),
};
return segment;
}
function serializeIDRef(id: number): string {
return '$' + id.toString(16);
}
function escapeStringValue(value: string): string {
if (value[0] === '$' || value[0] === '@') {
// We need to escape $ or @ prefixed strings since we use those to encode
// references to IDs and as special symbol values.
return '$' + value;
} else {
return value;
}
}
export function resolveModelToJSON(
request: Request,
parent: {+[key: string | number]: ReactModel} | $ReadOnlyArray<ReactModel>,
key: string,
value: ReactModel,
): ReactJSONValue {
// Special Symbols
switch (value) {
case REACT_ELEMENT_TYPE:
return '$';
case REACT_SERVER_BLOCK_TYPE:
return '@';
case REACT_LAZY_TYPE:
case REACT_BLOCK_TYPE:
invariant(
false,
'React Blocks (and Lazy Components) are expected to be replaced by a ' +
'compiler on the server. Try configuring your compiler set up and avoid ' +
'using React.lazy inside of Blocks.',
);
}
if (parent[0] === REACT_SERVER_BLOCK_TYPE) {
// We're currently encoding part of a Block. Look up which key.
switch (key) {
case '1': {
// Module reference
const moduleReference: ModuleReference<any> = (value: any);
try {
const moduleMetaData: ModuleMetaData = resolveModuleMetaData(
request.bundlerConfig,
moduleReference,
);
return (moduleMetaData: ReactJSONValue);
} catch (x) {
request.pendingChunks++;
const errorId = request.nextChunkId++;
emitErrorChunk(request, errorId, x);
return serializeIDRef(errorId);
}
}
case '2': {
// Load function
const load: () => ReactModel = (value: any);
try {
// Attempt to resolve the data.
return load();
} 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++;
const newSegment = createSegment(request, load);
const ping = newSegment.ping;
x.then(ping, ping);
return serializeIDRef(newSegment.id);
} else {
// This load failed, encode the error as a separate row and reference that.
request.pendingChunks++;
const errorId = request.nextChunkId++;
emitErrorChunk(request, errorId, x);
return serializeIDRef(errorId);
}
}
}
default: {
invariant(
false,
'A server block should never encode any other slots. This is a bug in React.',
);
}
}
}
if (typeof value === 'string') {
return escapeStringValue(value);
}
// Resolve server components.
while (
typeof value === 'object' &&
value !== null &&
value.$$typeof === REACT_ELEMENT_TYPE
) {
const prevDispatcher = ReactCurrentDispatcher.current;
// TODO: Concatenate keys of parents onto children.
const element: React$Element<any> = (value: any);
try {
ReactCurrentDispatcher.current = Dispatcher;
// Attempt to render the server component.
value = attemptResolveElement(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++;
const newSegment = createSegment(request, () => value);
const ping = newSegment.ping;
x.then(ping, ping);
return serializeIDRef(newSegment.id);
} else {
// Something errored. Don't bother encoding anything up to here.
throw x;
}
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
}
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.';
}
const processedChunk = processErrorChunk(request, id, message, stack);
request.completedErrorChunks.push(processedChunk);
}
function retrySegment(request: Request, segment: Segment): void {
const query = segment.query;
let value;
try {
value = query();
while (
typeof value === 'object' &&
value !== null &&
value.$$typeof === REACT_ELEMENT_TYPE
) {
// TODO: Concatenate keys of parents onto children.
const element: React$Element<any> = (value: any);
// Attempt to render the server component.
// Doing this here lets us reuse this same segment if the next component
// also suspends.
segment.query = () => value;
value = attemptResolveElement(element);
}
const 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.
const 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 {
const pingedSegments = request.pingedSegments;
request.pingedSegments = [];
for (let i = 0; i < pingedSegments.length; i++) {
const segment = pingedSegments[i];
retrySegment(request, segment);
}
if (request.flowing) {
flushCompletedChunks(request);
}
}
let reentrant = false;
function flushCompletedChunks(request: Request): void {
if (reentrant) {
return;
}
reentrant = true;
const destination = request.destination;
beginWriting(destination);
try {
const jsonChunks = request.completedJSONChunks;
let i = 0;
for (; i < jsonChunks.length; i++) {
request.pendingChunks--;
const chunk = jsonChunks[i];
if (!writeChunk(destination, chunk)) {
request.flowing = false;
i++;
break;
}
}
jsonChunks.splice(0, i);
const errorChunks = request.completedErrorChunks;
i = 0;
for (; i < errorChunks.length; i++) {
request.pendingChunks--;
const 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);
}
function unsupportedHook(): void {
invariant(false, 'This Hook is not supported in Server Components.');
}
const Dispatcher: DispatcherType = {
useMemo<T>(nextCreate: () => T): T {
return nextCreate();
},
useCallback<T>(callback: T): T {
return callback;
},
useDebugValue(): void {},
useDeferredValue<T>(value: T): T {
return value;
},
useTransition(): [(callback: () => void) => void, boolean] {
return [() => {}, false];
},
readContext: (unsupportedHook: any),
useContext: (unsupportedHook: any),
useReducer: (unsupportedHook: any),
useRef: (unsupportedHook: any),
useState: (unsupportedHook: any),
useLayoutEffect: (unsupportedHook: any),
useImperativeHandle: (unsupportedHook: any),
useEffect: (unsupportedHook: any),
useOpaqueIdentifier: (unsupportedHook: any),
useMutableSource: (unsupportedHook: any),
};