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 createRecoverableError,138 isRecoverableError,139 cloneRecoverableErrorAsFatal,140} from './ReactFizzHooks';141import {DefaultAsyncDispatcher} from './ReactFizzAsyncDispatcher';142import {143 getStackByComponentStackNode,144 getOwnerStackByComponentStackNodeInDev,145} from './ReactFizzComponentStack';146import {emptyTreeContext, pushTreeContext} from './ReactFizzTreeContext';147import {currentTaskInDEV, setCurrentTaskInDEV} from './ReactFizzCurrentTask';148import {149 callLazyInitInDEV,150 callComponentInDEV,151 callRenderInDEV,152} from './ReactFizzCallUserSpace';153import {154 getViewTransitionClassName,155 getViewTransitionName,156} from './ReactFizzViewTransitionComponent';157158import {resetOwnerStackLimit} from 'shared/ReactOwnerStackReset';159import {160 getIteratorFn,161 ASYNC_ITERATOR,162 REACT_ELEMENT_TYPE,163 REACT_PORTAL_TYPE,164 REACT_LAZY_TYPE,165 REACT_SUSPENSE_TYPE,166 REACT_LEGACY_HIDDEN_TYPE,167 REACT_STRICT_MODE_TYPE,168 REACT_PROFILER_TYPE,169 REACT_SUSPENSE_LIST_TYPE,170 REACT_FRAGMENT_TYPE,171 REACT_FORWARD_REF_TYPE,172 REACT_MEMO_TYPE,173 REACT_CONTEXT_TYPE,174 REACT_CONSUMER_TYPE,175 REACT_SCOPE_TYPE,176 REACT_VIEW_TRANSITION_TYPE,177 REACT_ACTIVITY_TYPE,178 REACT_OPTIMISTIC_KEY,179 REACT_RECOVERABLE_TYPE,180} from 'shared/ReactSymbols';181import ReactSharedInternals from 'shared/ReactSharedInternals';182import {183 disableLegacyContext,184 disableLegacyContextForFunctionComponents,185 enableScopeAPI,186 enableAsyncIterableChildren,187 enableViewTransition,188 enableViewTransitionParentEnterExit,189 enableFizzBlockingRender,190 enableAsyncDebugInfo,191 enableCPUSuspense,192} from 'shared/ReactFeatureFlags';193194import assign from 'shared/assign';195import noop from 'shared/noop';196import getComponentNameFromType from 'shared/getComponentNameFromType';197import isArray from 'shared/isArray';198import {REACT_RECOVERABLE_DIGEST} from 'shared/ReactRecoverable';199import {200 SuspenseException,201 getSuspendedThenable,202 ensureSuspendableThenableStateDEV,203 getSuspendedCallSiteStackDEV,204 getSuspendedCallSiteDebugTaskDEV,205 setCaptureSuspendedCallSiteDEV,206} from './ReactFizzThenable';207208// Linked list representing the identity of a component given the component/tag name and key.209// The name might be minified but we assume that it's going to be the same generated name. Typically210// because it's just the same compiled output in practice.211export type KeyNode = [212 Root | KeyNode /* parent */,213 string | null /* name */,214 string | number /* key */,215];216217type ResumeSlots =218 | null // nothing to resume219 | number // resume with segment ID at the root position220 | {[index: number]: number}; // resume with segmentID at the index221222type ReplaySuspenseBoundary = [223 string | null /* name */,224 string | number /* key */,225 Array<ReplayNode> /* content keyed children */,226 ResumeSlots /* content resumable slots */,227 null | ReplayNode /* fallback content */,228 number /* rootSegmentID */,229];230231type ReplayNode =232 | [233 string | null /* name */,234 string | number /* key */,235 Array<ReplayNode> /* keyed children */,236 ResumeSlots /* resumable slots */,237 ]238 | ReplaySuspenseBoundary;239240type PostponedHoles = {241 workingMap: Map<KeyNode, ReplayNode>,242 rootNodes: Array<ReplayNode>,243 rootSlots: ResumeSlots,244};245246type LegacyContext = {247 [key: string]: any,248};249250type SuspenseListRow = {251 pendingTasks: number, // The number of tasks, previous rows and inner suspense boundaries blocking this row.252 boundaries: null | Array<SuspenseBoundary>, // The boundaries in this row waiting to be unblocked by the previous row. (null means this row is not blocked)253 hoistables: HoistableState, // Any dependencies that this row depends on. Future rows need to also depend on it.254 inheritedHoistables: null | HoistableState, // Any dependencies that previous row depend on, that new boundaries of this row needs.255 together: boolean, // All the boundaries within this row must be revealed together.256 next: null | SuspenseListRow, // The next row blocked by this one.257};258259const CLIENT_RENDERED = 4; // if it errors or infinitely suspends260261type SuspenseBoundary = {262 status: 0 | 1 | 4 | 5,263 rootSegmentID: number,264 parentFlushed: boolean,265 pendingTasks: number, // when it reaches zero we can show this boundary's content266 row: null | SuspenseListRow, // the row that this boundary blocks from completing.267 completedSegments: Array<Segment>, // completed but not yet flushed segments.268 byteSize: number, // used to determine whether to inline children boundaries.269 defer: boolean, // never inline deferred boundaries270 fallbackAbortableTasks: Set<Task>, // used to cancel task on the fallback if the boundary completes or gets canceled.271 contentState: HoistableState,272 fallbackState: HoistableState,273 preamble: null | Preamble,274 tracked: null | {275 contentKeyPath: null | KeyNode, // used to track the path for replay nodes276 fallbackNode: null | ReplayNode, // used to track the fallback for replay nodes277 },278 errorDigest: ?string, // the error hash if it errors279 // DEV-only fields280 errorMessage?: null | string, // the error string if it errors281 errorStack?: null | string, // the error stack if it errors282 errorComponentStack?: null | string, // the error component stack if it errors283};284285type Ping = {286 resolve: () => void,287 reject: (error: mixed) => void,288};289290type RenderTask = {291 replay: null,292 node: ReactNodeList,293 childIndex: number,294 ping: Ping,295 blockedBoundary: Root | SuspenseBoundary,296 blockedSegment: Segment, // the segment we'll write to297 blockedPreamble: null | PreambleState,298 hoistableState: null | HoistableState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary299 abortSet: Set<Task>, // the abortable set that this task belongs to300 keyPath: Root | KeyNode, // the path of all parent keys currently rendering301 formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML)302 context: ContextSnapshot, // the current new context that this task is executing in303 treeContext: TreeContext, // the current tree context that this task is executing in304 row: null | SuspenseListRow, // the current SuspenseList row that this is rendering inside305 componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component306 thenableState: null | ThenableState,307 legacyContext: LegacyContext, // the current legacy context that this task is executing in308 debugTask: null | ConsoleTask, // DEV only309 // DON'T ANY MORE FIELDS. We at 16 in prod already which otherwise requires converting to a constructor.310 // Consider splitting into multiple objects or consolidating some fields.311};312313type ReplaySet = {314 nodes: Array<ReplayNode>, // the possible paths to follow down the replaying315 slots: ResumeSlots, // slots to resume316 pendingTasks: number, // tracks the number of tasks currently tracking this set of nodes317 // if pending tasks reach zero but there are still nodes left, it means we couldn't find318 // them all in the tree, so we need to abort and client render the boundary.319};320321type ReplayTask = {322 replay: ReplaySet,323 node: ReactNodeList,324 childIndex: number,325 ping: Ping,326 blockedBoundary: Root | SuspenseBoundary,327 blockedSegment: null, // we don't write to anything when we replay328 blockedPreamble: null,329 hoistableState: null | HoistableState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary330 abortSet: Set<Task>, // the abortable set that this task belongs to331 keyPath: Root | KeyNode, // the path of all parent keys currently rendering332 formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML)333 context: ContextSnapshot, // the current new context that this task is executing in334 treeContext: TreeContext, // the current tree context that this task is executing in335 row: null | SuspenseListRow, // the current SuspenseList row that this is rendering inside336 componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component337 thenableState: null | ThenableState,338 legacyContext: LegacyContext, // the current legacy context that this task is executing in339 debugTask: null | ConsoleTask, // DEV only340};341342export type Task = RenderTask | ReplayTask;343344const PENDING = 0;345const COMPLETED = 1;346const FLUSHED = 2;347const ABORTED = 3;348const ERRORED = 4;349const POSTPONED = 5;350351type Root = null;352353type Segment = {354 status: 0 | 1 | 2 | 3 | 4 | 5,355 parentFlushed: boolean, // typically a segment will be flushed by its parent, except if its parent was already flushed356 id: number, // starts as 0 and is lazily assigned if the parent flushes early357 +index: number, // the index within the parent's chunks or 0 at the root358 +chunks: Array<Chunk | PrecomputedChunk>,359 +children: Array<Segment>,360 +preambleChildren: Array<Segment>,361 // The context that this segment was created in.362 parentFormatContext: FormatContext,363 // If this segment represents a fallback, this is the content that will replace that fallback.364 boundary: null | SuspenseBoundary,365 // used to discern when text separator boundaries are needed366 lastPushedText: boolean,367 textEmbedded: boolean,368};369370// The ordering of these statuses matters. OPENING and OPEN are the only371// statuses in which newly scheduled work may be performed. Any status greater372// than OPEN represents a request that no longer admits work.373const OPENING = 10;374const OPEN = 11;375const CLOSING = 12;376const CLOSED = 13;377const STALLED_DEV = 14;378379// Passed to renderLifetimeController.abort(). Nothing reads the reason, but a380// call to abort() without one constructs an AbortError DOMException. Capturing381// the stack trace dominates that cost, and the cost grows with the depth of the382// stack.383const RENDER_ENDED = 'The render ended.';384385export opaque type Request = {386 destination: null | Destination,387 flushScheduled: boolean,388 +resumableState: ResumableState,389 +renderState: RenderState,390 +rootFormatContext: FormatContext,391 +progressiveChunkSize: number,392 status: 10 | 11 | 12 | 13 | 14,393 fatalError: mixed,394 aborted: boolean,395 nextSegmentId: number,396 allPendingTasks: number, // when it reaches zero, we can close the connection.397 pendingRootTasks: number, // when this reaches zero, we've finished at least the root boundary.398 completedRootSegment: null | Segment, // Completed but not yet flushed root segments.399 completedPreambleSegments: null | Array<Array<Segment>>, // contains the ready-to-flush segments that make up the preamble400 byteSize: number, // counts the number of bytes accumulated in the shell401 abortableTasks: Set<Task>,402 pingedTasks: Array<Task>, // High priority tasks that should be worked on first.403 currentTask: null | Task, // The task currently executing in this request.404 // Queues to flush in order of priority405 clientRenderedBoundaries: Array<SuspenseBoundary>, // Errored or client rendered but not yet flushed.406 completedBoundaries: Array<SuspenseBoundary>, // Completed but not yet fully flushed boundaries to show.407 partialBoundaries: Array<SuspenseBoundary>, // Partially completed boundaries that can flush its segments early.408 trackedPostpones: null | PostponedHoles, // Gets set to non-null while we want to track postponed holes. I.e. during a prerender.409 // While prerendering a postponed request that produced a real shell, this410 // holds the PostponedState returned by getPostponedState. getPostponedState411 // snapshots nextSegmentId before the (pull-driven) prelude flush runs, but the412 // flush outlines completed boundaries and advances nextSegmentId past that413 // snapshot. We finalize the snapshot from flushCompletedQueues so the resumed414 // render allocates segment ids strictly above the shell's; otherwise the shell415 // and resume emit duplicate B:/S: ids once concatenated. Stays null for live416 // renders and resumes.417 postponedState: null | PostponedState,418 // onError is called when an error happens anywhere in the tree. It might recover.419 // The return string is used in production primarily to avoid leaking internals, secondarily to save bytes.420 // Returning null/undefined will cause a default error message in production421 onError: (error: mixed, errorInfo: ThrownInfo) => ?string,422 // onBrowserBailout is called when Fizz recovers by intentionally deferring423 // rendering to the browser.424 onBrowserBailout: (error: mixed, errorInfo: ThrownInfo) => void,425 // onAllReady is called when all pending task is done but it may not have flushed yet.426 // This is a good time to start writing if you want only HTML and no intermediate steps.427 onAllReady: () => void,428 // onShellReady is called when there is at least a root fallback ready to show.429 // Typically you don't need this callback because it's best practice to always have a430 // root fallback ready so there's no need to wait.431 onShellReady: () => void,432 // onShellError is called when the shell didn't complete. That means you probably want to433 // emit a different response to the stream instead.434 onShellError: (error: mixed) => void,435 onFatalError: (error: mixed) => void,436 // Aborted once the render ends, whether it completed, failed fatally or was437 // aborted. Bounds the lifetime of anything that must not outlive the render.438 // Null until attachAbortSignal creates it, so a render that is given no439 // signal constructs no controller.440 renderLifetimeController: null | AbortController,441 // Form state that was the result of an MPA submission, if it was provided.442 formState: null | ReactFormState<any, any>,443 // DEV-only, warning dedupe444 didWarnForKey?: null | WeakSet<ComponentStackNode>,445};446447type Preamble = {448 content: PreambleState,449 fallback: PreambleState,450};451452function createPreamble(): Preamble {453 return {454 content: createPreambleState(),455 fallback: createPreambleState(),456 };457}458459// This is a default heuristic for how to split up the HTML content into progressive460// loading. Our goal is to be able to display additional new content about every 500ms.461// Faster than that is unnecessary and should be throttled on the client. It also462// adds unnecessary overhead to do more splits. We don't know if it's a higher or lower463// end device but higher end suffer less from the overhead than lower end does from464// not getting small enough pieces. We error on the side of low end.465// We base this on low end 3G speeds which is about 500kbits per second. We assume466// that there can be a reasonable drop off from max bandwidth which leaves you with467// as little as 80%. We can receive half of that each 500ms - at best. In practice,468// a little bandwidth is lost to processing and contention - e.g. CSS and images that469// are downloaded along with the main content. So we estimate about half of that to be470// the lower end throughput. In other words, we expect that you can at least show471// about 12.5kb of content per 500ms. Not counting starting latency for the first472// paint.473// 500 * 1024 / 8 * .8 * 0.5 / 2474const DEFAULT_PROGRESSIVE_CHUNK_SIZE = 12800;475476function getBlockingRenderMaxSize(request: Request): number {477 // We want to make sure that we can block the reveal of a well designed complete478 // shell but if you have constructed a too large shell (e.g. by not adding any479 // Suspense boundaries) then that might take too long to render. We shouldn't480 // punish users (or overzealous metrics tracking) in that scenario.481 // There's a trade off here. If this limit is too low then you can't fit a482 // reasonably well built UI within it without getting errors. If it's too high483 // then things that accidentally fall below it might take too long to load.484 // Web Vitals target 1.8 seconds for first paint and our goal to have the limit485 // be fast enough to hit that. For this argument we assume that most external486 // resources are already cached because it's a return visit, or inline styles.487 // If it's not, then it's highly unlikely that any render blocking instructions488 // we add has any impact what so ever on the paint.489 // Assuming a first byte of about 600ms which is kind of bad but common with a490 // decent static host. If it's longer e.g. due to dynamic rendering, then you491 // are going to bound by dynamic production of the content and you're better off492 // with Suspense boundaries anyway. This number doesn't matter much. Then you493 // have about 1.2 seconds left for bandwidth. On 3G that gives you about 112.5kb494 // worth of data. That's worth about 10x in terms of uncompressed bytes. Then we495 // half that just to account for longer latency, slower bandwidth and CPU processing.496 // Now we're down to about 500kb. In fact, looking at metrics we've collected with497 // rel="expect" examples and other documents, the impact on documents smaller than498 // that is within the noise. That's because there's enough happening within that499 // start up to not make HTML streaming not significantly better.500 // Content above the fold tends to be about 100-200kb tops. Therefore 500kb should501 // be enough head room for a good loading state. After that you should use502 // Suspense or SuspenseList to improve it.503 // Since this is highly related to the reason you would adjust the504 // progressiveChunkSize option, and always has to be higher, we define this limit505 // in terms of it. So if you want to increase the limit because you have high506 // bandwidth users, then you can adjust it up. If you are concerned about even507 // slower bandwidth then you can adjust it down.508 return request.progressiveChunkSize * 40; // 512kb by default.509}510511function isEligibleForOutlining(512 request: Request,513 boundary: SuspenseBoundary,514): boolean {515 // For very small boundaries, don't bother producing a fallback for outlining.516 // The larger this limit is, the more we can save on preparing fallbacks in case we end up517 // outlining.518 return (519 (boundary.byteSize > 500 ||520 hasSuspenseyContent(boundary.contentState, /* flushingInShell */ false) ||521 boundary.defer) &&522 // For boundaries that can possibly contribute to the preamble we don't want to outline523 // them regardless of their size since the fallbacks should only be emitted if we've524 // errored the boundary.525 boundary.preamble === null526 );527}528529function defaultErrorHandler(error: mixed) {530 if (531 typeof error === 'object' &&532 error !== null &&533 typeof error.environmentName === 'string'534 ) {535 // This was a Server error. We print the environment name in a badge just like we do with536 // replays of console logs to indicate that the source of this throw as actually the Server.537 bindToConsole('error', [error], error.environmentName)();538 } else {539 console['error'](error); // Don't transform to our wrapper540 }541 return null;542}543544function RequestInstance(545 this: $FlowFixMe,546 resumableState: ResumableState,547 renderState: RenderState,548 rootFormatContext: FormatContext,549 progressiveChunkSize: void | number,550 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),551 onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void),552 onAllReady: void | (() => void),553 onShellReady: void | (() => void),554 onShellError: void | ((error: mixed) => void),555 onFatalError: void | ((error: mixed) => void),556 formState: void | null | ReactFormState<any, any>,557) {558 const pingedTasks: Array<Task> = [];559 const abortSet: Set<Task> = new Set();560 this.destination = null;561 this.flushScheduled = false;562 this.resumableState = resumableState;563 this.renderState = renderState;564 this.rootFormatContext = rootFormatContext;565 this.progressiveChunkSize =566 progressiveChunkSize === undefined567 ? DEFAULT_PROGRESSIVE_CHUNK_SIZE568 : progressiveChunkSize;569 // $FlowFixMe[constant-condition]570 this.status = isWorkLoopExternallyDriven ? OPEN : OPENING;571 this.fatalError = null;572 this.aborted = false;573 this.nextSegmentId = 0;574 this.allPendingTasks = 0;575 this.pendingRootTasks = 0;576 this.completedRootSegment = null;577 this.completedPreambleSegments = null;578 this.byteSize = 0;579 this.abortableTasks = abortSet;580 this.pingedTasks = pingedTasks;581 this.currentTask = null;582 this.clientRenderedBoundaries = [] as Array<SuspenseBoundary>;583 this.completedBoundaries = [] as Array<SuspenseBoundary>;584 this.partialBoundaries = [] as Array<SuspenseBoundary>;585 this.trackedPostpones = null;586 this.postponedState = null;587 this.onError = onError === undefined ? defaultErrorHandler : onError;588 this.onBrowserBailout =589 onBrowserBailout === undefined ? noop : onBrowserBailout;590 this.onAllReady = onAllReady === undefined ? noop : onAllReady;591 this.onShellReady = onShellReady === undefined ? noop : onShellReady;592 this.onShellError = onShellError === undefined ? noop : onShellError;593 this.onFatalError = onFatalError === undefined ? noop : onFatalError;594 this.renderLifetimeController = null;595 this.formState = formState === undefined ? null : formState;596 if (__DEV__) {597 this.didWarnForKey = null;598 }599}600601export function createRequest(602 children: ReactNodeList,603 resumableState: ResumableState,604 renderState: RenderState,605 rootFormatContext: FormatContext,606 progressiveChunkSize: void | number,607 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),608 onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void),609 onAllReady: void | (() => void),610 onShellReady: void | (() => void),611 onShellError: void | ((error: mixed) => void),612 onFatalError: void | ((error: mixed) => void),613 formState: void | null | ReactFormState<any, any>,614): Request {615 if (__DEV__) {616 resetOwnerStackLimit();617 }618619 // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors620 const request: Request = new RequestInstance(621 resumableState,622 renderState,623 rootFormatContext,624 progressiveChunkSize,625 onError,626 onBrowserBailout,627 onAllReady,628 onShellReady,629 onShellError,630 onFatalError,631 formState,632 );633634 // This segment represents the root fallback.635 const rootSegment = createPendingSegment(636 request,637 0,638 null,639 rootFormatContext,640 // Root segments are never embedded in Text on either edge641 false,642 false,643 );644 // There is no parent so conceptually, we're unblocked to flush this segment.645 rootSegment.parentFlushed = true;646 const rootTask = createRenderTask(647 request,648 null,649 children,650 -1,651 null,652 rootSegment,653 null,654 null,655 request.abortableTasks,656 null,657 rootFormatContext,658 rootContextSnapshot,659 emptyTreeContext,660 null,661 null,662 emptyContextObject,663 null,664 );665 pushComponentStack(rootTask);666 request.pingedTasks.push(rootTask);667 return request;668}669670export function createPrerenderRequest(671 children: ReactNodeList,672 resumableState: ResumableState,673 renderState: RenderState,674 rootFormatContext: FormatContext,675 progressiveChunkSize: void | number,676 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),677 onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void),678 onAllReady: void | (() => void),679 onShellReady: void | (() => void),680 onShellError: void | ((error: mixed) => void),681 onFatalError: void | ((error: mixed) => void),682): Request {683 const request = createRequest(684 children,685 resumableState,686 renderState,687 rootFormatContext,688 progressiveChunkSize,689 onError,690 onBrowserBailout,691 onAllReady,692 onShellReady,693 onShellError,694 onFatalError,695 undefined,696 );697 // Start tracking postponed holes during this render.698 request.trackedPostpones = {699 workingMap: new Map(),700 rootNodes: [],701 rootSlots: null,702 };703 return request;704}705706export function resumeRequest(707 children: ReactNodeList,708 postponedState: PostponedState,709 renderState: RenderState,710 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),711 onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void),712 onAllReady: void | (() => void),713 onShellReady: void | (() => void),714 onShellError: void | ((error: mixed) => void),715 onFatalError: void | ((error: mixed) => void),716): Request {717 if (__DEV__) {718 resetOwnerStackLimit();719 }720721 // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors722 const request: Request = new RequestInstance(723 postponedState.resumableState,724 renderState,725 postponedState.rootFormatContext,726 postponedState.progressiveChunkSize,727 onError,728 onBrowserBailout,729 onAllReady,730 onShellReady,731 onShellError,732 onFatalError,733 null,734 );735 request.nextSegmentId = postponedState.nextSegmentId;736737 if (typeof postponedState.replaySlots === 'number') {738 // We have a resume slot at the very root. This is effectively just a full rerender.739 const rootSegment = createPendingSegment(740 request,741 0,742 null,743 postponedState.rootFormatContext,744 // Root segments are never embedded in Text on either edge745 false,746 false,747 );748 // There is no parent so conceptually, we're unblocked to flush this segment.749 rootSegment.parentFlushed = true;750 const rootTask = createRenderTask(751 request,752 null,753 children,754 -1,755 null,756 rootSegment,757 null,758 null,759 request.abortableTasks,760 null,761 postponedState.rootFormatContext,762 rootContextSnapshot,763 emptyTreeContext,764 null,765 null,766 emptyContextObject,767 null,768 );769 pushComponentStack(rootTask);770 request.pingedTasks.push(rootTask);771 return request;772 }773774 const replay: ReplaySet = {775 nodes: postponedState.replayNodes,776 slots: postponedState.replaySlots,777 pendingTasks: 0,778 };779 const rootTask = createReplayTask(780 request,781 null,782 replay,783 children,784 -1,785 null,786 null,787 request.abortableTasks,788 null,789 postponedState.rootFormatContext,790 rootContextSnapshot,791 emptyTreeContext,792 null,793 null,794 emptyContextObject,795 null,796 );797 pushComponentStack(rootTask);798 request.pingedTasks.push(rootTask);799 return request;800}801802export function resumeAndPrerenderRequest(803 children: ReactNodeList,804 postponedState: PostponedState,805 renderState: RenderState,806 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),807 onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void),808 onAllReady: void | (() => void),809 onShellReady: void | (() => void),810 onShellError: void | ((error: mixed) => void),811 onFatalError: void | ((error: mixed) => void),812): Request {813 const request = resumeRequest(814 children,815 postponedState,816 renderState,817 onError,818 onBrowserBailout,819 onAllReady,820 onShellReady,821 onShellError,822 onFatalError,823 );824 // Start tracking postponed holes during this render.825 request.trackedPostpones = {826 workingMap: new Map(),827 rootNodes: [],828 rootSlots: null,829 };830 return request;831}832833let currentRequest: null | Request = null;834835export function resolveRequest(): null | Request {836 if (currentRequest) return currentRequest;837 // $FlowFixMe[constant-condition]838 if (supportsRequestStorage) {839 const store = requestStorage.getStore();840 if (store) return store;841 }842 return null;843}844845function pingTask(request: Request, task: Task): void {846 const pingedTasks = request.pingedTasks;847 pingedTasks.push(task);848 // $FlowFixMe[constant-condition]849 if (isWorkLoopExternallyDriven) {850 return;851 } else {852 if (request.pingedTasks.length === 1) {853 request.flushScheduled = request.destination !== null;854 if (request.trackedPostpones !== null || request.status === OPENING) {855 scheduleMicrotask(() => performWork(request));856 } else {857 scheduleWork(() => performWork(request));858 }859 }860 }861}862863function pingRejectedTask(request: Request, task: Task, error: mixed): void {864 if (!request.aborted) {865 // Replaying the task is what gives ordinary render errors their complete866 // component stack.867 pingTask(request, task);868 return;869 }870 if (!task.abortSet.delete(task)) {871 // finishAbort already completed this task with the request's abort reason.872 return;873 }874 // abortTask synchronously claimed this task before abort listeners could875 // reject its wakeable. Finish it with the more specific reason before the876 // scheduled final abort uses the reason for the whole request.877 if (__DEV__) {878 finishAbortedTaskDEV(task, request, error);879 } else {880 finishAbortedTask(task, request, error);881 }882}883884function createSuspenseBoundary(885 request: Request,886 row: null | SuspenseListRow,887 fallbackAbortableTasks: Set<Task>,888 preamble: null | Preamble,889 defer: boolean,890): SuspenseBoundary {891 const boundary: SuspenseBoundary = {892 status: PENDING,893 rootSegmentID: -1,894 parentFlushed: false,895 pendingTasks: 0,896 row: row,897 completedSegments: [],898 byteSize: 0,899 defer: defer,900 fallbackAbortableTasks,901 errorDigest: null,902 contentState: createHoistableState(),903 fallbackState: createHoistableState(),904 preamble,905 tracked: null,906 };907 if (__DEV__) {908 // DEV-only fields for hidden class909 boundary.errorMessage = null;910 boundary.errorStack = null;911 boundary.errorComponentStack = null;912 }913 if (row !== null) {914 // This boundary will block this row from completing.915 row.pendingTasks++;916 const blockedBoundaries = row.boundaries;917 if (blockedBoundaries !== null) {918 // Previous rows will block this boundary itself from completing.919 request.allPendingTasks++;920 boundary.pendingTasks++;921 blockedBoundaries.push(boundary);922 }923 const inheritedHoistables = row.inheritedHoistables;924 if (inheritedHoistables !== null) {925 hoistHoistables(boundary.contentState, inheritedHoistables);926 }927 }928 return boundary;929}930931function createRenderTask(932 request: Request,933 thenableState: ThenableState | null,934 node: ReactNodeList,935 childIndex: number,936 blockedBoundary: Root | SuspenseBoundary,937 blockedSegment: Segment,938 blockedPreamble: null | PreambleState,939 hoistableState: null | HoistableState,940 abortSet: Set<Task>,941 keyPath: Root | KeyNode,942 formatContext: FormatContext,943 context: ContextSnapshot,944 treeContext: TreeContext,945 row: null | SuspenseListRow,946 componentStack: null | ComponentStackNode,947 legacyContext: LegacyContext,948 debugTask: null | ConsoleTask,949): RenderTask {950 request.allPendingTasks++;951 if (blockedBoundary === null) {952 request.pendingRootTasks++;953 } else {954 blockedBoundary.pendingTasks++;955 }956 if (row !== null) {957 row.pendingTasks++;958 }959 const task: RenderTask = {960 replay: null,961 node,962 childIndex,963 ping: {964 resolve: () => pingTask(request, task),965 reject: error => pingRejectedTask(request, task, error),966 },967 blockedBoundary,968 blockedSegment,969 blockedPreamble,970 hoistableState,971 abortSet,972 keyPath,973 formatContext,974 context,975 treeContext,976 row,977 componentStack,978 thenableState,979 } as any;980 if (!disableLegacyContext) {981 task.legacyContext = legacyContext;982 }983 if (__DEV__) {984 task.debugTask = debugTask;985 }986 abortSet.add(task);987 return task;988}989990function createReplayTask(991 request: Request,992 thenableState: ThenableState | null,993 replay: ReplaySet,994 node: ReactNodeList,995 childIndex: number,996 blockedBoundary: Root | SuspenseBoundary,997 hoistableState: null | HoistableState,998 abortSet: Set<Task>,999 keyPath: Root | KeyNode,1000 formatContext: FormatContext,1001 context: ContextSnapshot,1002 treeContext: TreeContext,1003 row: null | SuspenseListRow,1004 componentStack: null | ComponentStackNode,1005 legacyContext: LegacyContext,1006 debugTask: null | ConsoleTask,1007): ReplayTask {1008 request.allPendingTasks++;1009 if (blockedBoundary === null) {1010 request.pendingRootTasks++;1011 } else {1012 blockedBoundary.pendingTasks++;1013 }1014 if (row !== null) {1015 row.pendingTasks++;1016 }1017 replay.pendingTasks++;1018 const task: ReplayTask = {1019 replay,1020 node,1021 childIndex,1022 ping: {1023 resolve: () => pingTask(request, task),1024 reject: error => pingRejectedTask(request, task, error),1025 },1026 blockedBoundary,1027 blockedSegment: null,1028 blockedPreamble: null,1029 hoistableState,1030 abortSet,1031 keyPath,1032 formatContext,1033 context,1034 treeContext,1035 row,1036 componentStack,1037 thenableState,1038 } as any;1039 if (!disableLegacyContext) {1040 task.legacyContext = legacyContext;1041 }1042 if (__DEV__) {1043 task.debugTask = debugTask;1044 }1045 abortSet.add(task);1046 return task;1047}10481049function createPendingSegment(1050 request: Request,1051 index: number,1052 boundary: null | SuspenseBoundary,1053 parentFormatContext: FormatContext,1054 lastPushedText: boolean,1055 textEmbedded: boolean,1056): Segment {1057 return {1058 status: PENDING,1059 parentFlushed: false,1060 id: -1, // lazily assigned later1061 index,1062 chunks: [],1063 children: [],1064 preambleChildren: [],1065 parentFormatContext,1066 boundary,1067 lastPushedText,1068 textEmbedded,1069 };1070}10711072function getCurrentStackInDEV(): string {1073 if (__DEV__) {1074 if (currentTaskInDEV === null || currentTaskInDEV.componentStack === null) {1075 return '';1076 }1077 return getOwnerStackByComponentStackNodeInDev(1078 currentTaskInDEV.componentStack,1079 );1080 }1081 return '';1082}10831084function getStackFromNode(stackNode: ComponentStackNode): string {1085 return getStackByComponentStackNode(stackNode);1086}10871088function pushHaltedAwaitOnComponentStack(1089 task: Task,1090 debugInfo: void | null | ReactDebugInfo,1091): void {1092 if (!__DEV__) {1093 // eslint-disable-next-line react-internal/prod-error-codes1094 throw new Error(1095 'pushHaltedAwaitOnComponentStack should never be called in production. This is a bug in React.',1096 );1097 }1098 if (debugInfo != null) {1099 for (let i = debugInfo.length - 1; i >= 0; i--) {1100 const info = debugInfo[i];1101 if (info.awaited != null) {1102 const asyncInfo: ReactAsyncInfo = info as any;1103 const bestStack =1104 asyncInfo.debugStack == null ? asyncInfo.awaited : asyncInfo;1105 if (bestStack.debugStack !== undefined) {1106 task.componentStack = {1107 parent: task.componentStack,1108 type: asyncInfo,1109 owner: bestStack.owner,1110 stack: bestStack.debugStack,1111 };1112 task.debugTask = bestStack.debugTask as any;1113 break;1114 }1115 }1116 }1117 }1118}11191120// performWork + retryTask without mutation1121function rerenderStalledTask(request: Request, task: Task): void {1122 const prevStatus = request.status;1123 const prevAborted = request.aborted;1124 request.status = STALLED_DEV;1125 // This diagnostic replay must reach the suspended call site instead of1126 // taking the abort path.1127 request.aborted = false;11281129 const prevContext = getActiveContext();1130 const prevDispatcher = ReactSharedInternals.H;1131 ReactSharedInternals.H = HooksDispatcher;1132 const prevAsyncDispatcher = ReactSharedInternals.A;1133 ReactSharedInternals.A = DefaultAsyncDispatcher;11341135 const prevRequest = currentRequest;1136 currentRequest = request;11371138 const prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack;1139 ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;11401141 const prevResumableState = currentResumableState;1142 setCurrentResumableState(request.resumableState);1143 switchContext(task.context);1144 const prevTaskInDEV = currentTaskInDEV;1145 setCurrentTaskInDEV(task);1146 try {1147 retryNode(request, task);1148 } catch (x) {1149 // Suspended again.1150 resetHooksState();1151 } finally {1152 setCurrentTaskInDEV(prevTaskInDEV);1153 setCurrentResumableState(prevResumableState);11541155 ReactSharedInternals.H = prevDispatcher;1156 ReactSharedInternals.A = prevAsyncDispatcher;11571158 ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl;1159 if (prevDispatcher === HooksDispatcher) {1160 // This means that we were in a reentrant work loop. This could happen1161 // in a renderer that supports synchronous work like renderToString,1162 // when it's called from within another renderer.1163 // Normally we don't bother switching the contexts to their root/default1164 // values when leaving because we'll likely need the same or similar1165 // context again. However, when we're inside a synchronous loop like this1166 // we'll to restore the context to what it was before returning.1167 switchContext(prevContext);1168 }1169 currentRequest = prevRequest;1170 request.status = prevStatus;1171 request.aborted = prevAborted;1172 }1173}11741175function pushSuspendedCallSiteOnComponentStack(1176 request: Request,1177 task: Task,1178): void {1179 setCaptureSuspendedCallSiteDEV(true);1180 const restoreThenableState = ensureSuspendableThenableStateDEV(1181 // refined at the callsite1182 task.thenableState as any as ThenableState,1183 );1184 try {1185 rerenderStalledTask(request, task);1186 } finally {1187 restoreThenableState();1188 setCaptureSuspendedCallSiteDEV(false);1189 }11901191 const suspendCallSiteStack = getSuspendedCallSiteStackDEV();1192 const suspendCallSiteDebugTask = getSuspendedCallSiteDebugTaskDEV();11931194 if (suspendCallSiteStack !== null) {1195 const ownerStack = task.componentStack;1196 task.componentStack = {1197 // The owner of the suspended call site would be the owner of this task.1198 // We need the task itself otherwise we'd miss a frame.1199 owner: ownerStack,1200 parent: suspendCallSiteStack.parent,1201 stack: suspendCallSiteStack.stack,1202 type: suspendCallSiteStack.type,1203 };1204 }1205 task.debugTask = suspendCallSiteDebugTask;1206}12071208function pushServerComponentStack(1209 task: Task,1210 debugInfo: void | null | ReactDebugInfo,1211): void {1212 if (!__DEV__) {1213 // eslint-disable-next-line react-internal/prod-error-codes1214 throw new Error(1215 'pushServerComponentStack should never be called in production. This is a bug in React.',1216 );1217 }1218 // Build a Server Component parent stack from the debugInfo.1219 if (debugInfo != null) {1220 const stack: ReactDebugInfo = debugInfo;1221 for (let i = 0; i < stack.length; i++) {1222 const componentInfo: ReactComponentInfo = stack[i] as any;1223 if (typeof componentInfo.name !== 'string') {1224 continue;1225 }1226 if (componentInfo.debugStack === undefined) {1227 continue;1228 }1229 task.componentStack = {1230 parent: task.componentStack,1231 type: componentInfo,1232 owner: componentInfo.owner,1233 stack: componentInfo.debugStack,1234 };1235 task.debugTask = componentInfo.debugTask as any;1236 }1237 }1238}12391240function pushComponentStack(task: Task): void {1241 const node = task.node;1242 // Create the Component Stack frame for the element we're about to try.1243 // It's unfortunate that we need to do this refinement twice. Once for1244 // the stack frame and then once again while actually1245 if (typeof node === 'object' && node !== null) {1246 switch ((node as any).$$typeof) {1247 case REACT_ELEMENT_TYPE: {1248 const element: any = node;1249 const type = element.type;1250 const owner = __DEV__ ? element._owner : null;1251 const stack = __DEV__ ? element._debugStack : null;1252 if (__DEV__) {1253 pushServerComponentStack(task, element._debugInfo);1254 task.debugTask = element._debugTask;1255 }1256 task.componentStack = createComponentStackFromType(1257 task.componentStack,1258 type,1259 owner,1260 stack,1261 );1262 break;1263 }1264 case REACT_LAZY_TYPE: {1265 if (__DEV__) {1266 const lazyNode: LazyComponentType<any, any> = node as any;1267 pushServerComponentStack(task, lazyNode._debugInfo);1268 }1269 break;1270 }1271 default: {1272 if (__DEV__) {1273 const maybeUsable: Object = node;1274 if (typeof maybeUsable.then === 'function') {1275 const thenable: Thenable<ReactNodeList> = maybeUsable as any;1276 pushServerComponentStack(task, thenable._debugInfo);1277 }1278 }1279 }1280 }1281 }1282}12831284function createComponentStackFromType(1285 parent: null | ComponentStackNode,1286 type: Function | string | symbol,1287 owner: void | null | ReactComponentInfo | ComponentStackNode, // DEV only1288 stack: void | null | string | Error, // DEV only1289): ComponentStackNode {1290 if (__DEV__) {1291 return {1292 parent,1293 type,1294 owner,1295 stack,1296 };1297 }1298 return {1299 parent,1300 type,1301 };1302}13031304function replaceSuspenseComponentStackWithSuspenseFallbackStack(1305 componentStack: null | ComponentStackNode,1306): null | ComponentStackNode {1307 if (componentStack === null) {1308 return null;1309 }1310 return createComponentStackFromType(1311 componentStack.parent,1312 'Suspense Fallback',1313 __DEV__ ? componentStack.owner : null,1314 __DEV__ ? componentStack.stack : null,1315 );1316}13171318type ThrownInfo = {1319 componentStack?: string,1320};1321export type ErrorInfo = ThrownInfo;13221323function getThrownInfo(node: null | ComponentStackNode): ThrownInfo {1324 const errorInfo: ThrownInfo = {};1325 if (node) {1326 Object.defineProperty(errorInfo, 'componentStack', {1327 configurable: true,1328 enumerable: true,1329 get() {1330 // Lazyily generate the stack since it's expensive.1331 const stack = getStackFromNode(node);1332 Object.defineProperty(errorInfo, 'componentStack', {1333 value: stack,1334 });1335 return stack;1336 },1337 });1338 }1339 return errorInfo;1340}13411342function encodeErrorForBoundary(1343 boundary: SuspenseBoundary,1344 digest: ?string,1345 error: mixed,1346 thrownInfo: ThrownInfo,1347 wasAborted: boolean,1348) {1349 boundary.errorDigest = digest;1350 if (__DEV__) {1351 if (isRecoverableError(error)) {1352 boundary.errorMessage = wasAborted1353 ? 'Switched to client rendering because the server render was aborted ' +1354 'with a request to render on the client.'1355 : 'Switched to client rendering because a component requested it.';1356 boundary.errorComponentStack = thrownInfo.componentStack;1357 return;1358 }1359 let message, stack;1360 // In dev we additionally encode the error message and component stack on the boundary1361 if (error instanceof Error) {1362 // eslint-disable-next-line react-internal/safe-string-coercion1363 message = String(error.message);1364 // eslint-disable-next-line react-internal/safe-string-coercion1365 stack = String(error.stack);1366 } else if (typeof error === 'object' && error !== null) {1367 message = describeObjectForErrorMessage(error);1368 stack = null;1369 } else {1370 // eslint-disable-next-line react-internal/safe-string-coercion1371 message = String(error);1372 stack = null;1373 }1374 const prefix = wasAborted1375 ? 'Switched to client rendering because the server rendering aborted due to:\n\n'1376 : 'Switched to client rendering because the server rendering errored:\n\n';1377 boundary.errorMessage = prefix + message;1378 boundary.errorStack = stack !== null ? prefix + stack : null;1379 boundary.errorComponentStack = thrownInfo.componentStack;1380 }1381}13821383function logRecoverableError(1384 request: Request,1385 error: any,1386 errorInfo: ThrownInfo,1387 debugTask: null | ConsoleTask,1388): ?string {1389 if (isRecoverableError(error)) {1390 logBrowserBailout(request, error, errorInfo, debugTask);1391 return REACT_RECOVERABLE_DIGEST;1392 }13931394 // If this callback errors, we intentionally let that error bubble up to become a fatal error1395 // so that someone fixes the error reporting instead of hiding it.1396 const onError = request.onError;1397 const errorDigest =1398 __DEV__ && debugTask1399 ? debugTask.run(onError.bind(null, error, errorInfo))1400 : onError(error, errorInfo);1401 if (errorDigest != null && typeof errorDigest !== 'string') {1402 // We used to throw here but since this gets called from a variety of unprotected places it1403 // seems better to just warn and discard the returned value.1404 if (__DEV__) {1405 console.error(1406 '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',1407 typeof errorDigest,1408 );1409 }1410 return;1411 }1412 // An empty digest is reserved for React's internal client-render signal.1413 // Historically an empty digest was omitted from the wire format, so1414 // normalizing it to undefined preserves the existing user-space semantics.1415 return errorDigest === '' ? undefined : errorDigest;1416}14171418function logBrowserBailout(1419 request: Request,1420 error: mixed,1421 errorInfo: ThrownInfo,1422 debugTask: null | ConsoleTask,1423): void {1424 // If this callback errors, we intentionally let that error bubble up to1425 // become a fatal error, matching the behavior of onError.1426 const onBrowserBailout = request.onBrowserBailout;1427 if (__DEV__ && debugTask) {1428 debugTask.run(onBrowserBailout.bind(null, error, errorInfo));1429 } else {1430 onBrowserBailout(error, errorInfo);1431 }1432}14331434function fatalError(1435 request: Request,1436 error: mixed,1437 errorInfo: ThrownInfo,1438 debugTask: null | ConsoleTask,1439): void {1440 // This is called outside error handling code such as if the root errors outside1441 // a suspense boundary or if the root suspense boundary's fallback errors.1442 // It's also called if React itself or its host configs errors.1443 const onShellError = request.onShellError;1444 const onFatalError = request.onFatalError;1445 // Once the shell has completed it can't error anymore, so onShellError only1446 // fires while root tasks are still pending. onFatalError always fires because1447 // the error is always fatal to the request.1448 const shellComplete = request.pendingRootTasks === 0;1449 if (__DEV__ && debugTask) {1450 if (!shellComplete) {1451 debugTask.run(onShellError.bind(null, error));1452 }1453 debugTask.run(onFatalError.bind(null, error));1454 } else {1455 if (!shellComplete) {1456 onShellError(error);1457 }1458 onFatalError(error);1459 }1460 endRenderLifetime(request);1461 if (request.destination !== null) {1462 request.status = CLOSED;1463 closeWithError(request.destination, error);1464 } else {1465 request.status = CLOSING;1466 // abort() already stored the reason that every remaining task must1467 // observe. This error may only be a fatal diagnostic derived from it.1468 if (!request.aborted) {1469 request.fatalError = error;1470 }1471 }1472}14731474function renderSuspenseBoundary(1475 request: Request,1476 someTask: Task,1477 keyPath: KeyNode,1478 props: SuspenseProps,1479): void {1480 if (someTask.replay !== null) {1481 // If we're replaying through this pass, it means we're replaying through1482 // an already completed Suspense boundary. It's too late to do anything about it1483 // so we can just render through it.1484 const prevKeyPath = someTask.keyPath;1485 const prevContext = someTask.formatContext;1486 const prevRow = someTask.row;1487 someTask.keyPath = keyPath;1488 someTask.formatContext = getSuspenseContentFormatContext(1489 request.resumableState,1490 prevContext,1491 );1492 someTask.row = null;1493 const content: ReactNodeList = props.children;1494 try {1495 renderNode(request, someTask, content, -1);1496 } finally {1497 someTask.keyPath = prevKeyPath;1498 someTask.formatContext = prevContext;1499 someTask.row = prevRow;1500 }1501 return;1502 }1503 // $FlowFixMe[incompatible-type]: Refined.1504 const task: RenderTask = someTask;15051506 const prevKeyPath = task.keyPath;1507 const prevContext = task.formatContext;1508 const prevRow = task.row;1509 const parentBoundary = task.blockedBoundary;1510 const parentPreamble = task.blockedPreamble;1511 const parentHoistableState = task.hoistableState;1512 const parentSegment = task.blockedSegment;15131514 // Each time we enter a suspense boundary, we split out into a new segment for1515 // the fallback so that we can later replace that segment with the content.1516 // This also lets us split out the main content even if it doesn't suspend,1517 // in case it ends up generating a large subtree of content.1518 const fallback: ReactNodeList = props.fallback;1519 const content: ReactNodeList = props.children;1520 const defer: boolean = enableCPUSuspense && props.defer === true;15211522 const fallbackAbortSet: Set<Task> = new Set();1523 const newBoundary = createSuspenseBoundary(1524 request,1525 task.row,1526 fallbackAbortSet,1527 canHavePreamble(task.formatContext) ? createPreamble() : null,1528 defer,1529 );15301531 const insertionIndex = parentSegment.chunks.length;1532 // The children of the boundary segment is actually the fallback.1533 const boundarySegment = createPendingSegment(1534 request,1535 insertionIndex,1536 newBoundary,1537 task.formatContext,1538 // boundaries never require text embedding at their edges because comment nodes bound them1539 false,1540 false,1541 );1542 parentSegment.children.push(boundarySegment);1543 // The parentSegment has a child Segment at this index so we reset the lastPushedText marker on the parent1544 parentSegment.lastPushedText = false;15451546 // This segment is the actual child content. We can start rendering that immediately.1547 const contentRootSegment = createPendingSegment(1548 request,1549 0,1550 null,1551 task.formatContext,1552 // boundaries never require text embedding at their edges because comment nodes bound them1553 false,1554 false,1555 );1556 // We mark the root segment as having its parent flushed. It's not really flushed but there is1557 // no parent segment so there's nothing to wait on.1558 contentRootSegment.parentFlushed = true;15591560 const trackedPostpones = request.trackedPostpones;1561 if (trackedPostpones !== null || defer) {1562 // This is a prerender or deferred boundary. In this mode we want to render the fallback synchronously1563 // and schedule the content to render later. This is the opposite of what we do during a normal render1564 // where we try to skip rendering the fallback if the content itself can render synchronously15651566 // Stash the original stack frame.1567 const suspenseComponentStack = task.componentStack;15681569 const fallbackKeyPath: KeyNode = [1570 keyPath[0],1571 'Suspense Fallback',1572 keyPath[2],1573 ];1574 if (trackedPostpones !== null) {1575 const fallbackReplayNode: ReplayNode = [1576 fallbackKeyPath[1],1577 fallbackKeyPath[2],1578 [] as Array<ReplayNode>,1579 null,1580 ];1581 trackedPostpones.workingMap.set(fallbackKeyPath, fallbackReplayNode);1582 newBoundary.tracked = {1583 contentKeyPath: keyPath,1584 // We are rendering the fallback before the boundary content so we keep track of1585 // the fallback replay node until we determine if the primary content suspends1586 fallbackNode: fallbackReplayNode,1587 };1588 }15891590 task.blockedSegment = boundarySegment;1591 task.blockedPreamble =1592 newBoundary.preamble === null ? null : newBoundary.preamble.fallback;1593 task.keyPath = fallbackKeyPath;1594 task.formatContext = getSuspenseFallbackFormatContext(1595 request.resumableState,1596 prevContext,1597 );1598 task.componentStack =1599 replaceSuspenseComponentStackWithSuspenseFallbackStack(1600 suspenseComponentStack,1601 );1602 try {1603 renderNode(request, task, fallback, -1);1604 pushSegmentFinale(1605 boundarySegment.chunks,1606 request.renderState,1607 boundarySegment.lastPushedText,1608 boundarySegment.textEmbedded,1609 );1610 boundarySegment.status = COMPLETED;1611 finishedSegment(request, parentBoundary, boundarySegment);1612 } catch (thrownValue: mixed) {1613 if (request.aborted) {1614 boundarySegment.status = ABORTED;1615 } else {1616 boundarySegment.status = ERRORED;1617 }1618 throw thrownValue;1619 } finally {1620 task.blockedSegment = parentSegment;1621 task.blockedPreamble = parentPreamble;1622 task.keyPath = prevKeyPath;1623 task.formatContext = prevContext;1624 }16251626 // We create a suspended task for the primary content because we want to allow1627 // sibling fallbacks to be rendered first.1628 const suspendedPrimaryTask = createRenderTask(1629 request,1630 null,1631 content,1632 -1,1633 newBoundary,1634 contentRootSegment,1635 newBoundary.preamble === null ? null : newBoundary.preamble.content,1636 newBoundary.contentState,1637 task.abortSet,1638 keyPath,1639 getSuspenseContentFormatContext(1640 request.resumableState,1641 task.formatContext,1642 ),1643 task.context,1644 task.treeContext,1645 null, // The row gets reset inside the Suspense boundary.1646 suspenseComponentStack,1647 !disableLegacyContext ? task.legacyContext : emptyContextObject,1648 __DEV__ ? task.debugTask : null,1649 );1650 pushComponentStack(suspendedPrimaryTask);1651 request.pingedTasks.push(suspendedPrimaryTask);1652 } else {1653 // This is a normal render. We will attempt to synchronously render the boundary content1654 // If it is successful we will elide the fallback task but if it suspends or errors we schedule1655 // the fallback to render. Unlike with prerenders we attempt to deprioritize the fallback render16561657 // Currently this is running synchronously. We could instead schedule this to pingedTasks.1658 // I suspect that there might be some efficiency benefits from not creating the suspended task1659 // and instead just using the stack if possible.1660 // TODO: Call this directly instead of messing with saving and restoring contexts.16611662 // We can reuse the current context and task to render the content immediately without1663 // context switching. We just need to temporarily switch which boundary and which segment1664 // we're writing to. If something suspends, it'll spawn new suspended task with that context.1665 task.blockedBoundary = newBoundary;1666 task.blockedPreamble =1667 newBoundary.preamble === null ? null : newBoundary.preamble.content;1668 task.hoistableState = newBoundary.contentState;1669 task.blockedSegment = contentRootSegment;1670 task.keyPath = keyPath;1671 task.formatContext = getSuspenseContentFormatContext(1672 request.resumableState,1673 prevContext,1674 );1675 task.row = null;1676 try {1677 // We use the safe form because we don't handle suspending here. Only error handling.1678 renderNode(request, task, content, -1);1679 pushSegmentFinale(1680 contentRootSegment.chunks,1681 request.renderState,1682 contentRootSegment.lastPushedText,1683 contentRootSegment.textEmbedded,1684 );1685 contentRootSegment.status = COMPLETED;1686 finishedSegment(request, newBoundary, contentRootSegment);1687 queueCompletedSegment(newBoundary, contentRootSegment);1688 if (newBoundary.pendingTasks === 0 && newBoundary.status === PENDING) {1689 // This must have been the last segment we were waiting on. This boundary is now complete.1690 newBoundary.status = COMPLETED;1691 // Therefore we won't need the fallback. We early return so that we don't have to create1692 // the fallback. However, if this boundary ended up big enough to be eligible for outlining1693 // we can't do that because we might still need the fallback if we outline it.1694 if (!isEligibleForOutlining(request, newBoundary)) {1695 if (prevRow !== null) {1696 // If we have synchronously completed the boundary and it's not eligible for outlining1697 // then we don't have to wait for it to be flushed before we unblock future rows.1698 // This lets us inline small rows in order.1699 if (--prevRow.pendingTasks === 0) {1700 finishSuspenseListRow(request, prevRow);1701 }1702 }1703 if (request.pendingRootTasks === 0 && task.blockedPreamble) {1704 // The root is complete and this boundary may contribute part of the preamble.1705 // We eagerly attempt to prepare the preamble here because we expect most requests1706 // to have few boundaries which contribute preambles and it allow us to do this1707 // preparation work during the work phase rather than the when flushing.1708 preparePreamble(request);1709 }1710 return;1711 }1712 } else {1713 const boundaryRow = prevRow;1714 if (boundaryRow !== null && boundaryRow.together) {1715 tryToResolveTogetherRow(request, boundaryRow);1716 }1717 }1718 } catch (thrownValue: mixed) {1719 newBoundary.status = CLIENT_RENDERED;1720 let error: mixed;1721 if (request.aborted) {1722 contentRootSegment.status = ABORTED;1723 error = request.fatalError;1724 } else {1725 contentRootSegment.status = ERRORED;1726 error = thrownValue;1727 }17281729 const thrownInfo = getThrownInfo(task.componentStack);1730 const errorDigest = logRecoverableError(1731 request,1732 error,1733 thrownInfo,1734 __DEV__ ? task.debugTask : null,1735 );1736 encodeErrorForBoundary(1737 newBoundary,1738 errorDigest,1739 error,1740 thrownInfo,1741 false,1742 );17431744 untrackBoundary(request, newBoundary);17451746 // We don't need to decrement any task numbers because we didn't spawn any new task.1747 // We don't need to schedule any task because we know the parent has written yet.1748 // We do need to fallthrough to create the fallback though.1749 } finally {1750 task.blockedBoundary = parentBoundary;1751 task.blockedPreamble = parentPreamble;1752 task.hoistableState = parentHoistableState;1753 task.blockedSegment = parentSegment;1754 task.keyPath = prevKeyPath;1755 task.formatContext = prevContext;1756 task.row = prevRow;1757 }17581759 const fallbackKeyPath: KeyNode = [1760 keyPath[0],1761 'Suspense Fallback',1762 keyPath[2],1763 ];1764 // We create suspended task for the fallback because we don't want to actually work1765 // on it yet in case we finish the main content, so we queue for later.1766 const suspendedFallbackTask = createRenderTask(1767 request,1768 null,1769 fallback,1770 -1,1771 parentBoundary,1772 boundarySegment,1773 newBoundary.preamble === null ? null : newBoundary.preamble.fallback,1774 newBoundary.fallbackState,1775 fallbackAbortSet,1776 fallbackKeyPath,1777 getSuspenseFallbackFormatContext(1778 request.resumableState,1779 task.formatContext,1780 ),1781 task.context,1782 task.treeContext,1783 task.row,1784 replaceSuspenseComponentStackWithSuspenseFallbackStack(1785 task.componentStack,1786 ),1787 !disableLegacyContext ? task.legacyContext : emptyContextObject,1788 __DEV__ ? task.debugTask : null,1789 );1790 pushComponentStack(suspendedFallbackTask);1791 // TODO: This should be queued at a separate lower priority queue so that we only work1792 // on preparing fallbacks if we don't have any more main content to task on.1793 request.pingedTasks.push(suspendedFallbackTask);1794 }1795}17961797function replaySuspenseBoundary(1798 request: Request,1799 task: ReplayTask,1800 keyPath: KeyNode,1801 props: Object,1802 id: number,1803 childNodes: Array<ReplayNode>,1804 childSlots: ResumeSlots,1805 fallbackNodes: Array<ReplayNode>,1806 fallbackSlots: ResumeSlots,1807): void {1808 const prevKeyPath = task.keyPath;1809 const prevContext = task.formatContext;1810 const prevRow = task.row;1811 const previousReplaySet: ReplaySet = task.replay;18121813 const parentBoundary = task.blockedBoundary;1814 const parentHoistableState = task.hoistableState;18151816 const content: ReactNodeList = props.children;1817 const fallback: ReactNodeList = props.fallback;1818 const defer: boolean = enableCPUSuspense && props.defer === true;18191820 const fallbackAbortSet: Set<Task> = new Set();1821 const resumedBoundary = createSuspenseBoundary(1822 request,1823 task.row,1824 fallbackAbortSet,1825 canHavePreamble(task.formatContext) ? createPreamble() : null,1826 defer,1827 );1828 resumedBoundary.parentFlushed = true;1829 // We restore the same id of this boundary as was used during prerender.1830 resumedBoundary.rootSegmentID = id;18311832 // We can reuse the current context and task to render the content immediately without1833 // context switching. We just need to temporarily switch which boundary and replay node1834 // we're writing to. If something suspends, it'll spawn new suspended task with that context.1835 task.blockedBoundary = resumedBoundary;1836 task.hoistableState = resumedBoundary.contentState;1837 task.keyPath = keyPath;1838 task.formatContext = getSuspenseContentFormatContext(1839 request.resumableState,1840 prevContext,1841 );1842 task.row = null;1843 task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};18441845 try {1846 // We use the safe form because we don't handle suspending here. Only error handling.1847 renderNode(request, task, content, -1);18481849 if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) {1850 throw new Error(1851 "Couldn't find all resumable slots by key/index during replaying. " +1852 "The tree doesn't match so React will fallback to client rendering.",1853 );1854 }1855 task.replay.pendingTasks--;1856 if (1857 resumedBoundary.pendingTasks === 0 &&1858 resumedBoundary.status === PENDING1859 ) {1860 // This must have been the last segment we were waiting on. This boundary is now complete.1861 // Therefore we won't need the fallback. We early return so that we don't have to create1862 // the fallback.1863 resumedBoundary.status = COMPLETED;1864 request.completedBoundaries.push(resumedBoundary);1865 // We restore the parent componentStack. Semantically this is the same as1866 // popComponentStack(task) but we do this instead because it should be slightly1867 // faster1868 return;1869 }1870 } catch (thrownValue: mixed) {1871 resumedBoundary.status = CLIENT_RENDERED;1872 const error = request.aborted ? request.fatalError : thrownValue;1873 const thrownInfo = getThrownInfo(task.componentStack);1874 const errorDigest = logRecoverableError(1875 request,1876 error,1877 thrownInfo,1878 __DEV__ ? task.debugTask : null,1879 );1880 encodeErrorForBoundary(1881 resumedBoundary,1882 errorDigest,1883 error,1884 thrownInfo,1885 false,1886 );18871888 task.replay.pendingTasks--;18891890 // The parent already flushed in the prerender so we need to schedule this to be emitted.1891 request.clientRenderedBoundaries.push(resumedBoundary);18921893 // We don't need to decrement any task numbers because we didn't spawn any new task.1894 // We don't need to schedule any task because we know the parent has written yet.1895 // We do need to fallthrough to create the fallback though.1896 } finally {1897 task.blockedBoundary = parentBoundary;1898 task.hoistableState = parentHoistableState;1899 task.replay = previousReplaySet;1900 task.keyPath = prevKeyPath;1901 task.formatContext = prevContext;1902 task.row = prevRow;1903 }19041905 const fallbackKeyPath: KeyNode = [1906 keyPath[0],1907 'Suspense Fallback',1908 keyPath[2],1909 ];19101911 // We create suspended task for the fallback because we don't want to actually work1912 // on it yet in case we finish the main content, so we queue for later.1913 const fallbackReplay = {1914 nodes: fallbackNodes,1915 slots: fallbackSlots,1916 pendingTasks: 0,1917 };1918 const suspendedFallbackTask = createReplayTask(1919 request,1920 null,1921 fallbackReplay,1922 fallback,1923 -1,1924 parentBoundary,1925 resumedBoundary.fallbackState,1926 fallbackAbortSet,1927 fallbackKeyPath,1928 getSuspenseFallbackFormatContext(1929 request.resumableState,1930 task.formatContext,1931 ),1932 task.context,1933 task.treeContext,1934 task.row,1935 replaceSuspenseComponentStackWithSuspenseFallbackStack(task.componentStack),1936 !disableLegacyContext ? task.legacyContext : emptyContextObject,1937 __DEV__ ? task.debugTask : null,1938 );19391940 pushComponentStack(suspendedFallbackTask);1941 // TODO: This should be queued at a separate lower priority queue so that we only work1942 // on preparing fallbacks if we don't have any more main content to task on.1943 request.pingedTasks.push(suspendedFallbackTask);1944}19451946function finishSuspenseListRow(request: Request, row: SuspenseListRow): void {1947 // This row finished. Now we have to unblock all the next rows that were blocked on this.1948 unblockSuspenseListRow(request, row.next, row.hoistables);1949}19501951function unblockSuspenseListRow(1952 request: Request,1953 unblockedRow: null | SuspenseListRow,1954 inheritedHoistables: null | HoistableState,1955): void {1956 // We do this in a loop to avoid stack overflow for very long lists that get unblocked.1957 while (unblockedRow !== null) {1958 if (inheritedHoistables !== null) {1959 // Hoist any hoistables from the previous row into the next row so that it can be1960 // later transferred to all the rows.1961 hoistHoistables(unblockedRow.hoistables, inheritedHoistables);1962 // Mark the row itself for any newly discovered Suspense boundaries to inherit.1963 // This is different from hoistables because that also includes hoistables from1964 // all the boundaries below this row and not just previous rows.1965 unblockedRow.inheritedHoistables = inheritedHoistables;1966 }1967 // Unblocking the boundaries will decrement the count of this row but we keep it above1968 // zero so they never finish this row recursively.1969 const unblockedBoundaries = unblockedRow.boundaries;1970 if (unblockedBoundaries !== null) {1971 unblockedRow.boundaries = null;1972 for (let i = 0; i < unblockedBoundaries.length; i++) {1973 const unblockedBoundary = unblockedBoundaries[i];1974 if (inheritedHoistables !== null) {1975 hoistHoistables(unblockedBoundary.contentState, inheritedHoistables);1976 }1977 finishedTask(request, unblockedBoundary, null, null);1978 }1979 }1980 // Instead we decrement at the end to keep it all in this loop.1981 unblockedRow.pendingTasks--;1982 if (unblockedRow.pendingTasks > 0) {1983 // Still blocked.1984 break;1985 }1986 inheritedHoistables = unblockedRow.hoistables;1987 unblockedRow = unblockedRow.next;1988 }1989}19901991function trackPostponedSuspenseListRow(1992 request: Request,1993 trackedPostpones: PostponedHoles,1994 postponedRow: null | SuspenseListRow,1995): void {1996 // TODO: Because we unconditionally call this, it will be called by finishedTask1997 // and so ends up recursive which can lead to stack overflow for very long lists.1998 if (postponedRow !== null) {1999 const postponedBoundaries = postponedRow.boundaries;2000 if (postponedBoundaries !== null) {
Findings
✓ No findings reported for this file.