1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @flow8 */910import type {11 Destination,12 Chunk,13 PrecomputedChunk,14} from './ReactServerStreamConfig';15import type {16 ReactNodeList,17 ReactContext,18 ReactConsumerType,19 Wakeable,20 Thenable,21 ReactFormState,22 ReactComponentInfo,23 ReactDebugInfo,24 ReactAsyncInfo,25 ViewTransitionProps,26 ActivityProps,27 SuspenseProps,28 SuspenseListProps,29 SuspenseListRevealOrder,30 ReactKey,31} from 'shared/ReactTypes';32import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';33import type {34 RenderState,35 ResumableState,36 PreambleState,37 FormatContext,38 HoistableState,39} from './ReactFizzConfig';40import type {ContextSnapshot} from './ReactFizzNewContext';41import type {ComponentStackNode} from './ReactFizzComponentStack';42import type {TreeContext} from './ReactFizzTreeContext';43import type {ThenableState} from './ReactFizzThenable';4445import {describeObjectForErrorMessage} from 'shared/ReactSerializationErrors';4647import {48 scheduleWork,49 scheduleMicrotask,50 beginWriting,51 writeChunk,52 writeChunkAndReturn,53 completeWriting,54 flushBuffered,55 close,56 closeWithError,57 byteLengthOfChunk,58} from './ReactServerStreamConfig';59import {60 writeCompletedRoot,61 writePlaceholder,62 pushStartActivityBoundary,63 pushEndActivityBoundary,64 writeStartCompletedSuspenseBoundary,65 writeStartPendingSuspenseBoundary,66 writeStartClientRenderedSuspenseBoundary,67 writeEndCompletedSuspenseBoundary,68 writeEndPendingSuspenseBoundary,69 writeEndClientRenderedSuspenseBoundary,70 writeStartSegment,71 writeEndSegment,72 writeClientRenderBoundaryInstruction,73 writeCompletedBoundaryInstruction,74 writeCompletedSegmentInstruction,75 writeHoistablesForBoundary,76 pushTextInstance,77 pushStartInstance,78 pushEndInstance,79 pushSegmentFinale,80 getChildFormatContext,81 getSuspenseFallbackFormatContext,82 getSuspenseContentFormatContext,83 getViewTransitionFormatContext,84 writeHoistables,85 writePreambleStart,86 writePreambleEnd,87 writePostamble,88 hoistHoistables,89 createHoistableState,90 createPreambleState,91 isWorkLoopExternallyDriven,92 supportsRequestStorage,93 requestStorage,94 pushFormStateMarkerIsMatching,95 pushFormStateMarkerIsNotMatching,96 resetResumableState,97 completeResumableState,98 emitEarlyPreloads,99 bindToConsole,100 canHavePreamble,101 hoistPreambleState,102 isPreambleReady,103 isPreambleContext,104 hasSuspenseyContent,105} from './ReactFizzConfig';106import {107 constructClassInstance,108 mountClassInstance,109} from './ReactFizzClassComponent';110import {111 getMaskedContext,112 processChildContext,113 emptyContextObject,114} from './ReactFizzLegacyContext';115import {116 readContext,117 rootContextSnapshot,118 switchContext,119 getActiveContext,120 pushProvider,121 popProvider,122} from './ReactFizzNewContext';123import {124 prepareToUseHooks,125 prepareToUseThenableState,126 finishHooks,127 checkDidRenderIdHook,128 resetHooksState,129 HooksDispatcher,130 currentResumableState,131 setCurrentResumableState,132 getThenableStateAfterSuspending,133 unwrapThenable,134 readPreviousThenableFromState,135 getActionStateCount,136 getActionStateMatchingIndex,137} from './ReactFizzHooks';138import {DefaultAsyncDispatcher} from './ReactFizzAsyncDispatcher';139import {140 getStackByComponentStackNode,141 getOwnerStackByComponentStackNodeInDev,142} from './ReactFizzComponentStack';143import {emptyTreeContext, pushTreeContext} from './ReactFizzTreeContext';144import {currentTaskInDEV, setCurrentTaskInDEV} from './ReactFizzCurrentTask';145import {146 callLazyInitInDEV,147 callComponentInDEV,148 callRenderInDEV,149} from './ReactFizzCallUserSpace';150import {151 getViewTransitionClassName,152 getViewTransitionName,153} from './ReactFizzViewTransitionComponent';154155import {resetOwnerStackLimit} from 'shared/ReactOwnerStackReset';156import {157 getIteratorFn,158 ASYNC_ITERATOR,159 REACT_ELEMENT_TYPE,160 REACT_PORTAL_TYPE,161 REACT_LAZY_TYPE,162 REACT_SUSPENSE_TYPE,163 REACT_LEGACY_HIDDEN_TYPE,164 REACT_STRICT_MODE_TYPE,165 REACT_PROFILER_TYPE,166 REACT_SUSPENSE_LIST_TYPE,167 REACT_FRAGMENT_TYPE,168 REACT_FORWARD_REF_TYPE,169 REACT_MEMO_TYPE,170 REACT_CONTEXT_TYPE,171 REACT_CONSUMER_TYPE,172 REACT_SCOPE_TYPE,173 REACT_VIEW_TRANSITION_TYPE,174 REACT_ACTIVITY_TYPE,175 REACT_OPTIMISTIC_KEY,176} from 'shared/ReactSymbols';177import ReactSharedInternals from 'shared/ReactSharedInternals';178import {179 disableLegacyContext,180 disableLegacyContextForFunctionComponents,181 enableScopeAPI,182 enableAsyncIterableChildren,183 enableViewTransition,184 enableFizzBlockingRender,185 enableAsyncDebugInfo,186 enableCPUSuspense,187} from 'shared/ReactFeatureFlags';188189import assign from 'shared/assign';190import noop from 'shared/noop';191import getComponentNameFromType from 'shared/getComponentNameFromType';192import isArray from 'shared/isArray';193import {194 SuspenseException,195 getSuspendedThenable,196 ensureSuspendableThenableStateDEV,197 getSuspendedCallSiteStackDEV,198 getSuspendedCallSiteDebugTaskDEV,199 setCaptureSuspendedCallSiteDEV,200} from './ReactFizzThenable';201202// Linked list representing the identity of a component given the component/tag name and key.203// The name might be minified but we assume that it's going to be the same generated name. Typically204// because it's just the same compiled output in practice.205export type KeyNode = [206 Root | KeyNode /* parent */,207 string | null /* name */,208 string | number /* key */,209];210211type ResumeSlots =212 | null // nothing to resume213 | number // resume with segment ID at the root position214 | {[index: number]: number}; // resume with segmentID at the index215216type ReplaySuspenseBoundary = [217 string | null /* name */,218 string | number /* key */,219 Array<ReplayNode> /* content keyed children */,220 ResumeSlots /* content resumable slots */,221 null | ReplayNode /* fallback content */,222 number /* rootSegmentID */,223];224225type ReplayNode =226 | [227 string | null /* name */,228 string | number /* key */,229 Array<ReplayNode> /* keyed children */,230 ResumeSlots /* resumable slots */,231 ]232 | ReplaySuspenseBoundary;233234type PostponedHoles = {235 workingMap: Map<KeyNode, ReplayNode>,236 rootNodes: Array<ReplayNode>,237 rootSlots: ResumeSlots,238};239240type LegacyContext = {241 [key: string]: any,242};243244type SuspenseListRow = {245 pendingTasks: number, // The number of tasks, previous rows and inner suspense boundaries blocking this row.246 boundaries: null | Array<SuspenseBoundary>, // The boundaries in this row waiting to be unblocked by the previous row. (null means this row is not blocked)247 hoistables: HoistableState, // Any dependencies that this row depends on. Future rows need to also depend on it.248 inheritedHoistables: null | HoistableState, // Any dependencies that previous row depend on, that new boundaries of this row needs.249 together: boolean, // All the boundaries within this row must be revealed together.250 next: null | SuspenseListRow, // The next row blocked by this one.251};252253const CLIENT_RENDERED = 4; // if it errors or infinitely suspends254255type SuspenseBoundary = {256 status: 0 | 1 | 4 | 5,257 rootSegmentID: number,258 parentFlushed: boolean,259 pendingTasks: number, // when it reaches zero we can show this boundary's content260 row: null | SuspenseListRow, // the row that this boundary blocks from completing.261 completedSegments: Array<Segment>, // completed but not yet flushed segments.262 byteSize: number, // used to determine whether to inline children boundaries.263 defer: boolean, // never inline deferred boundaries264 fallbackAbortableTasks: Set<Task>, // used to cancel task on the fallback if the boundary completes or gets canceled.265 contentState: HoistableState,266 fallbackState: HoistableState,267 preamble: null | Preamble,268 tracked: null | {269 contentKeyPath: null | KeyNode, // used to track the path for replay nodes270 fallbackNode: null | ReplayNode, // used to track the fallback for replay nodes271 },272 errorDigest: ?string, // the error hash if it errors273 // DEV-only fields274 errorMessage?: null | string, // the error string if it errors275 errorStack?: null | string, // the error stack if it errors276 errorComponentStack?: null | string, // the error component stack if it errors277};278279type Ping = {280 resolve: () => void,281 reject: (error: mixed) => void,282};283284type RenderTask = {285 replay: null,286 node: ReactNodeList,287 childIndex: number,288 ping: Ping,289 blockedBoundary: Root | SuspenseBoundary,290 blockedSegment: Segment, // the segment we'll write to291 blockedPreamble: null | PreambleState,292 hoistableState: null | HoistableState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary293 abortSet: Set<Task>, // the abortable set that this task belongs to294 keyPath: Root | KeyNode, // the path of all parent keys currently rendering295 formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML)296 context: ContextSnapshot, // the current new context that this task is executing in297 treeContext: TreeContext, // the current tree context that this task is executing in298 row: null | SuspenseListRow, // the current SuspenseList row that this is rendering inside299 componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component300 thenableState: null | ThenableState,301 legacyContext: LegacyContext, // the current legacy context that this task is executing in302 debugTask: null | ConsoleTask, // DEV only303 // DON'T ANY MORE FIELDS. We at 16 in prod already which otherwise requires converting to a constructor.304 // Consider splitting into multiple objects or consolidating some fields.305};306307type ReplaySet = {308 nodes: Array<ReplayNode>, // the possible paths to follow down the replaying309 slots: ResumeSlots, // slots to resume310 pendingTasks: number, // tracks the number of tasks currently tracking this set of nodes311 // if pending tasks reach zero but there are still nodes left, it means we couldn't find312 // them all in the tree, so we need to abort and client render the boundary.313};314315type ReplayTask = {316 replay: ReplaySet,317 node: ReactNodeList,318 childIndex: number,319 ping: Ping,320 blockedBoundary: Root | SuspenseBoundary,321 blockedSegment: null, // we don't write to anything when we replay322 blockedPreamble: null,323 hoistableState: null | HoistableState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary324 abortSet: Set<Task>, // the abortable set that this task belongs to325 keyPath: Root | KeyNode, // the path of all parent keys currently rendering326 formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML)327 context: ContextSnapshot, // the current new context that this task is executing in328 treeContext: TreeContext, // the current tree context that this task is executing in329 row: null | SuspenseListRow, // the current SuspenseList row that this is rendering inside330 componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component331 thenableState: null | ThenableState,332 legacyContext: LegacyContext, // the current legacy context that this task is executing in333 debugTask: null | ConsoleTask, // DEV only334};335336export type Task = RenderTask | ReplayTask;337338const PENDING = 0;339const COMPLETED = 1;340const FLUSHED = 2;341const ABORTED = 3;342const ERRORED = 4;343const POSTPONED = 5;344345type Root = null;346347type Segment = {348 status: 0 | 1 | 2 | 3 | 4 | 5,349 parentFlushed: boolean, // typically a segment will be flushed by its parent, except if its parent was already flushed350 id: number, // starts as 0 and is lazily assigned if the parent flushes early351 +index: number, // the index within the parent's chunks or 0 at the root352 +chunks: Array<Chunk | PrecomputedChunk>,353 +children: Array<Segment>,354 +preambleChildren: Array<Segment>,355 // The context that this segment was created in.356 parentFormatContext: FormatContext,357 // If this segment represents a fallback, this is the content that will replace that fallback.358 boundary: null | SuspenseBoundary,359 // used to discern when text separator boundaries are needed360 lastPushedText: boolean,361 textEmbedded: boolean,362};363364// The ordering of these statuses matters. OPENING and OPEN are the only365// statuses in which newly scheduled work may be performed. Any status greater366// than OPEN represents a request that no longer admits work.367const OPENING = 10;368const OPEN = 11;369const CLOSING = 12;370const CLOSED = 13;371const STALLED_DEV = 14;372373export opaque type Request = {374 destination: null | Destination,375 flushScheduled: boolean,376 +resumableState: ResumableState,377 +renderState: RenderState,378 +rootFormatContext: FormatContext,379 +progressiveChunkSize: number,380 status: 10 | 11 | 12 | 13 | 14,381 fatalError: mixed,382 aborted: boolean,383 nextSegmentId: number,384 allPendingTasks: number, // when it reaches zero, we can close the connection.385 pendingRootTasks: number, // when this reaches zero, we've finished at least the root boundary.386 completedRootSegment: null | Segment, // Completed but not yet flushed root segments.387 completedPreambleSegments: null | Array<Array<Segment>>, // contains the ready-to-flush segments that make up the preamble388 byteSize: number, // counts the number of bytes accumulated in the shell389 abortableTasks: Set<Task>,390 pingedTasks: Array<Task>, // High priority tasks that should be worked on first.391 currentTask: null | Task, // The task currently executing in this request.392 // Queues to flush in order of priority393 clientRenderedBoundaries: Array<SuspenseBoundary>, // Errored or client rendered but not yet flushed.394 completedBoundaries: Array<SuspenseBoundary>, // Completed but not yet fully flushed boundaries to show.395 partialBoundaries: Array<SuspenseBoundary>, // Partially completed boundaries that can flush its segments early.396 trackedPostpones: null | PostponedHoles, // Gets set to non-null while we want to track postponed holes. I.e. during a prerender.397 // While prerendering a postponed request that produced a real shell, this398 // holds the PostponedState returned by getPostponedState. getPostponedState399 // snapshots nextSegmentId before the (pull-driven) prelude flush runs, but the400 // flush outlines completed boundaries and advances nextSegmentId past that401 // snapshot. We finalize the snapshot from flushCompletedQueues so the resumed402 // render allocates segment ids strictly above the shell's; otherwise the shell403 // and resume emit duplicate B:/S: ids once concatenated. Stays null for live404 // renders and resumes.405 postponedState: null | PostponedState,406 // onError is called when an error happens anywhere in the tree. It might recover.407 // The return string is used in production primarily to avoid leaking internals, secondarily to save bytes.408 // Returning null/undefined will cause a default error message in production409 onError: (error: mixed, errorInfo: ThrownInfo) => ?string,410 // onAllReady is called when all pending task is done but it may not have flushed yet.411 // This is a good time to start writing if you want only HTML and no intermediate steps.412 onAllReady: () => void,413 // onShellReady is called when there is at least a root fallback ready to show.414 // Typically you don't need this callback because it's best practice to always have a415 // root fallback ready so there's no need to wait.416 onShellReady: () => void,417 // onShellError is called when the shell didn't complete. That means you probably want to418 // emit a different response to the stream instead.419 onShellError: (error: mixed) => void,420 onFatalError: (error: mixed) => void,421 // Form state that was the result of an MPA submission, if it was provided.422 formState: null | ReactFormState<any, any>,423 // DEV-only, warning dedupe424 didWarnForKey?: null | WeakSet<ComponentStackNode>,425};426427type Preamble = {428 content: PreambleState,429 fallback: PreambleState,430};431432function createPreamble(): Preamble {433 return {434 content: createPreambleState(),435 fallback: createPreambleState(),436 };437}438439// This is a default heuristic for how to split up the HTML content into progressive440// loading. Our goal is to be able to display additional new content about every 500ms.441// Faster than that is unnecessary and should be throttled on the client. It also442// adds unnecessary overhead to do more splits. We don't know if it's a higher or lower443// end device but higher end suffer less from the overhead than lower end does from444// not getting small enough pieces. We error on the side of low end.445// We base this on low end 3G speeds which is about 500kbits per second. We assume446// that there can be a reasonable drop off from max bandwidth which leaves you with447// as little as 80%. We can receive half of that each 500ms - at best. In practice,448// a little bandwidth is lost to processing and contention - e.g. CSS and images that449// are downloaded along with the main content. So we estimate about half of that to be450// the lower end throughput. In other words, we expect that you can at least show451// about 12.5kb of content per 500ms. Not counting starting latency for the first452// paint.453// 500 * 1024 / 8 * .8 * 0.5 / 2454const DEFAULT_PROGRESSIVE_CHUNK_SIZE = 12800;455456function getBlockingRenderMaxSize(request: Request): number {457 // We want to make sure that we can block the reveal of a well designed complete458 // shell but if you have constructed a too large shell (e.g. by not adding any459 // Suspense boundaries) then that might take too long to render. We shouldn't460 // punish users (or overzealous metrics tracking) in that scenario.461 // There's a trade off here. If this limit is too low then you can't fit a462 // reasonably well built UI within it without getting errors. If it's too high463 // then things that accidentally fall below it might take too long to load.464 // Web Vitals target 1.8 seconds for first paint and our goal to have the limit465 // be fast enough to hit that. For this argument we assume that most external466 // resources are already cached because it's a return visit, or inline styles.467 // If it's not, then it's highly unlikely that any render blocking instructions468 // we add has any impact what so ever on the paint.469 // Assuming a first byte of about 600ms which is kind of bad but common with a470 // decent static host. If it's longer e.g. due to dynamic rendering, then you471 // are going to bound by dynamic production of the content and you're better off472 // with Suspense boundaries anyway. This number doesn't matter much. Then you473 // have about 1.2 seconds left for bandwidth. On 3G that gives you about 112.5kb474 // worth of data. That's worth about 10x in terms of uncompressed bytes. Then we475 // half that just to account for longer latency, slower bandwidth and CPU processing.476 // Now we're down to about 500kb. In fact, looking at metrics we've collected with477 // rel="expect" examples and other documents, the impact on documents smaller than478 // that is within the noise. That's because there's enough happening within that479 // start up to not make HTML streaming not significantly better.480 // Content above the fold tends to be about 100-200kb tops. Therefore 500kb should481 // be enough head room for a good loading state. After that you should use482 // Suspense or SuspenseList to improve it.483 // Since this is highly related to the reason you would adjust the484 // progressiveChunkSize option, and always has to be higher, we define this limit485 // in terms of it. So if you want to increase the limit because you have high486 // bandwidth users, then you can adjust it up. If you are concerned about even487 // slower bandwidth then you can adjust it down.488 return request.progressiveChunkSize * 40; // 512kb by default.489}490491function isEligibleForOutlining(492 request: Request,493 boundary: SuspenseBoundary,494): boolean {495 // For very small boundaries, don't bother producing a fallback for outlining.496 // The larger this limit is, the more we can save on preparing fallbacks in case we end up497 // outlining.498 return (499 (boundary.byteSize > 500 ||500 hasSuspenseyContent(boundary.contentState, /* flushingInShell */ false) ||501 boundary.defer) &&502 // For boundaries that can possibly contribute to the preamble we don't want to outline503 // them regardless of their size since the fallbacks should only be emitted if we've504 // errored the boundary.505 boundary.preamble === null506 );507}508509function defaultErrorHandler(error: mixed) {510 if (511 typeof error === 'object' &&512 error !== null &&513 typeof error.environmentName === 'string'514 ) {515 // This was a Server error. We print the environment name in a badge just like we do with516 // replays of console logs to indicate that the source of this throw as actually the Server.517 bindToConsole('error', [error], error.environmentName)();518 } else {519 console['error'](error); // Don't transform to our wrapper520 }521 return null;522}523524function RequestInstance(525 this: $FlowFixMe,526 resumableState: ResumableState,527 renderState: RenderState,528 rootFormatContext: FormatContext,529 progressiveChunkSize: void | number,530 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),531 onAllReady: void | (() => void),532 onShellReady: void | (() => void),533 onShellError: void | ((error: mixed) => void),534 onFatalError: void | ((error: mixed) => void),535 formState: void | null | ReactFormState<any, any>,536) {537 const pingedTasks: Array<Task> = [];538 const abortSet: Set<Task> = new Set();539 this.destination = null;540 this.flushScheduled = false;541 this.resumableState = resumableState;542 this.renderState = renderState;543 this.rootFormatContext = rootFormatContext;544 this.progressiveChunkSize =545 progressiveChunkSize === undefined546 ? DEFAULT_PROGRESSIVE_CHUNK_SIZE547 : progressiveChunkSize;548 // $FlowFixMe[constant-condition]549 this.status = isWorkLoopExternallyDriven ? OPEN : OPENING;550 this.fatalError = null;551 this.aborted = false;552 this.nextSegmentId = 0;553 this.allPendingTasks = 0;554 this.pendingRootTasks = 0;555 this.completedRootSegment = null;556 this.completedPreambleSegments = null;557 this.byteSize = 0;558 this.abortableTasks = abortSet;559 this.pingedTasks = pingedTasks;560 this.currentTask = null;561 this.clientRenderedBoundaries = [] as Array<SuspenseBoundary>;562 this.completedBoundaries = [] as Array<SuspenseBoundary>;563 this.partialBoundaries = [] as Array<SuspenseBoundary>;564 this.trackedPostpones = null;565 this.postponedState = null;566 this.onError = onError === undefined ? defaultErrorHandler : onError;567 this.onAllReady = onAllReady === undefined ? noop : onAllReady;568 this.onShellReady = onShellReady === undefined ? noop : onShellReady;569 this.onShellError = onShellError === undefined ? noop : onShellError;570 this.onFatalError = onFatalError === undefined ? noop : onFatalError;571 this.formState = formState === undefined ? null : formState;572 if (__DEV__) {573 this.didWarnForKey = null;574 }575}576577export function createRequest(578 children: ReactNodeList,579 resumableState: ResumableState,580 renderState: RenderState,581 rootFormatContext: FormatContext,582 progressiveChunkSize: void | number,583 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),584 onAllReady: void | (() => void),585 onShellReady: void | (() => void),586 onShellError: void | ((error: mixed) => void),587 onFatalError: void | ((error: mixed) => void),588 formState: void | null | ReactFormState<any, any>,589): Request {590 if (__DEV__) {591 resetOwnerStackLimit();592 }593594 // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors595 const request: Request = new RequestInstance(596 resumableState,597 renderState,598 rootFormatContext,599 progressiveChunkSize,600 onError,601 onAllReady,602 onShellReady,603 onShellError,604 onFatalError,605 formState,606 );607608 // This segment represents the root fallback.609 const rootSegment = createPendingSegment(610 request,611 0,612 null,613 rootFormatContext,614 // Root segments are never embedded in Text on either edge615 false,616 false,617 );618 // There is no parent so conceptually, we're unblocked to flush this segment.619 rootSegment.parentFlushed = true;620 const rootTask = createRenderTask(621 request,622 null,623 children,624 -1,625 null,626 rootSegment,627 null,628 null,629 request.abortableTasks,630 null,631 rootFormatContext,632 rootContextSnapshot,633 emptyTreeContext,634 null,635 null,636 emptyContextObject,637 null,638 );639 pushComponentStack(rootTask);640 request.pingedTasks.push(rootTask);641 return request;642}643644export function createPrerenderRequest(645 children: ReactNodeList,646 resumableState: ResumableState,647 renderState: RenderState,648 rootFormatContext: FormatContext,649 progressiveChunkSize: void | number,650 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),651 onAllReady: void | (() => void),652 onShellReady: void | (() => void),653 onShellError: void | ((error: mixed) => void),654 onFatalError: void | ((error: mixed) => void),655): Request {656 const request = createRequest(657 children,658 resumableState,659 renderState,660 rootFormatContext,661 progressiveChunkSize,662 onError,663 onAllReady,664 onShellReady,665 onShellError,666 onFatalError,667 undefined,668 );669 // Start tracking postponed holes during this render.670 request.trackedPostpones = {671 workingMap: new Map(),672 rootNodes: [],673 rootSlots: null,674 };675 return request;676}677678export function resumeRequest(679 children: ReactNodeList,680 postponedState: PostponedState,681 renderState: RenderState,682 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),683 onAllReady: void | (() => void),684 onShellReady: void | (() => void),685 onShellError: void | ((error: mixed) => void),686 onFatalError: void | ((error: mixed) => void),687): Request {688 if (__DEV__) {689 resetOwnerStackLimit();690 }691692 // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors693 const request: Request = new RequestInstance(694 postponedState.resumableState,695 renderState,696 postponedState.rootFormatContext,697 postponedState.progressiveChunkSize,698 onError,699 onAllReady,700 onShellReady,701 onShellError,702 onFatalError,703 null,704 );705 request.nextSegmentId = postponedState.nextSegmentId;706707 if (typeof postponedState.replaySlots === 'number') {708 // We have a resume slot at the very root. This is effectively just a full rerender.709 const rootSegment = createPendingSegment(710 request,711 0,712 null,713 postponedState.rootFormatContext,714 // Root segments are never embedded in Text on either edge715 false,716 false,717 );718 // There is no parent so conceptually, we're unblocked to flush this segment.719 rootSegment.parentFlushed = true;720 const rootTask = createRenderTask(721 request,722 null,723 children,724 -1,725 null,726 rootSegment,727 null,728 null,729 request.abortableTasks,730 null,731 postponedState.rootFormatContext,732 rootContextSnapshot,733 emptyTreeContext,734 null,735 null,736 emptyContextObject,737 null,738 );739 pushComponentStack(rootTask);740 request.pingedTasks.push(rootTask);741 return request;742 }743744 const replay: ReplaySet = {745 nodes: postponedState.replayNodes,746 slots: postponedState.replaySlots,747 pendingTasks: 0,748 };749 const rootTask = createReplayTask(750 request,751 null,752 replay,753 children,754 -1,755 null,756 null,757 request.abortableTasks,758 null,759 postponedState.rootFormatContext,760 rootContextSnapshot,761 emptyTreeContext,762 null,763 null,764 emptyContextObject,765 null,766 );767 pushComponentStack(rootTask);768 request.pingedTasks.push(rootTask);769 return request;770}771772export function resumeAndPrerenderRequest(773 children: ReactNodeList,774 postponedState: PostponedState,775 renderState: RenderState,776 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),777 onAllReady: void | (() => void),778 onShellReady: void | (() => void),779 onShellError: void | ((error: mixed) => void),780 onFatalError: void | ((error: mixed) => void),781): Request {782 const request = resumeRequest(783 children,784 postponedState,785 renderState,786 onError,787 onAllReady,788 onShellReady,789 onShellError,790 onFatalError,791 );792 // Start tracking postponed holes during this render.793 request.trackedPostpones = {794 workingMap: new Map(),795 rootNodes: [],796 rootSlots: null,797 };798 return request;799}800801let currentRequest: null | Request = null;802803export function resolveRequest(): null | Request {804 if (currentRequest) return currentRequest;805 // $FlowFixMe[constant-condition]806 if (supportsRequestStorage) {807 const store = requestStorage.getStore();808 if (store) return store;809 }810 return null;811}812813function pingTask(request: Request, task: Task): void {814 const pingedTasks = request.pingedTasks;815 pingedTasks.push(task);816 // $FlowFixMe[constant-condition]817 if (isWorkLoopExternallyDriven) {818 return;819 } else {820 if (request.pingedTasks.length === 1) {821 request.flushScheduled = request.destination !== null;822 if (request.trackedPostpones !== null || request.status === OPENING) {823 scheduleMicrotask(() => performWork(request));824 } else {825 scheduleWork(() => performWork(request));826 }827 }828 }829}830831function pingRejectedTask(request: Request, task: Task, error: mixed): void {832 if (!request.aborted) {833 // Replaying the task is what gives ordinary render errors their complete834 // component stack.835 pingTask(request, task);836 return;837 }838 if (!task.abortSet.delete(task)) {839 // finishAbort already completed this task with the request's abort reason.840 return;841 }842 // abortTask synchronously claimed this task before abort listeners could843 // reject its wakeable. Finish it with the more specific reason before the844 // scheduled final abort uses the reason for the whole request.845 if (__DEV__) {846 finishAbortedTaskDEV(task, request, error);847 } else {848 finishAbortedTask(task, request, error);849 }850}851852function createSuspenseBoundary(853 request: Request,854 row: null | SuspenseListRow,855 fallbackAbortableTasks: Set<Task>,856 preamble: null | Preamble,857 defer: boolean,858): SuspenseBoundary {859 const boundary: SuspenseBoundary = {860 status: PENDING,861 rootSegmentID: -1,862 parentFlushed: false,863 pendingTasks: 0,864 row: row,865 completedSegments: [],866 byteSize: 0,867 defer: defer,868 fallbackAbortableTasks,869 errorDigest: null,870 contentState: createHoistableState(),871 fallbackState: createHoistableState(),872 preamble,873 tracked: null,874 };875 if (__DEV__) {876 // DEV-only fields for hidden class877 boundary.errorMessage = null;878 boundary.errorStack = null;879 boundary.errorComponentStack = null;880 }881 if (row !== null) {882 // This boundary will block this row from completing.883 row.pendingTasks++;884 const blockedBoundaries = row.boundaries;885 if (blockedBoundaries !== null) {886 // Previous rows will block this boundary itself from completing.887 request.allPendingTasks++;888 boundary.pendingTasks++;889 blockedBoundaries.push(boundary);890 }891 const inheritedHoistables = row.inheritedHoistables;892 if (inheritedHoistables !== null) {893 hoistHoistables(boundary.contentState, inheritedHoistables);894 }895 }896 return boundary;897}898899function createRenderTask(900 request: Request,901 thenableState: ThenableState | null,902 node: ReactNodeList,903 childIndex: number,904 blockedBoundary: Root | SuspenseBoundary,905 blockedSegment: Segment,906 blockedPreamble: null | PreambleState,907 hoistableState: null | HoistableState,908 abortSet: Set<Task>,909 keyPath: Root | KeyNode,910 formatContext: FormatContext,911 context: ContextSnapshot,912 treeContext: TreeContext,913 row: null | SuspenseListRow,914 componentStack: null | ComponentStackNode,915 legacyContext: LegacyContext,916 debugTask: null | ConsoleTask,917): RenderTask {918 request.allPendingTasks++;919 if (blockedBoundary === null) {920 request.pendingRootTasks++;921 } else {922 blockedBoundary.pendingTasks++;923 }924 if (row !== null) {925 row.pendingTasks++;926 }927 const task: RenderTask = {928 replay: null,929 node,930 childIndex,931 ping: {932 resolve: () => pingTask(request, task),933 reject: error => pingRejectedTask(request, task, error),934 },935 blockedBoundary,936 blockedSegment,937 blockedPreamble,938 hoistableState,939 abortSet,940 keyPath,941 formatContext,942 context,943 treeContext,944 row,945 componentStack,946 thenableState,947 } as any;948 if (!disableLegacyContext) {949 task.legacyContext = legacyContext;950 }951 if (__DEV__) {952 task.debugTask = debugTask;953 }954 abortSet.add(task);955 return task;956}957958function createReplayTask(959 request: Request,960 thenableState: ThenableState | null,961 replay: ReplaySet,962 node: ReactNodeList,963 childIndex: number,964 blockedBoundary: Root | SuspenseBoundary,965 hoistableState: null | HoistableState,966 abortSet: Set<Task>,967 keyPath: Root | KeyNode,968 formatContext: FormatContext,969 context: ContextSnapshot,970 treeContext: TreeContext,971 row: null | SuspenseListRow,972 componentStack: null | ComponentStackNode,973 legacyContext: LegacyContext,974 debugTask: null | ConsoleTask,975): ReplayTask {976 request.allPendingTasks++;977 if (blockedBoundary === null) {978 request.pendingRootTasks++;979 } else {980 blockedBoundary.pendingTasks++;981 }982 if (row !== null) {983 row.pendingTasks++;984 }985 replay.pendingTasks++;986 const task: ReplayTask = {987 replay,988 node,989 childIndex,990 ping: {991 resolve: () => pingTask(request, task),992 reject: error => pingRejectedTask(request, task, error),993 },994 blockedBoundary,995 blockedSegment: null,996 blockedPreamble: null,997 hoistableState,998 abortSet,999 keyPath,1000 formatContext,1001 context,1002 treeContext,1003 row,1004 componentStack,1005 thenableState,1006 } as any;1007 if (!disableLegacyContext) {1008 task.legacyContext = legacyContext;1009 }1010 if (__DEV__) {1011 task.debugTask = debugTask;1012 }1013 abortSet.add(task);1014 return task;1015}10161017function createPendingSegment(1018 request: Request,1019 index: number,1020 boundary: null | SuspenseBoundary,1021 parentFormatContext: FormatContext,1022 lastPushedText: boolean,1023 textEmbedded: boolean,1024): Segment {1025 return {1026 status: PENDING,1027 parentFlushed: false,1028 id: -1, // lazily assigned later1029 index,1030 chunks: [],1031 children: [],1032 preambleChildren: [],1033 parentFormatContext,1034 boundary,1035 lastPushedText,1036 textEmbedded,1037 };1038}10391040function getCurrentStackInDEV(): string {1041 if (__DEV__) {1042 if (currentTaskInDEV === null || currentTaskInDEV.componentStack === null) {1043 return '';1044 }1045 return getOwnerStackByComponentStackNodeInDev(1046 currentTaskInDEV.componentStack,1047 );1048 }1049 return '';1050}10511052function getStackFromNode(stackNode: ComponentStackNode): string {1053 return getStackByComponentStackNode(stackNode);1054}10551056function pushHaltedAwaitOnComponentStack(1057 task: Task,1058 debugInfo: void | null | ReactDebugInfo,1059): void {1060 if (!__DEV__) {1061 // eslint-disable-next-line react-internal/prod-error-codes1062 throw new Error(1063 'pushHaltedAwaitOnComponentStack should never be called in production. This is a bug in React.',1064 );1065 }1066 if (debugInfo != null) {1067 for (let i = debugInfo.length - 1; i >= 0; i--) {1068 const info = debugInfo[i];1069 if (info.awaited != null) {1070 const asyncInfo: ReactAsyncInfo = info as any;1071 const bestStack =1072 asyncInfo.debugStack == null ? asyncInfo.awaited : asyncInfo;1073 if (bestStack.debugStack !== undefined) {1074 task.componentStack = {1075 parent: task.componentStack,1076 type: asyncInfo,1077 owner: bestStack.owner,1078 stack: bestStack.debugStack,1079 };1080 task.debugTask = bestStack.debugTask as any;1081 break;1082 }1083 }1084 }1085 }1086}10871088// performWork + retryTask without mutation1089function rerenderStalledTask(request: Request, task: Task): void {1090 const prevStatus = request.status;1091 const prevAborted = request.aborted;1092 request.status = STALLED_DEV;1093 // This diagnostic replay must reach the suspended call site instead of1094 // taking the abort path.1095 request.aborted = false;10961097 const prevContext = getActiveContext();1098 const prevDispatcher = ReactSharedInternals.H;1099 ReactSharedInternals.H = HooksDispatcher;1100 const prevAsyncDispatcher = ReactSharedInternals.A;1101 ReactSharedInternals.A = DefaultAsyncDispatcher;11021103 const prevRequest = currentRequest;1104 currentRequest = request;11051106 const prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack;1107 ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;11081109 const prevResumableState = currentResumableState;1110 setCurrentResumableState(request.resumableState);1111 switchContext(task.context);1112 const prevTaskInDEV = currentTaskInDEV;1113 setCurrentTaskInDEV(task);1114 try {1115 retryNode(request, task);1116 } catch (x) {1117 // Suspended again.1118 resetHooksState();1119 } finally {1120 setCurrentTaskInDEV(prevTaskInDEV);1121 setCurrentResumableState(prevResumableState);11221123 ReactSharedInternals.H = prevDispatcher;1124 ReactSharedInternals.A = prevAsyncDispatcher;11251126 ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl;1127 if (prevDispatcher === HooksDispatcher) {1128 // This means that we were in a reentrant work loop. This could happen1129 // in a renderer that supports synchronous work like renderToString,1130 // when it's called from within another renderer.1131 // Normally we don't bother switching the contexts to their root/default1132 // values when leaving because we'll likely need the same or similar1133 // context again. However, when we're inside a synchronous loop like this1134 // we'll to restore the context to what it was before returning.1135 switchContext(prevContext);1136 }1137 currentRequest = prevRequest;1138 request.status = prevStatus;1139 request.aborted = prevAborted;1140 }1141}11421143function pushSuspendedCallSiteOnComponentStack(1144 request: Request,1145 task: Task,1146): void {1147 setCaptureSuspendedCallSiteDEV(true);1148 const restoreThenableState = ensureSuspendableThenableStateDEV(1149 // refined at the callsite1150 task.thenableState as any as ThenableState,1151 );1152 try {1153 rerenderStalledTask(request, task);1154 } finally {1155 restoreThenableState();1156 setCaptureSuspendedCallSiteDEV(false);1157 }11581159 const suspendCallSiteStack = getSuspendedCallSiteStackDEV();1160 const suspendCallSiteDebugTask = getSuspendedCallSiteDebugTaskDEV();11611162 if (suspendCallSiteStack !== null) {1163 const ownerStack = task.componentStack;1164 task.componentStack = {1165 // The owner of the suspended call site would be the owner of this task.1166 // We need the task itself otherwise we'd miss a frame.1167 owner: ownerStack,1168 parent: suspendCallSiteStack.parent,1169 stack: suspendCallSiteStack.stack,1170 type: suspendCallSiteStack.type,1171 };1172 }1173 task.debugTask = suspendCallSiteDebugTask;1174}11751176function pushServerComponentStack(1177 task: Task,1178 debugInfo: void | null | ReactDebugInfo,1179): void {1180 if (!__DEV__) {1181 // eslint-disable-next-line react-internal/prod-error-codes1182 throw new Error(1183 'pushServerComponentStack should never be called in production. This is a bug in React.',1184 );1185 }1186 // Build a Server Component parent stack from the debugInfo.1187 if (debugInfo != null) {1188 const stack: ReactDebugInfo = debugInfo;1189 for (let i = 0; i < stack.length; i++) {1190 const componentInfo: ReactComponentInfo = stack[i] as any;1191 if (typeof componentInfo.name !== 'string') {1192 continue;1193 }1194 if (componentInfo.debugStack === undefined) {1195 continue;1196 }1197 task.componentStack = {1198 parent: task.componentStack,1199 type: componentInfo,1200 owner: componentInfo.owner,1201 stack: componentInfo.debugStack,1202 };1203 task.debugTask = componentInfo.debugTask as any;1204 }1205 }1206}12071208function pushComponentStack(task: Task): void {1209 const node = task.node;1210 // Create the Component Stack frame for the element we're about to try.1211 // It's unfortunate that we need to do this refinement twice. Once for1212 // the stack frame and then once again while actually1213 if (typeof node === 'object' && node !== null) {1214 switch ((node as any).$$typeof) {1215 case REACT_ELEMENT_TYPE: {1216 const element: any = node;1217 const type = element.type;1218 const owner = __DEV__ ? element._owner : null;1219 const stack = __DEV__ ? element._debugStack : null;1220 if (__DEV__) {1221 pushServerComponentStack(task, element._debugInfo);1222 task.debugTask = element._debugTask;1223 }1224 task.componentStack = createComponentStackFromType(1225 task.componentStack,1226 type,1227 owner,1228 stack,1229 );1230 break;1231 }1232 case REACT_LAZY_TYPE: {1233 if (__DEV__) {1234 const lazyNode: LazyComponentType<any, any> = node as any;1235 pushServerComponentStack(task, lazyNode._debugInfo);1236 }1237 break;1238 }1239 default: {1240 if (__DEV__) {1241 const maybeUsable: Object = node;1242 if (typeof maybeUsable.then === 'function') {1243 const thenable: Thenable<ReactNodeList> = maybeUsable as any;1244 pushServerComponentStack(task, thenable._debugInfo);1245 }1246 }1247 }1248 }1249 }1250}12511252function createComponentStackFromType(1253 parent: null | ComponentStackNode,1254 type: Function | string | symbol,1255 owner: void | null | ReactComponentInfo | ComponentStackNode, // DEV only1256 stack: void | null | string | Error, // DEV only1257): ComponentStackNode {1258 if (__DEV__) {1259 return {1260 parent,1261 type,1262 owner,1263 stack,1264 };1265 }1266 return {1267 parent,1268 type,1269 };1270}12711272function replaceSuspenseComponentStackWithSuspenseFallbackStack(1273 componentStack: null | ComponentStackNode,1274): null | ComponentStackNode {1275 if (componentStack === null) {1276 return null;1277 }1278 return createComponentStackFromType(1279 componentStack.parent,1280 'Suspense Fallback',1281 __DEV__ ? componentStack.owner : null,1282 __DEV__ ? componentStack.stack : null,1283 );1284}12851286type ThrownInfo = {1287 componentStack?: string,1288};1289export type ErrorInfo = ThrownInfo;12901291function getThrownInfo(node: null | ComponentStackNode): ThrownInfo {1292 const errorInfo: ThrownInfo = {};1293 if (node) {1294 Object.defineProperty(errorInfo, 'componentStack', {1295 configurable: true,1296 enumerable: true,1297 get() {1298 // Lazyily generate the stack since it's expensive.1299 const stack = getStackFromNode(node);1300 Object.defineProperty(errorInfo, 'componentStack', {1301 value: stack,1302 });1303 return stack;1304 },1305 });1306 }1307 return errorInfo;1308}13091310function encodeErrorForBoundary(1311 boundary: SuspenseBoundary,1312 digest: ?string,1313 error: mixed,1314 thrownInfo: ThrownInfo,1315 wasAborted: boolean,1316) {1317 boundary.errorDigest = digest;1318 if (__DEV__) {1319 let message, stack;1320 // In dev we additionally encode the error message and component stack on the boundary1321 if (error instanceof Error) {1322 // eslint-disable-next-line react-internal/safe-string-coercion1323 message = String(error.message);1324 // eslint-disable-next-line react-internal/safe-string-coercion1325 stack = String(error.stack);1326 } else if (typeof error === 'object' && error !== null) {1327 message = describeObjectForErrorMessage(error);1328 stack = null;1329 } else {1330 // eslint-disable-next-line react-internal/safe-string-coercion1331 message = String(error);1332 stack = null;1333 }1334 const prefix = wasAborted1335 ? 'Switched to client rendering because the server rendering aborted due to:\n\n'1336 : 'Switched to client rendering because the server rendering errored:\n\n';1337 boundary.errorMessage = prefix + message;1338 boundary.errorStack = stack !== null ? prefix + stack : null;1339 boundary.errorComponentStack = thrownInfo.componentStack;1340 }1341}13421343function logRecoverableError(1344 request: Request,1345 error: any,1346 errorInfo: ThrownInfo,1347 debugTask: null | ConsoleTask,1348): ?string {1349 // If this callback errors, we intentionally let that error bubble up to become a fatal error1350 // so that someone fixes the error reporting instead of hiding it.1351 const onError = request.onError;1352 const errorDigest =1353 __DEV__ && debugTask1354 ? debugTask.run(onError.bind(null, error, errorInfo))1355 : onError(error, errorInfo);1356 if (errorDigest != null && typeof errorDigest !== 'string') {1357 // We used to throw here but since this gets called from a variety of unprotected places it1358 // seems better to just warn and discard the returned value.1359 if (__DEV__) {1360 console.error(1361 '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 "%s" instead',1362 typeof errorDigest,1363 );1364 }1365 return;1366 }1367 return errorDigest;1368}13691370function fatalError(1371 request: Request,1372 error: mixed,1373 errorInfo: ThrownInfo,1374 debugTask: null | ConsoleTask,1375): void {1376 // This is called outside error handling code such as if the root errors outside1377 // a suspense boundary or if the root suspense boundary's fallback errors.1378 // It's also called if React itself or its host configs errors.1379 const onShellError = request.onShellError;1380 const onFatalError = request.onFatalError;1381 if (__DEV__ && debugTask) {1382 debugTask.run(onShellError.bind(null, error));1383 debugTask.run(onFatalError.bind(null, error));1384 } else {1385 onShellError(error);1386 onFatalError(error);1387 }1388 if (request.destination !== null) {1389 request.status = CLOSED;1390 closeWithError(request.destination, error);1391 } else {1392 request.status = CLOSING;1393 request.fatalError = error;1394 }1395}13961397function renderSuspenseBoundary(1398 request: Request,1399 someTask: Task,1400 keyPath: KeyNode,1401 props: SuspenseProps,1402): void {1403 if (someTask.replay !== null) {1404 // If we're replaying through this pass, it means we're replaying through1405 // an already completed Suspense boundary. It's too late to do anything about it1406 // so we can just render through it.1407 const prevKeyPath = someTask.keyPath;1408 const prevContext = someTask.formatContext;1409 const prevRow = someTask.row;1410 someTask.keyPath = keyPath;1411 someTask.formatContext = getSuspenseContentFormatContext(1412 request.resumableState,1413 prevContext,1414 );1415 someTask.row = null;1416 const content: ReactNodeList = props.children;1417 try {1418 renderNode(request, someTask, content, -1);1419 } finally {1420 someTask.keyPath = prevKeyPath;1421 someTask.formatContext = prevContext;1422 someTask.row = prevRow;1423 }1424 return;1425 }1426 // $FlowFixMe[incompatible-type]: Refined.1427 const task: RenderTask = someTask;14281429 const prevKeyPath = task.keyPath;1430 const prevContext = task.formatContext;1431 const prevRow = task.row;1432 const parentBoundary = task.blockedBoundary;1433 const parentPreamble = task.blockedPreamble;1434 const parentHoistableState = task.hoistableState;1435 const parentSegment = task.blockedSegment;14361437 // Each time we enter a suspense boundary, we split out into a new segment for1438 // the fallback so that we can later replace that segment with the content.1439 // This also lets us split out the main content even if it doesn't suspend,1440 // in case it ends up generating a large subtree of content.1441 const fallback: ReactNodeList = props.fallback;1442 const content: ReactNodeList = props.children;1443 const defer: boolean = enableCPUSuspense && props.defer === true;14441445 const fallbackAbortSet: Set<Task> = new Set();1446 const newBoundary = createSuspenseBoundary(1447 request,1448 task.row,1449 fallbackAbortSet,1450 canHavePreamble(task.formatContext) ? createPreamble() : null,1451 defer,1452 );14531454 const insertionIndex = parentSegment.chunks.length;1455 // The children of the boundary segment is actually the fallback.1456 const boundarySegment = createPendingSegment(1457 request,1458 insertionIndex,1459 newBoundary,1460 task.formatContext,1461 // boundaries never require text embedding at their edges because comment nodes bound them1462 false,1463 false,1464 );1465 parentSegment.children.push(boundarySegment);1466 // The parentSegment has a child Segment at this index so we reset the lastPushedText marker on the parent1467 parentSegment.lastPushedText = false;14681469 // This segment is the actual child content. We can start rendering that immediately.1470 const contentRootSegment = createPendingSegment(1471 request,1472 0,1473 null,1474 task.formatContext,1475 // boundaries never require text embedding at their edges because comment nodes bound them1476 false,1477 false,1478 );1479 // We mark the root segment as having its parent flushed. It's not really flushed but there is1480 // no parent segment so there's nothing to wait on.1481 contentRootSegment.parentFlushed = true;14821483 const trackedPostpones = request.trackedPostpones;1484 if (trackedPostpones !== null || defer) {1485 // This is a prerender or deferred boundary. In this mode we want to render the fallback synchronously1486 // and schedule the content to render later. This is the opposite of what we do during a normal render1487 // where we try to skip rendering the fallback if the content itself can render synchronously14881489 // Stash the original stack frame.1490 const suspenseComponentStack = task.componentStack;14911492 const fallbackKeyPath: KeyNode = [1493 keyPath[0],1494 'Suspense Fallback',1495 keyPath[2],1496 ];1497 if (trackedPostpones !== null) {1498 const fallbackReplayNode: ReplayNode = [1499 fallbackKeyPath[1],1500 fallbackKeyPath[2],1501 [] as Array<ReplayNode>,1502 null,1503 ];1504 trackedPostpones.workingMap.set(fallbackKeyPath, fallbackReplayNode);1505 newBoundary.tracked = {1506 contentKeyPath: keyPath,1507 // We are rendering the fallback before the boundary content so we keep track of1508 // the fallback replay node until we determine if the primary content suspends1509 fallbackNode: fallbackReplayNode,1510 };1511 }15121513 task.blockedSegment = boundarySegment;1514 task.blockedPreamble =1515 newBoundary.preamble === null ? null : newBoundary.preamble.fallback;1516 task.keyPath = fallbackKeyPath;1517 task.formatContext = getSuspenseFallbackFormatContext(1518 request.resumableState,1519 prevContext,1520 );1521 task.componentStack =1522 replaceSuspenseComponentStackWithSuspenseFallbackStack(1523 suspenseComponentStack,1524 );1525 try {1526 renderNode(request, task, fallback, -1);1527 pushSegmentFinale(1528 boundarySegment.chunks,1529 request.renderState,1530 boundarySegment.lastPushedText,1531 boundarySegment.textEmbedded,1532 );1533 boundarySegment.status = COMPLETED;1534 finishedSegment(request, parentBoundary, boundarySegment);1535 } catch (thrownValue: mixed) {1536 if (request.aborted) {1537 boundarySegment.status = ABORTED;1538 } else {1539 boundarySegment.status = ERRORED;1540 }1541 throw thrownValue;1542 } finally {1543 task.blockedSegment = parentSegment;1544 task.blockedPreamble = parentPreamble;1545 task.keyPath = prevKeyPath;1546 task.formatContext = prevContext;1547 }15481549 // We create a suspended task for the primary content because we want to allow1550 // sibling fallbacks to be rendered first.1551 const suspendedPrimaryTask = createRenderTask(1552 request,1553 null,1554 content,1555 -1,1556 newBoundary,1557 contentRootSegment,1558 newBoundary.preamble === null ? null : newBoundary.preamble.content,1559 newBoundary.contentState,1560 task.abortSet,1561 keyPath,1562 getSuspenseContentFormatContext(1563 request.resumableState,1564 task.formatContext,1565 ),1566 task.context,1567 task.treeContext,1568 null, // The row gets reset inside the Suspense boundary.1569 suspenseComponentStack,1570 !disableLegacyContext ? task.legacyContext : emptyContextObject,1571 __DEV__ ? task.debugTask : null,1572 );1573 pushComponentStack(suspendedPrimaryTask);1574 request.pingedTasks.push(suspendedPrimaryTask);1575 } else {1576 // This is a normal render. We will attempt to synchronously render the boundary content1577 // If it is successful we will elide the fallback task but if it suspends or errors we schedule1578 // the fallback to render. Unlike with prerenders we attempt to deprioritize the fallback render15791580 // Currently this is running synchronously. We could instead schedule this to pingedTasks.1581 // I suspect that there might be some efficiency benefits from not creating the suspended task1582 // and instead just using the stack if possible.1583 // TODO: Call this directly instead of messing with saving and restoring contexts.15841585 // We can reuse the current context and task to render the content immediately without1586 // context switching. We just need to temporarily switch which boundary and which segment1587 // we're writing to. If something suspends, it'll spawn new suspended task with that context.1588 task.blockedBoundary = newBoundary;1589 task.blockedPreamble =1590 newBoundary.preamble === null ? null : newBoundary.preamble.content;1591 task.hoistableState = newBoundary.contentState;1592 task.blockedSegment = contentRootSegment;1593 task.keyPath = keyPath;1594 task.formatContext = getSuspenseContentFormatContext(1595 request.resumableState,1596 prevContext,1597 );1598 task.row = null;1599 try {1600 // We use the safe form because we don't handle suspending here. Only error handling.1601 renderNode(request, task, content, -1);1602 pushSegmentFinale(1603 contentRootSegment.chunks,1604 request.renderState,1605 contentRootSegment.lastPushedText,1606 contentRootSegment.textEmbedded,1607 );1608 contentRootSegment.status = COMPLETED;1609 finishedSegment(request, newBoundary, contentRootSegment);1610 queueCompletedSegment(newBoundary, contentRootSegment);1611 if (newBoundary.pendingTasks === 0 && newBoundary.status === PENDING) {1612 // This must have been the last segment we were waiting on. This boundary is now complete.1613 newBoundary.status = COMPLETED;1614 // Therefore we won't need the fallback. We early return so that we don't have to create1615 // the fallback. However, if this boundary ended up big enough to be eligible for outlining1616 // we can't do that because we might still need the fallback if we outline it.1617 if (!isEligibleForOutlining(request, newBoundary)) {1618 if (prevRow !== null) {1619 // If we have synchronously completed the boundary and it's not eligible for outlining1620 // then we don't have to wait for it to be flushed before we unblock future rows.1621 // This lets us inline small rows in order.1622 if (--prevRow.pendingTasks === 0) {1623 finishSuspenseListRow(request, prevRow);1624 }1625 }1626 if (request.pendingRootTasks === 0 && task.blockedPreamble) {1627 // The root is complete and this boundary may contribute part of the preamble.1628 // We eagerly attempt to prepare the preamble here because we expect most requests1629 // to have few boundaries which contribute preambles and it allow us to do this1630 // preparation work during the work phase rather than the when flushing.1631 preparePreamble(request);1632 }1633 return;1634 }1635 } else {1636 const boundaryRow = prevRow;1637 if (boundaryRow !== null && boundaryRow.together) {1638 tryToResolveTogetherRow(request, boundaryRow);1639 }1640 }1641 } catch (thrownValue: mixed) {1642 newBoundary.status = CLIENT_RENDERED;1643 let error: mixed;1644 if (request.aborted) {1645 contentRootSegment.status = ABORTED;1646 error = request.fatalError;1647 } else {1648 contentRootSegment.status = ERRORED;1649 error = thrownValue;1650 }16511652 const thrownInfo = getThrownInfo(task.componentStack);1653 const errorDigest = logRecoverableError(1654 request,1655 error,1656 thrownInfo,1657 __DEV__ ? task.debugTask : null,1658 );1659 encodeErrorForBoundary(1660 newBoundary,1661 errorDigest,1662 error,1663 thrownInfo,1664 false,1665 );16661667 untrackBoundary(request, newBoundary);16681669 // We don't need to decrement any task numbers because we didn't spawn any new task.1670 // We don't need to schedule any task because we know the parent has written yet.1671 // We do need to fallthrough to create the fallback though.1672 } finally {1673 task.blockedBoundary = parentBoundary;1674 task.blockedPreamble = parentPreamble;1675 task.hoistableState = parentHoistableState;1676 task.blockedSegment = parentSegment;1677 task.keyPath = prevKeyPath;1678 task.formatContext = prevContext;1679 task.row = prevRow;1680 }16811682 const fallbackKeyPath: KeyNode = [1683 keyPath[0],1684 'Suspense Fallback',1685 keyPath[2],1686 ];1687 // We create suspended task for the fallback because we don't want to actually work1688 // on it yet in case we finish the main content, so we queue for later.1689 const suspendedFallbackTask = createRenderTask(1690 request,1691 null,1692 fallback,1693 -1,1694 parentBoundary,1695 boundarySegment,1696 newBoundary.preamble === null ? null : newBoundary.preamble.fallback,1697 newBoundary.fallbackState,1698 fallbackAbortSet,1699 fallbackKeyPath,1700 getSuspenseFallbackFormatContext(1701 request.resumableState,1702 task.formatContext,1703 ),1704 task.context,1705 task.treeContext,1706 task.row,1707 replaceSuspenseComponentStackWithSuspenseFallbackStack(1708 task.componentStack,1709 ),1710 !disableLegacyContext ? task.legacyContext : emptyContextObject,1711 __DEV__ ? task.debugTask : null,1712 );1713 pushComponentStack(suspendedFallbackTask);1714 // TODO: This should be queued at a separate lower priority queue so that we only work1715 // on preparing fallbacks if we don't have any more main content to task on.1716 request.pingedTasks.push(suspendedFallbackTask);1717 }1718}17191720function replaySuspenseBoundary(1721 request: Request,1722 task: ReplayTask,1723 keyPath: KeyNode,1724 props: Object,1725 id: number,1726 childNodes: Array<ReplayNode>,1727 childSlots: ResumeSlots,1728 fallbackNodes: Array<ReplayNode>,1729 fallbackSlots: ResumeSlots,1730): void {1731 const prevKeyPath = task.keyPath;1732 const prevContext = task.formatContext;1733 const prevRow = task.row;1734 const previousReplaySet: ReplaySet = task.replay;17351736 const parentBoundary = task.blockedBoundary;1737 const parentHoistableState = task.hoistableState;17381739 const content: ReactNodeList = props.children;1740 const fallback: ReactNodeList = props.fallback;1741 const defer: boolean = enableCPUSuspense && props.defer === true;17421743 const fallbackAbortSet: Set<Task> = new Set();1744 const resumedBoundary = createSuspenseBoundary(1745 request,1746 task.row,1747 fallbackAbortSet,1748 canHavePreamble(task.formatContext) ? createPreamble() : null,1749 defer,1750 );1751 resumedBoundary.parentFlushed = true;1752 // We restore the same id of this boundary as was used during prerender.1753 resumedBoundary.rootSegmentID = id;17541755 // We can reuse the current context and task to render the content immediately without1756 // context switching. We just need to temporarily switch which boundary and replay node1757 // we're writing to. If something suspends, it'll spawn new suspended task with that context.1758 task.blockedBoundary = resumedBoundary;1759 task.hoistableState = resumedBoundary.contentState;1760 task.keyPath = keyPath;1761 task.formatContext = getSuspenseContentFormatContext(1762 request.resumableState,1763 prevContext,1764 );1765 task.row = null;1766 task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};17671768 try {1769 // We use the safe form because we don't handle suspending here. Only error handling.1770 renderNode(request, task, content, -1);17711772 if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) {1773 throw new Error(1774 "Couldn't find all resumable slots by key/index during replaying. " +1775 "The tree doesn't match so React will fallback to client rendering.",1776 );1777 }1778 task.replay.pendingTasks--;1779 if (1780 resumedBoundary.pendingTasks === 0 &&1781 resumedBoundary.status === PENDING1782 ) {1783 // This must have been the last segment we were waiting on. This boundary is now complete.1784 // Therefore we won't need the fallback. We early return so that we don't have to create1785 // the fallback.1786 resumedBoundary.status = COMPLETED;1787 request.completedBoundaries.push(resumedBoundary);1788 // We restore the parent componentStack. Semantically this is the same as1789 // popComponentStack(task) but we do this instead because it should be slightly1790 // faster1791 return;1792 }1793 } catch (thrownValue: mixed) {1794 resumedBoundary.status = CLIENT_RENDERED;1795 const error = request.aborted ? request.fatalError : thrownValue;1796 const thrownInfo = getThrownInfo(task.componentStack);1797 const errorDigest = logRecoverableError(1798 request,1799 error,1800 thrownInfo,1801 __DEV__ ? task.debugTask : null,1802 );1803 encodeErrorForBoundary(1804 resumedBoundary,1805 errorDigest,1806 error,1807 thrownInfo,1808 false,1809 );18101811 task.replay.pendingTasks--;18121813 // The parent already flushed in the prerender so we need to schedule this to be emitted.1814 request.clientRenderedBoundaries.push(resumedBoundary);18151816 // We don't need to decrement any task numbers because we didn't spawn any new task.1817 // We don't need to schedule any task because we know the parent has written yet.1818 // We do need to fallthrough to create the fallback though.1819 } finally {1820 task.blockedBoundary = parentBoundary;1821 task.hoistableState = parentHoistableState;1822 task.replay = previousReplaySet;1823 task.keyPath = prevKeyPath;1824 task.formatContext = prevContext;1825 task.row = prevRow;1826 }18271828 const fallbackKeyPath: KeyNode = [1829 keyPath[0],1830 'Suspense Fallback',1831 keyPath[2],1832 ];18331834 // We create suspended task for the fallback because we don't want to actually work1835 // on it yet in case we finish the main content, so we queue for later.1836 const fallbackReplay = {1837 nodes: fallbackNodes,1838 slots: fallbackSlots,1839 pendingTasks: 0,1840 };1841 const suspendedFallbackTask = createReplayTask(1842 request,1843 null,1844 fallbackReplay,1845 fallback,1846 -1,1847 parentBoundary,1848 resumedBoundary.fallbackState,1849 fallbackAbortSet,1850 fallbackKeyPath,1851 getSuspenseFallbackFormatContext(1852 request.resumableState,1853 task.formatContext,1854 ),1855 task.context,1856 task.treeContext,1857 task.row,1858 replaceSuspenseComponentStackWithSuspenseFallbackStack(task.componentStack),1859 !disableLegacyContext ? task.legacyContext : emptyContextObject,1860 __DEV__ ? task.debugTask : null,1861 );18621863 pushComponentStack(suspendedFallbackTask);1864 // TODO: This should be queued at a separate lower priority queue so that we only work1865 // on preparing fallbacks if we don't have any more main content to task on.1866 request.pingedTasks.push(suspendedFallbackTask);1867}18681869function finishSuspenseListRow(request: Request, row: SuspenseListRow): void {1870 // This row finished. Now we have to unblock all the next rows that were blocked on this.1871 unblockSuspenseListRow(request, row.next, row.hoistables);1872}18731874function unblockSuspenseListRow(1875 request: Request,1876 unblockedRow: null | SuspenseListRow,1877 inheritedHoistables: null | HoistableState,1878): void {1879 // We do this in a loop to avoid stack overflow for very long lists that get unblocked.1880 while (unblockedRow !== null) {1881 if (inheritedHoistables !== null) {1882 // Hoist any hoistables from the previous row into the next row so that it can be1883 // later transferred to all the rows.1884 hoistHoistables(unblockedRow.hoistables, inheritedHoistables);1885 // Mark the row itself for any newly discovered Suspense boundaries to inherit.1886 // This is different from hoistables because that also includes hoistables from1887 // all the boundaries below this row and not just previous rows.1888 unblockedRow.inheritedHoistables = inheritedHoistables;1889 }1890 // Unblocking the boundaries will decrement the count of this row but we keep it above1891 // zero so they never finish this row recursively.1892 const unblockedBoundaries = unblockedRow.boundaries;1893 if (unblockedBoundaries !== null) {1894 unblockedRow.boundaries = null;1895 for (let i = 0; i < unblockedBoundaries.length; i++) {1896 const unblockedBoundary = unblockedBoundaries[i];1897 if (inheritedHoistables !== null) {1898 hoistHoistables(unblockedBoundary.contentState, inheritedHoistables);1899 }1900 finishedTask(request, unblockedBoundary, null, null);1901 }1902 }1903 // Instead we decrement at the end to keep it all in this loop.1904 unblockedRow.pendingTasks--;1905 if (unblockedRow.pendingTasks > 0) {1906 // Still blocked.1907 break;1908 }1909 inheritedHoistables = unblockedRow.hoistables;1910 unblockedRow = unblockedRow.next;1911 }1912}19131914function trackPostponedSuspenseListRow(1915 request: Request,1916 trackedPostpones: PostponedHoles,1917 postponedRow: null | SuspenseListRow,1918): void {1919 // TODO: Because we unconditionally call this, it will be called by finishedTask1920 // and so ends up recursive which can lead to stack overflow for very long lists.1921 if (postponedRow !== null) {1922 const postponedBoundaries = postponedRow.boundaries;1923 if (postponedBoundaries !== null) {1924 postponedRow.boundaries = null;1925 for (let i = 0; i < postponedBoundaries.length; i++) {1926 const postponedBoundary = postponedBoundaries[i];1927 trackPostponedBoundary(request, trackedPostpones, postponedBoundary);1928 finishedTask(request, postponedBoundary, null, null);1929 }1930 }1931 }1932}19331934function tryToResolveTogetherRow(1935 request: Request,1936 togetherRow: SuspenseListRow,1937): void {1938 // If we have a "together" row and all the pendingTasks are really the boundaries themselves,1939 // and we won't outline any of them then we can unblock this row early so that we can inline1940 // all the boundaries at once.1941 const boundaries = togetherRow.boundaries;1942 if (boundaries === null || togetherRow.pendingTasks !== boundaries.length) {1943 return;1944 }1945 let allCompleteAndInlinable = true;1946 for (let i = 0; i < boundaries.length; i++) {1947 const rowBoundary = boundaries[i];1948 if (1949 rowBoundary.pendingTasks !== 1 ||1950 rowBoundary.parentFlushed ||1951 isEligibleForOutlining(request, rowBoundary)1952 ) {1953 allCompleteAndInlinable = false;1954 break;1955 }1956 }1957 if (allCompleteAndInlinable) {1958 unblockSuspenseListRow(request, togetherRow, togetherRow.hoistables);1959 }1960}19611962function createSuspenseListRow(1963 previousRow: null | SuspenseListRow,1964): SuspenseListRow {1965 const newRow: SuspenseListRow = {1966 pendingTasks: 1, // At first the row is blocked on attempting rendering itself.1967 boundaries: null,1968 hoistables: createHoistableState(),1969 inheritedHoistables: null,1970 together: false,1971 next: null,1972 };1973 if (previousRow !== null && previousRow.pendingTasks > 0) {1974 // If the previous row is not done yet, we add ourselves to be blocked on it.1975 // When it finishes, we'll decrement our pending tasks.1976 newRow.pendingTasks++;1977 newRow.boundaries = [];1978 previousRow.next = newRow;1979 }1980 return newRow;1981}19821983function renderSuspenseListRows(1984 request: Request,1985 task: Task,1986 keyPath: KeyNode,1987 rows: Array<ReactNodeList>,1988 revealOrder: void | 'forwards' | 'backwards' | 'unstable_legacy-backwards',1989): void {1990 // This is a fork of renderChildrenArray that's aware of tracking rows.1991 const prevKeyPath = task.keyPath;1992 const prevTreeContext = task.treeContext;1993 const prevRow = task.row;1994 const previousComponentStack = task.componentStack;1995 let previousDebugTask = null;1996 if (__DEV__) {1997 previousDebugTask = task.debugTask;1998 // We read debugInfo from task.node.props.children instead of rows because it1999 // might have been an unwrapped iterable so we read from the original node.2000 pushServerComponentStack(
Findings
✓ No findings reported for this file.