mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
This wires up the use of `async_hooks` in the Node build (as well as the Edge build when a global is available) in DEV mode only. This will be used to track debug info about what suspended during an RSC pass. Enabled behind a flag for now.
1856 lines
57 KiB
JavaScript
1856 lines
57 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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 {Chunk, BinaryChunk, Destination} from './ReactServerStreamConfig';
|
|
|
|
import type {Postpone} from 'react/src/ReactPostpone';
|
|
|
|
import {
|
|
enableBinaryFlight,
|
|
enablePostpone,
|
|
enableTaint,
|
|
enableServerContext,
|
|
} from 'shared/ReactFeatureFlags';
|
|
|
|
import {
|
|
scheduleWork,
|
|
flushBuffered,
|
|
beginWriting,
|
|
writeChunkAndReturn,
|
|
stringToChunk,
|
|
typedArrayToBinaryChunk,
|
|
byteLengthOfChunk,
|
|
byteLengthOfBinaryChunk,
|
|
completeWriting,
|
|
close,
|
|
closeWithError,
|
|
} from './ReactServerStreamConfig';
|
|
|
|
export type {Destination, Chunk} from './ReactServerStreamConfig';
|
|
|
|
import type {
|
|
ClientManifest,
|
|
ClientReferenceMetadata,
|
|
ClientReference,
|
|
ClientReferenceKey,
|
|
ServerReference,
|
|
ServerReferenceId,
|
|
Hints,
|
|
HintCode,
|
|
HintModel,
|
|
} from './ReactFlightServerConfig';
|
|
import type {ContextSnapshot} from './ReactFlightNewContext';
|
|
import type {ThenableState} from './ReactFlightThenable';
|
|
import type {
|
|
ReactProviderType,
|
|
ServerContextJSONValue,
|
|
Wakeable,
|
|
Thenable,
|
|
PendingThenable,
|
|
FulfilledThenable,
|
|
RejectedThenable,
|
|
ReactServerContext,
|
|
} from 'shared/ReactTypes';
|
|
import type {LazyComponent} from 'react/src/ReactLazy';
|
|
|
|
import {
|
|
resolveClientReferenceMetadata,
|
|
getServerReferenceId,
|
|
getServerReferenceBoundArguments,
|
|
getClientReferenceKey,
|
|
isClientReference,
|
|
isServerReference,
|
|
supportsRequestStorage,
|
|
requestStorage,
|
|
prepareHostDispatcher,
|
|
createHints,
|
|
initAsyncDebugInfo,
|
|
} from './ReactFlightServerConfig';
|
|
|
|
import {
|
|
HooksDispatcher,
|
|
prepareToUseHooksForRequest,
|
|
prepareToUseHooksForComponent,
|
|
getThenableStateAfterSuspending,
|
|
resetHooksForRequest,
|
|
} from './ReactFlightHooks';
|
|
import {DefaultCacheDispatcher} from './flight/ReactFlightServerCache';
|
|
import {
|
|
pushProvider,
|
|
popProvider,
|
|
switchContext,
|
|
getActiveContext,
|
|
rootContextSnapshot,
|
|
} from './ReactFlightNewContext';
|
|
|
|
import {
|
|
getIteratorFn,
|
|
REACT_ELEMENT_TYPE,
|
|
REACT_FORWARD_REF_TYPE,
|
|
REACT_FRAGMENT_TYPE,
|
|
REACT_LAZY_TYPE,
|
|
REACT_MEMO_TYPE,
|
|
REACT_POSTPONE_TYPE,
|
|
REACT_PROVIDER_TYPE,
|
|
} from 'shared/ReactSymbols';
|
|
|
|
import {
|
|
describeValueForErrorMessage,
|
|
describeObjectForErrorMessage,
|
|
isSimpleObject,
|
|
jsxPropsParents,
|
|
jsxChildrenParents,
|
|
objectName,
|
|
} from 'shared/ReactSerializationErrors';
|
|
|
|
import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
|
|
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
|
import ReactServerSharedInternals from './ReactServerSharedInternals';
|
|
import isArray from 'shared/isArray';
|
|
import getPrototypeOf from 'shared/getPrototypeOf';
|
|
import binaryToComparableString from 'shared/binaryToComparableString';
|
|
|
|
import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
|
|
|
|
initAsyncDebugInfo();
|
|
|
|
const ObjectPrototype = Object.prototype;
|
|
|
|
type JSONValue =
|
|
| string
|
|
| boolean
|
|
| number
|
|
| null
|
|
| {+[key: string]: JSONValue}
|
|
| $ReadOnlyArray<JSONValue>;
|
|
|
|
const stringify = JSON.stringify;
|
|
|
|
type ReactJSONValue =
|
|
| string
|
|
| boolean
|
|
| number
|
|
| null
|
|
| $ReadOnlyArray<ReactJSONValue>
|
|
| ReactClientObject;
|
|
|
|
// Serializable values
|
|
export type ReactClientValue =
|
|
// Server Elements and Lazy Components are unwrapped on the Server
|
|
| React$Element<React$AbstractComponent<any, any>>
|
|
| LazyComponent<ReactClientValue, any>
|
|
// References are passed by their value
|
|
| ClientReference<any>
|
|
| ServerReference<any>
|
|
// The rest are passed as is. Sub-types can be passed in but lose their
|
|
// subtype, so the receiver can only accept once of these.
|
|
| React$Element<string>
|
|
| React$Element<ClientReference<any> & any>
|
|
| ReactServerContext<any>
|
|
| string
|
|
| boolean
|
|
| number
|
|
| symbol
|
|
| null
|
|
| void
|
|
| bigint
|
|
| Iterable<ReactClientValue>
|
|
| Array<ReactClientValue>
|
|
| Map<ReactClientValue, ReactClientValue>
|
|
| Set<ReactClientValue>
|
|
| Date
|
|
| ReactClientObject
|
|
| Promise<ReactClientValue>; // Thenable<ReactClientValue>
|
|
|
|
type ReactClientObject = {+[key: string]: ReactClientValue};
|
|
|
|
const PENDING = 0;
|
|
const COMPLETED = 1;
|
|
const ABORTED = 3;
|
|
const ERRORED = 4;
|
|
|
|
type Task = {
|
|
id: number,
|
|
status: 0 | 1 | 3 | 4,
|
|
model: ReactClientValue,
|
|
ping: () => void,
|
|
context: ContextSnapshot,
|
|
thenableState: ThenableState | null,
|
|
};
|
|
|
|
interface Reference {}
|
|
|
|
export type Request = {
|
|
status: 0 | 1 | 2,
|
|
flushScheduled: boolean,
|
|
fatalError: mixed,
|
|
destination: null | Destination,
|
|
bundlerConfig: ClientManifest,
|
|
cache: Map<Function, mixed>,
|
|
nextChunkId: number,
|
|
pendingChunks: number,
|
|
hints: Hints,
|
|
abortableTasks: Set<Task>,
|
|
pingedTasks: Array<Task>,
|
|
completedImportChunks: Array<Chunk>,
|
|
completedHintChunks: Array<Chunk>,
|
|
completedRegularChunks: Array<Chunk | BinaryChunk>,
|
|
completedErrorChunks: Array<Chunk>,
|
|
writtenSymbols: Map<symbol, number>,
|
|
writtenClientReferences: Map<ClientReferenceKey, number>,
|
|
writtenServerReferences: Map<ServerReference<any>, number>,
|
|
writtenProviders: Map<string, number>,
|
|
writtenObjects: WeakMap<Reference, number>, // -1 means "seen" but not outlined.
|
|
identifierPrefix: string,
|
|
identifierCount: number,
|
|
taintCleanupQueue: Array<string | bigint>,
|
|
onError: (error: mixed) => ?string,
|
|
onPostpone: (reason: string) => void,
|
|
toJSON: (key: string, value: ReactClientValue) => ReactJSONValue,
|
|
};
|
|
|
|
const {
|
|
TaintRegistryObjects,
|
|
TaintRegistryValues,
|
|
TaintRegistryByteLengths,
|
|
TaintRegistryPendingRequests,
|
|
ReactCurrentCache,
|
|
} = ReactServerSharedInternals;
|
|
const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
|
|
|
|
function throwTaintViolation(message: string) {
|
|
// eslint-disable-next-line react-internal/prod-error-codes
|
|
throw new Error(message);
|
|
}
|
|
|
|
function cleanupTaintQueue(request: Request): void {
|
|
const cleanupQueue = request.taintCleanupQueue;
|
|
TaintRegistryPendingRequests.delete(cleanupQueue);
|
|
for (let i = 0; i < cleanupQueue.length; i++) {
|
|
const entryValue = cleanupQueue[i];
|
|
const entry = TaintRegistryValues.get(entryValue);
|
|
if (entry !== undefined) {
|
|
if (entry.count === 1) {
|
|
TaintRegistryValues.delete(entryValue);
|
|
} else {
|
|
entry.count--;
|
|
}
|
|
}
|
|
}
|
|
cleanupQueue.length = 0;
|
|
}
|
|
|
|
function defaultErrorHandler(error: mixed) {
|
|
console['error'](error);
|
|
// Don't transform to our wrapper
|
|
}
|
|
|
|
function defaultPostponeHandler(reason: string) {
|
|
// Noop
|
|
}
|
|
|
|
const OPEN = 0;
|
|
const CLOSING = 1;
|
|
const CLOSED = 2;
|
|
|
|
export function createRequest(
|
|
model: ReactClientValue,
|
|
bundlerConfig: ClientManifest,
|
|
onError: void | ((error: mixed) => ?string),
|
|
context?: Array<[string, ServerContextJSONValue]>,
|
|
identifierPrefix?: string,
|
|
onPostpone: void | ((reason: string) => void),
|
|
): Request {
|
|
if (
|
|
ReactCurrentCache.current !== null &&
|
|
ReactCurrentCache.current !== DefaultCacheDispatcher
|
|
) {
|
|
throw new Error(
|
|
'Currently React only supports one RSC renderer at a time.',
|
|
);
|
|
}
|
|
prepareHostDispatcher();
|
|
ReactCurrentCache.current = DefaultCacheDispatcher;
|
|
|
|
const abortSet: Set<Task> = new Set();
|
|
const pingedTasks: Array<Task> = [];
|
|
const cleanupQueue: Array<string | bigint> = [];
|
|
if (enableTaint) {
|
|
TaintRegistryPendingRequests.add(cleanupQueue);
|
|
}
|
|
const hints = createHints();
|
|
const request: Request = {
|
|
status: OPEN,
|
|
flushScheduled: false,
|
|
fatalError: null,
|
|
destination: null,
|
|
bundlerConfig,
|
|
cache: new Map(),
|
|
nextChunkId: 0,
|
|
pendingChunks: 0,
|
|
hints,
|
|
abortableTasks: abortSet,
|
|
pingedTasks: pingedTasks,
|
|
completedImportChunks: ([]: Array<Chunk>),
|
|
completedHintChunks: ([]: Array<Chunk>),
|
|
completedRegularChunks: ([]: Array<Chunk | BinaryChunk>),
|
|
completedErrorChunks: ([]: Array<Chunk>),
|
|
writtenSymbols: new Map(),
|
|
writtenClientReferences: new Map(),
|
|
writtenServerReferences: new Map(),
|
|
writtenProviders: new Map(),
|
|
writtenObjects: new WeakMap(),
|
|
identifierPrefix: identifierPrefix || '',
|
|
identifierCount: 1,
|
|
taintCleanupQueue: cleanupQueue,
|
|
onError: onError === undefined ? defaultErrorHandler : onError,
|
|
onPostpone: onPostpone === undefined ? defaultPostponeHandler : onPostpone,
|
|
// $FlowFixMe[missing-this-annot]
|
|
toJSON: function (key: string, value: ReactClientValue): ReactJSONValue {
|
|
return resolveModelToJSON(request, this, key, value);
|
|
},
|
|
};
|
|
request.pendingChunks++;
|
|
const rootContext = createRootContext(context);
|
|
const rootTask = createTask(request, model, rootContext, abortSet);
|
|
pingedTasks.push(rootTask);
|
|
return request;
|
|
}
|
|
|
|
let currentRequest: null | Request = null;
|
|
|
|
export function resolveRequest(): null | Request {
|
|
if (currentRequest) return currentRequest;
|
|
if (supportsRequestStorage) {
|
|
const store = requestStorage.getStore();
|
|
if (store) return store;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function createRootContext(
|
|
reqContext?: Array<[string, ServerContextJSONValue]>,
|
|
) {
|
|
return importServerContexts(reqContext);
|
|
}
|
|
|
|
const POP = {};
|
|
|
|
function serializeThenable(request: Request, thenable: Thenable<any>): number {
|
|
request.pendingChunks++;
|
|
const newTask = createTask(
|
|
request,
|
|
null,
|
|
getActiveContext(),
|
|
request.abortableTasks,
|
|
);
|
|
|
|
switch (thenable.status) {
|
|
case 'fulfilled': {
|
|
// We have the resolved value, we can go ahead and schedule it for serialization.
|
|
newTask.model = thenable.value;
|
|
pingTask(request, newTask);
|
|
return newTask.id;
|
|
}
|
|
case 'rejected': {
|
|
const x = thenable.reason;
|
|
if (
|
|
enablePostpone &&
|
|
typeof x === 'object' &&
|
|
x !== null &&
|
|
(x: any).$$typeof === REACT_POSTPONE_TYPE
|
|
) {
|
|
const postponeInstance: Postpone = (x: any);
|
|
logPostpone(request, postponeInstance.message);
|
|
emitPostponeChunk(request, newTask.id, postponeInstance);
|
|
} else {
|
|
const digest = logRecoverableError(request, x);
|
|
emitErrorChunk(request, newTask.id, digest, x);
|
|
}
|
|
return newTask.id;
|
|
}
|
|
default: {
|
|
if (typeof thenable.status === 'string') {
|
|
// Only instrument the thenable if the status if not defined. If
|
|
// it's defined, but an unknown value, assume it's been instrumented by
|
|
// some custom userspace implementation. We treat it as "pending".
|
|
break;
|
|
}
|
|
const pendingThenable: PendingThenable<mixed> = (thenable: any);
|
|
pendingThenable.status = 'pending';
|
|
pendingThenable.then(
|
|
fulfilledValue => {
|
|
if (thenable.status === 'pending') {
|
|
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
|
|
fulfilledThenable.status = 'fulfilled';
|
|
fulfilledThenable.value = fulfilledValue;
|
|
}
|
|
},
|
|
(error: mixed) => {
|
|
if (thenable.status === 'pending') {
|
|
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
|
|
rejectedThenable.status = 'rejected';
|
|
rejectedThenable.reason = error;
|
|
}
|
|
},
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
|
|
thenable.then(
|
|
value => {
|
|
newTask.model = value;
|
|
pingTask(request, newTask);
|
|
},
|
|
reason => {
|
|
if (
|
|
enablePostpone &&
|
|
typeof reason === 'object' &&
|
|
reason !== null &&
|
|
(reason: any).$$typeof === REACT_POSTPONE_TYPE
|
|
) {
|
|
const postponeInstance: Postpone = (reason: any);
|
|
logPostpone(request, postponeInstance.message);
|
|
emitPostponeChunk(request, newTask.id, postponeInstance);
|
|
} else {
|
|
newTask.status = ERRORED;
|
|
const digest = logRecoverableError(request, reason);
|
|
emitErrorChunk(request, newTask.id, digest, reason);
|
|
}
|
|
request.abortableTasks.delete(newTask);
|
|
if (request.destination !== null) {
|
|
flushCompletedChunks(request, request.destination);
|
|
}
|
|
},
|
|
);
|
|
|
|
return newTask.id;
|
|
}
|
|
|
|
export function emitHint<Code: HintCode>(
|
|
request: Request,
|
|
code: Code,
|
|
model: HintModel<Code>,
|
|
): void {
|
|
emitHintChunk(request, code, model);
|
|
enqueueFlush(request);
|
|
}
|
|
|
|
export function getHints(request: Request): Hints {
|
|
return request.hints;
|
|
}
|
|
|
|
export function getCache(request: Request): Map<Function, mixed> {
|
|
return request.cache;
|
|
}
|
|
|
|
function readThenable<T>(thenable: Thenable<T>): T {
|
|
if (thenable.status === 'fulfilled') {
|
|
return thenable.value;
|
|
} else if (thenable.status === 'rejected') {
|
|
throw thenable.reason;
|
|
}
|
|
throw thenable;
|
|
}
|
|
|
|
function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
|
|
// This is a temporary fork of the `use` implementation until we accept
|
|
// promises everywhere.
|
|
const thenable: Thenable<mixed> = (wakeable: any);
|
|
switch (thenable.status) {
|
|
case 'fulfilled':
|
|
case 'rejected':
|
|
break;
|
|
default: {
|
|
if (typeof thenable.status === 'string') {
|
|
// Only instrument the thenable if the status if not defined. If
|
|
// it's defined, but an unknown value, assume it's been instrumented by
|
|
// some custom userspace implementation. We treat it as "pending".
|
|
break;
|
|
}
|
|
const pendingThenable: PendingThenable<mixed> = (thenable: any);
|
|
pendingThenable.status = 'pending';
|
|
pendingThenable.then(
|
|
fulfilledValue => {
|
|
if (thenable.status === 'pending') {
|
|
const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
|
|
fulfilledThenable.status = 'fulfilled';
|
|
fulfilledThenable.value = fulfilledValue;
|
|
}
|
|
},
|
|
(error: mixed) => {
|
|
if (thenable.status === 'pending') {
|
|
const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
|
|
rejectedThenable.status = 'rejected';
|
|
rejectedThenable.reason = error;
|
|
}
|
|
},
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
const lazyType: LazyComponent<any, Thenable<any>> = {
|
|
$$typeof: REACT_LAZY_TYPE,
|
|
_payload: thenable,
|
|
_init: readThenable,
|
|
};
|
|
return lazyType;
|
|
}
|
|
|
|
function attemptResolveElement(
|
|
request: Request,
|
|
type: any,
|
|
key: null | React$Key,
|
|
ref: mixed,
|
|
props: any,
|
|
prevThenableState: ThenableState | null,
|
|
): ReactClientValue {
|
|
if (ref !== null && ref !== undefined) {
|
|
// When the ref moves to the regular props object this will implicitly
|
|
// throw for functions. We could probably relax it to a DEV warning for other
|
|
// cases.
|
|
throw new Error(
|
|
'Refs cannot be used in Server Components, nor passed to Client Components.',
|
|
);
|
|
}
|
|
if (__DEV__) {
|
|
jsxPropsParents.set(props, type);
|
|
if (typeof props.children === 'object' && props.children !== null) {
|
|
jsxChildrenParents.set(props.children, type);
|
|
}
|
|
}
|
|
if (typeof type === 'function') {
|
|
if (isClientReference(type)) {
|
|
// This is a reference to a Client Component.
|
|
return [REACT_ELEMENT_TYPE, type, key, props];
|
|
}
|
|
// This is a server-side component.
|
|
prepareToUseHooksForComponent(prevThenableState);
|
|
const result = type(props);
|
|
if (
|
|
typeof result === 'object' &&
|
|
result !== null &&
|
|
typeof result.then === 'function'
|
|
) {
|
|
// When the return value is in children position we can resolve it immediately,
|
|
// to its value without a wrapper if it's synchronously available.
|
|
const thenable: Thenable<any> = result;
|
|
if (thenable.status === 'fulfilled') {
|
|
return thenable.value;
|
|
}
|
|
// TODO: Once we accept Promises as children on the client, we can just return
|
|
// the thenable here.
|
|
return createLazyWrapperAroundWakeable(result);
|
|
}
|
|
return result;
|
|
} else if (typeof type === 'string') {
|
|
// This is a host element. E.g. HTML.
|
|
return [REACT_ELEMENT_TYPE, type, key, props];
|
|
} else if (typeof type === 'symbol') {
|
|
if (type === REACT_FRAGMENT_TYPE) {
|
|
// For key-less fragments, we add a small optimization to avoid serializing
|
|
// it as a wrapper.
|
|
// TODO: If a key is specified, we should propagate its key to any children.
|
|
// Same as if a Server Component has a key.
|
|
return props.children;
|
|
}
|
|
// This might be a built-in React component. We'll let the client decide.
|
|
// Any built-in works as long as its props are serializable.
|
|
return [REACT_ELEMENT_TYPE, type, key, props];
|
|
} else if (type != null && typeof type === 'object') {
|
|
if (isClientReference(type)) {
|
|
// This is a reference to a Client Component.
|
|
return [REACT_ELEMENT_TYPE, type, key, props];
|
|
}
|
|
switch (type.$$typeof) {
|
|
case REACT_LAZY_TYPE: {
|
|
const payload = type._payload;
|
|
const init = type._init;
|
|
const wrappedType = init(payload);
|
|
return attemptResolveElement(
|
|
request,
|
|
wrappedType,
|
|
key,
|
|
ref,
|
|
props,
|
|
prevThenableState,
|
|
);
|
|
}
|
|
case REACT_FORWARD_REF_TYPE: {
|
|
const render = type.render;
|
|
prepareToUseHooksForComponent(prevThenableState);
|
|
return render(props, undefined);
|
|
}
|
|
case REACT_MEMO_TYPE: {
|
|
return attemptResolveElement(
|
|
request,
|
|
type.type,
|
|
key,
|
|
ref,
|
|
props,
|
|
prevThenableState,
|
|
);
|
|
}
|
|
case REACT_PROVIDER_TYPE: {
|
|
if (enableServerContext) {
|
|
pushProvider(type._context, props.value);
|
|
if (__DEV__) {
|
|
const extraKeys = Object.keys(props).filter(value => {
|
|
if (value === 'children' || value === 'value') {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
if (extraKeys.length !== 0) {
|
|
console.error(
|
|
'ServerContext can only have a value prop and children. Found: %s',
|
|
JSON.stringify(extraKeys),
|
|
);
|
|
}
|
|
}
|
|
return [
|
|
REACT_ELEMENT_TYPE,
|
|
type,
|
|
key,
|
|
// Rely on __popProvider being serialized last to pop the provider.
|
|
{value: props.value, children: props.children, __pop: POP},
|
|
];
|
|
}
|
|
// Fallthrough
|
|
}
|
|
}
|
|
}
|
|
throw new Error(
|
|
`Unsupported Server Component type: ${describeValueForErrorMessage(type)}`,
|
|
);
|
|
}
|
|
|
|
function pingTask(request: Request, task: Task): void {
|
|
const pingedTasks = request.pingedTasks;
|
|
pingedTasks.push(task);
|
|
if (pingedTasks.length === 1) {
|
|
request.flushScheduled = request.destination !== null;
|
|
scheduleWork(() => performWork(request));
|
|
}
|
|
}
|
|
|
|
function createTask(
|
|
request: Request,
|
|
model: ReactClientValue,
|
|
context: ContextSnapshot,
|
|
abortSet: Set<Task>,
|
|
): Task {
|
|
const id = request.nextChunkId++;
|
|
const task: Task = {
|
|
id,
|
|
status: PENDING,
|
|
model,
|
|
context,
|
|
ping: () => pingTask(request, task),
|
|
thenableState: null,
|
|
};
|
|
abortSet.add(task);
|
|
return task;
|
|
}
|
|
|
|
function serializeByValueID(id: number): string {
|
|
return '$' + id.toString(16);
|
|
}
|
|
|
|
function serializeLazyID(id: number): string {
|
|
return '$L' + id.toString(16);
|
|
}
|
|
|
|
function serializePromiseID(id: number): string {
|
|
return '$@' + id.toString(16);
|
|
}
|
|
|
|
function serializeServerReferenceID(id: number): string {
|
|
return '$F' + id.toString(16);
|
|
}
|
|
|
|
function serializeSymbolReference(name: string): string {
|
|
return '$S' + name;
|
|
}
|
|
|
|
function serializeProviderReference(name: string): string {
|
|
return '$P' + name;
|
|
}
|
|
|
|
function serializeNumber(number: number): string | number {
|
|
if (Number.isFinite(number)) {
|
|
if (number === 0 && 1 / number === -Infinity) {
|
|
return '$-0';
|
|
} else {
|
|
return number;
|
|
}
|
|
} else {
|
|
if (number === Infinity) {
|
|
return '$Infinity';
|
|
} else if (number === -Infinity) {
|
|
return '$-Infinity';
|
|
} else {
|
|
return '$NaN';
|
|
}
|
|
}
|
|
}
|
|
|
|
function serializeUndefined(): string {
|
|
return '$undefined';
|
|
}
|
|
|
|
function serializeDateFromDateJSON(dateJSON: string): string {
|
|
// JSON.stringify automatically calls Date.prototype.toJSON which calls toISOString.
|
|
// We need only tack on a $D prefix.
|
|
return '$D' + dateJSON;
|
|
}
|
|
|
|
function serializeBigInt(n: bigint): string {
|
|
return '$n' + n.toString(10);
|
|
}
|
|
|
|
function serializeRowHeader(tag: string, id: number) {
|
|
return id.toString(16) + ':' + tag;
|
|
}
|
|
|
|
function encodeReferenceChunk(
|
|
request: Request,
|
|
id: number,
|
|
reference: string,
|
|
): Chunk {
|
|
const json = stringify(reference);
|
|
const row = id.toString(16) + ':' + json + '\n';
|
|
return stringToChunk(row);
|
|
}
|
|
|
|
function serializeClientReference(
|
|
request: Request,
|
|
parent:
|
|
| {+[key: string | number]: ReactClientValue}
|
|
| $ReadOnlyArray<ReactClientValue>,
|
|
key: string,
|
|
clientReference: ClientReference<any>,
|
|
): string {
|
|
const clientReferenceKey: ClientReferenceKey =
|
|
getClientReferenceKey(clientReference);
|
|
const writtenClientReferences = request.writtenClientReferences;
|
|
const existingId = writtenClientReferences.get(clientReferenceKey);
|
|
if (existingId !== undefined) {
|
|
if (parent[0] === REACT_ELEMENT_TYPE && key === '1') {
|
|
// If we're encoding the "type" of an element, we can refer
|
|
// to that by a lazy reference instead of directly since React
|
|
// knows how to deal with lazy values. This lets us suspend
|
|
// on this component rather than its parent until the code has
|
|
// loaded.
|
|
return serializeLazyID(existingId);
|
|
}
|
|
return serializeByValueID(existingId);
|
|
}
|
|
try {
|
|
const clientReferenceMetadata: ClientReferenceMetadata =
|
|
resolveClientReferenceMetadata(request.bundlerConfig, clientReference);
|
|
request.pendingChunks++;
|
|
const importId = request.nextChunkId++;
|
|
emitImportChunk(request, importId, clientReferenceMetadata);
|
|
writtenClientReferences.set(clientReferenceKey, importId);
|
|
if (parent[0] === REACT_ELEMENT_TYPE && key === '1') {
|
|
// If we're encoding the "type" of an element, we can refer
|
|
// to that by a lazy reference instead of directly since React
|
|
// knows how to deal with lazy values. This lets us suspend
|
|
// on this component rather than its parent until the code has
|
|
// loaded.
|
|
return serializeLazyID(importId);
|
|
}
|
|
return serializeByValueID(importId);
|
|
} catch (x) {
|
|
request.pendingChunks++;
|
|
const errorId = request.nextChunkId++;
|
|
const digest = logRecoverableError(request, x);
|
|
emitErrorChunk(request, errorId, digest, x);
|
|
return serializeByValueID(errorId);
|
|
}
|
|
}
|
|
|
|
function outlineModel(request: Request, value: any): number {
|
|
request.pendingChunks++;
|
|
const newTask = createTask(
|
|
request,
|
|
value,
|
|
getActiveContext(),
|
|
request.abortableTasks,
|
|
);
|
|
retryTask(request, newTask);
|
|
return newTask.id;
|
|
}
|
|
|
|
function serializeServerReference(
|
|
request: Request,
|
|
parent:
|
|
| {+[key: string | number]: ReactClientValue}
|
|
| $ReadOnlyArray<ReactClientValue>,
|
|
key: string,
|
|
serverReference: ServerReference<any>,
|
|
): string {
|
|
const writtenServerReferences = request.writtenServerReferences;
|
|
const existingId = writtenServerReferences.get(serverReference);
|
|
if (existingId !== undefined) {
|
|
return serializeServerReferenceID(existingId);
|
|
}
|
|
|
|
const bound: null | Array<any> = getServerReferenceBoundArguments(
|
|
request.bundlerConfig,
|
|
serverReference,
|
|
);
|
|
const serverReferenceMetadata: {
|
|
id: ServerReferenceId,
|
|
bound: null | Promise<Array<any>>,
|
|
} = {
|
|
id: getServerReferenceId(request.bundlerConfig, serverReference),
|
|
bound: bound ? Promise.resolve(bound) : null,
|
|
};
|
|
const metadataId = outlineModel(request, serverReferenceMetadata);
|
|
writtenServerReferences.set(serverReference, metadataId);
|
|
return serializeServerReferenceID(metadataId);
|
|
}
|
|
|
|
function serializeLargeTextString(request: Request, text: string): string {
|
|
request.pendingChunks += 2;
|
|
const textId = request.nextChunkId++;
|
|
const textChunk = stringToChunk(text);
|
|
const binaryLength = byteLengthOfChunk(textChunk);
|
|
const row = textId.toString(16) + ':T' + binaryLength.toString(16) + ',';
|
|
const headerChunk = stringToChunk(row);
|
|
request.completedRegularChunks.push(headerChunk, textChunk);
|
|
return serializeByValueID(textId);
|
|
}
|
|
|
|
function serializeMap(
|
|
request: Request,
|
|
map: Map<ReactClientValue, ReactClientValue>,
|
|
): string {
|
|
const entries = Array.from(map);
|
|
for (let i = 0; i < entries.length; i++) {
|
|
const key = entries[i][0];
|
|
if (typeof key === 'object' && key !== null) {
|
|
const writtenObjects = request.writtenObjects;
|
|
const existingId = writtenObjects.get(key);
|
|
if (existingId === undefined) {
|
|
// Mark all object keys as seen so that they're always outlined.
|
|
writtenObjects.set(key, -1);
|
|
}
|
|
}
|
|
}
|
|
const id = outlineModel(request, entries);
|
|
return '$Q' + id.toString(16);
|
|
}
|
|
|
|
function serializeSet(request: Request, set: Set<ReactClientValue>): string {
|
|
const entries = Array.from(set);
|
|
for (let i = 0; i < entries.length; i++) {
|
|
const key = entries[i];
|
|
if (typeof key === 'object' && key !== null) {
|
|
const writtenObjects = request.writtenObjects;
|
|
const existingId = writtenObjects.get(key);
|
|
if (existingId === undefined) {
|
|
// Mark all object keys as seen so that they're always outlined.
|
|
writtenObjects.set(key, -1);
|
|
}
|
|
}
|
|
}
|
|
const id = outlineModel(request, entries);
|
|
return '$W' + id.toString(16);
|
|
}
|
|
|
|
function serializeTypedArray(
|
|
request: Request,
|
|
tag: string,
|
|
typedArray: $ArrayBufferView,
|
|
): string {
|
|
if (enableTaint) {
|
|
if (TaintRegistryByteLengths.has(typedArray.byteLength)) {
|
|
// If we have had any tainted values of this length, we check
|
|
// to see if these bytes matches any entries in the registry.
|
|
const tainted = TaintRegistryValues.get(
|
|
binaryToComparableString(typedArray),
|
|
);
|
|
if (tainted !== undefined) {
|
|
throwTaintViolation(tainted.message);
|
|
}
|
|
}
|
|
}
|
|
request.pendingChunks += 2;
|
|
const bufferId = request.nextChunkId++;
|
|
// TODO: Convert to little endian if that's not the server default.
|
|
const binaryChunk = typedArrayToBinaryChunk(typedArray);
|
|
const binaryLength = byteLengthOfBinaryChunk(binaryChunk);
|
|
const row =
|
|
bufferId.toString(16) + ':' + tag + binaryLength.toString(16) + ',';
|
|
const headerChunk = stringToChunk(row);
|
|
request.completedRegularChunks.push(headerChunk, binaryChunk);
|
|
return serializeByValueID(bufferId);
|
|
}
|
|
|
|
function escapeStringValue(value: string): string {
|
|
if (value[0] === '$') {
|
|
// We need to escape $ prefixed strings since we use those to encode
|
|
// references to IDs and as special symbol values.
|
|
return '$' + value;
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
let insideContextProps = null;
|
|
let isInsideContextValue = false;
|
|
let modelRoot: null | ReactClientValue = false;
|
|
|
|
function resolveModelToJSON(
|
|
request: Request,
|
|
parent:
|
|
| {+[key: string | number]: ReactClientValue}
|
|
| $ReadOnlyArray<ReactClientValue>,
|
|
key: string,
|
|
value: ReactClientValue,
|
|
): ReactJSONValue {
|
|
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
|
|
if (__DEV__) {
|
|
// $FlowFixMe[incompatible-use]
|
|
const originalValue = parent[key];
|
|
if (
|
|
typeof originalValue === 'object' &&
|
|
originalValue !== value &&
|
|
!(originalValue instanceof Date)
|
|
) {
|
|
if (objectName(originalValue) !== 'Object') {
|
|
const jsxParentType = jsxChildrenParents.get(parent);
|
|
if (typeof jsxParentType === 'string') {
|
|
console.error(
|
|
'%s objects cannot be rendered as text children. Try formatting it using toString().%s',
|
|
objectName(originalValue),
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
} else {
|
|
console.error(
|
|
'Only plain objects can be passed to Client Components from Server Components. ' +
|
|
'%s objects are not supported.%s',
|
|
objectName(originalValue),
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
}
|
|
} else {
|
|
console.error(
|
|
'Only plain objects can be passed to Client Components from Server Components. ' +
|
|
'Objects with toJSON methods are not supported. Convert it manually ' +
|
|
'to a simple value before passing it to props.%s',
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Special Symbols
|
|
switch (value) {
|
|
case REACT_ELEMENT_TYPE:
|
|
return '$';
|
|
}
|
|
|
|
if (__DEV__) {
|
|
if (
|
|
enableServerContext &&
|
|
parent[0] === REACT_ELEMENT_TYPE &&
|
|
parent[1] &&
|
|
(parent[1]: any).$$typeof === REACT_PROVIDER_TYPE &&
|
|
key === '3'
|
|
) {
|
|
insideContextProps = value;
|
|
} else if (insideContextProps === parent && key === 'value') {
|
|
isInsideContextValue = true;
|
|
} else if (insideContextProps === parent && key === 'children') {
|
|
isInsideContextValue = false;
|
|
}
|
|
}
|
|
|
|
// Resolve Server Components.
|
|
while (
|
|
typeof value === 'object' &&
|
|
value !== null &&
|
|
((value: any).$$typeof === REACT_ELEMENT_TYPE ||
|
|
(value: any).$$typeof === REACT_LAZY_TYPE)
|
|
) {
|
|
if (__DEV__) {
|
|
if (enableServerContext && isInsideContextValue) {
|
|
console.error('React elements are not allowed in ServerContext');
|
|
}
|
|
}
|
|
|
|
try {
|
|
switch ((value: any).$$typeof) {
|
|
case REACT_ELEMENT_TYPE: {
|
|
const writtenObjects = request.writtenObjects;
|
|
const existingId = writtenObjects.get(value);
|
|
if (existingId !== undefined) {
|
|
if (existingId === -1) {
|
|
// Seen but not yet outlined.
|
|
const newId = outlineModel(request, value);
|
|
return serializeByValueID(newId);
|
|
} else if (modelRoot === value) {
|
|
// This is the ID we're currently emitting so we need to write it
|
|
// once but if we discover it again, we refer to it by id.
|
|
modelRoot = null;
|
|
} else {
|
|
// We've already emitted this as an outlined object, so we can
|
|
// just refer to that by its existing ID.
|
|
return serializeByValueID(existingId);
|
|
}
|
|
} else {
|
|
// This is the first time we've seen this object. We may never see it again
|
|
// so we'll inline it. Mark it as seen. If we see it again, we'll outline.
|
|
writtenObjects.set(value, -1);
|
|
}
|
|
|
|
// TODO: Concatenate keys of parents onto children.
|
|
const element: React$Element<any> = (value: any);
|
|
// Attempt to render the Server Component.
|
|
value = attemptResolveElement(
|
|
request,
|
|
element.type,
|
|
element.key,
|
|
element.ref,
|
|
element.props,
|
|
null,
|
|
);
|
|
break;
|
|
}
|
|
case REACT_LAZY_TYPE: {
|
|
const payload = (value: any)._payload;
|
|
const init = (value: any)._init;
|
|
value = init(payload);
|
|
break;
|
|
}
|
|
}
|
|
} catch (thrownValue) {
|
|
const x =
|
|
thrownValue === SuspenseException
|
|
? // This is a special type of exception used for Suspense. For historical
|
|
// reasons, the rest of the Suspense implementation expects the thrown
|
|
// value to be a thenable, because before `use` existed that was the
|
|
// (unstable) API for suspending. This implementation detail can change
|
|
// later, once we deprecate the old API in favor of `use`.
|
|
getSuspendedThenable()
|
|
: thrownValue;
|
|
if (typeof x === 'object' && x !== null) {
|
|
// $FlowFixMe[method-unbinding]
|
|
if (typeof x.then === 'function') {
|
|
// Something suspended, we'll need to create a new task and resolve it later.
|
|
request.pendingChunks++;
|
|
const newTask = createTask(
|
|
request,
|
|
value,
|
|
getActiveContext(),
|
|
request.abortableTasks,
|
|
);
|
|
const ping = newTask.ping;
|
|
x.then(ping, ping);
|
|
newTask.thenableState = getThenableStateAfterSuspending();
|
|
return serializeLazyID(newTask.id);
|
|
} else if (enablePostpone && x.$$typeof === REACT_POSTPONE_TYPE) {
|
|
// Something postponed. We'll still send everything we have up until this point.
|
|
// We'll replace this element with a lazy reference that postpones on the client.
|
|
const postponeInstance: Postpone = (x: any);
|
|
request.pendingChunks++;
|
|
const postponeId = request.nextChunkId++;
|
|
logPostpone(request, postponeInstance.message);
|
|
emitPostponeChunk(request, postponeId, postponeInstance);
|
|
return serializeLazyID(postponeId);
|
|
}
|
|
}
|
|
// Something errored. We'll still send everything we have up until this point.
|
|
// We'll replace this element with a lazy reference that throws on the client
|
|
// once it gets rendered.
|
|
request.pendingChunks++;
|
|
const errorId = request.nextChunkId++;
|
|
const digest = logRecoverableError(request, x);
|
|
emitErrorChunk(request, errorId, digest, x);
|
|
return serializeLazyID(errorId);
|
|
}
|
|
}
|
|
|
|
if (value === null) {
|
|
return null;
|
|
}
|
|
|
|
if (typeof value === 'object') {
|
|
if (enableTaint) {
|
|
const tainted = TaintRegistryObjects.get(value);
|
|
if (tainted !== undefined) {
|
|
throwTaintViolation(tainted);
|
|
}
|
|
}
|
|
if (isClientReference(value)) {
|
|
return serializeClientReference(request, parent, key, (value: any));
|
|
}
|
|
|
|
const writtenObjects = request.writtenObjects;
|
|
const existingId = writtenObjects.get(value);
|
|
// $FlowFixMe[method-unbinding]
|
|
if (typeof value.then === 'function') {
|
|
if (existingId !== undefined) {
|
|
if (modelRoot === value) {
|
|
// This is the ID we're currently emitting so we need to write it
|
|
// once but if we discover it again, we refer to it by id.
|
|
modelRoot = null;
|
|
} else {
|
|
// We've seen this promise before, so we can just refer to the same result.
|
|
return serializePromiseID(existingId);
|
|
}
|
|
}
|
|
// We assume that any object with a .then property is a "Thenable" type,
|
|
// or a Promise type. Either of which can be represented by a Promise.
|
|
const promiseId = serializeThenable(request, (value: any));
|
|
writtenObjects.set(value, promiseId);
|
|
return serializePromiseID(promiseId);
|
|
}
|
|
|
|
if (enableServerContext) {
|
|
if ((value: any).$$typeof === REACT_PROVIDER_TYPE) {
|
|
const providerKey = ((value: any): ReactProviderType<any>)._context
|
|
._globalName;
|
|
const writtenProviders = request.writtenProviders;
|
|
let providerId = writtenProviders.get(key);
|
|
if (providerId === undefined) {
|
|
request.pendingChunks++;
|
|
providerId = request.nextChunkId++;
|
|
writtenProviders.set(providerKey, providerId);
|
|
emitProviderChunk(request, providerId, providerKey);
|
|
}
|
|
return serializeByValueID(providerId);
|
|
} else if (value === POP) {
|
|
popProvider();
|
|
if (__DEV__) {
|
|
insideContextProps = null;
|
|
isInsideContextValue = false;
|
|
}
|
|
return (undefined: any);
|
|
}
|
|
}
|
|
|
|
if (existingId !== undefined) {
|
|
if (existingId === -1) {
|
|
// Seen but not yet outlined.
|
|
const newId = outlineModel(request, value);
|
|
return serializeByValueID(newId);
|
|
} else if (modelRoot === value) {
|
|
// This is the ID we're currently emitting so we need to write it
|
|
// once but if we discover it again, we refer to it by id.
|
|
modelRoot = null;
|
|
} else {
|
|
// We've already emitted this as an outlined object, so we can
|
|
// just refer to that by its existing ID.
|
|
return serializeByValueID(existingId);
|
|
}
|
|
} else {
|
|
// This is the first time we've seen this object. We may never see it again
|
|
// so we'll inline it. Mark it as seen. If we see it again, we'll outline.
|
|
writtenObjects.set(value, -1);
|
|
}
|
|
|
|
if (isArray(value)) {
|
|
// $FlowFixMe[incompatible-return]
|
|
return value;
|
|
}
|
|
|
|
if (value instanceof Map) {
|
|
return serializeMap(request, value);
|
|
}
|
|
if (value instanceof Set) {
|
|
return serializeSet(request, value);
|
|
}
|
|
|
|
if (enableBinaryFlight) {
|
|
if (value instanceof ArrayBuffer) {
|
|
return serializeTypedArray(request, 'A', new Uint8Array(value));
|
|
}
|
|
if (value instanceof Int8Array) {
|
|
// char
|
|
return serializeTypedArray(request, 'C', value);
|
|
}
|
|
if (value instanceof Uint8Array) {
|
|
// unsigned char
|
|
return serializeTypedArray(request, 'c', value);
|
|
}
|
|
if (value instanceof Uint8ClampedArray) {
|
|
// unsigned clamped char
|
|
return serializeTypedArray(request, 'U', value);
|
|
}
|
|
if (value instanceof Int16Array) {
|
|
// sort
|
|
return serializeTypedArray(request, 'S', value);
|
|
}
|
|
if (value instanceof Uint16Array) {
|
|
// unsigned short
|
|
return serializeTypedArray(request, 's', value);
|
|
}
|
|
if (value instanceof Int32Array) {
|
|
// long
|
|
return serializeTypedArray(request, 'L', value);
|
|
}
|
|
if (value instanceof Uint32Array) {
|
|
// unsigned long
|
|
return serializeTypedArray(request, 'l', value);
|
|
}
|
|
if (value instanceof Float32Array) {
|
|
// float
|
|
return serializeTypedArray(request, 'F', value);
|
|
}
|
|
if (value instanceof Float64Array) {
|
|
// double
|
|
return serializeTypedArray(request, 'D', value);
|
|
}
|
|
if (value instanceof BigInt64Array) {
|
|
// number
|
|
return serializeTypedArray(request, 'N', value);
|
|
}
|
|
if (value instanceof BigUint64Array) {
|
|
// unsigned number
|
|
// We use "m" instead of "n" since JSON can start with "null"
|
|
return serializeTypedArray(request, 'm', value);
|
|
}
|
|
if (value instanceof DataView) {
|
|
return serializeTypedArray(request, 'V', value);
|
|
}
|
|
}
|
|
|
|
const iteratorFn = getIteratorFn(value);
|
|
if (iteratorFn) {
|
|
return Array.from((value: any));
|
|
}
|
|
|
|
// Verify that this is a simple plain object.
|
|
const proto = getPrototypeOf(value);
|
|
if (
|
|
proto !== ObjectPrototype &&
|
|
(proto === null || getPrototypeOf(proto) !== null)
|
|
) {
|
|
throw new Error(
|
|
'Only plain objects, and a few built-ins, can be passed to Client Components ' +
|
|
'from Server Components. Classes or null prototypes are not supported.',
|
|
);
|
|
}
|
|
if (__DEV__) {
|
|
if (objectName(value) !== 'Object') {
|
|
console.error(
|
|
'Only plain objects can be passed to Client Components from Server Components. ' +
|
|
'%s objects are not supported.%s',
|
|
objectName(value),
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
} else if (!isSimpleObject(value)) {
|
|
console.error(
|
|
'Only plain objects can be passed to Client Components from Server Components. ' +
|
|
'Classes or other objects with methods are not supported.%s',
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
} else if (Object.getOwnPropertySymbols) {
|
|
const symbols = Object.getOwnPropertySymbols(value);
|
|
if (symbols.length > 0) {
|
|
console.error(
|
|
'Only plain objects can be passed to Client Components from Server Components. ' +
|
|
'Objects with symbol properties like %s are not supported.%s',
|
|
symbols[0].description,
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// $FlowFixMe[incompatible-return]
|
|
return value;
|
|
}
|
|
|
|
if (typeof value === 'string') {
|
|
if (enableTaint) {
|
|
const tainted = TaintRegistryValues.get(value);
|
|
if (tainted !== undefined) {
|
|
throwTaintViolation(tainted.message);
|
|
}
|
|
}
|
|
// TODO: Maybe too clever. If we support URL there's no similar trick.
|
|
if (value[value.length - 1] === 'Z') {
|
|
// Possibly a Date, whose toJSON automatically calls toISOString
|
|
// $FlowFixMe[incompatible-use]
|
|
const originalValue = parent[key];
|
|
if (originalValue instanceof Date) {
|
|
return serializeDateFromDateJSON(value);
|
|
}
|
|
}
|
|
if (value.length >= 1024) {
|
|
// For large strings, we encode them outside the JSON payload so that we
|
|
// don't have to double encode and double parse the strings. This can also
|
|
// be more compact in case the string has a lot of escaped characters.
|
|
return serializeLargeTextString(request, value);
|
|
}
|
|
return escapeStringValue(value);
|
|
}
|
|
|
|
if (typeof value === 'boolean') {
|
|
return value;
|
|
}
|
|
|
|
if (typeof value === 'number') {
|
|
return serializeNumber(value);
|
|
}
|
|
|
|
if (typeof value === 'undefined') {
|
|
return serializeUndefined();
|
|
}
|
|
|
|
if (typeof value === 'function') {
|
|
if (enableTaint) {
|
|
const tainted = TaintRegistryObjects.get(value);
|
|
if (tainted !== undefined) {
|
|
throwTaintViolation(tainted);
|
|
}
|
|
}
|
|
if (isClientReference(value)) {
|
|
return serializeClientReference(request, parent, key, (value: any));
|
|
}
|
|
if (isServerReference(value)) {
|
|
return serializeServerReference(request, parent, key, (value: any));
|
|
}
|
|
if (/^on[A-Z]/.test(key)) {
|
|
throw new Error(
|
|
'Event handlers cannot be passed to Client Component props.' +
|
|
describeObjectForErrorMessage(parent, key) +
|
|
'\nIf you need interactivity, consider converting part of this to a Client Component.',
|
|
);
|
|
} else {
|
|
throw new Error(
|
|
'Functions cannot be passed directly to Client Components ' +
|
|
'unless you explicitly expose it by marking it with "use server".' +
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
}
|
|
}
|
|
|
|
if (typeof value === 'symbol') {
|
|
const writtenSymbols = request.writtenSymbols;
|
|
const existingId = writtenSymbols.get(value);
|
|
if (existingId !== undefined) {
|
|
return serializeByValueID(existingId);
|
|
}
|
|
// $FlowFixMe[incompatible-type] `description` might be undefined
|
|
const name: string = value.description;
|
|
|
|
if (Symbol.for(name) !== value) {
|
|
throw new Error(
|
|
'Only global symbols received from Symbol.for(...) can be passed to Client Components. ' +
|
|
`The symbol Symbol.for(${
|
|
// $FlowFixMe[incompatible-type] `description` might be undefined
|
|
value.description
|
|
}) cannot be found among global symbols.` +
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
}
|
|
|
|
request.pendingChunks++;
|
|
const symbolId = request.nextChunkId++;
|
|
emitSymbolChunk(request, symbolId, name);
|
|
writtenSymbols.set(value, symbolId);
|
|
return serializeByValueID(symbolId);
|
|
}
|
|
|
|
if (typeof value === 'bigint') {
|
|
if (enableTaint) {
|
|
const tainted = TaintRegistryValues.get(value);
|
|
if (tainted !== undefined) {
|
|
throwTaintViolation(tainted.message);
|
|
}
|
|
}
|
|
return serializeBigInt(value);
|
|
}
|
|
|
|
throw new Error(
|
|
`Type ${typeof value} is not supported in Client Component props.` +
|
|
describeObjectForErrorMessage(parent, key),
|
|
);
|
|
}
|
|
|
|
function logPostpone(request: Request, reason: string): void {
|
|
const onPostpone = request.onPostpone;
|
|
onPostpone(reason);
|
|
}
|
|
|
|
function logRecoverableError(request: Request, error: mixed): string {
|
|
const onError = request.onError;
|
|
const errorDigest = onError(error);
|
|
if (errorDigest != null && typeof errorDigest !== 'string') {
|
|
// eslint-disable-next-line react-internal/prod-error-codes
|
|
throw new Error(
|
|
`onError returned something with a type other than "string". onError should return a string and may return null or undefined but must not return anything else. It received something of type "${typeof errorDigest}" instead`,
|
|
);
|
|
}
|
|
return errorDigest || '';
|
|
}
|
|
|
|
function fatalError(request: Request, error: mixed): void {
|
|
if (enableTaint) {
|
|
cleanupTaintQueue(request);
|
|
}
|
|
// This is called outside error handling code such as if an error happens in React internals.
|
|
if (request.destination !== null) {
|
|
request.status = CLOSED;
|
|
closeWithError(request.destination, error);
|
|
} else {
|
|
request.status = CLOSING;
|
|
request.fatalError = error;
|
|
}
|
|
}
|
|
|
|
function emitPostponeChunk(
|
|
request: Request,
|
|
id: number,
|
|
postponeInstance: Postpone,
|
|
): void {
|
|
let row;
|
|
if (__DEV__) {
|
|
let reason = '';
|
|
let stack = '';
|
|
try {
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
reason = String(postponeInstance.message);
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
stack = String(postponeInstance.stack);
|
|
} catch (x) {}
|
|
row = serializeRowHeader('P', id) + stringify({reason, stack}) + '\n';
|
|
} else {
|
|
// No reason included in prod.
|
|
row = serializeRowHeader('P', id) + '\n';
|
|
}
|
|
const processedChunk = stringToChunk(row);
|
|
request.completedErrorChunks.push(processedChunk);
|
|
}
|
|
|
|
function emitErrorChunk(
|
|
request: Request,
|
|
id: number,
|
|
digest: string,
|
|
error: mixed,
|
|
): void {
|
|
let errorInfo: any;
|
|
if (__DEV__) {
|
|
let message;
|
|
let stack = '';
|
|
try {
|
|
if (error instanceof Error) {
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
message = String(error.message);
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
stack = String(error.stack);
|
|
} else {
|
|
message = 'Error: ' + (error: any);
|
|
}
|
|
} catch (x) {
|
|
message = 'An error occurred but serializing the error message failed.';
|
|
}
|
|
errorInfo = {digest, message, stack};
|
|
} else {
|
|
errorInfo = {digest};
|
|
}
|
|
const row = serializeRowHeader('E', id) + stringify(errorInfo) + '\n';
|
|
const processedChunk = stringToChunk(row);
|
|
request.completedErrorChunks.push(processedChunk);
|
|
}
|
|
|
|
function emitImportChunk(
|
|
request: Request,
|
|
id: number,
|
|
clientReferenceMetadata: ClientReferenceMetadata,
|
|
): void {
|
|
// $FlowFixMe[incompatible-type] stringify can return null
|
|
const json: string = stringify(clientReferenceMetadata);
|
|
const row = serializeRowHeader('I', id) + json + '\n';
|
|
const processedChunk = stringToChunk(row);
|
|
request.completedImportChunks.push(processedChunk);
|
|
}
|
|
|
|
function emitHintChunk<Code: HintCode>(
|
|
request: Request,
|
|
code: Code,
|
|
model: HintModel<Code>,
|
|
): void {
|
|
const json: string = stringify(model);
|
|
const id = request.nextChunkId++;
|
|
const row = serializeRowHeader('H' + code, id) + json + '\n';
|
|
const processedChunk = stringToChunk(row);
|
|
request.completedHintChunks.push(processedChunk);
|
|
}
|
|
|
|
function emitSymbolChunk(request: Request, id: number, name: string): void {
|
|
const symbolReference = serializeSymbolReference(name);
|
|
const processedChunk = encodeReferenceChunk(request, id, symbolReference);
|
|
request.completedImportChunks.push(processedChunk);
|
|
}
|
|
|
|
function emitProviderChunk(
|
|
request: Request,
|
|
id: number,
|
|
contextName: string,
|
|
): void {
|
|
const contextReference = serializeProviderReference(contextName);
|
|
const processedChunk = encodeReferenceChunk(request, id, contextReference);
|
|
request.completedRegularChunks.push(processedChunk);
|
|
}
|
|
|
|
function emitModelChunk(
|
|
request: Request,
|
|
id: number,
|
|
model: ReactClientValue,
|
|
): void {
|
|
// Track the root so we know that we have to emit this object even though it
|
|
// already has an ID. This is needed because we might see this object twice
|
|
// in the same toJSON if it is cyclic.
|
|
modelRoot = model;
|
|
// $FlowFixMe[incompatible-type] stringify can return null
|
|
const json: string = stringify(model, request.toJSON);
|
|
const row = id.toString(16) + ':' + json + '\n';
|
|
const processedChunk = stringToChunk(row);
|
|
request.completedRegularChunks.push(processedChunk);
|
|
}
|
|
|
|
function retryTask(request: Request, task: Task): void {
|
|
if (task.status !== PENDING) {
|
|
// We completed this by other means before we had a chance to retry it.
|
|
return;
|
|
}
|
|
|
|
switchContext(task.context);
|
|
try {
|
|
let value = task.model;
|
|
if (
|
|
typeof value === 'object' &&
|
|
value !== null &&
|
|
(value: any).$$typeof === REACT_ELEMENT_TYPE
|
|
) {
|
|
request.writtenObjects.set(value, task.id);
|
|
|
|
// TODO: Concatenate keys of parents onto children.
|
|
const element: React$Element<any> = (value: any);
|
|
|
|
// When retrying a component, reuse the thenableState from the
|
|
// previous attempt.
|
|
const prevThenableState = task.thenableState;
|
|
|
|
// Attempt to render the Server Component.
|
|
// Doing this here lets us reuse this same task if the next component
|
|
// also suspends.
|
|
task.model = value;
|
|
value = attemptResolveElement(
|
|
request,
|
|
element.type,
|
|
element.key,
|
|
element.ref,
|
|
element.props,
|
|
prevThenableState,
|
|
);
|
|
|
|
// Successfully finished this component. We're going to keep rendering
|
|
// using the same task, but we reset its thenable state before continuing.
|
|
task.thenableState = null;
|
|
|
|
// Keep rendering and reuse the same task. This inner loop is separate
|
|
// from the render above because we don't need to reset the thenable state
|
|
// until the next time something suspends and retries.
|
|
while (
|
|
typeof value === 'object' &&
|
|
value !== null &&
|
|
(value: any).$$typeof === REACT_ELEMENT_TYPE
|
|
) {
|
|
request.writtenObjects.set(value, task.id);
|
|
// TODO: Concatenate keys of parents onto children.
|
|
const nextElement: React$Element<any> = (value: any);
|
|
task.model = value;
|
|
value = attemptResolveElement(
|
|
request,
|
|
nextElement.type,
|
|
nextElement.key,
|
|
nextElement.ref,
|
|
nextElement.props,
|
|
null,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Track that this object is outlined and has an id.
|
|
if (typeof value === 'object' && value !== null) {
|
|
request.writtenObjects.set(value, task.id);
|
|
}
|
|
|
|
emitModelChunk(request, task.id, value);
|
|
request.abortableTasks.delete(task);
|
|
task.status = COMPLETED;
|
|
} catch (thrownValue) {
|
|
const x =
|
|
thrownValue === SuspenseException
|
|
? // This is a special type of exception used for Suspense. For historical
|
|
// reasons, the rest of the Suspense implementation expects the thrown
|
|
// value to be a thenable, because before `use` existed that was the
|
|
// (unstable) API for suspending. This implementation detail can change
|
|
// later, once we deprecate the old API in favor of `use`.
|
|
getSuspendedThenable()
|
|
: thrownValue;
|
|
if (typeof x === 'object' && x !== null) {
|
|
// $FlowFixMe[method-unbinding]
|
|
if (typeof x.then === 'function') {
|
|
// Something suspended again, let's pick it back up later.
|
|
const ping = task.ping;
|
|
x.then(ping, ping);
|
|
task.thenableState = getThenableStateAfterSuspending();
|
|
return;
|
|
} else if (enablePostpone && x.$$typeof === REACT_POSTPONE_TYPE) {
|
|
request.abortableTasks.delete(task);
|
|
task.status = ERRORED;
|
|
const postponeInstance: Postpone = (x: any);
|
|
logPostpone(request, postponeInstance.message);
|
|
emitPostponeChunk(request, task.id, postponeInstance);
|
|
return;
|
|
}
|
|
}
|
|
request.abortableTasks.delete(task);
|
|
task.status = ERRORED;
|
|
const digest = logRecoverableError(request, x);
|
|
emitErrorChunk(request, task.id, digest, x);
|
|
}
|
|
}
|
|
|
|
function performWork(request: Request): void {
|
|
const prevDispatcher = ReactCurrentDispatcher.current;
|
|
ReactCurrentDispatcher.current = HooksDispatcher;
|
|
const prevRequest = currentRequest;
|
|
currentRequest = request;
|
|
prepareToUseHooksForRequest(request);
|
|
|
|
try {
|
|
const pingedTasks = request.pingedTasks;
|
|
request.pingedTasks = [];
|
|
for (let i = 0; i < pingedTasks.length; i++) {
|
|
const task = pingedTasks[i];
|
|
retryTask(request, task);
|
|
}
|
|
if (request.destination !== null) {
|
|
flushCompletedChunks(request, request.destination);
|
|
}
|
|
} catch (error) {
|
|
logRecoverableError(request, error);
|
|
fatalError(request, error);
|
|
} finally {
|
|
ReactCurrentDispatcher.current = prevDispatcher;
|
|
resetHooksForRequest();
|
|
currentRequest = prevRequest;
|
|
}
|
|
}
|
|
|
|
function abortTask(task: Task, request: Request, errorId: number): void {
|
|
task.status = ABORTED;
|
|
// Instead of emitting an error per task.id, we emit a model that only
|
|
// has a single value referencing the error.
|
|
const ref = serializeByValueID(errorId);
|
|
const processedChunk = encodeReferenceChunk(request, task.id, ref);
|
|
request.completedErrorChunks.push(processedChunk);
|
|
}
|
|
|
|
function flushCompletedChunks(
|
|
request: Request,
|
|
destination: Destination,
|
|
): void {
|
|
beginWriting(destination);
|
|
try {
|
|
// We emit module chunks first in the stream so that
|
|
// they can be preloaded as early as possible.
|
|
const importsChunks = request.completedImportChunks;
|
|
let i = 0;
|
|
for (; i < importsChunks.length; i++) {
|
|
request.pendingChunks--;
|
|
const chunk = importsChunks[i];
|
|
const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
|
|
if (!keepWriting) {
|
|
request.destination = null;
|
|
i++;
|
|
break;
|
|
}
|
|
}
|
|
importsChunks.splice(0, i);
|
|
|
|
// Next comes hints.
|
|
const hintChunks = request.completedHintChunks;
|
|
i = 0;
|
|
for (; i < hintChunks.length; i++) {
|
|
const chunk = hintChunks[i];
|
|
const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
|
|
if (!keepWriting) {
|
|
request.destination = null;
|
|
i++;
|
|
break;
|
|
}
|
|
}
|
|
hintChunks.splice(0, i);
|
|
|
|
// Next comes model data.
|
|
const regularChunks = request.completedRegularChunks;
|
|
i = 0;
|
|
for (; i < regularChunks.length; i++) {
|
|
request.pendingChunks--;
|
|
const chunk = regularChunks[i];
|
|
const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
|
|
if (!keepWriting) {
|
|
request.destination = null;
|
|
i++;
|
|
break;
|
|
}
|
|
}
|
|
regularChunks.splice(0, i);
|
|
|
|
// Finally, errors are sent. The idea is that it's ok to delay
|
|
// any error messages and prioritize display of other parts of
|
|
// the page.
|
|
const errorChunks = request.completedErrorChunks;
|
|
i = 0;
|
|
for (; i < errorChunks.length; i++) {
|
|
request.pendingChunks--;
|
|
const chunk = errorChunks[i];
|
|
const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
|
|
if (!keepWriting) {
|
|
request.destination = null;
|
|
i++;
|
|
break;
|
|
}
|
|
}
|
|
errorChunks.splice(0, i);
|
|
} finally {
|
|
request.flushScheduled = false;
|
|
completeWriting(destination);
|
|
}
|
|
flushBuffered(destination);
|
|
if (request.pendingChunks === 0) {
|
|
// We're done.
|
|
if (enableTaint) {
|
|
cleanupTaintQueue(request);
|
|
}
|
|
close(destination);
|
|
}
|
|
}
|
|
|
|
export function startWork(request: Request): void {
|
|
request.flushScheduled = request.destination !== null;
|
|
if (supportsRequestStorage) {
|
|
scheduleWork(() => requestStorage.run(request, performWork, request));
|
|
} else {
|
|
scheduleWork(() => performWork(request));
|
|
}
|
|
}
|
|
|
|
function enqueueFlush(request: Request): void {
|
|
if (
|
|
request.flushScheduled === false &&
|
|
// If there are pinged tasks we are going to flush anyway after work completes
|
|
request.pingedTasks.length === 0 &&
|
|
// If there is no destination there is nothing we can flush to. A flush will
|
|
// happen when we start flowing again
|
|
request.destination !== null
|
|
) {
|
|
const destination = request.destination;
|
|
request.flushScheduled = true;
|
|
scheduleWork(() => flushCompletedChunks(request, destination));
|
|
}
|
|
}
|
|
|
|
export function startFlowing(request: Request, destination: Destination): void {
|
|
if (request.status === CLOSING) {
|
|
request.status = CLOSED;
|
|
closeWithError(destination, request.fatalError);
|
|
return;
|
|
}
|
|
if (request.status === CLOSED) {
|
|
return;
|
|
}
|
|
if (request.destination !== null) {
|
|
// We're already flowing.
|
|
return;
|
|
}
|
|
request.destination = destination;
|
|
try {
|
|
flushCompletedChunks(request, destination);
|
|
} catch (error) {
|
|
logRecoverableError(request, error);
|
|
fatalError(request, error);
|
|
}
|
|
}
|
|
|
|
export function stopFlowing(request: Request): void {
|
|
request.destination = null;
|
|
}
|
|
|
|
// This is called to early terminate a request. It creates an error at all pending tasks.
|
|
export function abort(request: Request, reason: mixed): void {
|
|
try {
|
|
const abortableTasks = request.abortableTasks;
|
|
if (abortableTasks.size > 0) {
|
|
// We have tasks to abort. We'll emit one error row and then emit a reference
|
|
// to that row from every row that's still remaining.
|
|
request.pendingChunks++;
|
|
const errorId = request.nextChunkId++;
|
|
if (
|
|
enablePostpone &&
|
|
typeof reason === 'object' &&
|
|
reason !== null &&
|
|
(reason: any).$$typeof === REACT_POSTPONE_TYPE
|
|
) {
|
|
const postponeInstance: Postpone = (reason: any);
|
|
logPostpone(request, postponeInstance.message);
|
|
emitPostponeChunk(request, errorId, postponeInstance);
|
|
} else {
|
|
const error =
|
|
reason === undefined
|
|
? new Error(
|
|
'The render was aborted by the server without a reason.',
|
|
)
|
|
: reason;
|
|
const digest = logRecoverableError(request, error);
|
|
emitErrorChunk(request, errorId, digest, error);
|
|
}
|
|
abortableTasks.forEach(task => abortTask(task, request, errorId));
|
|
abortableTasks.clear();
|
|
}
|
|
if (request.destination !== null) {
|
|
flushCompletedChunks(request, request.destination);
|
|
}
|
|
} catch (error) {
|
|
logRecoverableError(request, error);
|
|
fatalError(request, error);
|
|
}
|
|
}
|
|
|
|
function importServerContexts(
|
|
contexts?: Array<[string, ServerContextJSONValue]>,
|
|
) {
|
|
if (enableServerContext && contexts) {
|
|
const prevContext = getActiveContext();
|
|
switchContext(rootContextSnapshot);
|
|
for (let i = 0; i < contexts.length; i++) {
|
|
const [name, value] = contexts[i];
|
|
const context = getOrCreateServerContext(name);
|
|
pushProvider(context, value);
|
|
}
|
|
const importedContext = getActiveContext();
|
|
switchContext(prevContext);
|
|
return importedContext;
|
|
}
|
|
return rootContextSnapshot;
|
|
}
|