react/packages/react-server/src/ReactFizzServer.js
Sebastian Markbåge df061b3966
[Fizz] Don't bail out of flushing if we still have pending root tasks (#27385)
The idea for this check is that we shouldn't flush anything before we
flush the shell. That may or may not hold true in future formats like
RN.

It is a problem for resuming because with resuming it's possible to have
root tasks that are used for resuming but the shell was already flushed
so we can have completed boundaries before the shell has fully resumed.
What matters is whether the parent has already flushed or not.

It's not technically necessary to bail early because there won't be
anything in partialBoundaries or completedBoundaries because nothing
gets added there unless the parent has already flushed.

It's not exactly slow to have to check the length of three arrays so
it's probably not a big deal.

Flush partials in an early preamble needs further consideration
regardless.
2023-09-18 11:25:21 -04:00

4020 lines
128 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 {
Destination,
Chunk,
PrecomputedChunk,
} from './ReactServerStreamConfig';
import type {
ReactNodeList,
ReactContext,
ReactProviderType,
OffscreenMode,
Wakeable,
Thenable,
ReactFormState,
} from 'shared/ReactTypes';
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {
SuspenseBoundaryID,
RenderState,
ResumableState,
FormatContext,
BoundaryResources,
} from './ReactFizzConfig';
import type {ContextSnapshot} from './ReactFizzNewContext';
import type {ComponentStackNode} from './ReactFizzComponentStack';
import type {TreeContext} from './ReactFizzTreeContext';
import type {ThenableState} from './ReactFizzThenable';
import {
scheduleWork,
beginWriting,
writeChunk,
writeChunkAndReturn,
completeWriting,
flushBuffered,
close,
closeWithError,
} from './ReactServerStreamConfig';
import {
writeCompletedRoot,
writePlaceholder,
writeStartCompletedSuspenseBoundary,
writeStartPendingSuspenseBoundary,
writeStartClientRenderedSuspenseBoundary,
writeEndCompletedSuspenseBoundary,
writeEndPendingSuspenseBoundary,
writeEndClientRenderedSuspenseBoundary,
writeStartSegment,
writeEndSegment,
writeClientRenderBoundaryInstruction,
writeCompletedBoundaryInstruction,
writeCompletedSegmentInstruction,
pushTextInstance,
pushStartInstance,
pushEndInstance,
pushStartCompletedSuspenseBoundary,
pushEndCompletedSuspenseBoundary,
pushSegmentFinale,
UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
assignSuspenseBoundaryID,
getChildFormatContext,
writeResourcesForBoundary,
writePreamble,
writeHoistables,
writePostamble,
hoistResources,
setCurrentlyRenderingBoundaryResourcesTarget,
createBoundaryResources,
prepareHostDispatcher,
supportsRequestStorage,
requestStorage,
pushFormStateMarkerIsMatching,
pushFormStateMarkerIsNotMatching,
} from './ReactFizzConfig';
import {
constructClassInstance,
mountClassInstance,
} from './ReactFizzClassComponent';
import {
getMaskedContext,
processChildContext,
emptyContextObject,
} from './ReactFizzContext';
import {
readContext,
rootContextSnapshot,
switchContext,
getActiveContext,
pushProvider,
popProvider,
} from './ReactFizzNewContext';
import {
prepareToUseHooks,
finishHooks,
checkDidRenderIdHook,
resetHooksState,
HooksDispatcher,
currentResumableState,
setCurrentResumableState,
getThenableStateAfterSuspending,
unwrapThenable,
getFormStateCount,
getFormStateMatchingIndex,
} from './ReactFizzHooks';
import {DefaultCacheDispatcher} from './ReactFizzCache';
import {getStackByComponentStackNode} from './ReactFizzComponentStack';
import {emptyTreeContext, pushTreeContext} from './ReactFizzTreeContext';
import {
getIteratorFn,
REACT_ELEMENT_TYPE,
REACT_PORTAL_TYPE,
REACT_LAZY_TYPE,
REACT_SUSPENSE_TYPE,
REACT_LEGACY_HIDDEN_TYPE,
REACT_DEBUG_TRACING_MODE_TYPE,
REACT_STRICT_MODE_TYPE,
REACT_PROFILER_TYPE,
REACT_SUSPENSE_LIST_TYPE,
REACT_FRAGMENT_TYPE,
REACT_FORWARD_REF_TYPE,
REACT_MEMO_TYPE,
REACT_PROVIDER_TYPE,
REACT_CONTEXT_TYPE,
REACT_SERVER_CONTEXT_TYPE,
REACT_SCOPE_TYPE,
REACT_OFFSCREEN_TYPE,
REACT_POSTPONE_TYPE,
} from 'shared/ReactSymbols';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
disableLegacyContext,
disableModulePatternComponents,
enableScopeAPI,
enableSuspenseAvoidThisFallbackFizz,
enableFloat,
enableCache,
enablePostpone,
} from 'shared/ReactFeatureFlags';
import assign from 'shared/assign';
import getComponentNameFromType from 'shared/getComponentNameFromType';
import isArray from 'shared/isArray';
import {SuspenseException, getSuspendedThenable} from './ReactFizzThenable';
import type {Postpone} from 'react/src/ReactPostpone';
const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
const ReactCurrentCache = ReactSharedInternals.ReactCurrentCache;
const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
// Linked list representing the identity of a component given the component/tag name and key.
// The name might be minified but we assume that it's going to be the same generated name. Typically
// because it's just the same compiled output in practice.
export type KeyNode = [
Root | KeyNode /* parent */,
string | null /* name */,
string | number /* key */,
];
const REPLAY_NODE = 0;
const REPLAY_SUSPENSE_BOUNDARY = 1;
const RESUME_ELEMENT = 2;
const RESUME_SUSPENSE_BOUNDARY = 3;
const RESUME_SLOT = 4;
type ReplaySuspenseBoundary = [
1, // REPLAY_SUSPENSE_BOUNDARY
string | null /* name */,
string | number /* key */,
Array<ResumableNode> /* children */,
SuspenseBoundaryID /* id */,
number /* rootSegmentID */,
];
type ReplayNode =
| [
0, // REPLAY_NODE
string | null /* name */,
string | number /* key */,
Array<ResumableNode> /* children */,
]
| ReplaySuspenseBoundary;
type ResumeSuspenseBoundary = [
3, // RESUME_SUSPENSE_BOUNDARY
string | null /* name */,
string | number /* key */,
SuspenseBoundaryID /* id */,
number /* rootSegmentID */,
];
type ResumeElement = [
2, // RESUME_ELEMENT
string | null /* name */,
string | number /* key */,
number /* segment id */,
];
type ResumeSlot = [
4, // RESUME_SLOT
number /* index */,
number /* segment id */,
];
type ResumableNode =
| ReplayNode
| ResumeElement
| ResumeSuspenseBoundary
| ResumeSlot;
type PostponedHoles = {
workingMap: Map<KeyNode, ReplayNode>,
root: Array<ResumableNode>,
};
type LegacyContext = {
[key: string]: any,
};
const CLIENT_RENDERED = 4; // if it errors or infinitely suspends
type SuspenseBoundary = {
status: 0 | 1 | 4 | 5,
id: SuspenseBoundaryID,
rootSegmentID: number,
errorDigest: ?string, // the error hash if it errors
errorMessage?: string, // the error string if it errors
errorComponentStack?: string, // the error component stack if it errors
parentFlushed: boolean,
pendingTasks: number, // when it reaches zero we can show this boundary's content
completedSegments: Array<Segment>, // completed but not yet flushed segments.
byteSize: number, // used to determine whether to inline children boundaries.
fallbackAbortableTasks: Set<Task>, // used to cancel task on the fallback if the boundary completes or gets canceled.
resources: BoundaryResources,
keyPath: Root | KeyNode,
};
type RenderTask = {
replay: null,
node: ReactNodeList,
childIndex: number,
ping: () => void,
blockedBoundary: Root | SuspenseBoundary,
blockedSegment: Segment, // the segment we'll write to
abortSet: Set<Task>, // the abortable set that this task belongs to
keyPath: Root | KeyNode, // the path of all parent keys currently rendering
formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML)
legacyContext: LegacyContext, // the current legacy context that this task is executing in
context: ContextSnapshot, // the current new context that this task is executing in
treeContext: TreeContext, // the current tree context that this task is executing in
componentStack: null | ComponentStackNode, // DEV-only component stack
thenableState: null | ThenableState,
};
type ReplaySet = {
nodes: Array<ResumableNode>, // the possible paths to follow down the replaying
pendingTasks: number, // tracks the number of tasks currently tracking this set of nodes
// if pending tasks reach zero but there are still nodes left, it means we couldn't find
// them all in the tree, so we need to abort and client render the boundary.
};
type ReplayTask = {
replay: ReplaySet,
node: ReactNodeList,
childIndex: number,
ping: () => void,
blockedBoundary: Root | SuspenseBoundary,
blockedSegment: null, // we don't write to anything when we replay
abortSet: Set<Task>, // the abortable set that this task belongs to
keyPath: Root | KeyNode, // the path of all parent keys currently rendering
formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML)
legacyContext: LegacyContext, // the current legacy context that this task is executing in
context: ContextSnapshot, // the current new context that this task is executing in
treeContext: TreeContext, // the current tree context that this task is executing in
componentStack: null | ComponentStackNode, // DEV-only component stack
thenableState: null | ThenableState,
};
export type Task = RenderTask | ReplayTask;
const PENDING = 0;
const COMPLETED = 1;
const FLUSHED = 2;
const ABORTED = 3;
const ERRORED = 4;
const POSTPONED = 5;
type Root = null;
type Segment = {
status: 0 | 1 | 2 | 3 | 4 | 5,
parentFlushed: boolean, // typically a segment will be flushed by its parent, except if its parent was already flushed
id: number, // starts as 0 and is lazily assigned if the parent flushes early
+index: number, // the index within the parent's chunks or 0 at the root
+chunks: Array<Chunk | PrecomputedChunk>,
+children: Array<Segment>,
// The context that this segment was created in.
parentFormatContext: FormatContext,
// If this segment represents a fallback, this is the content that will replace that fallback.
+boundary: null | SuspenseBoundary,
// used to discern when text separator boundaries are needed
lastPushedText: boolean,
textEmbedded: boolean,
};
const OPEN = 0;
const CLOSING = 1;
const CLOSED = 2;
export opaque type Request = {
destination: null | Destination,
flushScheduled: boolean,
+resumableState: ResumableState,
+renderState: RenderState,
+rootFormatContext: FormatContext,
+progressiveChunkSize: number,
status: 0 | 1 | 2,
fatalError: mixed,
nextSegmentId: number,
allPendingTasks: number, // when it reaches zero, we can close the connection.
pendingRootTasks: number, // when this reaches zero, we've finished at least the root boundary.
completedRootSegment: null | Segment, // Completed but not yet flushed root segments.
abortableTasks: Set<Task>,
pingedTasks: Array<Task>, // High priority tasks that should be worked on first.
// Queues to flush in order of priority
clientRenderedBoundaries: Array<SuspenseBoundary>, // Errored or client rendered but not yet flushed.
completedBoundaries: Array<SuspenseBoundary>, // Completed but not yet fully flushed boundaries to show.
partialBoundaries: Array<SuspenseBoundary>, // Partially completed boundaries that can flush its segments early.
trackedPostpones: null | PostponedHoles, // Gets set to non-null while we want to track postponed holes. I.e. during a prerender.
// onError is called when an error happens anywhere in the tree. It might recover.
// The return string is used in production primarily to avoid leaking internals, secondarily to save bytes.
// Returning null/undefined will cause a defualt error message in production
onError: (error: mixed) => ?string,
// onAllReady is called when all pending task is done but it may not have flushed yet.
// This is a good time to start writing if you want only HTML and no intermediate steps.
onAllReady: () => void,
// onShellReady is called when there is at least a root fallback ready to show.
// Typically you don't need this callback because it's best practice to always have a
// root fallback ready so there's no need to wait.
onShellReady: () => void,
// onShellError is called when the shell didn't complete. That means you probably want to
// emit a different response to the stream instead.
onShellError: (error: mixed) => void,
onFatalError: (error: mixed) => void,
// onPostpone is called when postpone() is called anywhere in the tree, which will defer
// rendering - e.g. to the client. This is considered intentional and not an error.
onPostpone: (reason: string) => void,
// Form state that was the result of an MPA submission, if it was provided.
formState: null | ReactFormState<any, any>,
};
// This is a default heuristic for how to split up the HTML content into progressive
// loading. Our goal is to be able to display additional new content about every 500ms.
// Faster than that is unnecessary and should be throttled on the client. It also
// adds unnecessary overhead to do more splits. We don't know if it's a higher or lower
// end device but higher end suffer less from the overhead than lower end does from
// not getting small enough pieces. We error on the side of low end.
// We base this on low end 3G speeds which is about 500kbits per second. We assume
// that there can be a reasonable drop off from max bandwidth which leaves you with
// as little as 80%. We can receive half of that each 500ms - at best. In practice,
// a little bandwidth is lost to processing and contention - e.g. CSS and images that
// are downloaded along with the main content. So we estimate about half of that to be
// the lower end throughput. In other words, we expect that you can at least show
// about 12.5kb of content per 500ms. Not counting starting latency for the first
// paint.
// 500 * 1024 / 8 * .8 * 0.5 / 2
const DEFAULT_PROGRESSIVE_CHUNK_SIZE = 12800;
function defaultErrorHandler(error: mixed) {
console['error'](error); // Don't transform to our wrapper
return null;
}
function noop(): void {}
export function createRequest(
children: ReactNodeList,
resumableState: ResumableState,
renderState: RenderState,
rootFormatContext: FormatContext,
progressiveChunkSize: void | number,
onError: void | ((error: mixed) => ?string),
onAllReady: void | (() => void),
onShellReady: void | (() => void),
onShellError: void | ((error: mixed) => void),
onFatalError: void | ((error: mixed) => void),
onPostpone: void | ((reason: string) => void),
formState: void | null | ReactFormState<any, any>,
): Request {
prepareHostDispatcher();
const pingedTasks: Array<Task> = [];
const abortSet: Set<Task> = new Set();
const request: Request = {
destination: null,
flushScheduled: false,
resumableState,
renderState,
rootFormatContext,
progressiveChunkSize:
progressiveChunkSize === undefined
? DEFAULT_PROGRESSIVE_CHUNK_SIZE
: progressiveChunkSize,
status: OPEN,
fatalError: null,
nextSegmentId: 0,
allPendingTasks: 0,
pendingRootTasks: 0,
completedRootSegment: null,
abortableTasks: abortSet,
pingedTasks: pingedTasks,
clientRenderedBoundaries: ([]: Array<SuspenseBoundary>),
completedBoundaries: ([]: Array<SuspenseBoundary>),
partialBoundaries: ([]: Array<SuspenseBoundary>),
trackedPostpones: null,
onError: onError === undefined ? defaultErrorHandler : onError,
onPostpone: onPostpone === undefined ? noop : onPostpone,
onAllReady: onAllReady === undefined ? noop : onAllReady,
onShellReady: onShellReady === undefined ? noop : onShellReady,
onShellError: onShellError === undefined ? noop : onShellError,
onFatalError: onFatalError === undefined ? noop : onFatalError,
formState: formState === undefined ? null : formState,
};
// This segment represents the root fallback.
const rootSegment = createPendingSegment(
request,
0,
null,
rootFormatContext,
// Root segments are never embedded in Text on either edge
false,
false,
);
// There is no parent so conceptually, we're unblocked to flush this segment.
rootSegment.parentFlushed = true;
const rootTask = createRenderTask(
request,
null,
children,
-1,
null,
rootSegment,
abortSet,
null,
rootFormatContext,
emptyContextObject,
rootContextSnapshot,
emptyTreeContext,
);
pingedTasks.push(rootTask);
return request;
}
export function createPrerenderRequest(
children: ReactNodeList,
resumableState: ResumableState,
renderState: RenderState,
rootFormatContext: FormatContext,
progressiveChunkSize: void | number,
onError: void | ((error: mixed) => ?string),
onAllReady: void | (() => void),
onShellReady: void | (() => void),
onShellError: void | ((error: mixed) => void),
onFatalError: void | ((error: mixed) => void),
onPostpone: void | ((reason: string) => void),
): Request {
const request = createRequest(
children,
resumableState,
renderState,
rootFormatContext,
progressiveChunkSize,
onError,
onAllReady,
onShellReady,
onShellError,
onFatalError,
onPostpone,
);
// Start tracking postponed holes during this render.
request.trackedPostpones = {workingMap: new Map(), root: []};
return request;
}
export function resumeRequest(
children: ReactNodeList,
postponedState: PostponedState,
renderState: RenderState,
onError: void | ((error: mixed) => ?string),
onAllReady: void | (() => void),
onShellReady: void | (() => void),
onShellError: void | ((error: mixed) => void),
onFatalError: void | ((error: mixed) => void),
onPostpone: void | ((reason: string) => void),
): Request {
prepareHostDispatcher();
const pingedTasks: Array<Task> = [];
const abortSet: Set<Task> = new Set();
const request: Request = {
destination: null,
flushScheduled: false,
resumableState: postponedState.resumableState,
renderState,
rootFormatContext: postponedState.rootFormatContext,
progressiveChunkSize: postponedState.progressiveChunkSize,
status: OPEN,
fatalError: null,
nextSegmentId: 0,
allPendingTasks: 0,
pendingRootTasks: 0,
completedRootSegment: null,
abortableTasks: abortSet,
pingedTasks: pingedTasks,
clientRenderedBoundaries: ([]: Array<SuspenseBoundary>),
completedBoundaries: ([]: Array<SuspenseBoundary>),
partialBoundaries: ([]: Array<SuspenseBoundary>),
trackedPostpones: null,
onError: onError === undefined ? defaultErrorHandler : onError,
onPostpone: onPostpone === undefined ? noop : onPostpone,
onAllReady: onAllReady === undefined ? noop : onAllReady,
onShellReady: onShellReady === undefined ? noop : onShellReady,
onShellError: onShellError === undefined ? noop : onShellError,
onFatalError: onFatalError === undefined ? noop : onFatalError,
formState: null,
};
const rootTask = createReplayTask(
request,
null,
{nodes: postponedState.resumablePath, pendingTasks: 0},
children,
-1,
null,
abortSet,
null,
postponedState.rootFormatContext,
emptyContextObject,
rootContextSnapshot,
emptyTreeContext,
);
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 pingTask(request: Request, task: Task): void {
const pingedTasks = request.pingedTasks;
pingedTasks.push(task);
if (request.pingedTasks.length === 1) {
request.flushScheduled = request.destination !== null;
scheduleWork(() => performWork(request));
}
}
function createSuspenseBoundary(
request: Request,
fallbackAbortableTasks: Set<Task>,
keyPath: Root | KeyNode,
): SuspenseBoundary {
return {
status: PENDING,
id: UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
rootSegmentID: -1,
parentFlushed: false,
pendingTasks: 0,
completedSegments: [],
byteSize: 0,
fallbackAbortableTasks,
errorDigest: null,
resources: createBoundaryResources(),
keyPath,
};
}
function createRenderTask(
request: Request,
thenableState: ThenableState | null,
node: ReactNodeList,
childIndex: number,
blockedBoundary: Root | SuspenseBoundary,
blockedSegment: Segment,
abortSet: Set<Task>,
keyPath: Root | KeyNode,
formatContext: FormatContext,
legacyContext: LegacyContext,
context: ContextSnapshot,
treeContext: TreeContext,
): RenderTask {
request.allPendingTasks++;
if (blockedBoundary === null) {
request.pendingRootTasks++;
} else {
blockedBoundary.pendingTasks++;
}
const task: RenderTask = ({
replay: null,
node,
childIndex,
ping: () => pingTask(request, task),
blockedBoundary,
blockedSegment,
abortSet,
keyPath,
formatContext,
legacyContext,
context,
treeContext,
thenableState,
}: any);
if (__DEV__) {
task.componentStack = null;
}
abortSet.add(task);
return task;
}
function createReplayTask(
request: Request,
thenableState: ThenableState | null,
replay: ReplaySet,
node: ReactNodeList,
childIndex: number,
blockedBoundary: Root | SuspenseBoundary,
abortSet: Set<Task>,
keyPath: Root | KeyNode,
formatContext: FormatContext,
legacyContext: LegacyContext,
context: ContextSnapshot,
treeContext: TreeContext,
): ReplayTask {
request.allPendingTasks++;
if (blockedBoundary === null) {
request.pendingRootTasks++;
} else {
blockedBoundary.pendingTasks++;
}
replay.pendingTasks++;
const task: ReplayTask = ({
replay,
node,
childIndex,
ping: () => pingTask(request, task),
blockedBoundary,
blockedSegment: null,
abortSet,
keyPath,
formatContext,
legacyContext,
context,
treeContext,
thenableState,
}: any);
if (__DEV__) {
task.componentStack = null;
}
abortSet.add(task);
return task;
}
function createPendingSegment(
request: Request,
index: number,
boundary: null | SuspenseBoundary,
parentFormatContext: FormatContext,
lastPushedText: boolean,
textEmbedded: boolean,
): Segment {
return {
status: PENDING,
id: -1, // lazily assigned later
index,
parentFlushed: false,
chunks: [],
children: [],
parentFormatContext,
boundary,
lastPushedText,
textEmbedded,
};
}
// DEV-only global reference to the currently executing task
let currentTaskInDEV: null | Task = null;
function getCurrentStackInDEV(): string {
if (__DEV__) {
if (currentTaskInDEV === null || currentTaskInDEV.componentStack === null) {
return '';
}
return getStackByComponentStackNode(currentTaskInDEV.componentStack);
}
return '';
}
function pushBuiltInComponentStackInDEV(task: Task, type: string): void {
if (__DEV__) {
task.componentStack = {
tag: 0,
parent: task.componentStack,
type,
};
}
}
function pushFunctionComponentStackInDEV(task: Task, type: Function): void {
if (__DEV__) {
task.componentStack = {
tag: 1,
parent: task.componentStack,
type,
};
}
}
function pushClassComponentStackInDEV(task: Task, type: Function): void {
if (__DEV__) {
task.componentStack = {
tag: 2,
parent: task.componentStack,
type,
};
}
}
function popComponentStackInDEV(task: Task): void {
if (__DEV__) {
if (task.componentStack === null) {
console.error(
'Unexpectedly popped too many stack frames. This is a bug in React.',
);
} else {
task.componentStack = task.componentStack.parent;
}
}
}
// stash the component stack of an unwinding error until it is processed
let lastBoundaryErrorComponentStackDev: ?string = null;
function captureBoundaryErrorDetailsDev(
boundary: SuspenseBoundary,
error: mixed,
) {
if (__DEV__) {
let errorMessage;
if (typeof error === 'string') {
errorMessage = error;
} else if (error && typeof error.message === 'string') {
errorMessage = error.message;
} else {
// eslint-disable-next-line react-internal/safe-string-coercion
errorMessage = String(error);
}
const errorComponentStack =
lastBoundaryErrorComponentStackDev || getCurrentStackInDEV();
lastBoundaryErrorComponentStackDev = null;
boundary.errorMessage = errorMessage;
boundary.errorComponentStack = errorComponentStack;
}
}
function logPostpone(request: Request, reason: string): void {
// If this callback errors, we intentionally let that error bubble up to become a fatal error
// so that someone fixes the error reporting instead of hiding it.
request.onPostpone(reason);
}
function logRecoverableError(request: Request, error: any): ?string {
// If this callback errors, we intentionally let that error bubble up to become a fatal error
// so that someone fixes the error reporting instead of hiding it.
const errorDigest = request.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 {
// This is called outside error handling code such as if the root errors outside
// a suspense boundary or if the root suspense boundary's fallback errors.
// It's also called if React itself or its host configs errors.
const onShellError = request.onShellError;
onShellError(error);
const onFatalError = request.onFatalError;
onFatalError(error);
if (request.destination !== null) {
request.status = CLOSED;
closeWithError(request.destination, error);
} else {
request.status = CLOSING;
request.fatalError = error;
}
}
function renderSuspenseBoundary(
request: Request,
someTask: Task,
keyPath: Root | KeyNode,
props: Object,
): void {
if (someTask.replay !== null) {
throw new Error(
'Did not expect to see a Suspense boundary in this slot. ' +
"The tree doesn't match so React will fallback to client rendering.",
);
}
// $FlowFixMe: Refined.
const task: RenderTask = someTask;
pushBuiltInComponentStackInDEV(task, 'Suspense');
const prevKeyPath = task.keyPath;
const parentBoundary = task.blockedBoundary;
const parentSegment = task.blockedSegment;
// Each time we enter a suspense boundary, we split out into a new segment for
// the fallback so that we can later replace that segment with the content.
// This also lets us split out the main content even if it doesn't suspend,
// in case it ends up generating a large subtree of content.
const fallback: ReactNodeList = props.fallback;
const content: ReactNodeList = props.children;
const fallbackAbortSet: Set<Task> = new Set();
const newBoundary = createSuspenseBoundary(
request,
fallbackAbortSet,
keyPath,
);
const insertionIndex = parentSegment.chunks.length;
// The children of the boundary segment is actually the fallback.
const boundarySegment = createPendingSegment(
request,
insertionIndex,
newBoundary,
task.formatContext,
// boundaries never require text embedding at their edges because comment nodes bound them
false,
false,
);
parentSegment.children.push(boundarySegment);
// The parentSegment has a child Segment at this index so we reset the lastPushedText marker on the parent
parentSegment.lastPushedText = false;
// This segment is the actual child content. We can start rendering that immediately.
const contentRootSegment = createPendingSegment(
request,
0,
null,
task.formatContext,
// boundaries never require text embedding at their edges because comment nodes bound them
false,
false,
);
// We mark the root segment as having its parent flushed. It's not really flushed but there is
// no parent segment so there's nothing to wait on.
contentRootSegment.parentFlushed = true;
// Currently this is running synchronously. We could instead schedule this to pingedTasks.
// I suspect that there might be some efficiency benefits from not creating the suspended task
// and instead just using the stack if possible.
// TODO: Call this directly instead of messing with saving and restoring contexts.
// We can reuse the current context and task to render the content immediately without
// context switching. We just need to temporarily switch which boundary and which segment
// we're writing to. If something suspends, it'll spawn new suspended task with that context.
task.blockedBoundary = newBoundary;
task.blockedSegment = contentRootSegment;
if (enableFloat) {
setCurrentlyRenderingBoundaryResourcesTarget(
request.renderState,
newBoundary.resources,
);
}
task.keyPath = keyPath;
try {
// We use the safe form because we don't handle suspending here. Only error handling.
renderNode(request, task, content, -1);
pushSegmentFinale(
contentRootSegment.chunks,
request.renderState,
contentRootSegment.lastPushedText,
contentRootSegment.textEmbedded,
);
contentRootSegment.status = COMPLETED;
queueCompletedSegment(newBoundary, contentRootSegment);
if (newBoundary.pendingTasks === 0 && newBoundary.status === PENDING) {
newBoundary.status = COMPLETED;
// This must have been the last segment we were waiting on. This boundary is now complete.
// Therefore we won't need the fallback. We early return so that we don't have to create
// the fallback.
popComponentStackInDEV(task);
return;
}
} catch (error) {
contentRootSegment.status = ERRORED;
newBoundary.status = CLIENT_RENDERED;
let errorDigest;
if (
enablePostpone &&
typeof error === 'object' &&
error !== null &&
error.$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (error: any);
logPostpone(request, postponeInstance.message);
// TODO: Figure out a better signal than a magic digest value.
errorDigest = 'POSTPONE';
} else {
errorDigest = logRecoverableError(request, error);
}
newBoundary.errorDigest = errorDigest;
if (__DEV__) {
captureBoundaryErrorDetailsDev(newBoundary, error);
}
// We don't need to decrement any task numbers because we didn't spawn any new task.
// We don't need to schedule any task because we know the parent has written yet.
// We do need to fallthrough to create the fallback though.
} finally {
if (enableFloat) {
setCurrentlyRenderingBoundaryResourcesTarget(
request.renderState,
parentBoundary ? parentBoundary.resources : null,
);
}
task.blockedBoundary = parentBoundary;
task.blockedSegment = parentSegment;
task.keyPath = prevKeyPath;
}
// We create suspended task for the fallback because we don't want to actually work
// on it yet in case we finish the main content, so we queue for later.
const suspendedFallbackTask = createRenderTask(
request,
null,
fallback,
-1,
parentBoundary,
boundarySegment,
fallbackAbortSet,
// TODO: Should distinguish key path of fallback and primary tasks
keyPath,
task.formatContext,
task.legacyContext,
task.context,
task.treeContext,
);
if (__DEV__) {
suspendedFallbackTask.componentStack = task.componentStack;
}
// TODO: This should be queued at a separate lower priority queue so that we only work
// on preparing fallbacks if we don't have any more main content to task on.
request.pingedTasks.push(suspendedFallbackTask);
popComponentStackInDEV(task);
}
function replaySuspenseBoundary(
request: Request,
task: ReplayTask,
props: Object,
replayNode: ReplaySuspenseBoundary,
): void {
pushBuiltInComponentStackInDEV(task, 'Suspense');
const previousReplaySet: ReplaySet = task.replay;
const parentBoundary = task.blockedBoundary;
const content: ReactNodeList = props.children;
const fallbackAbortSet: Set<Task> = new Set();
const resumedBoundary = createSuspenseBoundary(
request,
fallbackAbortSet,
task.keyPath,
);
resumedBoundary.parentFlushed = true;
// We restore the same id of this boundary as was used during prerender.
resumedBoundary.id = replayNode[4];
resumedBoundary.rootSegmentID = replayNode[5];
// We can reuse the current context and task to render the content immediately without
// context switching. We just need to temporarily switch which boundary and replay node
// we're writing to. If something suspends, it'll spawn new suspended task with that context.
task.blockedBoundary = resumedBoundary;
task.replay = {nodes: replayNode[3], pendingTasks: 1};
if (enableFloat) {
// Does this even matter for replaying?
setCurrentlyRenderingBoundaryResourcesTarget(
request.renderState,
resumedBoundary.resources,
);
}
try {
// We use the safe form because we don't handle suspending here. Only error handling.
renderNode(request, task, content, -1);
if (
resumedBoundary.pendingTasks === 0 &&
resumedBoundary.status === PENDING
) {
resumedBoundary.status = COMPLETED;
request.completedBoundaries.push(resumedBoundary);
}
if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) {
throw new Error(
"Couldn't find all resumable slots by key/index during replaying. " +
"The tree doesn't match so React will fallback to client rendering.",
);
}
task.replay.pendingTasks--;
} catch (error) {
resumedBoundary.status = CLIENT_RENDERED;
let errorDigest;
if (
enablePostpone &&
typeof error === 'object' &&
error !== null &&
error.$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (error: any);
logPostpone(request, postponeInstance.message);
// TODO: Figure out a better signal than a magic digest value.
errorDigest = 'POSTPONE';
} else {
errorDigest = logRecoverableError(request, error);
}
resumedBoundary.errorDigest = errorDigest;
if (__DEV__) {
captureBoundaryErrorDetailsDev(resumedBoundary, error);
}
task.replay.pendingTasks--;
// We don't need to decrement any task numbers because we didn't spawn any new task.
// We don't need to schedule any task because we know the parent has written yet.
// We do need to fallthrough to create the fallback though.
} finally {
if (enableFloat) {
setCurrentlyRenderingBoundaryResourcesTarget(
request.renderState,
parentBoundary ? parentBoundary.resources : null,
);
}
task.blockedBoundary = parentBoundary;
task.replay = previousReplaySet;
}
// TODO: Should this be in the finally?
popComponentStackInDEV(task);
}
function resumeSuspenseBoundary(
request: Request,
task: ReplayTask,
props: Object,
replayNode: ResumeSuspenseBoundary,
): void {
pushBuiltInComponentStackInDEV(task, 'Suspense');
const previousReplaySet: ReplaySet = task.replay;
const parentBoundary = task.blockedBoundary;
const content: ReactNodeList = props.children;
const fallbackAbortSet: Set<Task> = new Set();
const resumedBoundary = createSuspenseBoundary(
request,
fallbackAbortSet,
task.keyPath,
);
resumedBoundary.parentFlushed = true;
// We restore the same id of this boundary as was used during prerender.
resumedBoundary.id = replayNode[3];
resumedBoundary.rootSegmentID = replayNode[4];
const resumedSegment = createPendingSegment(
request,
0,
null,
task.formatContext,
false,
false,
);
resumedSegment.parentFlushed = true;
resumedSegment.id = replayNode[4];
// We can reuse the current context and task to render the content immediately without
// context switching. We just need to temporarily switch which boundary and replay node
// we're writing to. If something suspends, it'll spawn new suspended task with that context.
task.blockedBoundary = resumedBoundary;
if (enableFloat) {
// Does this even matter for replaying?
setCurrentlyRenderingBoundaryResourcesTarget(
request.renderState,
resumedBoundary.resources,
);
}
try {
// Convert the current ReplayTask to a RenderTask.
const renderTask: RenderTask = (task: any);
renderTask.replay = null;
renderTask.blockedSegment = resumedSegment;
// We use the safe form because we don't handle suspending here. Only error handling.
renderNode(request, task, content, -1);
resumedSegment.status = COMPLETED;
queueCompletedSegment(resumedBoundary, resumedSegment);
if (
resumedBoundary.pendingTasks === 0 &&
resumedBoundary.status === PENDING
) {
resumedBoundary.status = COMPLETED;
request.completedBoundaries.push(resumedBoundary);
}
} catch (error) {
resumedBoundary.status = CLIENT_RENDERED;
let errorDigest;
if (
enablePostpone &&
typeof error === 'object' &&
error !== null &&
error.$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (error: any);
logPostpone(request, postponeInstance.message);
// TODO: Figure out a better signal than a magic digest value.
errorDigest = 'POSTPONE';
} else {
errorDigest = logRecoverableError(request, error);
}
resumedBoundary.errorDigest = errorDigest;
if (__DEV__) {
captureBoundaryErrorDetailsDev(resumedBoundary, error);
}
// We don't need to decrement any task numbers because we didn't spawn any new task.
// We don't need to schedule any task because we know the parent has written yet.
// We do need to fallthrough to create the fallback though.
} finally {
if (enableFloat) {
setCurrentlyRenderingBoundaryResourcesTarget(
request.renderState,
parentBoundary ? parentBoundary.resources : null,
);
}
task.blockedBoundary = parentBoundary;
// Restore to a ReplayTask
task.blockedSegment = null;
task.replay = previousReplaySet;
}
// TODO: Should this be in the finally?
popComponentStackInDEV(task);
}
function renderBackupSuspenseBoundary(
request: Request,
task: Task,
keyPath: Root | KeyNode,
props: Object,
) {
pushBuiltInComponentStackInDEV(task, 'Suspense');
const content = props.children;
const segment = task.blockedSegment;
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
if (segment === null) {
// Replay
renderNode(request, task, content, -1);
} else {
// Render
pushStartCompletedSuspenseBoundary(segment.chunks);
renderNode(request, task, content, -1);
pushEndCompletedSuspenseBoundary(segment.chunks);
}
task.keyPath = prevKeyPath;
popComponentStackInDEV(task);
}
function renderHostElement(
request: Request,
task: Task,
keyPath: Root | KeyNode,
type: string,
props: Object,
): void {
pushBuiltInComponentStackInDEV(task, type);
const segment = task.blockedSegment;
if (segment === null) {
// Replay
const children = props.children; // TODO: Make this a Config for replaying.
const prevContext = task.formatContext;
const prevKeyPath = task.keyPath;
task.formatContext = getChildFormatContext(prevContext, type, props);
task.keyPath = keyPath;
// We use the non-destructive form because if something suspends, we still
// need to pop back up and finish this subtree of HTML.
renderNode(request, task, children, -1);
// We expect that errors will fatal the whole task and that we don't need
// the correct context. Therefore this is not in a finally.
task.formatContext = prevContext;
task.keyPath = prevKeyPath;
} else {
// Render
const children = pushStartInstance(
segment.chunks,
type,
props,
request.resumableState,
request.renderState,
task.formatContext,
segment.lastPushedText,
);
segment.lastPushedText = false;
const prevContext = task.formatContext;
const prevKeyPath = task.keyPath;
task.formatContext = getChildFormatContext(prevContext, type, props);
task.keyPath = keyPath;
// We use the non-destructive form because if something suspends, we still
// need to pop back up and finish this subtree of HTML.
renderNode(request, task, children, -1);
// We expect that errors will fatal the whole task and that we don't need
// the correct context. Therefore this is not in a finally.
task.formatContext = prevContext;
task.keyPath = prevKeyPath;
pushEndInstance(
segment.chunks,
type,
props,
request.resumableState,
prevContext,
);
segment.lastPushedText = false;
}
popComponentStackInDEV(task);
}
function shouldConstruct(Component: any) {
return Component.prototype && Component.prototype.isReactComponent;
}
function renderWithHooks<Props, SecondArg>(
request: Request,
task: Task,
keyPath: Root | KeyNode,
prevThenableState: ThenableState | null,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
): any {
const componentIdentity = {};
prepareToUseHooks(
request,
task,
keyPath,
componentIdentity,
prevThenableState,
);
const result = Component(props, secondArg);
return finishHooks(Component, props, result, secondArg);
}
function finishClassComponent(
request: Request,
task: Task,
keyPath: Root | KeyNode,
instance: any,
Component: any,
props: any,
): ReactNodeList {
const nextChildren = instance.render();
if (__DEV__) {
if (instance.props !== props) {
if (!didWarnAboutReassigningProps) {
console.error(
'It looks like %s is reassigning its own `this.props` while rendering. ' +
'This is not supported and can lead to confusing bugs.',
getComponentNameFromType(Component) || 'a component',
);
}
didWarnAboutReassigningProps = true;
}
}
if (!disableLegacyContext) {
const childContextTypes = Component.childContextTypes;
if (childContextTypes !== null && childContextTypes !== undefined) {
const previousContext = task.legacyContext;
const mergedContext = processChildContext(
instance,
Component,
previousContext,
childContextTypes,
);
task.legacyContext = mergedContext;
renderNodeDestructive(request, task, null, nextChildren, -1);
task.legacyContext = previousContext;
return;
}
}
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
renderNodeDestructive(request, task, null, nextChildren, -1);
task.keyPath = prevKeyPath;
}
function renderClassComponent(
request: Request,
task: Task,
keyPath: Root | KeyNode,
Component: any,
props: any,
): void {
pushClassComponentStackInDEV(task, Component);
const maskedContext = !disableLegacyContext
? getMaskedContext(Component, task.legacyContext)
: undefined;
const instance = constructClassInstance(Component, props, maskedContext);
mountClassInstance(instance, Component, props, maskedContext);
finishClassComponent(request, task, keyPath, instance, Component, props);
popComponentStackInDEV(task);
}
const didWarnAboutBadClass: {[string]: boolean} = {};
const didWarnAboutModulePatternComponent: {[string]: boolean} = {};
const didWarnAboutContextTypeOnFunctionComponent: {[string]: boolean} = {};
const didWarnAboutGetDerivedStateOnFunctionComponent: {[string]: boolean} = {};
let didWarnAboutReassigningProps = false;
const didWarnAboutDefaultPropsOnFunctionComponent: {[string]: boolean} = {};
let didWarnAboutGenerators = false;
let didWarnAboutMaps = false;
let hasWarnedAboutUsingContextAsConsumer = false;
// This would typically be a function component but we still support module pattern
// components for some reason.
function renderIndeterminateComponent(
request: Request,
task: Task,
keyPath: Root | KeyNode,
prevThenableState: ThenableState | null,
Component: any,
props: any,
): void {
let legacyContext;
if (!disableLegacyContext) {
legacyContext = getMaskedContext(Component, task.legacyContext);
}
pushFunctionComponentStackInDEV(task, Component);
if (__DEV__) {
if (
Component.prototype &&
typeof Component.prototype.render === 'function'
) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutBadClass[componentName]) {
console.error(
"The <%s /> component appears to have a render method, but doesn't extend React.Component. " +
'This is likely to cause errors. Change %s to extend React.Component instead.',
componentName,
componentName,
);
didWarnAboutBadClass[componentName] = true;
}
}
}
const value = renderWithHooks(
request,
task,
keyPath,
prevThenableState,
Component,
props,
legacyContext,
);
const hasId = checkDidRenderIdHook();
const formStateCount = getFormStateCount();
const formStateMatchingIndex = getFormStateMatchingIndex();
if (__DEV__) {
// Support for module components is deprecated and is removed behind a flag.
// Whether or not it would crash later, we want to show a good message in DEV first.
if (
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutModulePatternComponent[componentName]) {
console.error(
'The <%s /> component appears to be a function component that returns a class instance. ' +
'Change %s to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
"`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " +
'cannot be called with `new` by React.',
componentName,
componentName,
componentName,
);
didWarnAboutModulePatternComponent[componentName] = true;
}
}
}
if (
// Run these checks in production only if the flag is off.
// Eventually we'll delete this branch altogether.
!disableModulePatternComponents &&
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
if (__DEV__) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutModulePatternComponent[componentName]) {
console.error(
'The <%s /> component appears to be a function component that returns a class instance. ' +
'Change %s to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
"`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " +
'cannot be called with `new` by React.',
componentName,
componentName,
componentName,
);
didWarnAboutModulePatternComponent[componentName] = true;
}
}
mountClassInstance(value, Component, props, legacyContext);
finishClassComponent(request, task, keyPath, value, Component, props);
} else {
// Proceed under the assumption that this is a function component
if (__DEV__) {
if (disableLegacyContext && Component.contextTypes) {
console.error(
'%s uses the legacy contextTypes API which is no longer supported. ' +
'Use React.createContext() with React.useContext() instead.',
getComponentNameFromType(Component) || 'Unknown',
);
}
}
if (__DEV__) {
validateFunctionComponentInDev(Component);
}
finishFunctionComponent(
request,
task,
keyPath,
value,
hasId,
formStateCount,
formStateMatchingIndex,
);
}
popComponentStackInDEV(task);
}
function finishFunctionComponent(
request: Request,
task: Task,
keyPath: Root | KeyNode,
children: ReactNodeList,
hasId: boolean,
formStateCount: number,
formStateMatchingIndex: number,
) {
let didEmitFormStateMarkers = false;
if (formStateCount !== 0 && request.formState !== null) {
// For each useFormState hook, emit a marker that indicates whether we
// rendered using the form state passed at the root. We only emit these
// markers if form state is passed at the root.
const segment = task.blockedSegment;
if (segment === null) {
// Implies we're in reumable mode.
} else {
didEmitFormStateMarkers = true;
const target = segment.chunks;
for (let i = 0; i < formStateCount; i++) {
if (i === formStateMatchingIndex) {
pushFormStateMarkerIsMatching(target);
} else {
pushFormStateMarkerIsNotMatching(target);
}
}
}
}
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
if (hasId) {
// This component materialized an id. We treat this as its own level, with
// a single "child" slot.
const prevTreeContext = task.treeContext;
const totalChildren = 1;
const index = 0;
// Modify the id context. Because we'll need to reset this if something
// suspends or errors, we'll use the non-destructive render path.
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
renderNode(request, task, children, -1);
// Like the other contexts, this does not need to be in a finally block
// because renderNode takes care of unwinding the stack.
task.treeContext = prevTreeContext;
} else if (didEmitFormStateMarkers) {
// If there were formState hooks, we must use the non-destructive path
// because this component is not a pure indirection; we emitted markers
// to the stream.
renderNode(request, task, children, -1);
} else {
// We're now successfully past this task, and we haven't modified the
// context stack. We don't have to pop back to the previous task every
// again, so we can use the destructive recursive form.
renderNodeDestructive(request, task, null, children, -1);
}
task.keyPath = prevKeyPath;
}
function validateFunctionComponentInDev(Component: any): void {
if (__DEV__) {
if (Component) {
if (Component.childContextTypes) {
console.error(
'%s(...): childContextTypes cannot be defined on a function component.',
Component.displayName || Component.name || 'Component',
);
}
}
if (Component.defaultProps !== undefined) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
console.error(
'%s: Support for defaultProps will be removed from function components ' +
'in a future major release. Use JavaScript default parameters instead.',
componentName,
);
didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true;
}
}
if (typeof Component.getDerivedStateFromProps === 'function') {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutGetDerivedStateOnFunctionComponent[componentName]) {
console.error(
'%s: Function components do not support getDerivedStateFromProps.',
componentName,
);
didWarnAboutGetDerivedStateOnFunctionComponent[componentName] = true;
}
}
if (
typeof Component.contextType === 'object' &&
Component.contextType !== null
) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutContextTypeOnFunctionComponent[componentName]) {
console.error(
'%s: Function components do not support contextType.',
componentName,
);
didWarnAboutContextTypeOnFunctionComponent[componentName] = true;
}
}
}
}
function resolveDefaultProps(Component: any, baseProps: Object): Object {
if (Component && Component.defaultProps) {
// Resolve default props. Taken from ReactElement
const props = assign({}, baseProps);
const defaultProps = Component.defaultProps;
for (const propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
return props;
}
return baseProps;
}
function renderForwardRef(
request: Request,
task: Task,
keyPath: Root | KeyNode,
prevThenableState: null | ThenableState,
type: any,
props: Object,
ref: any,
): void {
pushFunctionComponentStackInDEV(task, type.render);
const children = renderWithHooks(
request,
task,
keyPath,
prevThenableState,
type.render,
props,
ref,
);
const hasId = checkDidRenderIdHook();
const formStateCount = getFormStateCount();
const formStateMatchingIndex = getFormStateMatchingIndex();
finishFunctionComponent(
request,
task,
keyPath,
children,
hasId,
formStateCount,
formStateMatchingIndex,
);
popComponentStackInDEV(task);
}
function renderMemo(
request: Request,
task: Task,
keyPath: Root | KeyNode,
prevThenableState: ThenableState | null,
type: any,
props: Object,
ref: any,
): void {
const innerType = type.type;
const resolvedProps = resolveDefaultProps(innerType, props);
renderElement(
request,
task,
keyPath,
prevThenableState,
innerType,
resolvedProps,
ref,
);
}
function renderContextConsumer(
request: Request,
task: Task,
keyPath: Root | KeyNode,
context: ReactContext<any>,
props: Object,
): void {
// The logic below for Context differs depending on PROD or DEV mode. In
// DEV mode, we create a separate object for Context.Consumer that acts
// like a proxy to Context. This proxy object adds unnecessary code in PROD
// so we use the old behaviour (Context.Consumer references Context) to
// reduce size and overhead. The separate object references context via
// a property called "_context", which also gives us the ability to check
// in DEV mode if this property exists or not and warn if it does not.
if (__DEV__) {
if ((context: any)._context === undefined) {
// This may be because it's a Context (rather than a Consumer).
// Or it may be because it's older React where they're the same thing.
// We only want to warn if we're sure it's a new React.
if (context !== context.Consumer) {
if (!hasWarnedAboutUsingContextAsConsumer) {
hasWarnedAboutUsingContextAsConsumer = true;
console.error(
'Rendering <Context> directly is not supported and will be removed in ' +
'a future major release. Did you mean to render <Context.Consumer> instead?',
);
}
}
} else {
context = (context: any)._context;
}
}
const render = props.children;
if (__DEV__) {
if (typeof render !== 'function') {
console.error(
'A context consumer was rendered with multiple children, or a child ' +
"that isn't a function. A context consumer expects a single child " +
'that is a function. If you did pass a function, make sure there ' +
'is no trailing or leading whitespace around it.',
);
}
}
const newValue = readContext(context);
const newChildren = render(newValue);
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
renderNodeDestructive(request, task, null, newChildren, -1);
task.keyPath = prevKeyPath;
}
function renderContextProvider(
request: Request,
task: Task,
keyPath: Root | KeyNode,
type: ReactProviderType<any>,
props: Object,
): void {
const context = type._context;
const value = props.value;
const children = props.children;
let prevSnapshot;
if (__DEV__) {
prevSnapshot = task.context;
}
const prevKeyPath = task.keyPath;
task.context = pushProvider(context, value);
task.keyPath = keyPath;
renderNodeDestructive(request, task, null, children, -1);
task.context = popProvider(context);
task.keyPath = prevKeyPath;
if (__DEV__) {
if (prevSnapshot !== task.context) {
console.error(
'Popping the context provider did not return back to the original snapshot. This is a bug in React.',
);
}
}
}
function renderLazyComponent(
request: Request,
task: Task,
keyPath: Root | KeyNode,
prevThenableState: ThenableState | null,
lazyComponent: LazyComponentType<any, any>,
props: Object,
ref: any,
): void {
pushBuiltInComponentStackInDEV(task, 'Lazy');
const payload = lazyComponent._payload;
const init = lazyComponent._init;
const Component = init(payload);
const resolvedProps = resolveDefaultProps(Component, props);
renderElement(
request,
task,
keyPath,
prevThenableState,
Component,
resolvedProps,
ref,
);
popComponentStackInDEV(task);
}
function renderOffscreen(
request: Request,
task: Task,
keyPath: Root | KeyNode,
props: Object,
): void {
const mode: ?OffscreenMode = (props.mode: any);
if (mode === 'hidden') {
// A hidden Offscreen boundary is not server rendered. Prerendering happens
// on the client.
} else {
// A visible Offscreen boundary is treated exactly like a fragment: a
// pure indirection.
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
renderNodeDestructive(request, task, null, props.children, -1);
task.keyPath = prevKeyPath;
}
}
function renderElement(
request: Request,
task: Task,
keyPath: Root | KeyNode,
prevThenableState: ThenableState | null,
type: any,
props: Object,
ref: any,
): void {
if (typeof type === 'function') {
if (shouldConstruct(type)) {
renderClassComponent(request, task, keyPath, type, props);
return;
} else {
renderIndeterminateComponent(
request,
task,
keyPath,
prevThenableState,
type,
props,
);
return;
}
}
if (typeof type === 'string') {
renderHostElement(request, task, keyPath, type, props);
return;
}
switch (type) {
// LegacyHidden acts the same as a fragment. This only works because we
// currently assume that every instance of LegacyHidden is accompanied by a
// host component wrapper. In the hidden mode, the host component is given a
// `hidden` attribute, which ensures that the initial HTML is not visible.
// To support the use of LegacyHidden as a true fragment, without an extra
// DOM node, we would have to hide the initial HTML in some other way.
// TODO: Delete in LegacyHidden. It's an unstable API only used in the
// www build. As a migration step, we could add a special prop to Offscreen
// that simulates the old behavior (no hiding, no change to effects).
case REACT_LEGACY_HIDDEN_TYPE:
case REACT_DEBUG_TRACING_MODE_TYPE:
case REACT_STRICT_MODE_TYPE:
case REACT_PROFILER_TYPE:
case REACT_FRAGMENT_TYPE: {
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
renderNodeDestructive(request, task, null, props.children, -1);
task.keyPath = prevKeyPath;
return;
}
case REACT_OFFSCREEN_TYPE: {
renderOffscreen(request, task, keyPath, props);
return;
}
case REACT_SUSPENSE_LIST_TYPE: {
pushBuiltInComponentStackInDEV(task, 'SuspenseList');
// TODO: SuspenseList should control the boundaries.
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
renderNodeDestructive(request, task, null, props.children, -1);
task.keyPath = prevKeyPath;
popComponentStackInDEV(task);
return;
}
case REACT_SCOPE_TYPE: {
if (enableScopeAPI) {
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
renderNodeDestructive(request, task, null, props.children, -1);
task.keyPath = prevKeyPath;
return;
}
throw new Error('ReactDOMServer does not yet support scope components.');
}
case REACT_SUSPENSE_TYPE: {
if (
enableSuspenseAvoidThisFallbackFizz &&
props.unstable_avoidThisFallback === true
) {
renderBackupSuspenseBoundary(request, task, keyPath, props);
} else {
renderSuspenseBoundary(request, task, keyPath, props);
}
return;
}
}
if (typeof type === 'object' && type !== null) {
switch (type.$$typeof) {
case REACT_FORWARD_REF_TYPE: {
renderForwardRef(
request,
task,
keyPath,
prevThenableState,
type,
props,
ref,
);
return;
}
case REACT_MEMO_TYPE: {
renderMemo(request, task, keyPath, prevThenableState, type, props, ref);
return;
}
case REACT_PROVIDER_TYPE: {
renderContextProvider(request, task, keyPath, type, props);
return;
}
case REACT_CONTEXT_TYPE: {
renderContextConsumer(request, task, keyPath, type, props);
return;
}
case REACT_LAZY_TYPE: {
renderLazyComponent(
request,
task,
keyPath,
prevThenableState,
type,
props,
);
return;
}
}
}
let info = '';
if (__DEV__) {
if (
type === undefined ||
(typeof type === 'object' &&
type !== null &&
Object.keys(type).length === 0)
) {
info +=
' You likely forgot to export your component from the file ' +
"it's defined in, or you might have mixed up default and " +
'named imports.';
}
}
throw new Error(
'Element type is invalid: expected a string (for built-in ' +
'components) or a class/function (for composite components) ' +
`but got: ${type == null ? type : typeof type}.${info}`,
);
}
function resumeNode(
request: Request,
task: ReplayTask,
segmentId: number,
node: ReactNodeList,
childIndex: number,
): void {
const prevReplay = task.replay;
const blockedBoundary = task.blockedBoundary;
const resumedSegment = createPendingSegment(
request,
0,
null,
task.formatContext,
false,
false,
);
resumedSegment.id = segmentId;
resumedSegment.parentFlushed = true;
try {
// Convert the current ReplayTask to a RenderTask.
const renderTask: RenderTask = (task: any);
renderTask.replay = null;
renderTask.blockedSegment = resumedSegment;
renderNode(request, task, node, childIndex);
resumedSegment.status = COMPLETED;
if (blockedBoundary === null) {
request.completedRootSegment = resumedSegment;
} else {
queueCompletedSegment(blockedBoundary, resumedSegment);
if (blockedBoundary.parentFlushed) {
request.partialBoundaries.push(blockedBoundary);
}
}
} finally {
// Restore to a ReplayTask.
task.replay = prevReplay;
task.blockedSegment = null;
}
}
function resumeElement(
request: Request,
task: ReplayTask,
keyPath: Root | KeyNode,
segmentId: number,
prevThenableState: ThenableState | null,
type: any,
props: Object,
ref: any,
): void {
const prevReplay = task.replay;
const blockedBoundary = task.blockedBoundary;
const resumedSegment = createPendingSegment(
request,
0,
null,
task.formatContext,
false,
false,
);
resumedSegment.id = segmentId;
resumedSegment.parentFlushed = true;
try {
// Convert the current ReplayTask to a RenderTask.
const renderTask: RenderTask = (task: any);
renderTask.replay = null;
renderTask.blockedSegment = resumedSegment;
renderElement(request, task, keyPath, prevThenableState, type, props, ref);
resumedSegment.status = COMPLETED;
if (blockedBoundary === null) {
request.completedRootSegment = resumedSegment;
} else {
queueCompletedSegment(blockedBoundary, resumedSegment);
if (blockedBoundary.parentFlushed) {
request.partialBoundaries.push(blockedBoundary);
}
}
} finally {
// Restore to a ReplayTask.
task.replay = prevReplay;
task.blockedSegment = null;
}
}
function replayElement(
request: Request,
task: ReplayTask,
keyPath: Root | KeyNode,
prevThenableState: ThenableState | null,
name: null | string,
keyOrIndex: number | string,
childIndex: number,
type: any,
props: Object,
ref: any,
replay: ReplaySet,
): void {
// We're replaying. Find the path to follow.
const replayNodes = replay.nodes;
for (let i = 0; i < replayNodes.length; i++) {
// Flow doesn't support refinement on tuples so we do it manually here.
const candidate: any = replayNodes[i];
switch (candidate[0]) {
case REPLAY_NODE: {
const node: ReplayNode = candidate;
if (keyOrIndex === node[2]) {
// Let's double check that the component name matches as a precaution.
if (name !== null && name !== node[1]) {
throw new Error(
'Expected to see a component of type "' +
name +
'" in this slot. ' +
"The tree doesn't match so React will fallback to client rendering.",
);
}
// Matched a replayable path.
task.replay = {nodes: node[3], pendingTasks: 1};
try {
renderElement(
request,
task,
keyPath,
prevThenableState,
type,
props,
ref,
);
// We finished rendering this node, so now we can consume this
// slot. This must happen after in case we rerender this task.
replayNodes.splice(i, 1);
} finally {
task.replay.pendingTasks--;
if (
task.replay.pendingTasks === 0 &&
task.replay.nodes.length > 0
) {
throw new Error(
"Couldn't find all resumable slots by key/index during replaying. " +
"The tree doesn't match so React will fallback to client rendering.",
);
}
task.replay = replay;
}
}
continue;
}
case REPLAY_SUSPENSE_BOUNDARY: {
const node: ReplaySuspenseBoundary = candidate;
if (keyOrIndex === node[2]) {
// Let's double check that the component type matches.
if (type !== REACT_SUSPENSE_TYPE) {
throw new Error(
'Expected to see a Suspense boundary in this slot. ' +
"The tree doesn't match so React will fallback to client rendering.",
);
}
// Matched a replayable path.
replaySuspenseBoundary(request, task, props, node);
// We finished rendering this node, so now we can consume this
// slot. This must happen after in case we rerender this task.
replayNodes.splice(i, 1);
}
continue;
}
case RESUME_ELEMENT: {
const node: ResumeElement = candidate;
if (keyOrIndex === node[2]) {
// Let's double check that the component name matches as a precaution.
if (name !== node[1]) {
throw new Error(
'Expected to see a component of type "' +
(name || 'unknown') +
'" in this slot. ' +
"The tree doesn't match so React will fallback to client rendering.",
);
}
// Matched a resumable element.
const segmentId = node[3];
resumeElement(
request,
task,
keyPath,
segmentId,
prevThenableState,
type,
props,
ref,
);
// We finished rendering this node, so now we can consume this
// slot. This must happen after in case we rerender this task.
replayNodes.splice(i, 1);
}
continue;
}
case RESUME_SUSPENSE_BOUNDARY: {
const node: ResumeSuspenseBoundary = candidate;
if (keyOrIndex === node[2]) {
// Let's double check that the component name matches as a precaution.
if (type !== REACT_SUSPENSE_TYPE) {
throw new Error(
'Expected to see a Suspense boundary in this slot. ' +
"The tree doesn't match so React will fallback to client rendering.",
);
}
// Matched a resumable suspense boundary.
resumeSuspenseBoundary(request, task, props, node);
// We finished rendering this node, so now we can consume this
// slot. This must happen after in case we rerender this task.
replayNodes.splice(i, 1);
}
continue;
}
// For RESUME_SLOT we ignore them here and assume we've handled them
// separately already.
}
}
// We didn't find any matching nodes. We assume that this element was already
// rendered in the prelude and skip it.
}
// $FlowFixMe[missing-local-annot]
function validateIterable(iterable, iteratorFn: Function): void {
if (__DEV__) {
// We don't support rendering Generators because it's a mutation.
// See https://github.com/facebook/react/issues/12995
if (
typeof Symbol === 'function' &&
iterable[Symbol.toStringTag] === 'Generator'
) {
if (!didWarnAboutGenerators) {
console.error(
'Using Generators as children is unsupported and will likely yield ' +
'unexpected results because enumerating a generator mutates it. ' +
'You may convert it to an array with `Array.from()` or the ' +
'`[...spread]` operator before rendering. Keep in mind ' +
'you might need to polyfill these features for older browsers.',
);
}
didWarnAboutGenerators = true;
}
// Warn about using Maps as children
if ((iterable: any).entries === iteratorFn) {
if (!didWarnAboutMaps) {
console.error(
'Using Maps as children is not supported. ' +
'Use an array of keyed ReactElements instead.',
);
}
didWarnAboutMaps = true;
}
}
}
function renderNodeDestructive(
request: Request,
task: Task,
// The thenable state reused from the previous attempt, if any. This is almost
// always null, except when called by retryTask.
prevThenableState: ThenableState | null,
node: ReactNodeList,
childIndex: number,
): void {
if (__DEV__) {
// In Dev we wrap renderNodeDestructiveImpl in a try / catch so we can capture
// a component stack at the right place in the tree. We don't do this in renderNode
// becuase it is not called at every layer of the tree and we may lose frames
try {
return renderNodeDestructiveImpl(
request,
task,
prevThenableState,
node,
childIndex,
);
} catch (x) {
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
// This is a Wakable, noop
} else {
// This is an error, stash the component stack if it is null.
lastBoundaryErrorComponentStackDev =
lastBoundaryErrorComponentStackDev !== null
? lastBoundaryErrorComponentStackDev
: getCurrentStackInDEV();
}
// rethrow so normal suspense logic can handle thrown value accordingly
throw x;
}
} else {
return renderNodeDestructiveImpl(
request,
task,
prevThenableState,
node,
childIndex,
);
}
}
// This function by it self renders a node and consumes the task by mutating it
// to update the current execution state.
function renderNodeDestructiveImpl(
request: Request,
task: Task,
prevThenableState: ThenableState | null,
node: ReactNodeList,
childIndex: number,
): void {
// Stash the node we're working on. We'll pick up from this task in case
// something suspends.
task.node = node;
task.childIndex = childIndex;
// Handle object types
if (typeof node === 'object' && node !== null) {
switch ((node: any).$$typeof) {
case REACT_ELEMENT_TYPE: {
const element: React$Element<any> = (node: any);
const type = element.type;
const key = element.key;
const props = element.props;
const ref = element.ref;
const name = getComponentNameFromType(type);
const keyOrIndex =
key == null ? (childIndex === -1 ? 0 : childIndex) : key;
const keyPath = [task.keyPath, name, keyOrIndex];
if (task.replay !== null) {
replayElement(
request,
task,
keyPath,
prevThenableState,
name,
keyOrIndex,
childIndex,
type,
props,
ref,
task.replay,
);
// No matches found for this node. We assume it's already emitted in the
// prelude and skip it during the replay.
} else {
// We're doing a plain render.
renderElement(
request,
task,
keyPath,
prevThenableState,
type,
props,
ref,
);
}
return;
}
case REACT_PORTAL_TYPE:
throw new Error(
'Portals are not currently supported by the server renderer. ' +
'Render them conditionally so that they only appear on the client render.',
);
case REACT_LAZY_TYPE: {
const lazyNode: LazyComponentType<any, any> = (node: any);
const payload = lazyNode._payload;
const init = lazyNode._init;
let resolvedNode;
if (__DEV__) {
try {
resolvedNode = init(payload);
} catch (x) {
if (
typeof x === 'object' &&
x !== null &&
typeof x.then === 'function'
) {
// this Lazy initializer is suspending. push a temporary frame onto the stack so it can be
// popped off in spawnNewSuspendedTask. This aligns stack behavior between Lazy in element position
// vs Component position. We do not want the frame for Errors so we exclusively do this in
// the wakeable branch
pushBuiltInComponentStackInDEV(task, 'Lazy');
}
throw x;
}
} else {
resolvedNode = init(payload);
}
renderNodeDestructive(request, task, null, resolvedNode, childIndex);
return;
}
}
if (isArray(node)) {
renderChildrenArray(request, task, node, childIndex);
return;
}
const iteratorFn = getIteratorFn(node);
if (iteratorFn) {
if (__DEV__) {
validateIterable(node, iteratorFn);
}
const iterator = iteratorFn.call(node);
if (iterator) {
// We need to know how many total children are in this set, so that we
// can allocate enough id slots to acommodate them. So we must exhaust
// the iterator before we start recursively rendering the children.
// TODO: This is not great but I think it's inherent to the id
// generation algorithm.
let step = iterator.next();
// If there are not entries, we need to push an empty so we start by checking that.
if (!step.done) {
const children = [];
do {
children.push(step.value);
step = iterator.next();
} while (!step.done);
renderChildrenArray(request, task, children, childIndex);
return;
}
return;
}
}
// Usables are a valid React node type. When React encounters a Usable in
// a child position, it unwraps it using the same algorithm as `use`. For
// example, for promises, React will throw an exception to unwind the
// stack, then replay the component once the promise resolves.
//
// A difference from `use` is that React will keep unwrapping the value
// until it reaches a non-Usable type.
//
// e.g. Usable<Usable<Usable<T>>> should resolve to T
const maybeUsable: Object = node;
if (typeof maybeUsable.then === 'function') {
const thenable: Thenable<ReactNodeList> = (maybeUsable: any);
return renderNodeDestructiveImpl(
request,
task,
null,
unwrapThenable(thenable),
childIndex,
);
}
if (
maybeUsable.$$typeof === REACT_CONTEXT_TYPE ||
maybeUsable.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
const context: ReactContext<ReactNodeList> = (maybeUsable: any);
return renderNodeDestructiveImpl(
request,
task,
null,
readContext(context),
childIndex,
);
}
// $FlowFixMe[method-unbinding]
const childString = Object.prototype.toString.call(node);
throw new Error(
`Objects are not valid as a React child (found: ${
childString === '[object Object]'
? 'object with keys {' + Object.keys(node).join(', ') + '}'
: childString
}). ` +
'If you meant to render a collection of children, use an array ' +
'instead.',
);
}
if (typeof node === 'string') {
const segment = task.blockedSegment;
if (segment === null) {
// We assume a text node doesn't have a representation in the replay set,
// since it can't postpone. If it does, it'll be left unmatched and error.
} else {
segment.lastPushedText = pushTextInstance(
segment.chunks,
node,
request.renderState,
segment.lastPushedText,
);
}
return;
}
if (typeof node === 'number') {
const segment = task.blockedSegment;
if (segment === null) {
// We assume a text node doesn't have a representation in the replay set,
// since it can't postpone. If it does, it'll be left unmatched and error.
} else {
segment.lastPushedText = pushTextInstance(
segment.chunks,
'' + node,
request.renderState,
segment.lastPushedText,
);
}
return;
}
if (__DEV__) {
if (typeof node === 'function') {
console.error(
'Functions are not valid as a React child. This may happen if ' +
'you return a Component instead of <Component /> from render. ' +
'Or maybe you meant to call this function rather than return it.',
);
}
}
}
function renderChildrenArray(
request: Request,
task: Task,
children: Array<any>,
childIndex: number,
): void {
const prevKeyPath = task.keyPath;
if (childIndex !== -1) {
task.keyPath = [task.keyPath, 'Fragment', childIndex];
if (task.replay !== null) {
// If we're supposed follow this array, we'd expect to see a ReplayNode matching
// this fragment.
const replayTask: ReplayTask = task;
const replay = task.replay;
const replayNodes = replay.nodes;
for (let j = 0; j < replayNodes.length; j++) {
const replayNode = replayNodes[j];
if (replayNode[0] !== REPLAY_NODE) {
continue;
}
const node: ReplayNode = (replayNode: any);
if (node[2] !== childIndex) {
continue;
}
// Matched a replayable path.
replayTask.replay = {nodes: node[3], pendingTasks: 1};
try {
renderChildrenArray(request, task, children, -1);
} finally {
replayTask.replay.pendingTasks--;
if (
replayTask.replay.pendingTasks === 0 &&
replayTask.replay.nodes.length > 0
) {
throw new Error(
"Couldn't find all resumable slots by key/index during replaying. " +
"The tree doesn't match so React will fallback to client rendering.",
);
}
replayTask.replay = replay;
}
// We finished rendering this node, so now we can consume this
// slot. This must happen after in case we rerender this task.
replayNodes.splice(j, 1);
break;
}
task.keyPath = prevKeyPath;
return;
}
}
const prevTreeContext = task.treeContext;
const totalChildren = children.length;
if (task.replay !== null) {
// Replay
// First we need to check if we have any resume slots at this level.
// TODO: This could be simpler if we just stored RESUME_SLOT in a separate set.
let hadOtherReplayNodes = false;
const replayNodes = task.replay.nodes;
for (let j = 0; j < replayNodes.length; ) {
const replayNode = replayNodes[j];
if (replayNode[0] !== RESUME_SLOT) {
hadOtherReplayNodes = true;
j++; // skip
continue;
}
const resumeSlot: ResumeSlot = (replayNode: any);
const i = resumeSlot[1]; // The index of the child to resume.
const segmentId = resumeSlot[2];
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
resumeNode(request, task, segmentId, children[i], i);
// We finished rendering this node, so now we can consume this
// slot. This must happen after in case we rerender this task.
replayNodes.splice(j, 1);
}
// If had non-resume slot nodes, we need to also try to match them below.
if (!hadOtherReplayNodes) {
// If we didn't, we can bail early.
task.treeContext = prevTreeContext;
task.keyPath = prevKeyPath;
return;
}
}
for (let i = 0; i < totalChildren; i++) {
const node = children[i];
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
// We need to use the non-destructive form so that we can safely pop back
// up and render the sibling if something suspends.
renderNode(request, task, node, i);
}
// Because this context is always set right before rendering every child, we
// only need to reset it to the previous value at the very end.
task.treeContext = prevTreeContext;
task.keyPath = prevKeyPath;
}
function trackPostpone(
request: Request,
trackedPostpones: PostponedHoles,
task: Task,
segment: Segment,
): void {
segment.status = POSTPONED;
const keyPath = task.keyPath;
if (keyPath === null) {
throw new Error(
'It should not be possible to postpone at the root. This is a bug in React.',
);
}
const boundary = task.blockedBoundary;
if (boundary !== null && boundary.status === PENDING) {
boundary.status = POSTPONED;
// We need to eagerly assign it an ID because we'll need to refer to
// it before flushing and we know that we can't inline it.
boundary.id = assignSuspenseBoundaryID(
request.renderState,
request.resumableState,
);
boundary.rootSegmentID = request.nextSegmentId++;
const boundaryKeyPath = boundary.keyPath;
if (boundaryKeyPath === null) {
throw new Error(
'It should not be possible to postpone at the root. This is a bug in React.',
);
}
if (boundaryKeyPath === keyPath && task.childIndex === -1) {
// Since we postponed directly in the Suspense boundary we can't have written anything
// to its segment. Therefore this will end up becoming the root segment.
segment.id = boundary.rootSegmentID;
// We postponed directly inside the Suspense boundary so we mark this for resuming.
const boundaryNode: ResumeSuspenseBoundary = [
RESUME_SUSPENSE_BOUNDARY,
boundaryKeyPath[1],
boundaryKeyPath[2],
boundary.id,
boundary.rootSegmentID,
];
addToReplayParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
return;
} else {
const children: Array<ResumableNode> = [];
const boundaryNode: ReplaySuspenseBoundary = [
REPLAY_SUSPENSE_BOUNDARY,
boundaryKeyPath[1],
boundaryKeyPath[2],
children,
boundary.id,
boundary.rootSegmentID,
];
trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode);
addToReplayParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
// Fall through to add the child node.
}
}
// We know that this will leave a hole so we might as well assign an ID now.
// We might have one already if we had a parent that gave us its ID.
if (segment.id === -1) {
if (segment.parentFlushed && boundary !== null) {
// If this segment's parent was already flushed, it means we really just
// skipped the parent and this segment is now the root.
segment.id = boundary.rootSegmentID;
} else {
segment.id = request.nextSegmentId++;
}
}
if (task.childIndex === -1) {
// Resume starting from directly inside the previous parent element.
const resumableElement: ResumeElement = [
RESUME_ELEMENT,
keyPath[1],
keyPath[2],
segment.id,
];
addToReplayParent(resumableElement, keyPath[0], trackedPostpones);
} else {
// Resume at the slot within the array
const resumableNode = [RESUME_SLOT, task.childIndex, segment.id];
addToReplayParent(resumableNode, keyPath, trackedPostpones);
}
}
function injectPostponedHole(
request: Request,
task: RenderTask,
reason: string,
): Segment {
logPostpone(request, reason);
// Something suspended, we'll need to create a new segment and resolve it later.
const segment = task.blockedSegment;
const insertionIndex = segment.chunks.length;
const newSegment = createPendingSegment(
request,
insertionIndex,
null,
task.formatContext,
// Adopt the parent segment's leading text embed
segment.lastPushedText,
// Assume we are text embedded at the trailing edge
true,
);
segment.children.push(newSegment);
// Reset lastPushedText for current Segment since the new Segment "consumed" it
segment.lastPushedText = false;
return newSegment;
}
function spawnNewSuspendedReplayTask(
request: Request,
task: ReplayTask,
thenableState: ThenableState | null,
x: Wakeable,
): void {
const newTask = createReplayTask(
request,
thenableState,
task.replay,
task.node,
task.childIndex,
task.blockedBoundary,
task.abortSet,
task.keyPath,
task.formatContext,
task.legacyContext,
task.context,
task.treeContext,
);
if (__DEV__) {
if (task.componentStack !== null) {
// We pop one task off the stack because the node that suspended will be tried again,
// which will add it back onto the stack.
newTask.componentStack = task.componentStack.parent;
}
}
const ping = newTask.ping;
x.then(ping, ping);
}
function spawnNewSuspendedRenderTask(
request: Request,
task: RenderTask,
thenableState: ThenableState | null,
x: Wakeable,
): void {
// Something suspended, we'll need to create a new segment and resolve it later.
const segment = task.blockedSegment;
const insertionIndex = segment.chunks.length;
const newSegment = createPendingSegment(
request,
insertionIndex,
null,
task.formatContext,
// Adopt the parent segment's leading text embed
segment.lastPushedText,
// Assume we are text embedded at the trailing edge
true,
);
segment.children.push(newSegment);
// Reset lastPushedText for current Segment since the new Segment "consumed" it
segment.lastPushedText = false;
const newTask = createRenderTask(
request,
thenableState,
task.node,
task.childIndex,
task.blockedBoundary,
newSegment,
task.abortSet,
task.keyPath,
task.formatContext,
task.legacyContext,
task.context,
task.treeContext,
);
if (__DEV__) {
if (task.componentStack !== null) {
// We pop one task off the stack because the node that suspended will be tried again,
// which will add it back onto the stack.
newTask.componentStack = task.componentStack.parent;
}
}
const ping = newTask.ping;
x.then(ping, ping);
}
// This is a non-destructive form of rendering a node. If it suspends it spawns
// a new task and restores the context of this task to what it was before.
function renderNode(
request: Request,
task: Task,
node: ReactNodeList,
childIndex: number,
): void {
// Snapshot the current context in case something throws to interrupt the
// process.
const previousFormatContext = task.formatContext;
const previousLegacyContext = task.legacyContext;
const previousContext = task.context;
const previousKeyPath = task.keyPath;
const previousTreeContext = task.treeContext;
let previousComponentStack = null;
if (__DEV__) {
previousComponentStack = task.componentStack;
}
let x;
// Store how much we've pushed at this point so we can reset it in case something
// suspended partially through writing something.
const segment = task.blockedSegment;
if (segment === null) {
// Replay
try {
return renderNodeDestructive(request, task, null, node, childIndex);
} catch (thrownValue) {
resetHooksState();
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') {
const wakeable: Wakeable = (x: any);
const thenableState = getThenableStateAfterSuspending();
spawnNewSuspendedReplayTask(
request,
// $FlowFixMe: Refined.
task,
thenableState,
wakeable,
);
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
task.formatContext = previousFormatContext;
task.legacyContext = previousLegacyContext;
task.context = previousContext;
task.keyPath = previousKeyPath;
task.treeContext = previousTreeContext;
// Restore all active ReactContexts to what they were before.
switchContext(previousContext);
if (__DEV__) {
task.componentStack = previousComponentStack;
}
return;
}
}
// TODO: Abort any undiscovered Suspense boundaries in the ResumableNode.
}
} else {
// Render
const childrenLength = segment.children.length;
const chunkLength = segment.chunks.length;
try {
return renderNodeDestructive(request, task, null, node, childIndex);
} catch (thrownValue) {
resetHooksState();
// Reset the write pointers to where we started.
segment.children.length = childrenLength;
segment.chunks.length = chunkLength;
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') {
const wakeable: Wakeable = (x: any);
const thenableState = getThenableStateAfterSuspending();
spawnNewSuspendedRenderTask(
request,
// $FlowFixMe: Refined.
task,
thenableState,
wakeable,
);
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
task.formatContext = previousFormatContext;
task.legacyContext = previousLegacyContext;
task.context = previousContext;
task.keyPath = previousKeyPath;
task.treeContext = previousTreeContext;
// Restore all active ReactContexts to what they were before.
switchContext(previousContext);
if (__DEV__) {
task.componentStack = previousComponentStack;
}
return;
}
if (
enablePostpone &&
request.trackedPostpones !== null &&
x.$$typeof === REACT_POSTPONE_TYPE &&
task.blockedBoundary !== null // TODO: Support holes in the shell
) {
// If we're tracking postpones, we inject a hole here and continue rendering
// sibling. Similar to suspending. If we're not tracking, we treat it more like
// an error. Notably this doesn't spawn a new task since nothing will fill it
// in during this prerender.
const postponeInstance: Postpone = (x: any);
const trackedPostpones = request.trackedPostpones;
const postponedSegment = injectPostponedHole(
request,
((task: any): RenderTask), // We don't use ReplayTasks in prerenders.
postponeInstance.message,
);
trackPostpone(request, trackedPostpones, task, postponedSegment);
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
task.formatContext = previousFormatContext;
task.legacyContext = previousLegacyContext;
task.context = previousContext;
task.keyPath = previousKeyPath;
task.treeContext = previousTreeContext;
// Restore all active ReactContexts to what they were before.
switchContext(previousContext);
if (__DEV__) {
task.componentStack = previousComponentStack;
}
return;
}
}
}
}
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
task.formatContext = previousFormatContext;
task.legacyContext = previousLegacyContext;
task.context = previousContext;
task.keyPath = previousKeyPath;
task.treeContext = previousTreeContext;
// Restore all active ReactContexts to what they were before.
switchContext(previousContext);
if (__DEV__) {
task.componentStack = previousComponentStack;
}
// We assume that we don't need the correct context.
// Let's terminate the rest of the tree and don't render any siblings.
throw x;
}
function erroredTask(
request: Request,
boundary: Root | SuspenseBoundary,
error: mixed,
) {
// Report the error to a global handler.
let errorDigest;
if (
enablePostpone &&
typeof error === 'object' &&
error !== null &&
error.$$typeof === REACT_POSTPONE_TYPE
) {
const postponeInstance: Postpone = (error: any);
logPostpone(request, postponeInstance.message);
// TODO: Figure out a better signal than a magic digest value.
errorDigest = 'POSTPONE';
} else {
errorDigest = logRecoverableError(request, error);
}
if (boundary === null) {
// TODO: If the shell errors during a replay, that's not a fatal error. Instead
// we should be able to recover by client rendering all the root boundaries in
// the ReplaySet and any already matched.
fatalError(request, error);
} else {
boundary.pendingTasks--;
if (boundary.status !== CLIENT_RENDERED) {
boundary.status = CLIENT_RENDERED;
boundary.errorDigest = errorDigest;
if (__DEV__) {
captureBoundaryErrorDetailsDev(boundary, error);
}
// Regardless of what happens next, this boundary won't be displayed,
// so we can flush it, if the parent already flushed.
if (boundary.parentFlushed) {
// We don't have a preference where in the queue this goes since it's likely
// to error on the client anyway. However, intentionally client-rendered
// boundaries should be flushed earlier so that they can start on the client.
// We reuse the same queue for errors.
request.clientRenderedBoundaries.push(boundary);
}
}
}
request.allPendingTasks--;
if (request.allPendingTasks === 0) {
const onAllReady = request.onAllReady;
onAllReady();
}
}
function abortTaskSoft(this: Request, task: Task): void {
// This aborts task without aborting the parent boundary that it blocks.
// It's used for when we didn't need this task to complete the tree.
// If task was needed, then it should use abortTask instead.
const request: Request = this;
const boundary = task.blockedBoundary;
const segment = task.blockedSegment;
if (segment !== null) {
segment.status = ABORTED;
finishedTask(request, boundary, segment);
}
}
function abortRemainingResumableNodes(
nodes: Array<ResumableNode>,
error: mixed,
): void {
// TODO: Abort any undiscovered Suspense boundaries in the ReplaySet.
}
function abortTask(task: Task, request: Request, error: mixed): void {
// This aborts the task and aborts the parent that it blocks, putting it into
// client rendered mode.
const boundary = task.blockedBoundary;
const segment = task.blockedSegment;
if (segment === null) {
// $FlowFixMe: Refined.
const replay: ReplaySet = task.replay;
replay.pendingTasks--;
if (replay.pendingTasks === 0) {
abortRemainingResumableNodes(replay.nodes, error);
}
} else {
segment.status = ABORTED;
}
if (boundary === null) {
request.allPendingTasks--;
// We didn't complete the root so we have nothing to show. We can close
// the request;
if (request.status !== CLOSING && request.status !== CLOSED) {
logRecoverableError(request, error);
fatalError(request, error);
}
} else {
boundary.pendingTasks--;
if (boundary.status !== CLIENT_RENDERED) {
boundary.status = CLIENT_RENDERED;
boundary.errorDigest = request.onError(error);
if (__DEV__) {
const errorPrefix =
'The server did not finish this Suspense boundary: ';
let errorMessage;
if (error && typeof error.message === 'string') {
errorMessage = errorPrefix + error.message;
} else {
// eslint-disable-next-line react-internal/safe-string-coercion
errorMessage = errorPrefix + String(error);
}
const previousTaskInDev = currentTaskInDEV;
currentTaskInDEV = task;
try {
captureBoundaryErrorDetailsDev(boundary, errorMessage);
} finally {
currentTaskInDEV = previousTaskInDev;
}
}
if (boundary.parentFlushed) {
request.clientRenderedBoundaries.push(boundary);
}
}
// If this boundary was still pending then we haven't already cancelled its fallbacks.
// We'll need to abort the fallbacks, which will also error that parent boundary.
boundary.fallbackAbortableTasks.forEach(fallbackTask =>
abortTask(fallbackTask, request, error),
);
boundary.fallbackAbortableTasks.clear();
request.allPendingTasks--;
if (request.allPendingTasks === 0) {
const onAllReady = request.onAllReady;
onAllReady();
}
}
}
function queueCompletedSegment(
boundary: SuspenseBoundary,
segment: Segment,
): void {
if (
segment.chunks.length === 0 &&
segment.children.length === 1 &&
segment.children[0].boundary === null
) {
// This is an empty segment. There's nothing to write, so we can instead transfer the ID
// to the child. That way any existing references point to the child.
const childSegment = segment.children[0];
childSegment.id = segment.id;
childSegment.parentFlushed = true;
if (childSegment.status === COMPLETED) {
queueCompletedSegment(boundary, childSegment);
}
} else {
const completedSegments = boundary.completedSegments;
completedSegments.push(segment);
}
}
function finishedTask(
request: Request,
boundary: Root | SuspenseBoundary,
segment: null | Segment,
) {
if (boundary === null) {
if (segment !== null && segment.parentFlushed) {
if (request.completedRootSegment !== null) {
throw new Error(
'There can only be one root segment. This is a bug in React.',
);
}
request.completedRootSegment = segment;
}
request.pendingRootTasks--;
if (request.pendingRootTasks === 0) {
// We have completed the shell so the shell can't error anymore.
request.onShellError = noop;
const onShellReady = request.onShellReady;
onShellReady();
}
} else {
boundary.pendingTasks--;
if (boundary.status === CLIENT_RENDERED) {
// This already errored.
} else if (boundary.pendingTasks === 0) {
if (boundary.status === PENDING) {
boundary.status = COMPLETED;
}
// This must have been the last segment we were waiting on. This boundary is now complete.
if (segment !== null && segment.parentFlushed) {
// Our parent segment already flushed, so we need to schedule this segment to be emitted.
// If it is a segment that was aborted, we'll write other content instead so we don't need
// to emit it.
if (segment.status === COMPLETED) {
queueCompletedSegment(boundary, segment);
}
}
if (boundary.parentFlushed) {
// The segment might be part of a segment that didn't flush yet, but if the boundary's
// parent flushed, we need to schedule the boundary to be emitted.
request.completedBoundaries.push(boundary);
}
// We can now cancel any pending task on the fallback since we won't need to show it anymore.
// This needs to happen after we read the parentFlushed flags because aborting can finish
// work which can trigger user code, which can start flushing, which can change those flags.
boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request);
boundary.fallbackAbortableTasks.clear();
} else {
if (segment !== null && segment.parentFlushed) {
// Our parent already flushed, so we need to schedule this segment to be emitted.
// If it is a segment that was aborted, we'll write other content instead so we don't need
// to emit it.
if (segment.status === COMPLETED) {
queueCompletedSegment(boundary, segment);
const completedSegments = boundary.completedSegments;
if (completedSegments.length === 1) {
// This is the first time since we last flushed that we completed anything.
// We can schedule this boundary to emit its partially completed segments early
// in case the parent has already been flushed.
if (boundary.parentFlushed) {
request.partialBoundaries.push(boundary);
}
}
}
}
}
}
request.allPendingTasks--;
if (request.allPendingTasks === 0) {
// This needs to be called at the very end so that we can synchronously write the result
// in the callback if needed.
const onAllReady = request.onAllReady;
onAllReady();
}
}
function retryTask(request: Request, task: Task): void {
if (enableFloat) {
const blockedBoundary = task.blockedBoundary;
setCurrentlyRenderingBoundaryResourcesTarget(
request.renderState,
blockedBoundary ? blockedBoundary.resources : null,
);
}
const segment = task.blockedSegment;
if (segment === null) {
retryReplayTask(
request,
// $FlowFixMe: Refined.
task,
);
} else {
retryRenderTask(
request,
// $FlowFixMe: Refined.
task,
segment,
);
}
}
function retryRenderTask(
request: Request,
task: RenderTask,
segment: Segment,
): void {
if (segment.status !== PENDING) {
// We completed this by other means before we had a chance to retry it.
return;
}
// We restore the context to what it was when we suspended.
// We don't restore it after we leave because it's likely that we'll end up
// needing a very similar context soon again.
switchContext(task.context);
let prevTaskInDEV = null;
if (__DEV__) {
prevTaskInDEV = currentTaskInDEV;
currentTaskInDEV = task;
}
const childrenLength = segment.children.length;
const chunkLength = segment.chunks.length;
try {
// We call the destructive form that mutates this task. That way if something
// suspends again, we can reuse the same task instead of spawning a new one.
// Reset the task's thenable state before continuing, so that if a later
// component suspends we can reuse the same task object. If the same
// component suspends again, the thenable state will be restored.
const prevThenableState = task.thenableState;
task.thenableState = null;
renderNodeDestructive(
request,
task,
prevThenableState,
task.node,
task.childIndex,
);
pushSegmentFinale(
segment.chunks,
request.renderState,
segment.lastPushedText,
segment.textEmbedded,
);
task.abortSet.delete(task);
segment.status = COMPLETED;
finishedTask(request, task.blockedBoundary, segment);
} catch (thrownValue) {
resetHooksState();
// Reset the write pointers to where we started.
segment.children.length = childrenLength;
segment.chunks.length = chunkLength;
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 &&
request.trackedPostpones !== null &&
x.$$typeof === REACT_POSTPONE_TYPE &&
task.blockedBoundary !== null // TODO: Support holes in the shell
) {
// If we're tracking postpones, we mark this segment as postponed and finish
// the task without filling it in. If we're not tracking, we treat it more like
// an error.
const trackedPostpones = request.trackedPostpones;
task.abortSet.delete(task);
const postponeInstance: Postpone = (x: any);
logPostpone(request, postponeInstance.message);
trackPostpone(request, trackedPostpones, task, segment);
finishedTask(request, task.blockedBoundary, segment);
return;
}
}
task.abortSet.delete(task);
segment.status = ERRORED;
erroredTask(request, task.blockedBoundary, x);
return;
} finally {
if (enableFloat) {
setCurrentlyRenderingBoundaryResourcesTarget(request.renderState, null);
}
if (__DEV__) {
currentTaskInDEV = prevTaskInDEV;
}
}
}
function retryReplayTask(request: Request, task: ReplayTask): void {
if (task.replay.pendingTasks === 0) {
// There are no pending tasks working on this set, so we must have aborted.
return;
}
// We restore the context to what it was when we suspended.
// We don't restore it after we leave because it's likely that we'll end up
// needing a very similar context soon again.
switchContext(task.context);
let prevTaskInDEV = null;
if (__DEV__) {
prevTaskInDEV = currentTaskInDEV;
currentTaskInDEV = task;
}
try {
// We call the destructive form that mutates this task. That way if something
// suspends again, we can reuse the same task instead of spawning a new one.
// Reset the task's thenable state before continuing, so that if a later
// component suspends we can reuse the same task object. If the same
// component suspends again, the thenable state will be restored.
const prevThenableState = task.thenableState;
task.thenableState = null;
renderNodeDestructive(request, task, prevThenableState, task.node, -1);
if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) {
throw new Error(
"Couldn't find all resumable slots by key/index during replaying. " +
"The tree doesn't match so React will fallback to client rendering.",
);
}
task.replay.pendingTasks--;
task.abortSet.delete(task);
finishedTask(request, task.blockedBoundary, null);
} catch (thrownValue) {
resetHooksState();
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;
}
}
task.replay.pendingTasks--;
task.abortSet.delete(task);
erroredTask(request, task.blockedBoundary, x);
return;
} finally {
if (enableFloat) {
setCurrentlyRenderingBoundaryResourcesTarget(request.renderState, null);
}
if (__DEV__) {
currentTaskInDEV = prevTaskInDEV;
}
}
}
export function performWork(request: Request): void {
if (request.status === CLOSED) {
return;
}
const prevContext = getActiveContext();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = HooksDispatcher;
let prevCacheDispatcher;
if (enableCache) {
prevCacheDispatcher = ReactCurrentCache.current;
ReactCurrentCache.current = DefaultCacheDispatcher;
}
const prevRequest = currentRequest;
currentRequest = request;
let prevGetCurrentStackImpl;
if (__DEV__) {
prevGetCurrentStackImpl = ReactDebugCurrentFrame.getCurrentStack;
ReactDebugCurrentFrame.getCurrentStack = getCurrentStackInDEV;
}
const prevResumableState = currentResumableState;
setCurrentResumableState(request.resumableState);
try {
const pingedTasks = request.pingedTasks;
let i;
for (i = 0; i < pingedTasks.length; i++) {
const task = pingedTasks[i];
retryTask(request, task);
}
pingedTasks.splice(0, i);
if (request.destination !== null) {
flushCompletedQueues(request, request.destination);
}
} catch (error) {
logRecoverableError(request, error);
fatalError(request, error);
} finally {
setCurrentResumableState(prevResumableState);
ReactCurrentDispatcher.current = prevDispatcher;
if (enableCache) {
ReactCurrentCache.current = prevCacheDispatcher;
}
if (__DEV__) {
ReactDebugCurrentFrame.getCurrentStack = prevGetCurrentStackImpl;
}
if (prevDispatcher === HooksDispatcher) {
// This means that we were in a reentrant work loop. This could happen
// in a renderer that supports synchronous work like renderToString,
// when it's called from within another renderer.
// Normally we don't bother switching the contexts to their root/default
// values when leaving because we'll likely need the same or similar
// context again. However, when we're inside a synchronous loop like this
// we'll to restore the context to what it was before returning.
switchContext(prevContext);
}
currentRequest = prevRequest;
}
}
function flushSubtree(
request: Request,
destination: Destination,
segment: Segment,
): boolean {
segment.parentFlushed = true;
switch (segment.status) {
case PENDING: {
// We're emitting a placeholder for this segment to be filled in later.
// Therefore we'll need to assign it an ID - to refer to it by.
segment.id = request.nextSegmentId++;
// Fallthrough
}
case POSTPONED: {
const segmentID = segment.id;
// When this segment finally completes it won't be embedded in text since it will flush separately
segment.lastPushedText = false;
segment.textEmbedded = false;
return writePlaceholder(destination, request.renderState, segmentID);
}
case COMPLETED: {
segment.status = FLUSHED;
let r = true;
const chunks = segment.chunks;
let chunkIdx = 0;
const children = segment.children;
for (let childIdx = 0; childIdx < children.length; childIdx++) {
const nextChild = children[childIdx];
// Write all the chunks up until the next child.
for (; chunkIdx < nextChild.index; chunkIdx++) {
writeChunk(destination, chunks[chunkIdx]);
}
r = flushSegment(request, destination, nextChild);
}
// Finally just write all the remaining chunks
for (; chunkIdx < chunks.length - 1; chunkIdx++) {
writeChunk(destination, chunks[chunkIdx]);
}
if (chunkIdx < chunks.length) {
r = writeChunkAndReturn(destination, chunks[chunkIdx]);
}
return r;
}
default: {
throw new Error(
'Aborted, errored or already flushed boundaries should not be flushed again. This is a bug in React.',
);
}
}
}
function flushSegment(
request: Request,
destination: Destination,
segment: Segment,
): boolean {
const boundary = segment.boundary;
if (boundary === null) {
// Not a suspense boundary.
return flushSubtree(request, destination, segment);
}
boundary.parentFlushed = true;
// This segment is a Suspense boundary. We need to decide whether to
// emit the content or the fallback now.
if (boundary.status === CLIENT_RENDERED) {
// Emit a client rendered suspense boundary wrapper.
// We never queue the inner boundary so we'll never emit its content or partial segments.
writeStartClientRenderedSuspenseBoundary(
destination,
request.renderState,
boundary.errorDigest,
boundary.errorMessage,
boundary.errorComponentStack,
);
// Flush the fallback.
flushSubtree(request, destination, segment);
return writeEndClientRenderedSuspenseBoundary(
destination,
request.renderState,
);
} else if (boundary.status !== COMPLETED) {
if (boundary.status === PENDING) {
// For pending boundaries we lazily assign an ID to the boundary
// and root segment.
boundary.id = assignSuspenseBoundaryID(
request.renderState,
request.resumableState,
);
boundary.rootSegmentID = request.nextSegmentId++;
}
if (boundary.completedSegments.length > 0) {
// If this is at least partially complete, we can queue it to be partially emitted early.
request.partialBoundaries.push(boundary);
}
// This boundary is still loading. Emit a pending suspense boundary wrapper.
/// This is the first time we should have referenced this ID.
const id = boundary.id;
writeStartPendingSuspenseBoundary(destination, request.renderState, id);
// Flush the fallback.
flushSubtree(request, destination, segment);
return writeEndPendingSuspenseBoundary(destination, request.renderState);
} else if (boundary.byteSize > request.progressiveChunkSize) {
// This boundary is large and will be emitted separately so that we can progressively show
// other content. We add it to the queue during the flush because we have to ensure that
// the parent flushes first so that there's something to inject it into.
// We also have to make sure that it's emitted into the queue in a deterministic slot.
// I.e. we can't insert it here when it completes.
// Assign an ID to refer to the future content by.
boundary.rootSegmentID = request.nextSegmentId++;
request.completedBoundaries.push(boundary);
// Emit a pending rendered suspense boundary wrapper.
writeStartPendingSuspenseBoundary(
destination,
request.renderState,
boundary.id,
);
// Flush the fallback.
flushSubtree(request, destination, segment);
return writeEndPendingSuspenseBoundary(destination, request.renderState);
} else {
if (enableFloat) {
hoistResources(request.renderState, boundary.resources);
}
// We can inline this boundary's content as a complete boundary.
writeStartCompletedSuspenseBoundary(destination, request.renderState);
const completedSegments = boundary.completedSegments;
if (completedSegments.length !== 1) {
throw new Error(
'A previously unvisited boundary must have exactly one root segment. This is a bug in React.',
);
}
const contentSegment = completedSegments[0];
flushSegment(request, destination, contentSegment);
return writeEndCompletedSuspenseBoundary(destination, request.renderState);
}
}
function flushClientRenderedBoundary(
request: Request,
destination: Destination,
boundary: SuspenseBoundary,
): boolean {
return writeClientRenderBoundaryInstruction(
destination,
request.resumableState,
request.renderState,
boundary.id,
boundary.errorDigest,
boundary.errorMessage,
boundary.errorComponentStack,
);
}
function flushSegmentContainer(
request: Request,
destination: Destination,
segment: Segment,
): boolean {
writeStartSegment(
destination,
request.renderState,
segment.parentFormatContext,
segment.id,
);
flushSegment(request, destination, segment);
return writeEndSegment(destination, segment.parentFormatContext);
}
function flushCompletedBoundary(
request: Request,
destination: Destination,
boundary: SuspenseBoundary,
): boolean {
if (enableFloat) {
setCurrentlyRenderingBoundaryResourcesTarget(
request.renderState,
boundary.resources,
);
}
const completedSegments = boundary.completedSegments;
let i = 0;
for (; i < completedSegments.length; i++) {
const segment = completedSegments[i];
flushPartiallyCompletedSegment(request, destination, boundary, segment);
}
completedSegments.length = 0;
if (enableFloat) {
writeResourcesForBoundary(
destination,
boundary.resources,
request.renderState,
);
}
return writeCompletedBoundaryInstruction(
destination,
request.resumableState,
request.renderState,
boundary.id,
boundary.rootSegmentID,
boundary.resources,
);
}
function flushPartialBoundary(
request: Request,
destination: Destination,
boundary: SuspenseBoundary,
): boolean {
if (enableFloat) {
setCurrentlyRenderingBoundaryResourcesTarget(
request.renderState,
boundary.resources,
);
}
const completedSegments = boundary.completedSegments;
let i = 0;
for (; i < completedSegments.length; i++) {
const segment = completedSegments[i];
if (
!flushPartiallyCompletedSegment(request, destination, boundary, segment)
) {
i++;
completedSegments.splice(0, i);
// Only write as much as the buffer wants. Something higher priority
// might want to write later.
return false;
}
}
completedSegments.splice(0, i);
if (enableFloat) {
// The way this is structured we only write resources for partial boundaries
// if there is no backpressure. Later before we complete the boundary we
// will write resources regardless of backpressure before we emit the
// completion instruction
return writeResourcesForBoundary(
destination,
boundary.resources,
request.renderState,
);
} else {
return true;
}
}
function flushPartiallyCompletedSegment(
request: Request,
destination: Destination,
boundary: SuspenseBoundary,
segment: Segment,
): boolean {
if (segment.status === FLUSHED) {
// We've already flushed this inline.
return true;
}
const segmentID = segment.id;
if (segmentID === -1) {
// This segment wasn't previously referred to. This happens at the root of
// a boundary. We make kind of a leap here and assume this is the root.
const rootSegmentID = (segment.id = boundary.rootSegmentID);
if (rootSegmentID === -1) {
throw new Error(
'A root segment ID must have been assigned by now. This is a bug in React.',
);
}
return flushSegmentContainer(request, destination, segment);
} else if (segmentID === boundary.rootSegmentID) {
// When we emit postponed boundaries, we might have assigned the ID already
// but it's still the root segment so we can't inject it into the parent yet.
return flushSegmentContainer(request, destination, segment);
} else {
flushSegmentContainer(request, destination, segment);
return writeCompletedSegmentInstruction(
destination,
request.resumableState,
request.renderState,
segmentID,
);
}
}
function flushCompletedQueues(
request: Request,
destination: Destination,
): void {
beginWriting(destination);
try {
// The structure of this is to go through each queue one by one and write
// until the sink tells us to stop. When we should stop, we still finish writing
// that item fully and then yield. At that point we remove the already completed
// items up until the point we completed them.
let i;
const completedRootSegment = request.completedRootSegment;
if (completedRootSegment !== null) {
if (request.pendingRootTasks === 0) {
if (enableFloat) {
writePreamble(
destination,
request.resumableState,
request.renderState,
request.allPendingTasks === 0,
);
}
flushSegment(request, destination, completedRootSegment);
request.completedRootSegment = null;
writeCompletedRoot(destination, request.resumableState);
} else {
// We haven't flushed the root yet so we don't need to check any other branches further down
return;
}
}
if (enableFloat) {
writeHoistables(destination, request.resumableState, request.renderState);
}
// We emit client rendering instructions for already emitted boundaries first.
// This is so that we can signal to the client to start client rendering them as
// soon as possible.
const clientRenderedBoundaries = request.clientRenderedBoundaries;
for (i = 0; i < clientRenderedBoundaries.length; i++) {
const boundary = clientRenderedBoundaries[i];
if (!flushClientRenderedBoundary(request, destination, boundary)) {
request.destination = null;
i++;
clientRenderedBoundaries.splice(0, i);
return;
}
}
clientRenderedBoundaries.splice(0, i);
// Next we emit any complete boundaries. It's better to favor boundaries
// that are completely done since we can actually show them, than it is to emit
// any individual segments from a partially complete boundary.
const completedBoundaries = request.completedBoundaries;
for (i = 0; i < completedBoundaries.length; i++) {
const boundary = completedBoundaries[i];
if (!flushCompletedBoundary(request, destination, boundary)) {
request.destination = null;
i++;
completedBoundaries.splice(0, i);
return;
}
}
completedBoundaries.splice(0, i);
// Allow anything written so far to flush to the underlying sink before
// we continue with lower priorities.
completeWriting(destination);
beginWriting(destination);
// TODO: Here we'll emit data used by hydration.
// Next we emit any segments of any boundaries that are partially complete
// but not deeply complete.
const partialBoundaries = request.partialBoundaries;
for (i = 0; i < partialBoundaries.length; i++) {
const boundary = partialBoundaries[i];
if (!flushPartialBoundary(request, destination, boundary)) {
request.destination = null;
i++;
partialBoundaries.splice(0, i);
return;
}
}
partialBoundaries.splice(0, i);
// Next we check the completed boundaries again. This may have had
// boundaries added to it in case they were too larged to be inlined.
// New ones might be added in this loop.
const largeBoundaries = request.completedBoundaries;
for (i = 0; i < largeBoundaries.length; i++) {
const boundary = largeBoundaries[i];
if (!flushCompletedBoundary(request, destination, boundary)) {
request.destination = null;
i++;
largeBoundaries.splice(0, i);
return;
}
}
largeBoundaries.splice(0, i);
} finally {
if (
request.allPendingTasks === 0 &&
request.pingedTasks.length === 0 &&
request.clientRenderedBoundaries.length === 0 &&
request.completedBoundaries.length === 0
// We don't need to check any partially completed segments because
// either they have pending task or they're complete.
) {
request.flushScheduled = false;
if (enableFloat) {
// We write the trailing tags but only if don't have any data to resume.
// If we need to resume we'll write the postamble in the resume instead.
if (
!enablePostpone ||
request.trackedPostpones === null ||
// We check the working map instead of the root because the root could've
// been mutated at this point if it was passed straight through to resume().
request.trackedPostpones.workingMap.size === 0
) {
writePostamble(destination, request.resumableState);
}
}
completeWriting(destination);
flushBuffered(destination);
if (__DEV__) {
if (request.abortableTasks.size !== 0) {
console.error(
'There was still abortable task at the root when we closed. This is a bug in React.',
);
}
}
// We're done.
close(destination);
} else {
completeWriting(destination);
flushBuffered(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(() => flushCompletedQueues(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 {
flushCompletedQueues(request, destination);
} catch (error) {
logRecoverableError(request, error);
fatalError(request, error);
}
}
// This is called to early terminate a request. It puts all pending boundaries in client rendered state.
export function abort(request: Request, reason: mixed): void {
try {
const abortableTasks = request.abortableTasks;
if (abortableTasks.size > 0) {
const error =
reason === undefined
? new Error('The render was aborted by the server without a reason.')
: reason;
abortableTasks.forEach(task => abortTask(task, request, error));
abortableTasks.clear();
}
if (request.destination !== null) {
flushCompletedQueues(request, request.destination);
}
} catch (error) {
logRecoverableError(request, error);
fatalError(request, error);
}
}
export function flushResources(request: Request): void {
enqueueFlush(request);
}
export function getFormState(
request: Request,
): ReactFormState<any, any> | null {
return request.formState;
}
export function getResumableState(request: Request): ResumableState {
return request.resumableState;
}
function addToReplayParent(
node: ResumableNode,
parentKeyPath: Root | KeyNode,
trackedPostpones: PostponedHoles,
): void {
if (parentKeyPath === null) {
trackedPostpones.root.push(node);
} else {
const workingMap = trackedPostpones.workingMap;
let parentNode = workingMap.get(parentKeyPath);
if (parentNode === undefined) {
parentNode = ([
REPLAY_NODE,
parentKeyPath[1],
parentKeyPath[2],
([]: Array<ResumableNode>),
]: ReplayNode);
workingMap.set(parentKeyPath, parentNode);
addToReplayParent(parentNode, parentKeyPath[0], trackedPostpones);
}
parentNode[3].push(node);
}
}
export type PostponedState = {
nextSegmentId: number,
rootFormatContext: FormatContext,
progressiveChunkSize: number,
resumableState: ResumableState,
resumablePath: Array<ResumableNode>,
};
// Returns the state of a postponed request or null if nothing was postponed.
export function getPostponedState(request: Request): null | PostponedState {
const trackedPostpones = request.trackedPostpones;
if (trackedPostpones === null || trackedPostpones.root.length === 0) {
return null;
}
return {
nextSegmentId: request.nextSegmentId,
rootFormatContext: request.rootFormatContext,
progressiveChunkSize: request.progressiveChunkSize,
resumableState: request.resumableState,
resumablePath: trackedPostpones.root,
};
}