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 enableCPUSuspense,191} from 'shared/ReactFeatureFlags';192193import assign from 'shared/assign';194import noop from 'shared/noop';195import getComponentNameFromType from 'shared/getComponentNameFromType';196import isArray from 'shared/isArray';197import {REACT_RECOVERABLE_DIGEST} from 'shared/ReactRecoverable';198import {199 SuspenseException,200 getSuspendedThenable,201 ensureSuspendableThenableStateDEV,202 getSuspendedCallSiteStackDEV,203 getSuspendedCallSiteDebugTaskDEV,204 setCaptureSuspendedCallSiteDEV,205} from './ReactFizzThenable';206207// Linked list representing the identity of a component given the component/tag name and key.208// The name might be minified but we assume that it's going to be the same generated name. Typically209// because it's just the same compiled output in practice.210export type KeyNode = [211 Root | KeyNode /* parent */,212 string | null /* name */,213 string | number /* key */,214];215216type ResumeSlots =217 | null // nothing to resume218 | number // resume with segment ID at the root position219 | {[index: number]: number}; // resume with segmentID at the index220221type ReplaySuspenseBoundary = [222 string | null /* name */,223 string | number /* key */,224 Array<ReplayNode> /* content keyed children */,225 ResumeSlots /* content resumable slots */,226 null | ReplayNode /* fallback content */,227 number /* rootSegmentID */,228];229230type ReplayNode =231 | [232 string | null /* name */,233 string | number /* key */,234 Array<ReplayNode> /* keyed children */,235 ResumeSlots /* resumable slots */,236 ]237 | ReplaySuspenseBoundary;238239type PostponedHoles = {240 workingMap: Map<KeyNode, ReplayNode>,241 rootNodes: Array<ReplayNode>,242 rootSlots: ResumeSlots,243};244245type LegacyContext = {246 [key: string]: any,247};248249type SuspenseListRow = {250 pendingTasks: number, // The number of tasks, previous rows and inner suspense boundaries blocking this row.251 boundaries: null | Array<SuspenseBoundary>, // The boundaries in this row waiting to be unblocked by the previous row. (null means this row is not blocked)252 hoistables: HoistableState, // Any dependencies that this row depends on. Future rows need to also depend on it.253 inheritedHoistables: null | HoistableState, // Any dependencies that previous row depend on, that new boundaries of this row needs.254 together: boolean, // All the boundaries within this row must be revealed together.255 next: null | SuspenseListRow, // The next row blocked by this one.256};257258const CLIENT_RENDERED = 4; // if it errors or infinitely suspends259260type SuspenseBoundary = {261 status: 0 | 1 | 4 | 5,262 rootSegmentID: number,263 parentFlushed: boolean,264 pendingTasks: number, // when it reaches zero we can show this boundary's content265 row: null | SuspenseListRow, // the row that this boundary blocks from completing.266 completedSegments: Array<Segment>, // completed but not yet flushed segments.267 byteSize: number, // used to determine whether to inline children boundaries.268 defer: boolean, // never inline deferred boundaries269 fallbackAbortableTasks: Set<Task>, // used to cancel task on the fallback if the boundary completes or gets canceled.270 contentState: HoistableState,271 fallbackState: HoistableState,272 preamble: null | Preamble,273 tracked: null | {274 contentKeyPath: null | KeyNode, // used to track the path for replay nodes275 fallbackNode: null | ReplayNode, // used to track the fallback for replay nodes276 },277 errorDigest: ?string, // the error hash if it errors278 // DEV-only fields279 errorMessage?: null | string, // the error string if it errors280 errorStack?: null | string, // the error stack if it errors281 errorComponentStack?: null | string, // the error component stack if it errors282};283284type Ping = {285 resolve: () => void,286 reject: (error: mixed) => void,287};288289type RenderTask = {290 replay: null,291 node: ReactNodeList,292 childIndex: number,293 ping: Ping,294 blockedBoundary: Root | SuspenseBoundary,295 blockedSegment: Segment, // the segment we'll write to296 blockedPreamble: null | PreambleState,297 hoistableState: null | HoistableState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary298 abortSet: Set<Task>, // the abortable set that this task belongs to299 keyPath: Root | KeyNode, // the path of all parent keys currently rendering300 formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML)301 context: ContextSnapshot, // the current new context that this task is executing in302 treeContext: TreeContext, // the current tree context that this task is executing in303 row: null | SuspenseListRow, // the current SuspenseList row that this is rendering inside304 componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component305 thenableState: null | ThenableState,306 legacyContext: LegacyContext, // the current legacy context that this task is executing in307 debugTask: null | ConsoleTask, // DEV only308 // DON'T ANY MORE FIELDS. We at 16 in prod already which otherwise requires converting to a constructor.309 // Consider splitting into multiple objects or consolidating some fields.310};311312type ReplaySet = {313 nodes: Array<ReplayNode>, // the possible paths to follow down the replaying314 slots: ResumeSlots, // slots to resume315 pendingTasks: number, // tracks the number of tasks currently tracking this set of nodes316 // if pending tasks reach zero but there are still nodes left, it means we couldn't find317 // them all in the tree, so we need to abort and client render the boundary.318};319320type ReplayTask = {321 replay: ReplaySet,322 node: ReactNodeList,323 childIndex: number,324 ping: Ping,325 blockedBoundary: Root | SuspenseBoundary,326 blockedSegment: null, // we don't write to anything when we replay327 blockedPreamble: null,328 hoistableState: null | HoistableState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary329 abortSet: Set<Task>, // the abortable set that this task belongs to330 keyPath: Root | KeyNode, // the path of all parent keys currently rendering331 formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML)332 context: ContextSnapshot, // the current new context that this task is executing in333 treeContext: TreeContext, // the current tree context that this task is executing in334 row: null | SuspenseListRow, // the current SuspenseList row that this is rendering inside335 componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component336 thenableState: null | ThenableState,337 legacyContext: LegacyContext, // the current legacy context that this task is executing in338 debugTask: null | ConsoleTask, // DEV only339};340341export type Task = RenderTask | ReplayTask;342343const PENDING = 0;344const COMPLETED = 1;345const FLUSHED = 2;346const ABORTED = 3;347const ERRORED = 4;348const POSTPONED = 5;349350type Root = null;351352type Segment = {353 status: 0 | 1 | 2 | 3 | 4 | 5,354 parentFlushed: boolean, // typically a segment will be flushed by its parent, except if its parent was already flushed355 id: number, // starts as 0 and is lazily assigned if the parent flushes early356 +index: number, // the index within the parent's chunks or 0 at the root357 +chunks: Array<Chunk | PrecomputedChunk>,358 +children: Array<Segment>,359 +preambleChildren: Array<Segment>,360 // The context that this segment was created in.361 parentFormatContext: FormatContext,362 // If this segment represents a fallback, this is the content that will replace that fallback.363 boundary: null | SuspenseBoundary,364 // used to discern when text separator boundaries are needed365 lastPushedText: boolean,366 textEmbedded: boolean,367};368369// The ordering of these statuses matters. OPENING and OPEN are the only370// statuses in which newly scheduled work may be performed. Any status greater371// than OPEN represents a request that no longer admits work.372const OPENING = 10;373const OPEN = 11;374const CLOSING = 12;375const CLOSED = 13;376const STALLED_DEV = 14;377378// Passed to renderLifetimeController.abort(). Nothing reads the reason, but a379// call to abort() without one constructs an AbortError DOMException. Capturing380// the stack trace dominates that cost, and the cost grows with the depth of the381// stack.382const RENDER_ENDED = 'The render ended.';383384export opaque type Request = {385 destination: null | Destination,386 flushScheduled: boolean,387 +resumableState: ResumableState,388 +renderState: RenderState,389 +rootFormatContext: FormatContext,390 +progressiveChunkSize: number,391 status: 10 | 11 | 12 | 13 | 14,392 fatalError: mixed,393 aborted: boolean,394 nextSegmentId: number,395 allPendingTasks: number, // when it reaches zero, we can close the connection.396 pendingRootTasks: number, // when this reaches zero, we've finished at least the root boundary.397 completedRootSegment: null | Segment, // Completed but not yet flushed root segments.398 completedPreambleSegments: null | Array<Array<Segment>>, // contains the ready-to-flush segments that make up the preamble399 byteSize: number, // counts the number of bytes accumulated in the shell400 abortableTasks: Set<Task>,401 pingedTasks: Array<Task>, // High priority tasks that should be worked on first.402 currentTask: null | Task, // The task currently executing in this request.403 // Queues to flush in order of priority404 clientRenderedBoundaries: Array<SuspenseBoundary>, // Errored or client rendered but not yet flushed.405 completedBoundaries: Array<SuspenseBoundary>, // Completed but not yet fully flushed boundaries to show.406 partialBoundaries: Array<SuspenseBoundary>, // Partially completed boundaries that can flush its segments early.407 trackedPostpones: null | PostponedHoles, // Gets set to non-null while we want to track postponed holes. I.e. during a prerender.408 // While prerendering a postponed request that produced a real shell, this409 // holds the PostponedState returned by getPostponedState. getPostponedState410 // snapshots nextSegmentId before the (pull-driven) prelude flush runs, but the411 // flush outlines completed boundaries and advances nextSegmentId past that412 // snapshot. We finalize the snapshot from flushCompletedQueues so the resumed413 // render allocates segment ids strictly above the shell's; otherwise the shell414 // and resume emit duplicate B:/S: ids once concatenated. Stays null for live415 // renders and resumes.416 postponedState: null | PostponedState,417 // onError is called when an error happens anywhere in the tree. It might recover.418 // The return string is used in production primarily to avoid leaking internals, secondarily to save bytes.419 // Returning null/undefined will cause a default error message in production420 onError: (error: mixed, errorInfo: ThrownInfo) => ?string,421 // onBrowserBailout is called when Fizz recovers by intentionally deferring422 // rendering to the browser.423 onBrowserBailout: (error: mixed, errorInfo: ThrownInfo) => void,424 // onAllReady is called when all pending task is done but it may not have flushed yet.425 // This is a good time to start writing if you want only HTML and no intermediate steps.426 onAllReady: () => void,427 // onShellReady is called when there is at least a root fallback ready to show.428 // Typically you don't need this callback because it's best practice to always have a429 // root fallback ready so there's no need to wait.430 onShellReady: () => void,431 // onShellError is called when the shell didn't complete. That means you probably want to432 // emit a different response to the stream instead.433 onShellError: (error: mixed) => void,434 onFatalError: (error: mixed) => void,435 // Aborted once the render ends, whether it completed, failed fatally or was436 // aborted. Bounds the lifetime of anything that must not outlive the render.437 // Null until attachAbortSignal creates it, so a render that is given no438 // signal constructs no controller.439 renderLifetimeController: null | AbortController,440 // Form state that was the result of an MPA submission, if it was provided.441 formState: null | ReactFormState<any, any>,442 // DEV-only, warning dedupe443 didWarnForKey?: null | WeakSet<ComponentStackNode>,444};445446type Preamble = {447 content: PreambleState,448 fallback: PreambleState,449};450451function createPreamble(): Preamble {452 return {453 content: createPreambleState(),454 fallback: createPreambleState(),455 };456}457458// This is a default heuristic for how to split up the HTML content into progressive459// loading. Our goal is to be able to display additional new content about every 500ms.460// Faster than that is unnecessary and should be throttled on the client. It also461// adds unnecessary overhead to do more splits. We don't know if it's a higher or lower462// end device but higher end suffer less from the overhead than lower end does from463// not getting small enough pieces. We error on the side of low end.464// We base this on low end 3G speeds which is about 500kbits per second. We assume465// that there can be a reasonable drop off from max bandwidth which leaves you with466// as little as 80%. We can receive half of that each 500ms - at best. In practice,467// a little bandwidth is lost to processing and contention - e.g. CSS and images that468// are downloaded along with the main content. So we estimate about half of that to be469// the lower end throughput. In other words, we expect that you can at least show470// about 12.5kb of content per 500ms. Not counting starting latency for the first471// paint.472// 500 * 1024 / 8 * .8 * 0.5 / 2473const DEFAULT_PROGRESSIVE_CHUNK_SIZE = 12800;474475function getBlockingRenderMaxSize(request: Request): number {476 // We want to make sure that we can block the reveal of a well designed complete477 // shell but if you have constructed a too large shell (e.g. by not adding any478 // Suspense boundaries) then that might take too long to render. We shouldn't479 // punish users (or overzealous metrics tracking) in that scenario.480 // There's a trade off here. If this limit is too low then you can't fit a481 // reasonably well built UI within it without getting errors. If it's too high482 // then things that accidentally fall below it might take too long to load.483 // Web Vitals target 1.8 seconds for first paint and our goal to have the limit484 // be fast enough to hit that. For this argument we assume that most external485 // resources are already cached because it's a return visit, or inline styles.486 // If it's not, then it's highly unlikely that any render blocking instructions487 // we add has any impact what so ever on the paint.488 // Assuming a first byte of about 600ms which is kind of bad but common with a489 // decent static host. If it's longer e.g. due to dynamic rendering, then you490 // are going to bound by dynamic production of the content and you're better off491 // with Suspense boundaries anyway. This number doesn't matter much. Then you492 // have about 1.2 seconds left for bandwidth. On 3G that gives you about 112.5kb493 // worth of data. That's worth about 10x in terms of uncompressed bytes. Then we494 // half that just to account for longer latency, slower bandwidth and CPU processing.495 // Now we're down to about 500kb. In fact, looking at metrics we've collected with496 // rel="expect" examples and other documents, the impact on documents smaller than497 // that is within the noise. That's because there's enough happening within that498 // start up to not make HTML streaming not significantly better.499 // Content above the fold tends to be about 100-200kb tops. Therefore 500kb should500 // be enough head room for a good loading state. After that you should use501 // Suspense or SuspenseList to improve it.502 // Since this is highly related to the reason you would adjust the503 // progressiveChunkSize option, and always has to be higher, we define this limit504 // in terms of it. So if you want to increase the limit because you have high505 // bandwidth users, then you can adjust it up. If you are concerned about even506 // slower bandwidth then you can adjust it down.507 return request.progressiveChunkSize * 40; // 512kb by default.508}509510function isEligibleForOutlining(511 request: Request,512 boundary: SuspenseBoundary,513): boolean {514 // For very small boundaries, don't bother producing a fallback for outlining.515 // The larger this limit is, the more we can save on preparing fallbacks in case we end up516 // outlining.517 return (518 (boundary.byteSize > 500 ||519 hasSuspenseyContent(boundary.contentState, /* flushingInShell */ false) ||520 boundary.defer) &&521 // For boundaries that can possibly contribute to the preamble we don't want to outline522 // them regardless of their size since the fallbacks should only be emitted if we've523 // errored the boundary.524 boundary.preamble === null525 );526}527528function defaultErrorHandler(error: mixed) {529 if (530 typeof error === 'object' &&531 error !== null &&532 typeof error.environmentName === 'string'533 ) {534 // This was a Server error. We print the environment name in a badge just like we do with535 // replays of console logs to indicate that the source of this throw as actually the Server.536 bindToConsole('error', [error], error.environmentName)();537 } else {538 console['error'](error); // Don't transform to our wrapper539 }540 return null;541}542543function RequestInstance(544 this: $FlowFixMe,545 resumableState: ResumableState,546 renderState: RenderState,547 rootFormatContext: FormatContext,548 progressiveChunkSize: void | number,549 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),550 onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void),551 onAllReady: void | (() => void),552 onShellReady: void | (() => void),553 onShellError: void | ((error: mixed) => void),554 onFatalError: void | ((error: mixed) => void),555 formState: void | null | ReactFormState<any, any>,556) {557 const pingedTasks: Array<Task> = [];558 const abortSet: Set<Task> = new Set();559 this.destination = null;560 this.flushScheduled = false;561 this.resumableState = resumableState;562 this.renderState = renderState;563 this.rootFormatContext = rootFormatContext;564 this.progressiveChunkSize =565 progressiveChunkSize === undefined566 ? DEFAULT_PROGRESSIVE_CHUNK_SIZE567 : progressiveChunkSize;568 // $FlowFixMe[constant-condition]569 this.status = isWorkLoopExternallyDriven ? OPEN : OPENING;570 this.fatalError = null;571 this.aborted = false;572 this.nextSegmentId = 0;573 this.allPendingTasks = 0;574 this.pendingRootTasks = 0;575 this.completedRootSegment = null;576 this.completedPreambleSegments = null;577 this.byteSize = 0;578 this.abortableTasks = abortSet;579 this.pingedTasks = pingedTasks;580 this.currentTask = null;581 this.clientRenderedBoundaries = [] as Array<SuspenseBoundary>;582 this.completedBoundaries = [] as Array<SuspenseBoundary>;583 this.partialBoundaries = [] as Array<SuspenseBoundary>;584 this.trackedPostpones = null;585 this.postponedState = null;586 this.onError = onError === undefined ? defaultErrorHandler : onError;587 this.onBrowserBailout =588 onBrowserBailout === undefined ? noop : onBrowserBailout;589 this.onAllReady = onAllReady === undefined ? noop : onAllReady;590 this.onShellReady = onShellReady === undefined ? noop : onShellReady;591 this.onShellError = onShellError === undefined ? noop : onShellError;592 this.onFatalError = onFatalError === undefined ? noop : onFatalError;593 this.renderLifetimeController = null;594 this.formState = formState === undefined ? null : formState;595 if (__DEV__) {596 this.didWarnForKey = null;597 }598}599600export function createRequest(601 children: ReactNodeList,602 resumableState: ResumableState,603 renderState: RenderState,604 rootFormatContext: FormatContext,605 progressiveChunkSize: void | number,606 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),607 onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void),608 onAllReady: void | (() => void),609 onShellReady: void | (() => void),610 onShellError: void | ((error: mixed) => void),611 onFatalError: void | ((error: mixed) => void),612 formState: void | null | ReactFormState<any, any>,613): Request {614 if (__DEV__) {615 resetOwnerStackLimit();616 }617618 // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors619 const request: Request = new RequestInstance(620 resumableState,621 renderState,622 rootFormatContext,623 progressiveChunkSize,624 onError,625 onBrowserBailout,626 onAllReady,627 onShellReady,628 onShellError,629 onFatalError,630 formState,631 );632633 // This segment represents the root fallback.634 const rootSegment = createPendingSegment(635 request,636 0,637 null,638 rootFormatContext,639 // Root segments are never embedded in Text on either edge640 false,641 false,642 );643 // There is no parent so conceptually, we're unblocked to flush this segment.644 rootSegment.parentFlushed = true;645 const rootTask = createRenderTask(646 request,647 null,648 children,649 -1,650 null,651 rootSegment,652 null,653 null,654 request.abortableTasks,655 null,656 rootFormatContext,657 rootContextSnapshot,658 emptyTreeContext,659 null,660 null,661 emptyContextObject,662 null,663 );664 pushComponentStack(rootTask);665 request.pingedTasks.push(rootTask);666 return request;667}668669export function createPrerenderRequest(670 children: ReactNodeList,671 resumableState: ResumableState,672 renderState: RenderState,673 rootFormatContext: FormatContext,674 progressiveChunkSize: void | number,675 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),676 onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void),677 onAllReady: void | (() => void),678 onShellReady: void | (() => void),679 onShellError: void | ((error: mixed) => void),680 onFatalError: void | ((error: mixed) => void),681): Request {682 const request = createRequest(683 children,684 resumableState,685 renderState,686 rootFormatContext,687 progressiveChunkSize,688 onError,689 onBrowserBailout,690 onAllReady,691 onShellReady,692 onShellError,693 onFatalError,694 undefined,695 );696 // Start tracking postponed holes during this render.697 request.trackedPostpones = {698 workingMap: new Map(),699 rootNodes: [],700 rootSlots: null,701 };702 return request;703}704705export function resumeRequest(706 children: ReactNodeList,707 postponedState: PostponedState,708 renderState: RenderState,709 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),710 onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void),711 onAllReady: void | (() => void),712 onShellReady: void | (() => void),713 onShellError: void | ((error: mixed) => void),714 onFatalError: void | ((error: mixed) => void),715): Request {716 if (__DEV__) {717 resetOwnerStackLimit();718 }719720 // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors721 const request: Request = new RequestInstance(722 postponedState.resumableState,723 renderState,724 postponedState.rootFormatContext,725 postponedState.progressiveChunkSize,726 onError,727 onBrowserBailout,728 onAllReady,729 onShellReady,730 onShellError,731 onFatalError,732 null,733 );734 request.nextSegmentId = postponedState.nextSegmentId;735736 if (typeof postponedState.replaySlots === 'number') {737 // We have a resume slot at the very root. This is effectively just a full rerender.738 const rootSegment = createPendingSegment(739 request,740 0,741 null,742 postponedState.rootFormatContext,743 // Root segments are never embedded in Text on either edge744 false,745 false,746 );747 // There is no parent so conceptually, we're unblocked to flush this segment.748 rootSegment.parentFlushed = true;749 const rootTask = createRenderTask(750 request,751 null,752 children,753 -1,754 null,755 rootSegment,756 null,757 null,758 request.abortableTasks,759 null,760 postponedState.rootFormatContext,761 rootContextSnapshot,762 emptyTreeContext,763 null,764 null,765 emptyContextObject,766 null,767 );768 pushComponentStack(rootTask);769 request.pingedTasks.push(rootTask);770 return request;771 }772773 const replay: ReplaySet = {774 nodes: postponedState.replayNodes,775 slots: postponedState.replaySlots,776 pendingTasks: 0,777 };778 const rootTask = createReplayTask(779 request,780 null,781 replay,782 children,783 -1,784 null,785 null,786 request.abortableTasks,787 null,788 postponedState.rootFormatContext,789 rootContextSnapshot,790 emptyTreeContext,791 null,792 null,793 emptyContextObject,794 null,795 );796 pushComponentStack(rootTask);797 request.pingedTasks.push(rootTask);798 return request;799}800801export function resumeAndPrerenderRequest(802 children: ReactNodeList,803 postponedState: PostponedState,804 renderState: RenderState,805 onError: void | ((error: mixed, errorInfo: ErrorInfo) => ?string),806 onBrowserBailout: void | ((error: mixed, errorInfo: ErrorInfo) => void),807 onAllReady: void | (() => void),808 onShellReady: void | (() => void),809 onShellError: void | ((error: mixed) => void),810 onFatalError: void | ((error: mixed) => void),811): Request {812 const request = resumeRequest(813 children,814 postponedState,815 renderState,816 onError,817 onBrowserBailout,818 onAllReady,819 onShellReady,820 onShellError,821 onFatalError,822 );823 // Start tracking postponed holes during this render.824 request.trackedPostpones = {825 workingMap: new Map(),826 rootNodes: [],827 rootSlots: null,828 };829 return request;830}831832let currentRequest: null | Request = null;833834export function resolveRequest(): null | Request {835 if (currentRequest) return currentRequest;836 // $FlowFixMe[constant-condition]837 if (supportsRequestStorage) {838 const store = requestStorage.getStore();839 if (store) return store;840 }841 return null;842}843844function pingTask(request: Request, task: Task): void {845 const pingedTasks = request.pingedTasks;846 pingedTasks.push(task);847 // $FlowFixMe[constant-condition]848 if (isWorkLoopExternallyDriven) {849 return;850 } else {851 if (request.pingedTasks.length === 1) {852 request.flushScheduled = request.destination !== null;853 if (request.trackedPostpones !== null || request.status === OPENING) {854 scheduleMicrotask(() => performWork(request));855 } else {856 scheduleWork(() => performWork(request));857 }858 }859 }860}861862function pingRejectedTask(request: Request, task: Task, error: mixed): void {863 if (!request.aborted) {864 // Replaying the task is what gives ordinary render errors their complete865 // component stack.866 pingTask(request, task);867 return;868 }869 if (!task.abortSet.delete(task)) {870 // finishAbort already completed this task with the request's abort reason.871 return;872 }873 // abortTask synchronously claimed this task before abort listeners could874 // reject its wakeable. Finish it with the more specific reason before the875 // scheduled final abort uses the reason for the whole request.876 if (__DEV__) {877 finishAbortedTaskDEV(task, request, error);878 } else {879 finishAbortedTask(task, request, error);880 }881}882883function createSuspenseBoundary(884 request: Request,885 row: null | SuspenseListRow,886 fallbackAbortableTasks: Set<Task>,887 preamble: null | Preamble,888 defer: boolean,889): SuspenseBoundary {890 const boundary: SuspenseBoundary = {891 status: PENDING,892 rootSegmentID: -1,893 parentFlushed: false,894 pendingTasks: 0,895 row: row,896 completedSegments: [],897 byteSize: 0,898 defer: defer,899 fallbackAbortableTasks,900 errorDigest: null,901 contentState: createHoistableState(),902 fallbackState: createHoistableState(),903 preamble,904 tracked: null,905 };906 if (__DEV__) {907 // DEV-only fields for hidden class908 boundary.errorMessage = null;909 boundary.errorStack = null;910 boundary.errorComponentStack = null;911 }912 if (row !== null) {913 // This boundary will block this row from completing.914 row.pendingTasks++;915 const blockedBoundaries = row.boundaries;916 if (blockedBoundaries !== null) {917 // Previous rows will block this boundary itself from completing.918 request.allPendingTasks++;919 boundary.pendingTasks++;920 blockedBoundaries.push(boundary);921 }922 const inheritedHoistables = row.inheritedHoistables;923 if (inheritedHoistables !== null) {924 hoistHoistables(boundary.contentState, inheritedHoistables);925 }926 }927 return boundary;928}929930function createRenderTask(931 request: Request,932 thenableState: ThenableState | null,933 node: ReactNodeList,934 childIndex: number,935 blockedBoundary: Root | SuspenseBoundary,936 blockedSegment: Segment,937 blockedPreamble: null | PreambleState,938 hoistableState: null | HoistableState,939 abortSet: Set<Task>,940 keyPath: Root | KeyNode,941 formatContext: FormatContext,942 context: ContextSnapshot,943 treeContext: TreeContext,944 row: null | SuspenseListRow,945 componentStack: null | ComponentStackNode,946 legacyContext: LegacyContext,947 debugTask: null | ConsoleTask,948): RenderTask {949 request.allPendingTasks++;950 if (blockedBoundary === null) {951 request.pendingRootTasks++;952 } else {953 blockedBoundary.pendingTasks++;954 }955 if (row !== null) {956 row.pendingTasks++;957 }958 const task: RenderTask = {959 replay: null,960 node,961 childIndex,962 ping: {963 resolve: () => pingTask(request, task),964 reject: error => pingRejectedTask(request, task, error),965 },966 blockedBoundary,967 blockedSegment,968 blockedPreamble,969 hoistableState,970 abortSet,971 keyPath,972 formatContext,973 context,974 treeContext,975 row,976 componentStack,977 thenableState,978 } as any;979 if (!disableLegacyContext) {980 task.legacyContext = legacyContext;981 }982 if (__DEV__) {983 task.debugTask = debugTask;984 }985 abortSet.add(task);986 return task;987}988989function createReplayTask(990 request: Request,991 thenableState: ThenableState | null,992 replay: ReplaySet,993 node: ReactNodeList,994 childIndex: number,995 blockedBoundary: Root | SuspenseBoundary,996 hoistableState: null | HoistableState,997 abortSet: Set<Task>,998 keyPath: Root | KeyNode,999 formatContext: FormatContext,1000 context: ContextSnapshot,1001 treeContext: TreeContext,1002 row: null | SuspenseListRow,1003 componentStack: null | ComponentStackNode,1004 legacyContext: LegacyContext,1005 debugTask: null | ConsoleTask,1006): ReplayTask {1007 request.allPendingTasks++;1008 if (blockedBoundary === null) {1009 request.pendingRootTasks++;1010 } else {1011 blockedBoundary.pendingTasks++;1012 }1013 if (row !== null) {1014 row.pendingTasks++;1015 }1016 replay.pendingTasks++;1017 const task: ReplayTask = {1018 replay,1019 node,1020 childIndex,1021 ping: {1022 resolve: () => pingTask(request, task),1023 reject: error => pingRejectedTask(request, task, error),1024 },1025 blockedBoundary,1026 blockedSegment: null,1027 blockedPreamble: null,1028 hoistableState,1029 abortSet,1030 keyPath,1031 formatContext,1032 context,1033 treeContext,1034 row,1035 componentStack,1036 thenableState,1037 } as any;1038 if (!disableLegacyContext) {1039 task.legacyContext = legacyContext;1040 }1041 if (__DEV__) {1042 task.debugTask = debugTask;1043 }1044 abortSet.add(task);1045 return task;1046}10471048function createPendingSegment(1049 request: Request,1050 index: number,1051 boundary: null | SuspenseBoundary,1052 parentFormatContext: FormatContext,1053 lastPushedText: boolean,1054 textEmbedded: boolean,1055): Segment {1056 return {1057 status: PENDING,1058 parentFlushed: false,1059 id: -1, // lazily assigned later1060 index,1061 chunks: [],1062 children: [],1063 preambleChildren: [],1064 parentFormatContext,1065 boundary,1066 lastPushedText,1067 textEmbedded,1068 };1069}10701071function getCurrentStackInDEV(): string {1072 if (__DEV__) {1073 if (currentTaskInDEV === null || currentTaskInDEV.componentStack === null) {1074 return '';1075 }1076 return getOwnerStackByComponentStackNodeInDev(1077 currentTaskInDEV.componentStack,1078 );1079 }1080 return '';1081}10821083function getStackFromNode(stackNode: ComponentStackNode): string {1084 return getStackByComponentStackNode(stackNode);1085}10861087function pushHaltedAwaitOnComponentStack(1088 task: Task,1089 debugInfo: void | null | ReactDebugInfo,1090): void {1091 if (!__DEV__) {1092 // eslint-disable-next-line react-internal/prod-error-codes1093 throw new Error(1094 'pushHaltedAwaitOnComponentStack should never be called in production. This is a bug in React.',1095 );1096 }1097 if (debugInfo != null) {1098 for (let i = debugInfo.length - 1; i >= 0; i--) {1099 const info = debugInfo[i];1100 if (info.awaited != null) {1101 const asyncInfo: ReactAsyncInfo = info as any;1102 const bestStack =1103 asyncInfo.debugStack == null ? asyncInfo.awaited : asyncInfo;1104 if (bestStack.debugStack !== undefined) {1105 task.componentStack = {1106 parent: task.componentStack,1107 type: asyncInfo,1108 owner: bestStack.owner,1109 stack: bestStack.debugStack,1110 };1111 task.debugTask = bestStack.debugTask as any;1112 break;1113 }1114 }1115 }1116 }1117}11181119// performWork + retryTask without mutation1120function rerenderStalledTask(request: Request, task: Task): void {1121 const prevStatus = request.status;1122 const prevAborted = request.aborted;1123 request.status = STALLED_DEV;1124 // This diagnostic replay must reach the suspended call site instead of1125 // taking the abort path.1126 request.aborted = false;11271128 const prevContext = getActiveContext();1129 const prevDispatcher = ReactSharedInternals.H;1130 ReactSharedInternals.H = HooksDispatcher;1131 const prevAsyncDispatcher = ReactSharedInternals.A;1132 ReactSharedInternals.A = DefaultAsyncDispatcher;11331134 const prevRequest = currentRequest;1135 currentRequest = request;11361137 const prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack;1138 ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;11391140 const prevResumableState = currentResumableState;1141 setCurrentResumableState(request.resumableState);1142 switchContext(task.context);1143 const prevTaskInDEV = currentTaskInDEV;1144 setCurrentTaskInDEV(task);1145 try {1146 retryNode(request, task);1147 } catch (x) {1148 // Suspended again.1149 resetHooksState();1150 } finally {1151 setCurrentTaskInDEV(prevTaskInDEV);1152 setCurrentResumableState(prevResumableState);11531154 ReactSharedInternals.H = prevDispatcher;1155 ReactSharedInternals.A = prevAsyncDispatcher;11561157 ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl;1158 if (prevDispatcher === HooksDispatcher) {1159 // This means that we were in a reentrant work loop. This could happen1160 // in a renderer that supports synchronous work like renderToString,1161 // when it's called from within another renderer.1162 // Normally we don't bother switching the contexts to their root/default1163 // values when leaving because we'll likely need the same or similar1164 // context again. However, when we're inside a synchronous loop like this1165 // we'll to restore the context to what it was before returning.1166 switchContext(prevContext);1167 }1168 currentRequest = prevRequest;1169 request.status = prevStatus;1170 request.aborted = prevAborted;1171 }1172}11731174function pushSuspendedCallSiteOnComponentStack(1175 request: Request,1176 task: Task,1177): void {1178 setCaptureSuspendedCallSiteDEV(true);1179 const restoreThenableState = ensureSuspendableThenableStateDEV(1180 // refined at the callsite1181 task.thenableState as any as ThenableState,1182 );1183 try {1184 rerenderStalledTask(request, task);1185 } finally {1186 restoreThenableState();1187 setCaptureSuspendedCallSiteDEV(false);1188 }11891190 const suspendCallSiteStack = getSuspendedCallSiteStackDEV();1191 const suspendCallSiteDebugTask = getSuspendedCallSiteDebugTaskDEV();11921193 if (suspendCallSiteStack !== null) {1194 const ownerStack = task.componentStack;1195 task.componentStack = {1196 // The owner of the suspended call site would be the owner of this task.1197 // We need the task itself otherwise we'd miss a frame.1198 owner: ownerStack,1199 parent: suspendCallSiteStack.parent,1200 stack: suspendCallSiteStack.stack,1201 type: suspendCallSiteStack.type,1202 };1203 }1204 task.debugTask = suspendCallSiteDebugTask;1205}12061207function pushServerComponentStack(1208 task: Task,1209 debugInfo: void | null | ReactDebugInfo,1210): void {1211 if (!__DEV__) {1212 // eslint-disable-next-line react-internal/prod-error-codes1213 throw new Error(1214 'pushServerComponentStack should never be called in production. This is a bug in React.',1215 );1216 }1217 // Build a Server Component parent stack from the debugInfo.1218 if (debugInfo != null) {1219 const stack: ReactDebugInfo = debugInfo;1220 for (let i = 0; i < stack.length; i++) {1221 const componentInfo: ReactComponentInfo = stack[i] as any;1222 if (typeof componentInfo.name !== 'string') {1223 continue;1224 }1225 if (componentInfo.debugStack === undefined) {1226 continue;1227 }1228 task.componentStack = {1229 parent: task.componentStack,1230 type: componentInfo,1231 owner: componentInfo.owner,1232 stack: componentInfo.debugStack,1233 };1234 task.debugTask = componentInfo.debugTask as any;1235 }1236 }1237}12381239function pushComponentStack(task: Task): void {1240 const node = task.node;1241 // Create the Component Stack frame for the element we're about to try.1242 // It's unfortunate that we need to do this refinement twice. Once for1243 // the stack frame and then once again while actually1244 if (typeof node === 'object' && node !== null) {1245 switch ((node as any).$$typeof) {1246 case REACT_ELEMENT_TYPE: {1247 const element: any = node;1248 const type = element.type;1249 const owner = __DEV__ ? element._owner : null;1250 const stack = __DEV__ ? element._debugStack : null;1251 if (__DEV__) {1252 pushServerComponentStack(task, element._debugInfo);1253 task.debugTask = element._debugTask;1254 }1255 task.componentStack = createComponentStackFromType(1256 task.componentStack,1257 type,1258 owner,1259 stack,1260 );1261 break;1262 }1263 case REACT_LAZY_TYPE: {1264 if (__DEV__) {1265 const lazyNode: LazyComponentType<any, any> = node as any;1266 pushServerComponentStack(task, lazyNode._debugInfo);1267 }1268 break;1269 }1270 default: {1271 if (__DEV__) {1272 const maybeUsable: Object = node;1273 if (typeof maybeUsable.then === 'function') {1274 const thenable: Thenable<ReactNodeList> = maybeUsable as any;1275 pushServerComponentStack(task, thenable._debugInfo);1276 }1277 }1278 }1279 }1280 }1281}12821283function createComponentStackFromType(1284 parent: null | ComponentStackNode,1285 type: Function | string | symbol,1286 owner: void | null | ReactComponentInfo | ComponentStackNode, // DEV only1287 stack: void | null | string | Error, // DEV only1288): ComponentStackNode {1289 if (__DEV__) {1290 return {1291 parent,1292 type,1293 owner,1294 stack,1295 };1296 }1297 return {1298 parent,1299 type,1300 };1301}13021303function replaceSuspenseComponentStackWithSuspenseFallbackStack(1304 componentStack: null | ComponentStackNode,1305): null | ComponentStackNode {1306 if (componentStack === null) {1307 return null;1308 }1309 return createComponentStackFromType(1310 componentStack.parent,1311 'Suspense Fallback',1312 __DEV__ ? componentStack.owner : null,1313 __DEV__ ? componentStack.stack : null,1314 );1315}13161317type ThrownInfo = {1318 componentStack?: string,1319};1320export type ErrorInfo = ThrownInfo;13211322function getThrownInfo(node: null | ComponentStackNode): ThrownInfo {1323 const errorInfo: ThrownInfo = {};1324 if (node) {1325 Object.defineProperty(errorInfo, 'componentStack', {1326 configurable: true,1327 enumerable: true,1328 get() {1329 // Lazyily generate the stack since it's expensive.1330 const stack = getStackFromNode(node);1331 Object.defineProperty(errorInfo, 'componentStack', {1332 value: stack,1333 });1334 return stack;1335 },1336 });1337 }1338 return errorInfo;1339}13401341function encodeErrorForBoundary(1342 boundary: SuspenseBoundary,1343 digest: ?string,1344 error: mixed,1345 thrownInfo: ThrownInfo,1346 wasAborted: boolean,1347) {1348 boundary.errorDigest = digest;1349 if (__DEV__) {1350 if (isRecoverableError(error)) {1351 boundary.errorMessage = wasAborted1352 ? 'Switched to client rendering because the server render was aborted ' +1353 'with a request to render on the client.'1354 : 'Switched to client rendering because a component requested it.';1355 boundary.errorComponentStack = thrownInfo.componentStack;1356 return;1357 }1358 let message, stack;1359 // In dev we additionally encode the error message and component stack on the boundary1360 if (error instanceof Error) {1361 // eslint-disable-next-line react-internal/safe-string-coercion1362 message = String(error.message);1363 // eslint-disable-next-line react-internal/safe-string-coercion1364 stack = String(error.stack);1365 } else if (typeof error === 'object' && error !== null) {1366 message = describeObjectForErrorMessage(error);1367 stack = null;1368 } else {1369 // eslint-disable-next-line react-internal/safe-string-coercion1370 message = String(error);1371 stack = null;1372 }1373 const prefix = wasAborted1374 ? 'Switched to client rendering because the server rendering aborted due to:\n\n'1375 : 'Switched to client rendering because the server rendering errored:\n\n';1376 boundary.errorMessage = prefix + message;1377 boundary.errorStack = stack !== null ? prefix + stack : null;1378 boundary.errorComponentStack = thrownInfo.componentStack;1379 }1380}13811382function logRecoverableError(1383 request: Request,1384 error: any,1385 errorInfo: ThrownInfo,1386 debugTask: null | ConsoleTask,1387): ?string {1388 if (isRecoverableError(error)) {1389 logBrowserBailout(request, error, errorInfo, debugTask);1390 return REACT_RECOVERABLE_DIGEST;1391 }13921393 // If this callback errors, we intentionally let that error bubble up to become a fatal error1394 // so that someone fixes the error reporting instead of hiding it.1395 const onError = request.onError;1396 const errorDigest =1397 __DEV__ && debugTask1398 ? debugTask.run(onError.bind(null, error, errorInfo))1399 : onError(error, errorInfo);1400 if (errorDigest != null && typeof errorDigest !== 'string') {1401 // We used to throw here but since this gets called from a variety of unprotected places it1402 // seems better to just warn and discard the returned value.1403 if (__DEV__) {1404 console.error(1405 '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',1406 typeof errorDigest,1407 );1408 }1409 return;1410 }1411 // An empty digest is reserved for React's internal client-render signal.1412 // Historically an empty digest was omitted from the wire format, so1413 // normalizing it to undefined preserves the existing user-space semantics.1414 return errorDigest === '' ? undefined : errorDigest;1415}14161417function logBrowserBailout(1418 request: Request,1419 error: mixed,1420 errorInfo: ThrownInfo,1421 debugTask: null | ConsoleTask,1422): void {1423 // If this callback errors, we intentionally let that error bubble up to1424 // become a fatal error, matching the behavior of onError.1425 const onBrowserBailout = request.onBrowserBailout;1426 if (__DEV__ && debugTask) {1427 debugTask.run(onBrowserBailout.bind(null, error, errorInfo));1428 } else {1429 onBrowserBailout(error, errorInfo);1430 }1431}14321433function fatalError(1434 request: Request,1435 error: mixed,1436 errorInfo: ThrownInfo,1437 debugTask: null | ConsoleTask,1438): void {1439 // This is called outside error handling code such as if the root errors outside1440 // a suspense boundary or if the root suspense boundary's fallback errors.1441 // It's also called if React itself or its host configs errors.1442 const onShellError = request.onShellError;1443 const onFatalError = request.onFatalError;1444 // Once the shell has completed it can't error anymore, so onShellError only1445 // fires while root tasks are still pending. onFatalError always fires because1446 // the error is always fatal to the request.1447 const shellComplete = request.pendingRootTasks === 0;1448 if (__DEV__ && debugTask) {1449 if (!shellComplete) {1450 debugTask.run(onShellError.bind(null, error));1451 }1452 debugTask.run(onFatalError.bind(null, error));1453 } else {1454 if (!shellComplete) {1455 onShellError(error);1456 }1457 onFatalError(error);1458 }1459 endRenderLifetime(request);1460 if (request.destination !== null) {1461 request.status = CLOSED;1462 closeWithError(request.destination, error);1463 } else {1464 request.status = CLOSING;1465 // abort() already stored the reason that every remaining task must1466 // observe. This error may only be a fatal diagnostic derived from it.1467 if (!request.aborted) {1468 request.fatalError = error;1469 }1470 }1471}14721473function renderSuspenseBoundary(1474 request: Request,1475 someTask: Task,1476 keyPath: KeyNode,1477 props: SuspenseProps,1478): void {1479 if (someTask.replay !== null) {1480 // If we're replaying through this pass, it means we're replaying through1481 // an already completed Suspense boundary. It's too late to do anything about it1482 // so we can just render through it.1483 const prevKeyPath = someTask.keyPath;1484 const prevContext = someTask.formatContext;1485 const prevRow = someTask.row;1486 someTask.keyPath = keyPath;1487 someTask.formatContext = getSuspenseContentFormatContext(1488 request.resumableState,1489 prevContext,1490 );1491 someTask.row = null;1492 const content: ReactNodeList = props.children;1493 try {1494 renderNode(request, someTask, content, -1);1495 } finally {1496 someTask.keyPath = prevKeyPath;1497 someTask.formatContext = prevContext;1498 someTask.row = prevRow;1499 }1500 return;1501 }1502 // $FlowFixMe[incompatible-type]: Refined.1503 const task: RenderTask = someTask;15041505 const prevKeyPath = task.keyPath;1506 const prevContext = task.formatContext;1507 const prevRow = task.row;1508 const parentBoundary = task.blockedBoundary;1509 const parentPreamble = task.blockedPreamble;1510 const parentHoistableState = task.hoistableState;1511 const parentSegment = task.blockedSegment;15121513 // Each time we enter a suspense boundary, we split out into a new segment for1514 // the fallback so that we can later replace that segment with the content.1515 // This also lets us split out the main content even if it doesn't suspend,1516 // in case it ends up generating a large subtree of content.1517 const fallback: ReactNodeList = props.fallback;1518 const content: ReactNodeList = props.children;1519 const defer: boolean = enableCPUSuspense && props.defer === true;15201521 const fallbackAbortSet: Set<Task> = new Set();1522 const newBoundary = createSuspenseBoundary(1523 request,1524 task.row,1525 fallbackAbortSet,1526 canHavePreamble(task.formatContext) ? createPreamble() : null,1527 defer,1528 );15291530 const insertionIndex = parentSegment.chunks.length;1531 // The children of the boundary segment is actually the fallback.1532 const boundarySegment = createPendingSegment(1533 request,1534 insertionIndex,1535 newBoundary,1536 task.formatContext,1537 // boundaries never require text embedding at their edges because comment nodes bound them1538 false,1539 false,1540 );1541 parentSegment.children.push(boundarySegment);1542 // The parentSegment has a child Segment at this index so we reset the lastPushedText marker on the parent1543 parentSegment.lastPushedText = false;15441545 // This segment is the actual child content. We can start rendering that immediately.1546 const contentRootSegment = createPendingSegment(1547 request,1548 0,1549 null,1550 task.formatContext,1551 // boundaries never require text embedding at their edges because comment nodes bound them1552 false,1553 false,1554 );1555 // We mark the root segment as having its parent flushed. It's not really flushed but there is1556 // no parent segment so there's nothing to wait on.1557 contentRootSegment.parentFlushed = true;15581559 const trackedPostpones = request.trackedPostpones;1560 if (trackedPostpones !== null || defer) {1561 // This is a prerender or deferred boundary. In this mode we want to render the fallback synchronously1562 // and schedule the content to render later. This is the opposite of what we do during a normal render1563 // where we try to skip rendering the fallback if the content itself can render synchronously15641565 // Stash the original stack frame.1566 const suspenseComponentStack = task.componentStack;15671568 const fallbackKeyPath: KeyNode = [1569 keyPath[0],1570 'Suspense Fallback',1571 keyPath[2],1572 ];1573 if (trackedPostpones !== null) {1574 const fallbackReplayNode: ReplayNode = [1575 fallbackKeyPath[1],1576 fallbackKeyPath[2],1577 [] as Array<ReplayNode>,1578 null,1579 ];1580 trackedPostpones.workingMap.set(fallbackKeyPath, fallbackReplayNode);1581 newBoundary.tracked = {1582 contentKeyPath: keyPath,1583 // We are rendering the fallback before the boundary content so we keep track of1584 // the fallback replay node until we determine if the primary content suspends1585 fallbackNode: fallbackReplayNode,1586 };1587 }15881589 task.blockedSegment = boundarySegment;1590 task.blockedPreamble =1591 newBoundary.preamble === null ? null : newBoundary.preamble.fallback;1592 task.keyPath = fallbackKeyPath;1593 task.formatContext = getSuspenseFallbackFormatContext(1594 request.resumableState,1595 prevContext,1596 );1597 task.componentStack =1598 replaceSuspenseComponentStackWithSuspenseFallbackStack(1599 suspenseComponentStack,1600 );1601 try {1602 renderNode(request, task, fallback, -1);1603 pushSegmentFinale(1604 boundarySegment.chunks,1605 request.renderState,1606 boundarySegment.lastPushedText,1607 boundarySegment.textEmbedded,1608 );1609 boundarySegment.status = COMPLETED;1610 finishedSegment(request, parentBoundary, boundarySegment);1611 } catch (thrownValue: mixed) {1612 if (request.aborted) {1613 boundarySegment.status = ABORTED;1614 } else {1615 boundarySegment.status = ERRORED;1616 }1617 throw thrownValue;1618 } finally {1619 task.blockedSegment = parentSegment;1620 task.blockedPreamble = parentPreamble;1621 task.keyPath = prevKeyPath;1622 task.formatContext = prevContext;1623 }16241625 // We create a suspended task for the primary content because we want to allow1626 // sibling fallbacks to be rendered first.1627 const suspendedPrimaryTask = createRenderTask(1628 request,1629 null,1630 content,1631 -1,1632 newBoundary,1633 contentRootSegment,1634 newBoundary.preamble === null ? null : newBoundary.preamble.content,1635 newBoundary.contentState,1636 task.abortSet,1637 keyPath,1638 getSuspenseContentFormatContext(1639 request.resumableState,1640 task.formatContext,1641 ),1642 task.context,1643 task.treeContext,1644 null, // The row gets reset inside the Suspense boundary.1645 suspenseComponentStack,1646 !disableLegacyContext ? task.legacyContext : emptyContextObject,1647 __DEV__ ? task.debugTask : null,1648 );1649 pushComponentStack(suspendedPrimaryTask);1650 request.pingedTasks.push(suspendedPrimaryTask);1651 } else {1652 // This is a normal render. We will attempt to synchronously render the boundary content1653 // If it is successful we will elide the fallback task but if it suspends or errors we schedule1654 // the fallback to render. Unlike with prerenders we attempt to deprioritize the fallback render16551656 // Currently this is running synchronously. We could instead schedule this to pingedTasks.1657 // I suspect that there might be some efficiency benefits from not creating the suspended task1658 // and instead just using the stack if possible.1659 // TODO: Call this directly instead of messing with saving and restoring contexts.16601661 // We can reuse the current context and task to render the content immediately without1662 // context switching. We just need to temporarily switch which boundary and which segment1663 // we're writing to. If something suspends, it'll spawn new suspended task with that context.1664 task.blockedBoundary = newBoundary;1665 task.blockedPreamble =1666 newBoundary.preamble === null ? null : newBoundary.preamble.content;1667 task.hoistableState = newBoundary.contentState;1668 task.blockedSegment = contentRootSegment;1669 task.keyPath = keyPath;1670 task.formatContext = getSuspenseContentFormatContext(1671 request.resumableState,1672 prevContext,1673 );1674 task.row = null;1675 try {1676 // We use the safe form because we don't handle suspending here. Only error handling.1677 renderNode(request, task, content, -1);1678 pushSegmentFinale(1679 contentRootSegment.chunks,1680 request.renderState,1681 contentRootSegment.lastPushedText,1682 contentRootSegment.textEmbedded,1683 );1684 contentRootSegment.status = COMPLETED;1685 finishedSegment(request, newBoundary, contentRootSegment);1686 queueCompletedSegment(newBoundary, contentRootSegment);1687 if (newBoundary.pendingTasks === 0 && newBoundary.status === PENDING) {1688 // This must have been the last segment we were waiting on. This boundary is now complete.1689 newBoundary.status = COMPLETED;1690 // Therefore we won't need the fallback. We early return so that we don't have to create1691 // the fallback. However, if this boundary ended up big enough to be eligible for outlining1692 // we can't do that because we might still need the fallback if we outline it.1693 if (!isEligibleForOutlining(request, newBoundary)) {1694 if (prevRow !== null) {1695 // If we have synchronously completed the boundary and it's not eligible for outlining1696 // then we don't have to wait for it to be flushed before we unblock future rows.1697 // This lets us inline small rows in order.1698 if (--prevRow.pendingTasks === 0) {1699 finishSuspenseListRow(request, prevRow);1700 }1701 }1702 if (request.pendingRootTasks === 0 && task.blockedPreamble) {1703 // The root is complete and this boundary may contribute part of the preamble.1704 // We eagerly attempt to prepare the preamble here because we expect most requests1705 // to have few boundaries which contribute preambles and it allow us to do this1706 // preparation work during the work phase rather than the when flushing.1707 preparePreamble(request);1708 }1709 return;1710 }1711 } else {1712 const boundaryRow = prevRow;1713 if (boundaryRow !== null && boundaryRow.together) {1714 tryToResolveTogetherRow(request, boundaryRow);1715 }1716 }1717 } catch (thrownValue: mixed) {1718 newBoundary.status = CLIENT_RENDERED;1719 let error: mixed;1720 if (request.aborted) {1721 contentRootSegment.status = ABORTED;1722 error = request.fatalError;1723 } else {1724 contentRootSegment.status = ERRORED;1725 error = thrownValue;1726 }17271728 const thrownInfo = getThrownInfo(task.componentStack);1729 const errorDigest = logRecoverableError(1730 request,1731 error,1732 thrownInfo,1733 __DEV__ ? task.debugTask : null,1734 );1735 encodeErrorForBoundary(1736 newBoundary,1737 errorDigest,1738 error,1739 thrownInfo,1740 false,1741 );17421743 untrackBoundary(request, newBoundary);17441745 // We don't need to decrement any task numbers because we didn't spawn any new task.1746 // We don't need to schedule any task because we know the parent has written yet.1747 // We do need to fallthrough to create the fallback though.1748 } finally {1749 task.blockedBoundary = parentBoundary;1750 task.blockedPreamble = parentPreamble;1751 task.hoistableState = parentHoistableState;1752 task.blockedSegment = parentSegment;1753 task.keyPath = prevKeyPath;1754 task.formatContext = prevContext;1755 task.row = prevRow;1756 }17571758 const fallbackKeyPath: KeyNode = [1759 keyPath[0],1760 'Suspense Fallback',1761 keyPath[2],1762 ];1763 // We create suspended task for the fallback because we don't want to actually work1764 // on it yet in case we finish the main content, so we queue for later.1765 const suspendedFallbackTask = createRenderTask(1766 request,1767 null,1768 fallback,1769 -1,1770 parentBoundary,1771 boundarySegment,1772 newBoundary.preamble === null ? null : newBoundary.preamble.fallback,1773 newBoundary.fallbackState,1774 fallbackAbortSet,1775 fallbackKeyPath,1776 getSuspenseFallbackFormatContext(1777 request.resumableState,1778 task.formatContext,1779 ),1780 task.context,1781 task.treeContext,1782 task.row,1783 replaceSuspenseComponentStackWithSuspenseFallbackStack(1784 task.componentStack,1785 ),1786 !disableLegacyContext ? task.legacyContext : emptyContextObject,1787 __DEV__ ? task.debugTask : null,1788 );1789 pushComponentStack(suspendedFallbackTask);1790 // TODO: This should be queued at a separate lower priority queue so that we only work1791 // on preparing fallbacks if we don't have any more main content to task on.1792 request.pingedTasks.push(suspendedFallbackTask);1793 }1794}17951796function replaySuspenseBoundary(1797 request: Request,1798 task: ReplayTask,1799 keyPath: KeyNode,1800 props: Object,1801 id: number,1802 childNodes: Array<ReplayNode>,1803 childSlots: ResumeSlots,1804 fallbackNodes: Array<ReplayNode>,1805 fallbackSlots: ResumeSlots,1806): void {1807 const prevKeyPath = task.keyPath;1808 const prevContext = task.formatContext;1809 const prevRow = task.row;1810 const previousReplaySet: ReplaySet = task.replay;18111812 const parentBoundary = task.blockedBoundary;1813 const parentHoistableState = task.hoistableState;18141815 const content: ReactNodeList = props.children;1816 const fallback: ReactNodeList = props.fallback;1817 const defer: boolean = enableCPUSuspense && props.defer === true;18181819 const fallbackAbortSet: Set<Task> = new Set();1820 const resumedBoundary = createSuspenseBoundary(1821 request,1822 task.row,1823 fallbackAbortSet,1824 canHavePreamble(task.formatContext) ? createPreamble() : null,1825 defer,1826 );1827 resumedBoundary.parentFlushed = true;1828 // We restore the same id of this boundary as was used during prerender.1829 resumedBoundary.rootSegmentID = id;18301831 const fallbackKeyPath: KeyNode = [1832 keyPath[0],1833 'Suspense Fallback',1834 keyPath[2],1835 ];1836 const trackedPostpones = request.trackedPostpones;1837 if (trackedPostpones !== null) {1838 const fallbackReplayNode: ReplayNode = [1839 fallbackKeyPath[1],1840 fallbackKeyPath[2],1841 [] as Array<ReplayNode>,1842 null,1843 ];1844 trackedPostpones.workingMap.set(fallbackKeyPath, fallbackReplayNode);1845 resumedBoundary.tracked = {1846 contentKeyPath: keyPath,1847 fallbackNode: fallbackReplayNode,1848 };1849 }18501851 // We can reuse the current context and task to render the content immediately without1852 // context switching. We just need to temporarily switch which boundary and replay node1853 // we're writing to. If something suspends, it'll spawn new suspended task with that context.1854 task.blockedBoundary = resumedBoundary;1855 task.hoistableState = resumedBoundary.contentState;1856 task.keyPath = keyPath;1857 task.formatContext = getSuspenseContentFormatContext(1858 request.resumableState,1859 prevContext,1860 );1861 task.row = null;1862 task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};18631864 try {1865 // We use the safe form because we don't handle suspending here. Only error handling.1866 renderNode(request, task, content, -1);18671868 if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) {1869 throw new Error(1870 "Couldn't find all resumable slots by key/index during replaying. " +1871 "The tree doesn't match so React will fallback to client rendering.",1872 );1873 }1874 task.replay.pendingTasks--;1875 if (1876 resumedBoundary.pendingTasks === 0 &&1877 resumedBoundary.status === PENDING1878 ) {1879 // This must have been the last segment we were waiting on. This boundary is now complete.1880 // Therefore we won't need the fallback. We early return so that we don't have to create1881 // the fallback.1882 resumedBoundary.status = COMPLETED;1883 request.completedBoundaries.push(resumedBoundary);1884 // We restore the parent componentStack. Semantically this is the same as1885 // popComponentStack(task) but we do this instead because it should be slightly1886 // faster1887 return;1888 }1889 } catch (thrownValue: mixed) {1890 resumedBoundary.status = CLIENT_RENDERED;1891 const error = request.aborted ? request.fatalError : thrownValue;1892 const thrownInfo = getThrownInfo(task.componentStack);1893 const errorDigest = logRecoverableError(1894 request,1895 error,1896 thrownInfo,1897 __DEV__ ? task.debugTask : null,1898 );1899 encodeErrorForBoundary(1900 resumedBoundary,1901 errorDigest,1902 error,1903 thrownInfo,1904 false,1905 );19061907 task.replay.pendingTasks--;19081909 // The parent already flushed in the prerender so we need to schedule this to be emitted.1910 request.clientRenderedBoundaries.push(resumedBoundary);19111912 // We don't need to decrement any task numbers because we didn't spawn any new task.1913 // We don't need to schedule any task because we know the parent has written yet.1914 // We do need to fallthrough to create the fallback though.1915 } finally {1916 task.blockedBoundary = parentBoundary;1917 task.hoistableState = parentHoistableState;1918 task.replay = previousReplaySet;1919 task.keyPath = prevKeyPath;1920 task.formatContext = prevContext;1921 task.row = prevRow;1922 }19231924 // We create suspended task for the fallback because we don't want to actually work1925 // on it yet in case we finish the main content, so we queue for later.1926 const fallbackReplay = {1927 nodes: fallbackNodes,1928 slots: fallbackSlots,1929 pendingTasks: 0,1930 };1931 const suspendedFallbackTask = createReplayTask(1932 request,1933 null,1934 fallbackReplay,1935 fallback,1936 -1,1937 parentBoundary,1938 resumedBoundary.fallbackState,1939 fallbackAbortSet,1940 fallbackKeyPath,1941 getSuspenseFallbackFormatContext(1942 request.resumableState,1943 task.formatContext,1944 ),1945 task.context,1946 task.treeContext,1947 task.row,1948 replaceSuspenseComponentStackWithSuspenseFallbackStack(task.componentStack),1949 !disableLegacyContext ? task.legacyContext : emptyContextObject,1950 __DEV__ ? task.debugTask : null,1951 );19521953 pushComponentStack(suspendedFallbackTask);1954 // TODO: This should be queued at a separate lower priority queue so that we only work1955 // on preparing fallbacks if we don't have any more main content to task on.1956 request.pingedTasks.push(suspendedFallbackTask);1957}19581959function finishSuspenseListRow(request: Request, row: SuspenseListRow): void {1960 // This row finished. Now we have to unblock all the next rows that were blocked on this.1961 unblockSuspenseListRow(request, row.next, row.hoistables);1962}19631964function unblockSuspenseListRow(1965 request: Request,1966 unblockedRow: null | SuspenseListRow,1967 inheritedHoistables: null | HoistableState,1968): void {1969 // We do this in a loop to avoid stack overflow for very long lists that get unblocked.1970 while (unblockedRow !== null) {1971 if (inheritedHoistables !== null) {1972 // Hoist any hoistables from the previous row into the next row so that it can be1973 // later transferred to all the rows.1974 hoistHoistables(unblockedRow.hoistables, inheritedHoistables);1975 // Mark the row itself for any newly discovered Suspense boundaries to inherit.1976 // This is different from hoistables because that also includes hoistables from1977 // all the boundaries below this row and not just previous rows.1978 unblockedRow.inheritedHoistables = inheritedHoistables;1979 }1980 // Unblocking the boundaries will decrement the count of this row but we keep it above1981 // zero so they never finish this row recursively.1982 const unblockedBoundaries = unblockedRow.boundaries;1983 if (unblockedBoundaries !== null) {1984 unblockedRow.boundaries = null;1985 for (let i = 0; i < unblockedBoundaries.length; i++) {1986 const unblockedBoundary = unblockedBoundaries[i];1987 if (inheritedHoistables !== null) {1988 hoistHoistables(unblockedBoundary.contentState, inheritedHoistables);1989 }1990 finishedTask(request, unblockedBoundary, null, null);1991 }1992 }1993 // Instead we decrement at the end to keep it all in this loop.1994 unblockedRow.pendingTasks--;1995 if (unblockedRow.pendingTasks > 0) {1996 // Still blocked.1997 break;1998 }1999 inheritedHoistables = unblockedRow.hoistables;2000 unblockedRow = unblockedRow.next;
Findings
✓ No findings reported for this file.