| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | * |
| 7 | * @flow |
| 8 | */ |
| 9 | |
| 10 | import type { |
| 11 | Destination, |
| 12 | Chunk, |
| 13 | PrecomputedChunk, |
| 14 | } from './ReactServerStreamConfig'; |
| 15 | import 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'; |
| 32 | import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; |
| 33 | import type { |
| 34 | RenderState, |
| 35 | ResumableState, |
| 36 | PreambleState, |
| 37 | FormatContext, |
| 38 | HoistableState, |
| 39 | } from './ReactFizzConfig'; |
| 40 | import type {ContextSnapshot} from './ReactFizzNewContext'; |
| 41 | import type {ComponentStackNode} from './ReactFizzComponentStack'; |
| 42 | import type {TreeContext} from './ReactFizzTreeContext'; |
| 43 | import type {ThenableState} from './ReactFizzThenable'; |
| 44 | |
| 45 | import {describeObjectForErrorMessage} from 'shared/ReactSerializationErrors'; |
| 46 | |
| 47 | import { |
| 48 | scheduleWork, |
| 49 | scheduleMicrotask, |
| 50 | beginWriting, |
| 51 | writeChunk, |
| 52 | writeChunkAndReturn, |
| 53 | completeWriting, |
| 54 | flushBuffered, |
| 55 | close, |
| 56 | closeWithError, |
| 57 | byteLengthOfChunk, |
| 58 | } from './ReactServerStreamConfig'; |
| 59 | import { |
| 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'; |
| 106 | import { |
| 107 | constructClassInstance, |
| 108 | mountClassInstance, |
| 109 | } from './ReactFizzClassComponent'; |
| 110 | import { |
| 111 | getMaskedContext, |
| 112 | processChildContext, |
| 113 | emptyContextObject, |
| 114 | } from './ReactFizzLegacyContext'; |
| 115 | import { |
| 116 | readContext, |
| 117 | rootContextSnapshot, |
| 118 | switchContext, |
| 119 | getActiveContext, |
| 120 | pushProvider, |
| 121 | popProvider, |
| 122 | } from './ReactFizzNewContext'; |
| 123 | import { |
| 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'; |
| 141 | import {DefaultAsyncDispatcher} from './ReactFizzAsyncDispatcher'; |
| 142 | import { |
| 143 | getStackByComponentStackNode, |
| 144 | getOwnerStackByComponentStackNodeInDev, |
| 145 | } from './ReactFizzComponentStack'; |
| 146 | import {emptyTreeContext, pushTreeContext} from './ReactFizzTreeContext'; |
| 147 | import {currentTaskInDEV, setCurrentTaskInDEV} from './ReactFizzCurrentTask'; |
| 148 | import { |
| 149 | callLazyInitInDEV, |
| 150 | callComponentInDEV, |
| 151 | callRenderInDEV, |
| 152 | } from './ReactFizzCallUserSpace'; |
| 153 | import { |
| 154 | getViewTransitionClassName, |
| 155 | getViewTransitionName, |
| 156 | } from './ReactFizzViewTransitionComponent'; |
| 157 | |
| 158 | import {resetOwnerStackLimit} from 'shared/ReactOwnerStackReset'; |
| 159 | import { |
| 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'; |
| 181 | import ReactSharedInternals from 'shared/ReactSharedInternals'; |
| 182 | import { |
| 183 | disableLegacyContext, |
| 184 | disableLegacyContextForFunctionComponents, |
| 185 | enableScopeAPI, |
| 186 | enableAsyncIterableChildren, |
| 187 | enableViewTransition, |
| 188 | enableViewTransitionParentEnterExit, |
| 189 | enableFizzBlockingRender, |
| 190 | enableAsyncDebugInfo, |
| 191 | enableCPUSuspense, |
| 192 | } from 'shared/ReactFeatureFlags'; |
| 193 | |
| 194 | import assign from 'shared/assign'; |
| 195 | import noop from 'shared/noop'; |
| 196 | import getComponentNameFromType from 'shared/getComponentNameFromType'; |
| 197 | import isArray from 'shared/isArray'; |
| 198 | import {REACT_RECOVERABLE_DIGEST} from 'shared/ReactRecoverable'; |
| 199 | import { |
| 200 | SuspenseException, |
| 201 | getSuspendedThenable, |
| 202 | ensureSuspendableThenableStateDEV, |
| 203 | getSuspendedCallSiteStackDEV, |
| 204 | getSuspendedCallSiteDebugTaskDEV, |
| 205 | setCaptureSuspendedCallSiteDEV, |
| 206 | } from './ReactFizzThenable'; |
| 207 | |
| 208 | // 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. Typically |
| 210 | // because it's just the same compiled output in practice. |
| 211 | export type KeyNode = [ |
| 212 | Root | KeyNode /* parent */, |
| 213 | string | null /* name */, |
| 214 | string | number /* key */, |
| 215 | ]; |
| 216 | |
| 217 | type ResumeSlots = |
| 218 | | null // nothing to resume |
| 219 | | number // resume with segment ID at the root position |
| 220 | | {[index: number]: number}; // resume with segmentID at the index |
| 221 | |
| 222 | type 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 | ]; |
| 230 | |
| 231 | type ReplayNode = |
| 232 | | [ |
| 233 | string | null /* name */, |
| 234 | string | number /* key */, |
| 235 | Array<ReplayNode> /* keyed children */, |
| 236 | ResumeSlots /* resumable slots */, |
| 237 | ] |
| 238 | | ReplaySuspenseBoundary; |
| 239 | |
| 240 | type PostponedHoles = { |
| 241 | workingMap: Map<KeyNode, ReplayNode>, |
| 242 | rootNodes: Array<ReplayNode>, |
| 243 | rootSlots: ResumeSlots, |
| 244 | }; |
| 245 | |
| 246 | type LegacyContext = { |
| 247 | [key: string]: any, |
| 248 | }; |
| 249 | |
| 250 | type 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 | }; |
| 258 | |
| 259 | const CLIENT_RENDERED = 4; // if it errors or infinitely suspends |
| 260 | |
| 261 | type 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 content |
| 266 | 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 boundaries |
| 270 | 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 nodes |
| 276 | fallbackNode: null | ReplayNode, // used to track the fallback for replay nodes |
| 277 | }, |
| 278 | errorDigest: ?string, // the error hash if it errors |
| 279 | // DEV-only fields |
| 280 | errorMessage?: null | string, // the error string if it errors |
| 281 | errorStack?: null | string, // the error stack if it errors |
| 282 | errorComponentStack?: null | string, // the error component stack if it errors |
| 283 | }; |
| 284 | |
| 285 | type Ping = { |
| 286 | resolve: () => void, |
| 287 | reject: (error: mixed) => void, |
| 288 | }; |
| 289 | |
| 290 | type 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 to |
| 297 | blockedPreamble: null | PreambleState, |
| 298 | hoistableState: null | HoistableState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary |
| 299 | abortSet: Set<Task>, // the abortable set that this task belongs to |
| 300 | keyPath: Root | KeyNode, // the path of all parent keys currently rendering |
| 301 | formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML) |
| 302 | context: ContextSnapshot, // the current new context that this task is executing in |
| 303 | treeContext: TreeContext, // the current tree context that this task is executing in |
| 304 | row: null | SuspenseListRow, // the current SuspenseList row that this is rendering inside |
| 305 | componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component |
| 306 | thenableState: null | ThenableState, |
| 307 | legacyContext: LegacyContext, // the current legacy context that this task is executing in |
| 308 | debugTask: null | ConsoleTask, // DEV only |
| 309 | // 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 | }; |
| 312 | |
| 313 | type ReplaySet = { |
| 314 | nodes: Array<ReplayNode>, // the possible paths to follow down the replaying |
| 315 | slots: ResumeSlots, // slots to resume |
| 316 | pendingTasks: number, // tracks the number of tasks currently tracking this set of nodes |
| 317 | // if pending tasks reach zero but there are still nodes left, it means we couldn't find |
| 318 | // them all in the tree, so we need to abort and client render the boundary. |
| 319 | }; |
| 320 | |
| 321 | type 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 replay |
| 328 | blockedPreamble: null, |
| 329 | hoistableState: null | HoistableState, // Boundary state we'll mutate while rendering. This may not equal the state of the blockedBoundary |
| 330 | abortSet: Set<Task>, // the abortable set that this task belongs to |
| 331 | keyPath: Root | KeyNode, // the path of all parent keys currently rendering |
| 332 | formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML) |
| 333 | context: ContextSnapshot, // the current new context that this task is executing in |
| 334 | treeContext: TreeContext, // the current tree context that this task is executing in |
| 335 | row: null | SuspenseListRow, // the current SuspenseList row that this is rendering inside |
| 336 | componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component |
| 337 | thenableState: null | ThenableState, |
| 338 | legacyContext: LegacyContext, // the current legacy context that this task is executing in |
| 339 | debugTask: null | ConsoleTask, // DEV only |
| 340 | }; |
| 341 | |
| 342 | export type Task = RenderTask | ReplayTask; |
| 343 | |
| 344 | const PENDING = 0; |
| 345 | const COMPLETED = 1; |
| 346 | const FLUSHED = 2; |
| 347 | const ABORTED = 3; |
| 348 | const ERRORED = 4; |
| 349 | const POSTPONED = 5; |
| 350 | |
| 351 | type Root = null; |
| 352 | |
| 353 | type 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 flushed |
| 356 | id: number, // starts as 0 and is lazily assigned if the parent flushes early |
| 357 | +index: number, // the index within the parent's chunks or 0 at the root |
| 358 | +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 needed |
| 366 | lastPushedText: boolean, |
| 367 | textEmbedded: boolean, |
| 368 | }; |
| 369 | |
| 370 | // The ordering of these statuses matters. OPENING and OPEN are the only |
| 371 | // statuses in which newly scheduled work may be performed. Any status greater |
| 372 | // than OPEN represents a request that no longer admits work. |
| 373 | const OPENING = 10; |
| 374 | const OPEN = 11; |
| 375 | const CLOSING = 12; |
| 376 | const CLOSED = 13; |
| 377 | const STALLED_DEV = 14; |
| 378 | |
| 379 | // Passed to renderLifetimeController.abort(). Nothing reads the reason, but a |
| 380 | // call to abort() without one constructs an AbortError DOMException. Capturing |
| 381 | // the stack trace dominates that cost, and the cost grows with the depth of the |
| 382 | // stack. |
| 383 | const RENDER_ENDED = 'The render ended.'; |
| 384 | |
| 385 | export 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 preamble |
| 400 | byteSize: number, // counts the number of bytes accumulated in the shell |
| 401 | 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 priority |
| 405 | 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, this |
| 410 | // holds the PostponedState returned by getPostponedState. getPostponedState |
| 411 | // snapshots nextSegmentId before the (pull-driven) prelude flush runs, but the |
| 412 | // flush outlines completed boundaries and advances nextSegmentId past that |
| 413 | // snapshot. We finalize the snapshot from flushCompletedQueues so the resumed |
| 414 | // render allocates segment ids strictly above the shell's; otherwise the shell |
| 415 | // and resume emit duplicate B:/S: ids once concatenated. Stays null for live |
| 416 | // 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 production |
| 421 | onError: (error: mixed, errorInfo: ThrownInfo) => ?string, |
| 422 | // onBrowserBailout is called when Fizz recovers by intentionally deferring |
| 423 | // 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 a |
| 430 | // 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 to |
| 433 | // 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 was |
| 437 | // aborted. Bounds the lifetime of anything that must not outlive the render. |
| 438 | // Null until attachAbortSignal creates it, so a render that is given no |
| 439 | // 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 dedupe |
| 444 | didWarnForKey?: null | WeakSet<ComponentStackNode>, |
| 445 | }; |
| 446 | |
| 447 | type Preamble = { |
| 448 | content: PreambleState, |
| 449 | fallback: PreambleState, |
| 450 | }; |
| 451 | |
| 452 | function createPreamble(): Preamble { |
| 453 | return { |
| 454 | content: createPreambleState(), |
| 455 | fallback: createPreambleState(), |
| 456 | }; |
| 457 | } |
| 458 | |
| 459 | // This is a default heuristic for how to split up the HTML content into progressive |
| 460 | // 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 also |
| 462 | // adds unnecessary overhead to do more splits. We don't know if it's a higher or lower |
| 463 | // end device but higher end suffer less from the overhead than lower end does from |
| 464 | // 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 assume |
| 466 | // that there can be a reasonable drop off from max bandwidth which leaves you with |
| 467 | // 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 that |
| 469 | // are downloaded along with the main content. So we estimate about half of that to be |
| 470 | // the lower end throughput. In other words, we expect that you can at least show |
| 471 | // about 12.5kb of content per 500ms. Not counting starting latency for the first |
| 472 | // paint. |
| 473 | // 500 * 1024 / 8 * .8 * 0.5 / 2 |
| 474 | const DEFAULT_PROGRESSIVE_CHUNK_SIZE = 12800; |
| 475 | |
| 476 | function getBlockingRenderMaxSize(request: Request): number { |
| 477 | // We want to make sure that we can block the reveal of a well designed complete |
| 478 | // shell but if you have constructed a too large shell (e.g. by not adding any |
| 479 | // Suspense boundaries) then that might take too long to render. We shouldn't |
| 480 | // 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 a |
| 482 | // reasonably well built UI within it without getting errors. If it's too high |
| 483 | // 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 limit |
| 485 | // be fast enough to hit that. For this argument we assume that most external |
| 486 | // 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 instructions |
| 488 | // 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 a |
| 490 | // decent static host. If it's longer e.g. due to dynamic rendering, then you |
| 491 | // are going to bound by dynamic production of the content and you're better off |
| 492 | // with Suspense boundaries anyway. This number doesn't matter much. Then you |
| 493 | // have about 1.2 seconds left for bandwidth. On 3G that gives you about 112.5kb |
| 494 | // worth of data. That's worth about 10x in terms of uncompressed bytes. Then we |
| 495 | // 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 with |
| 497 | // rel="expect" examples and other documents, the impact on documents smaller than |
| 498 | // that is within the noise. That's because there's enough happening within that |
| 499 | // start up to not make HTML streaming not significantly better. |
| 500 | // Content above the fold tends to be about 100-200kb tops. Therefore 500kb should |
| 501 | // be enough head room for a good loading state. After that you should use |
| 502 | // Suspense or SuspenseList to improve it. |
| 503 | // Since this is highly related to the reason you would adjust the |
| 504 | // progressiveChunkSize option, and always has to be higher, we define this limit |
| 505 | // in terms of it. So if you want to increase the limit because you have high |
| 506 | // bandwidth users, then you can adjust it up. If you are concerned about even |
| 507 | // slower bandwidth then you can adjust it down. |
| 508 | return request.progressiveChunkSize * 40; // 512kb by default. |
| 509 | } |
| 510 | |
| 511 | function 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 up |
| 517 | // 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 outline |
| 523 | // them regardless of their size since the fallbacks should only be emitted if we've |
| 524 | // errored the boundary. |
| 525 | boundary.preamble === null |
| 526 | ); |
| 527 | } |
| 528 | |
| 529 | function 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 with |
| 536 | // 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 wrapper |
| 540 | } |
| 541 | return null; |
| 542 | } |
| 543 | |
| 544 | function 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 === undefined |
| 567 | ? DEFAULT_PROGRESSIVE_CHUNK_SIZE |
| 568 | : 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 | } |
| 600 | |
| 601 | export 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 | } |
| 618 | |
| 619 | // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors |
| 620 | 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 | ); |
| 633 | |
| 634 | // 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 edge |
| 641 | 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 | } |
| 669 | |
| 670 | export 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 | } |
| 705 | |
| 706 | export 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 | } |
| 720 | |
| 721 | // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors |
| 722 | 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; |
| 736 | |
| 737 | 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 edge |
| 745 | 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 | } |
| 773 | |
| 774 | 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 | } |
| 801 | |
| 802 | export 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 | } |
| 832 | |
| 833 | let currentRequest: null | Request = null; |
| 834 | |
| 835 | export 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 | } |
| 844 | |
| 845 | function 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 | } |
| 862 | |
| 863 | function pingRejectedTask(request: Request, task: Task, error: mixed): void { |
| 864 | if (!request.aborted) { |
| 865 | // Replaying the task is what gives ordinary render errors their complete |
| 866 | // 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 could |
| 875 | // reject its wakeable. Finish it with the more specific reason before the |
| 876 | // 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 | } |
| 883 | |
| 884 | function 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 class |
| 909 | 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 | } |
| 930 | |
| 931 | function 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 | } |
| 989 | |
| 990 | function 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 | } |
| 1048 | |
| 1049 | function 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 later |
| 1061 | index, |
| 1062 | chunks: [], |
| 1063 | children: [], |
| 1064 | preambleChildren: [], |
| 1065 | parentFormatContext, |
| 1066 | boundary, |
| 1067 | lastPushedText, |
| 1068 | textEmbedded, |
| 1069 | }; |
| 1070 | } |
| 1071 | |
| 1072 | function 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 | } |
| 1083 | |
| 1084 | function getStackFromNode(stackNode: ComponentStackNode): string { |
| 1085 | return getStackByComponentStackNode(stackNode); |
| 1086 | } |
| 1087 | |
| 1088 | function pushHaltedAwaitOnComponentStack( |
| 1089 | task: Task, |
| 1090 | debugInfo: void | null | ReactDebugInfo, |
| 1091 | ): void { |
| 1092 | if (!__DEV__) { |
| 1093 | // eslint-disable-next-line react-internal/prod-error-codes |
| 1094 | 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 | } |
| 1119 | |
| 1120 | // performWork + retryTask without mutation |
| 1121 | function 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 of |
| 1126 | // taking the abort path. |
| 1127 | request.aborted = false; |
| 1128 | |
| 1129 | const prevContext = getActiveContext(); |
| 1130 | const prevDispatcher = ReactSharedInternals.H; |
| 1131 | ReactSharedInternals.H = HooksDispatcher; |
| 1132 | const prevAsyncDispatcher = ReactSharedInternals.A; |
| 1133 | ReactSharedInternals.A = DefaultAsyncDispatcher; |
| 1134 | |
| 1135 | const prevRequest = currentRequest; |
| 1136 | currentRequest = request; |
| 1137 | |
| 1138 | const prevGetCurrentStackImpl = ReactSharedInternals.getCurrentStack; |
| 1139 | ReactSharedInternals.getCurrentStack = getCurrentStackInDEV; |
| 1140 | |
| 1141 | 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); |
| 1154 | |
| 1155 | ReactSharedInternals.H = prevDispatcher; |
| 1156 | ReactSharedInternals.A = prevAsyncDispatcher; |
| 1157 | |
| 1158 | ReactSharedInternals.getCurrentStack = prevGetCurrentStackImpl; |
| 1159 | if (prevDispatcher === HooksDispatcher) { |
| 1160 | // This means that we were in a reentrant work loop. This could happen |
| 1161 | // 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/default |
| 1164 | // values when leaving because we'll likely need the same or similar |
| 1165 | // context again. However, when we're inside a synchronous loop like this |
| 1166 | // 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 | } |
| 1174 | |
| 1175 | function pushSuspendedCallSiteOnComponentStack( |
| 1176 | request: Request, |
| 1177 | task: Task, |
| 1178 | ): void { |
| 1179 | setCaptureSuspendedCallSiteDEV(true); |
| 1180 | const restoreThenableState = ensureSuspendableThenableStateDEV( |
| 1181 | // refined at the callsite |
| 1182 | task.thenableState as any as ThenableState, |
| 1183 | ); |
| 1184 | try { |
| 1185 | rerenderStalledTask(request, task); |
| 1186 | } finally { |
| 1187 | restoreThenableState(); |
| 1188 | setCaptureSuspendedCallSiteDEV(false); |
| 1189 | } |
| 1190 | |
| 1191 | const suspendCallSiteStack = getSuspendedCallSiteStackDEV(); |
| 1192 | const suspendCallSiteDebugTask = getSuspendedCallSiteDebugTaskDEV(); |
| 1193 | |
| 1194 | 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 | } |
| 1207 | |
| 1208 | function pushServerComponentStack( |
| 1209 | task: Task, |
| 1210 | debugInfo: void | null | ReactDebugInfo, |
| 1211 | ): void { |
| 1212 | if (!__DEV__) { |
| 1213 | // eslint-disable-next-line react-internal/prod-error-codes |
| 1214 | 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 | } |
| 1239 | |
| 1240 | function 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 for |
| 1244 | // the stack frame and then once again while actually |
| 1245 | 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 | } |
| 1283 | |
| 1284 | function createComponentStackFromType( |
| 1285 | parent: null | ComponentStackNode, |
| 1286 | type: Function | string | symbol, |
| 1287 | owner: void | null | ReactComponentInfo | ComponentStackNode, // DEV only |
| 1288 | stack: void | null | string | Error, // DEV only |
| 1289 | ): ComponentStackNode { |
| 1290 | if (__DEV__) { |
| 1291 | return { |
| 1292 | parent, |
| 1293 | type, |
| 1294 | owner, |
| 1295 | stack, |
| 1296 | }; |
| 1297 | } |
| 1298 | return { |
| 1299 | parent, |
| 1300 | type, |
| 1301 | }; |
| 1302 | } |
| 1303 | |
| 1304 | function 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 | } |
| 1317 | |
| 1318 | type ThrownInfo = { |
| 1319 | componentStack?: string, |
| 1320 | }; |
| 1321 | export type ErrorInfo = ThrownInfo; |
| 1322 | |
| 1323 | function 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 | } |
| 1341 | |
| 1342 | function 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 = wasAborted |
| 1353 | ? '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 boundary |
| 1361 | if (error instanceof Error) { |
| 1362 | // eslint-disable-next-line react-internal/safe-string-coercion |
| 1363 | message = String(error.message); |
| 1364 | // eslint-disable-next-line react-internal/safe-string-coercion |
| 1365 | 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-coercion |
| 1371 | message = String(error); |
| 1372 | stack = null; |
| 1373 | } |
| 1374 | const prefix = wasAborted |
| 1375 | ? '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 | } |
| 1382 | |
| 1383 | function 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 | } |
| 1393 | |
| 1394 | // If this callback errors, we intentionally let that error bubble up to become a fatal error |
| 1395 | // so that someone fixes the error reporting instead of hiding it. |
| 1396 | const onError = request.onError; |
| 1397 | const errorDigest = |
| 1398 | __DEV__ && debugTask |
| 1399 | ? 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 it |
| 1403 | // 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, so |
| 1414 | // normalizing it to undefined preserves the existing user-space semantics. |
| 1415 | return errorDigest === '' ? undefined : errorDigest; |
| 1416 | } |
| 1417 | |
| 1418 | function 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 to |
| 1425 | // 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 | } |
| 1433 | |
| 1434 | function 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 outside |
| 1441 | // 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 only |
| 1446 | // fires while root tasks are still pending. onFatalError always fires because |
| 1447 | // 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 must |
| 1467 | // observe. This error may only be a fatal diagnostic derived from it. |
| 1468 | if (!request.aborted) { |
| 1469 | request.fatalError = error; |
| 1470 | } |
| 1471 | } |
| 1472 | } |
| 1473 | |
| 1474 | function 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 through |
| 1482 | // an already completed Suspense boundary. It's too late to do anything about it |
| 1483 | // 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; |
| 1505 | |
| 1506 | 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; |
| 1513 | |
| 1514 | // Each time we enter a suspense boundary, we split out into a new segment for |
| 1515 | // 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; |
| 1521 | |
| 1522 | 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 | ); |
| 1530 | |
| 1531 | 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 them |
| 1539 | 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 parent |
| 1544 | parentSegment.lastPushedText = false; |
| 1545 | |
| 1546 | // 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 them |
| 1553 | false, |
| 1554 | false, |
| 1555 | ); |
| 1556 | // We mark the root segment as having its parent flushed. It's not really flushed but there is |
| 1557 | // no parent segment so there's nothing to wait on. |
| 1558 | contentRootSegment.parentFlushed = true; |
| 1559 | |
| 1560 | 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 synchronously |
| 1563 | // and schedule the content to render later. This is the opposite of what we do during a normal render |
| 1564 | // where we try to skip rendering the fallback if the content itself can render synchronously |
| 1565 | |
| 1566 | // Stash the original stack frame. |
| 1567 | const suspenseComponentStack = task.componentStack; |
| 1568 | |
| 1569 | 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 of |
| 1585 | // the fallback replay node until we determine if the primary content suspends |
| 1586 | fallbackNode: fallbackReplayNode, |
| 1587 | }; |
| 1588 | } |
| 1589 | |
| 1590 | 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 | } |
| 1625 | |
| 1626 | // We create a suspended task for the primary content because we want to allow |
| 1627 | // 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 content |
| 1654 | // If it is successful we will elide the fallback task but if it suspends or errors we schedule |
| 1655 | // the fallback to render. Unlike with prerenders we attempt to deprioritize the fallback render |
| 1656 | |
| 1657 | // 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 task |
| 1659 | // and instead just using the stack if possible. |
| 1660 | // TODO: Call this directly instead of messing with saving and restoring contexts. |
| 1661 | |
| 1662 | // We can reuse the current context and task to render the content immediately without |
| 1663 | // context switching. We just need to temporarily switch which boundary and which segment |
| 1664 | // 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 create |
| 1692 | // the fallback. However, if this boundary ended up big enough to be eligible for outlining |
| 1693 | // 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 outlining |
| 1697 | // 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 requests |
| 1706 | // to have few boundaries which contribute preambles and it allow us to do this |
| 1707 | // 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 | } |
| 1728 | |
| 1729 | 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 | ); |
| 1743 | |
| 1744 | untrackBoundary(request, newBoundary); |
| 1745 | |
| 1746 | // 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 | } |
| 1758 | |
| 1759 | 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 work |
| 1765 | // 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 work |
| 1792 | // on preparing fallbacks if we don't have any more main content to task on. |
| 1793 | request.pingedTasks.push(suspendedFallbackTask); |
| 1794 | } |
| 1795 | } |
| 1796 | |
| 1797 | function 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; |
| 1812 | |
| 1813 | const parentBoundary = task.blockedBoundary; |
| 1814 | const parentHoistableState = task.hoistableState; |
| 1815 | |
| 1816 | const content: ReactNodeList = props.children; |
| 1817 | const fallback: ReactNodeList = props.fallback; |
| 1818 | const defer: boolean = enableCPUSuspense && props.defer === true; |
| 1819 | |
| 1820 | 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; |
| 1831 | |
| 1832 | // We can reuse the current context and task to render the content immediately without |
| 1833 | // context switching. We just need to temporarily switch which boundary and replay node |
| 1834 | // 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}; |
| 1844 | |
| 1845 | try { |
| 1846 | // We use the safe form because we don't handle suspending here. Only error handling. |
| 1847 | renderNode(request, task, content, -1); |
| 1848 | |
| 1849 | 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 === PENDING |
| 1859 | ) { |
| 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 create |
| 1862 | // the fallback. |
| 1863 | resumedBoundary.status = COMPLETED; |
| 1864 | request.completedBoundaries.push(resumedBoundary); |
| 1865 | // We restore the parent componentStack. Semantically this is the same as |
| 1866 | // popComponentStack(task) but we do this instead because it should be slightly |
| 1867 | // faster |
| 1868 | 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 | ); |
| 1887 | |
| 1888 | task.replay.pendingTasks--; |
| 1889 | |
| 1890 | // The parent already flushed in the prerender so we need to schedule this to be emitted. |
| 1891 | request.clientRenderedBoundaries.push(resumedBoundary); |
| 1892 | |
| 1893 | // 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 | } |
| 1904 | |
| 1905 | const fallbackKeyPath: KeyNode = [ |
| 1906 | keyPath[0], |
| 1907 | 'Suspense Fallback', |
| 1908 | keyPath[2], |
| 1909 | ]; |
| 1910 | |
| 1911 | // We create suspended task for the fallback because we don't want to actually work |
| 1912 | // 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 | ); |
| 1939 | |
| 1940 | pushComponentStack(suspendedFallbackTask); |
| 1941 | // TODO: This should be queued at a separate lower priority queue so that we only work |
| 1942 | // on preparing fallbacks if we don't have any more main content to task on. |
| 1943 | request.pingedTasks.push(suspendedFallbackTask); |
| 1944 | } |
| 1945 | |
| 1946 | function 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 | } |
| 1950 | |
| 1951 | function 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 be |
| 1960 | // 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 from |
| 1964 | // 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 above |
| 1968 | // 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 | } |
| 1990 | |
| 1991 | function 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 finishedTask |
| 1997 | // 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) { |
| 2001 | postponedRow.boundaries = null; |
| 2002 | for (let i = 0; i < postponedBoundaries.length; i++) { |
| 2003 | const postponedBoundary = postponedBoundaries[i]; |
| 2004 | trackPostponedBoundary(request, trackedPostpones, postponedBoundary); |
| 2005 | finishedTask(request, postponedBoundary, null, null); |
| 2006 | } |
| 2007 | } |
| 2008 | } |
| 2009 | } |
| 2010 | |
| 2011 | function tryToResolveTogetherRow( |
| 2012 | request: Request, |
| 2013 | togetherRow: SuspenseListRow, |
| 2014 | ): void { |
| 2015 | // If we have a "together" row and all the pendingTasks are really the boundaries themselves, |
| 2016 | // and we won't outline any of them then we can unblock this row early so that we can inline |
| 2017 | // all the boundaries at once. |
| 2018 | const boundaries = togetherRow.boundaries; |
| 2019 | if (boundaries === null || togetherRow.pendingTasks !== boundaries.length) { |
| 2020 | return; |
| 2021 | } |
| 2022 | let allCompleteAndInlinable = true; |
| 2023 | for (let i = 0; i < boundaries.length; i++) { |
| 2024 | const rowBoundary = boundaries[i]; |
| 2025 | if ( |
| 2026 | rowBoundary.pendingTasks !== 1 || |
| 2027 | rowBoundary.parentFlushed || |
| 2028 | isEligibleForOutlining(request, rowBoundary) |
| 2029 | ) { |
| 2030 | allCompleteAndInlinable = false; |
| 2031 | break; |
| 2032 | } |
| 2033 | } |
| 2034 | if (allCompleteAndInlinable) { |
| 2035 | unblockSuspenseListRow(request, togetherRow, togetherRow.hoistables); |
| 2036 | } |
| 2037 | } |
| 2038 | |
| 2039 | function createSuspenseListRow( |
| 2040 | previousRow: null | SuspenseListRow, |
| 2041 | ): SuspenseListRow { |
| 2042 | const newRow: SuspenseListRow = { |
| 2043 | pendingTasks: 1, // At first the row is blocked on attempting rendering itself. |
| 2044 | boundaries: null, |
| 2045 | hoistables: createHoistableState(), |
| 2046 | inheritedHoistables: null, |
| 2047 | together: false, |
| 2048 | next: null, |
| 2049 | }; |
| 2050 | if (previousRow !== null && previousRow.pendingTasks > 0) { |
| 2051 | // If the previous row is not done yet, we add ourselves to be blocked on it. |
| 2052 | // When it finishes, we'll decrement our pending tasks. |
| 2053 | newRow.pendingTasks++; |
| 2054 | newRow.boundaries = []; |
| 2055 | previousRow.next = newRow; |
| 2056 | } |
| 2057 | return newRow; |
| 2058 | } |
| 2059 | |
| 2060 | function renderSuspenseListRows( |
| 2061 | request: Request, |
| 2062 | task: Task, |
| 2063 | keyPath: KeyNode, |
| 2064 | rows: Array<ReactNodeList>, |
| 2065 | revealOrder: void | 'forwards' | 'backwards' | 'unstable_legacy-backwards', |
| 2066 | ): void { |
| 2067 | // This is a fork of renderChildrenArray that's aware of tracking rows. |
| 2068 | const prevKeyPath = task.keyPath; |
| 2069 | const prevTreeContext = task.treeContext; |
| 2070 | const prevRow = task.row; |
| 2071 | const previousComponentStack = task.componentStack; |
| 2072 | let previousDebugTask = null; |
| 2073 | if (__DEV__) { |
| 2074 | previousDebugTask = task.debugTask; |
| 2075 | // We read debugInfo from task.node.props.children instead of rows because it |
| 2076 | // might have been an unwrapped iterable so we read from the original node. |
| 2077 | pushServerComponentStack( |
| 2078 | task, |
| 2079 | (task.node as any).props.children._debugInfo, |
| 2080 | ); |
| 2081 | } |
| 2082 | |
| 2083 | task.keyPath = keyPath; |
| 2084 | |
| 2085 | const totalChildren = rows.length; |
| 2086 | let previousSuspenseListRow: null | SuspenseListRow = null; |
| 2087 | if (task.replay !== null) { |
| 2088 | // Replay |
| 2089 | // First we need to check if we have any resume slots at this level. |
| 2090 | const resumeSlots = task.replay.slots; |
| 2091 | if (resumeSlots !== null && typeof resumeSlots === 'object') { |
| 2092 | for (let n = 0; n < totalChildren; n++) { |
| 2093 | // Since we are going to resume into a slot whose order was already |
| 2094 | // determined by the prerender, we can safely resume it even in reverse |
| 2095 | // render order. |
| 2096 | const i = |
| 2097 | revealOrder !== 'backwards' && |
| 2098 | revealOrder !== 'unstable_legacy-backwards' |
| 2099 | ? n |
| 2100 | : totalChildren - 1 - n; |
| 2101 | const node = rows[i]; |
| 2102 | task.row = previousSuspenseListRow = createSuspenseListRow( |
| 2103 | previousSuspenseListRow, |
| 2104 | ); |
| 2105 | task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i); |
| 2106 | const resumeSegmentID = resumeSlots[i]; |
| 2107 | // TODO: If this errors we should still continue with the next sibling. |
| 2108 | if (typeof resumeSegmentID === 'number') { |
| 2109 | resumeNode(request, task, resumeSegmentID, node, i); |
| 2110 | // We finished rendering this node, so now we can consume this |
| 2111 | // slot. This must happen after in case we rerender this task. |
| 2112 | delete resumeSlots[i]; |
| 2113 | } else { |
| 2114 | renderNode(request, task, node, i); |
| 2115 | } |
| 2116 | if (--previousSuspenseListRow.pendingTasks === 0) { |
| 2117 | finishSuspenseListRow(request, previousSuspenseListRow); |
| 2118 | } |
| 2119 | } |
| 2120 | } else { |
| 2121 | for (let n = 0; n < totalChildren; n++) { |
| 2122 | // Since we are going to resume into a slot whose order was already |
| 2123 | // determined by the prerender, we can safely resume it even in reverse |
| 2124 | // render order. |
| 2125 | const i = |
| 2126 | revealOrder !== 'backwards' && |
| 2127 | revealOrder !== 'unstable_legacy-backwards' |
| 2128 | ? n |
| 2129 | : totalChildren - 1 - n; |
| 2130 | const node = rows[i]; |
| 2131 | if (__DEV__) { |
| 2132 | warnForMissingKey(request, task, node); |
| 2133 | } |
| 2134 | task.row = previousSuspenseListRow = createSuspenseListRow( |
| 2135 | previousSuspenseListRow, |
| 2136 | ); |
| 2137 | task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i); |
| 2138 | renderNode(request, task, node, i); |
| 2139 | if (--previousSuspenseListRow.pendingTasks === 0) { |
| 2140 | finishSuspenseListRow(request, previousSuspenseListRow); |
| 2141 | } |
| 2142 | } |
| 2143 | } |
| 2144 | } else { |
| 2145 | task = task as any as RenderTask; // Refined |
| 2146 | if ( |
| 2147 | revealOrder !== 'backwards' && |
| 2148 | revealOrder !== 'unstable_legacy-backwards' |
| 2149 | ) { |
| 2150 | // Forwards direction |
| 2151 | for (let i = 0; i < totalChildren; i++) { |
| 2152 | const node = rows[i]; |
| 2153 | if (__DEV__) { |
| 2154 | warnForMissingKey(request, task, node); |
| 2155 | } |
| 2156 | task.row = previousSuspenseListRow = createSuspenseListRow( |
| 2157 | previousSuspenseListRow, |
| 2158 | ); |
| 2159 | task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i); |
| 2160 | renderNode(request, task, node, i); |
| 2161 | if (--previousSuspenseListRow.pendingTasks === 0) { |
| 2162 | finishSuspenseListRow(request, previousSuspenseListRow); |
| 2163 | } |
| 2164 | } |
| 2165 | } else { |
| 2166 | // For backwards direction we need to do things a bit differently. |
| 2167 | // We give each row its own segment so that we can render the content in |
| 2168 | // reverse order but still emit it in the right order when we flush. |
| 2169 | const parentSegment = task.blockedSegment; |
| 2170 | const childIndex = parentSegment.children.length; |
| 2171 | const insertionIndex = parentSegment.chunks.length; |
| 2172 | for (let n = 0; n < totalChildren; n++) { |
| 2173 | const i = |
| 2174 | revealOrder === 'unstable_legacy-backwards' |
| 2175 | ? totalChildren - 1 - n |
| 2176 | : n; |
| 2177 | const node = rows[i]; |
| 2178 | task.row = previousSuspenseListRow = createSuspenseListRow( |
| 2179 | previousSuspenseListRow, |
| 2180 | ); |
| 2181 | task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i); |
| 2182 | const newSegment = createPendingSegment( |
| 2183 | request, |
| 2184 | insertionIndex, |
| 2185 | null, |
| 2186 | task.formatContext, |
| 2187 | // Assume we are text embedded at the trailing edges |
| 2188 | i === 0 ? parentSegment.lastPushedText : true, |
| 2189 | true, |
| 2190 | ); |
| 2191 | // Insert in the beginning of the sequence, which will insert before any previous rows. |
| 2192 | parentSegment.children.splice(childIndex, 0, newSegment); |
| 2193 | task.blockedSegment = newSegment; |
| 2194 | if (__DEV__) { |
| 2195 | warnForMissingKey(request, task, node); |
| 2196 | } |
| 2197 | try { |
| 2198 | renderNode(request, task, node, i); |
| 2199 | pushSegmentFinale( |
| 2200 | newSegment.chunks, |
| 2201 | request.renderState, |
| 2202 | newSegment.lastPushedText, |
| 2203 | newSegment.textEmbedded, |
| 2204 | ); |
| 2205 | newSegment.status = COMPLETED; |
| 2206 | finishedSegment(request, task.blockedBoundary, newSegment); |
| 2207 | if (--previousSuspenseListRow.pendingTasks === 0) { |
| 2208 | finishSuspenseListRow(request, previousSuspenseListRow); |
| 2209 | } |
| 2210 | } catch (thrownValue: mixed) { |
| 2211 | if (request.aborted) { |
| 2212 | newSegment.status = ABORTED; |
| 2213 | } else { |
| 2214 | newSegment.status = ERRORED; |
| 2215 | } |
| 2216 | throw thrownValue; |
| 2217 | } |
| 2218 | } |
| 2219 | task.blockedSegment = parentSegment; |
| 2220 | // Reset lastPushedText for current Segment since the new Segments "consumed" it |
| 2221 | parentSegment.lastPushedText = false; |
| 2222 | } |
| 2223 | } |
| 2224 | |
| 2225 | if ( |
| 2226 | prevRow !== null && |
| 2227 | previousSuspenseListRow !== null && |
| 2228 | previousSuspenseListRow.pendingTasks > 0 |
| 2229 | ) { |
| 2230 | // If we are part of an outer SuspenseList and our last row is still pending, then that blocks |
| 2231 | // the parent row from completing. We can continue the chain. |
| 2232 | prevRow.pendingTasks++; |
| 2233 | previousSuspenseListRow.next = prevRow; |
| 2234 | } |
| 2235 | |
| 2236 | // Because this context is always set right before rendering every child, we |
| 2237 | // only need to reset it to the previous value at the very end. |
| 2238 | task.treeContext = prevTreeContext; |
| 2239 | task.row = prevRow; |
| 2240 | task.keyPath = prevKeyPath; |
| 2241 | if (__DEV__) { |
| 2242 | task.componentStack = previousComponentStack; |
| 2243 | task.debugTask = previousDebugTask; |
| 2244 | } |
| 2245 | } |
| 2246 | |
| 2247 | function renderSuspenseList( |
| 2248 | request: Request, |
| 2249 | task: Task, |
| 2250 | keyPath: KeyNode, |
| 2251 | props: SuspenseListProps, |
| 2252 | ): void { |
| 2253 | const children: any = props.children; |
| 2254 | const revealOrder: SuspenseListRevealOrder = props.revealOrder; |
| 2255 | // TODO: Support tail hidden/collapsed modes. |
| 2256 | // const tailMode: SuspenseListTailMode = props.tail; |
| 2257 | if (revealOrder !== 'independent' && revealOrder !== 'together') { |
| 2258 | // For ordered reveal, we need to produce rows from the children. |
| 2259 | if (isArray(children)) { |
| 2260 | renderSuspenseListRows(request, task, keyPath, children, revealOrder); |
| 2261 | return; |
| 2262 | } |
| 2263 | const iteratorFn = getIteratorFn(children); |
| 2264 | if (iteratorFn) { |
| 2265 | const iterator = iteratorFn.call(children); |
| 2266 | if (iterator) { |
| 2267 | if (__DEV__) { |
| 2268 | validateIterable(task, children, -1, iterator, iteratorFn); |
| 2269 | } |
| 2270 | // TODO: We currently use the same id algorithm as regular nodes |
| 2271 | // but we need a new algorithm for SuspenseList that doesn't require |
| 2272 | // a full set to be loaded up front to support Async Iterable. |
| 2273 | // When we have that, we shouldn't buffer anymore. |
| 2274 | let step = iterator.next(); |
| 2275 | if (!step.done) { |
| 2276 | const rows = []; |
| 2277 | do { |
| 2278 | rows.push(step.value); |
| 2279 | step = iterator.next(); |
| 2280 | } while (!step.done); |
| 2281 | renderSuspenseListRows(request, task, keyPath, children, revealOrder); |
| 2282 | } |
| 2283 | return; |
| 2284 | } |
| 2285 | } |
| 2286 | if ( |
| 2287 | enableAsyncIterableChildren && |
| 2288 | typeof (children as any)[ASYNC_ITERATOR] === 'function' |
| 2289 | ) { |
| 2290 | const iterator: AsyncIterator<ReactNodeList> = (children as any)[ |
| 2291 | ASYNC_ITERATOR |
| 2292 | ](); |
| 2293 | if (iterator) { |
| 2294 | if (__DEV__) { |
| 2295 | validateAsyncIterable(task, children as any, -1, iterator); |
| 2296 | } |
| 2297 | // TODO: Update the task.children to be the iterator to avoid asking |
| 2298 | // for new iterators, but we currently warn for rendering these |
| 2299 | // so needs some refactoring to deal with the warning. |
| 2300 | |
| 2301 | // Restore the thenable state before resuming. |
| 2302 | const prevThenableState = task.thenableState; |
| 2303 | task.thenableState = null; |
| 2304 | prepareToUseThenableState(prevThenableState); |
| 2305 | |
| 2306 | // We need to know how many total rows are in this set, so that we |
| 2307 | // can allocate enough id slots to acommodate them. So we must exhaust |
| 2308 | // the iterator before we start recursively rendering the rows. |
| 2309 | // TODO: This is not great but I think it's inherent to the id |
| 2310 | // generation algorithm. |
| 2311 | |
| 2312 | const rows = []; |
| 2313 | |
| 2314 | let done = false; |
| 2315 | |
| 2316 | // $FlowFixMe[invalid-compare] |
| 2317 | if (iterator === children) { |
| 2318 | // If it's an iterator we need to continue reading where we left |
| 2319 | // off. We can do that by reading the first few rows from the previous |
| 2320 | // thenable state. |
| 2321 | // $FlowFixMe[underconstrained-implicit-instantiation] |
| 2322 | let step = readPreviousThenableFromState(); |
| 2323 | while (step !== undefined) { |
| 2324 | if (step.done) { |
| 2325 | done = true; |
| 2326 | break; |
| 2327 | } |
| 2328 | rows.push(step.value); |
| 2329 | step = readPreviousThenableFromState(); |
| 2330 | } |
| 2331 | } |
| 2332 | |
| 2333 | if (!done) { |
| 2334 | let step = unwrapThenable(iterator.next()); |
| 2335 | while (!step.done) { |
| 2336 | rows.push(step.value); |
| 2337 | step = unwrapThenable(iterator.next()); |
| 2338 | } |
| 2339 | } |
| 2340 | renderSuspenseListRows(request, task, keyPath, rows, revealOrder); |
| 2341 | return; |
| 2342 | } |
| 2343 | } |
| 2344 | // This case will warn on the client. It's the same as independent revealOrder. |
| 2345 | } |
| 2346 | |
| 2347 | if (revealOrder === 'together') { |
| 2348 | const prevKeyPath = task.keyPath; |
| 2349 | const prevRow = task.row; |
| 2350 | const newRow = (task.row = createSuspenseListRow(null)); |
| 2351 | // This will cause boundaries to block on this row, but there's nothing to |
| 2352 | // unblock them. We'll use the partial flushing pass to unblock them. |
| 2353 | newRow.boundaries = []; |
| 2354 | newRow.together = true; |
| 2355 | task.keyPath = keyPath; |
| 2356 | renderNodeDestructive(request, task, children, -1); |
| 2357 | if (--newRow.pendingTasks === 0) { |
| 2358 | finishSuspenseListRow(request, newRow); |
| 2359 | } |
| 2360 | task.keyPath = prevKeyPath; |
| 2361 | task.row = prevRow; |
| 2362 | if (prevRow !== null && newRow.pendingTasks > 0) { |
| 2363 | // If we are part of an outer SuspenseList and our row is still pending, then that blocks |
| 2364 | // the parent row from completing. We can continue the chain. |
| 2365 | prevRow.pendingTasks++; |
| 2366 | newRow.next = prevRow; |
| 2367 | } |
| 2368 | return; |
| 2369 | } |
| 2370 | // For other reveal order modes, we just render it as a fragment. |
| 2371 | const prevKeyPath = task.keyPath; |
| 2372 | task.keyPath = keyPath; |
| 2373 | renderNodeDestructive(request, task, children, -1); |
| 2374 | task.keyPath = prevKeyPath; |
| 2375 | } |
| 2376 | |
| 2377 | function renderPreamble( |
| 2378 | request: Request, |
| 2379 | task: RenderTask, |
| 2380 | blockedSegment: Segment, |
| 2381 | node: ReactNodeList, |
| 2382 | ): void { |
| 2383 | const preambleSegment = createPendingSegment( |
| 2384 | request, |
| 2385 | 0, |
| 2386 | null, |
| 2387 | task.formatContext, |
| 2388 | false, |
| 2389 | false, |
| 2390 | ); |
| 2391 | blockedSegment.preambleChildren.push(preambleSegment); |
| 2392 | task.blockedSegment = preambleSegment; |
| 2393 | try { |
| 2394 | renderNode(request, task, node, -1); |
| 2395 | pushSegmentFinale( |
| 2396 | preambleSegment.chunks, |
| 2397 | request.renderState, |
| 2398 | preambleSegment.lastPushedText, |
| 2399 | preambleSegment.textEmbedded, |
| 2400 | ); |
| 2401 | preambleSegment.status = COMPLETED; |
| 2402 | finishedSegment(request, task.blockedBoundary, preambleSegment); |
| 2403 | } finally { |
| 2404 | task.blockedSegment = blockedSegment; |
| 2405 | } |
| 2406 | } |
| 2407 | |
| 2408 | function renderHostElement( |
| 2409 | request: Request, |
| 2410 | task: Task, |
| 2411 | keyPath: KeyNode, |
| 2412 | type: string, |
| 2413 | props: Object, |
| 2414 | ): void { |
| 2415 | const segment = task.blockedSegment; |
| 2416 | if (segment === null) { |
| 2417 | // Replay |
| 2418 | const children = props.children; // TODO: Make this a Config for replaying. |
| 2419 | const prevContext = task.formatContext; |
| 2420 | const prevKeyPath = task.keyPath; |
| 2421 | task.formatContext = getChildFormatContext(prevContext, type, props); |
| 2422 | task.keyPath = keyPath; |
| 2423 | |
| 2424 | // We use the non-destructive form because if something suspends, we still |
| 2425 | // need to pop back up and finish this subtree of HTML. |
| 2426 | renderNode(request, task, children, -1); |
| 2427 | |
| 2428 | // We expect that errors will fatal the whole task and that we don't need |
| 2429 | // the correct context. Therefore this is not in a finally. |
| 2430 | task.formatContext = prevContext; |
| 2431 | task.keyPath = prevKeyPath; |
| 2432 | } else { |
| 2433 | // Render |
| 2434 | // RenderTask always has a preambleState |
| 2435 | const children = pushStartInstance( |
| 2436 | segment.chunks, |
| 2437 | type, |
| 2438 | props, |
| 2439 | request.resumableState, |
| 2440 | request.renderState, |
| 2441 | task.blockedPreamble, |
| 2442 | task.hoistableState, |
| 2443 | task.formatContext, |
| 2444 | segment.lastPushedText, |
| 2445 | ); |
| 2446 | segment.lastPushedText = false; |
| 2447 | const prevContext = task.formatContext; |
| 2448 | const prevKeyPath = task.keyPath; |
| 2449 | task.keyPath = keyPath; |
| 2450 | |
| 2451 | const newContext = (task.formatContext = getChildFormatContext( |
| 2452 | prevContext, |
| 2453 | type, |
| 2454 | props, |
| 2455 | )); |
| 2456 | if (isPreambleContext(newContext)) { |
| 2457 | // $FlowFixMe[incompatible-type]: Refined |
| 2458 | renderPreamble(request, task as RenderTask, segment, children); |
| 2459 | } else { |
| 2460 | // We use the non-destructive form because if something suspends, we still |
| 2461 | // need to pop back up and finish this subtree of HTML. |
| 2462 | renderNode(request, task, children, -1); |
| 2463 | } |
| 2464 | |
| 2465 | // We expect that errors will fatal the whole task and that we don't need |
| 2466 | // the correct context. Therefore this is not in a finally. |
| 2467 | task.formatContext = prevContext; |
| 2468 | task.keyPath = prevKeyPath; |
| 2469 | pushEndInstance( |
| 2470 | segment.chunks, |
| 2471 | type, |
| 2472 | props, |
| 2473 | request.resumableState, |
| 2474 | prevContext, |
| 2475 | ); |
| 2476 | segment.lastPushedText = false; |
| 2477 | } |
| 2478 | } |
| 2479 | |
| 2480 | function shouldConstruct(Component: any) { |
| 2481 | return Component.prototype && Component.prototype.isReactComponent; |
| 2482 | } |
| 2483 | |
| 2484 | function renderWithHooks<Props, SecondArg>( |
| 2485 | request: Request, |
| 2486 | task: Task, |
| 2487 | keyPath: KeyNode, |
| 2488 | Component: (p: Props, arg: SecondArg) => any, |
| 2489 | props: Props, |
| 2490 | secondArg: SecondArg, |
| 2491 | ): any { |
| 2492 | // Reset the task's thenable state before continuing, so that if a later |
| 2493 | // component suspends we can reuse the same task object. If the same |
| 2494 | // component suspends again, the thenable state will be restored. |
| 2495 | const prevThenableState = task.thenableState; |
| 2496 | task.thenableState = null; |
| 2497 | const componentIdentity = {}; |
| 2498 | prepareToUseHooks( |
| 2499 | request, |
| 2500 | task, |
| 2501 | keyPath, |
| 2502 | componentIdentity, |
| 2503 | prevThenableState, |
| 2504 | ); |
| 2505 | let result; |
| 2506 | if (__DEV__) { |
| 2507 | result = callComponentInDEV(Component, props, secondArg); |
| 2508 | } else { |
| 2509 | result = Component(props, secondArg); |
| 2510 | } |
| 2511 | return finishHooks(Component, props, result, secondArg); |
| 2512 | } |
| 2513 | |
| 2514 | function finishClassComponent( |
| 2515 | request: Request, |
| 2516 | task: Task, |
| 2517 | keyPath: KeyNode, |
| 2518 | instance: any, |
| 2519 | Component: any, |
| 2520 | props: any, |
| 2521 | ): ReactNodeList { |
| 2522 | let nextChildren; |
| 2523 | if (__DEV__) { |
| 2524 | nextChildren = callRenderInDEV(instance) as any; |
| 2525 | } else { |
| 2526 | nextChildren = instance.render(); |
| 2527 | } |
| 2528 | if (request.aborted) { |
| 2529 | // eslint-disable-next-line no-throw-literal |
| 2530 | throw null; |
| 2531 | } |
| 2532 | |
| 2533 | if (__DEV__) { |
| 2534 | if (instance.props !== props) { |
| 2535 | if (!didWarnAboutReassigningProps) { |
| 2536 | console.error( |
| 2537 | 'It looks like %s is reassigning its own `this.props` while rendering. ' + |
| 2538 | 'This is not supported and can lead to confusing bugs.', |
| 2539 | getComponentNameFromType(Component) || 'a component', |
| 2540 | ); |
| 2541 | } |
| 2542 | didWarnAboutReassigningProps = true; |
| 2543 | } |
| 2544 | } |
| 2545 | |
| 2546 | if (!disableLegacyContext) { |
| 2547 | const childContextTypes = Component.childContextTypes; |
| 2548 | if (childContextTypes !== null && childContextTypes !== undefined) { |
| 2549 | const previousContext = task.legacyContext; |
| 2550 | const mergedContext = processChildContext( |
| 2551 | instance, |
| 2552 | Component, |
| 2553 | previousContext, |
| 2554 | childContextTypes, |
| 2555 | ); |
| 2556 | task.legacyContext = mergedContext; |
| 2557 | renderNodeDestructive(request, task, nextChildren, -1); |
| 2558 | task.legacyContext = previousContext; |
| 2559 | return; |
| 2560 | } |
| 2561 | } |
| 2562 | |
| 2563 | const prevKeyPath = task.keyPath; |
| 2564 | task.keyPath = keyPath; |
| 2565 | renderNodeDestructive(request, task, nextChildren, -1); |
| 2566 | task.keyPath = prevKeyPath; |
| 2567 | } |
| 2568 | |
| 2569 | export function resolveClassComponentProps( |
| 2570 | Component: any, |
| 2571 | baseProps: Object, |
| 2572 | ): Object { |
| 2573 | let newProps = baseProps; |
| 2574 | |
| 2575 | // Remove ref from the props object, if it exists. |
| 2576 | if ('ref' in baseProps) { |
| 2577 | newProps = {} as any; |
| 2578 | for (const propName in baseProps) { |
| 2579 | if (propName !== 'ref') { |
| 2580 | newProps[propName] = baseProps[propName]; |
| 2581 | } |
| 2582 | } |
| 2583 | } |
| 2584 | |
| 2585 | // Resolve default props. |
| 2586 | const defaultProps = Component.defaultProps; |
| 2587 | if (defaultProps) { |
| 2588 | // We may have already copied the props object above to remove ref. If so, |
| 2589 | // we can modify that. Otherwise, copy the props object with Object.assign. |
| 2590 | if (newProps === baseProps) { |
| 2591 | newProps = assign({}, newProps, baseProps); |
| 2592 | } |
| 2593 | // Taken from old JSX runtime, where this used to live. |
| 2594 | for (const propName in defaultProps) { |
| 2595 | if (newProps[propName] === undefined) { |
| 2596 | newProps[propName] = defaultProps[propName]; |
| 2597 | } |
| 2598 | } |
| 2599 | } |
| 2600 | |
| 2601 | return newProps; |
| 2602 | } |
| 2603 | |
| 2604 | function renderClassComponent( |
| 2605 | request: Request, |
| 2606 | task: Task, |
| 2607 | keyPath: KeyNode, |
| 2608 | Component: any, |
| 2609 | props: any, |
| 2610 | ): void { |
| 2611 | const resolvedProps = resolveClassComponentProps(Component, props); |
| 2612 | const maskedContext = !disableLegacyContext |
| 2613 | ? getMaskedContext(Component, task.legacyContext) |
| 2614 | : undefined; |
| 2615 | const instance = constructClassInstance( |
| 2616 | Component, |
| 2617 | resolvedProps, |
| 2618 | maskedContext, |
| 2619 | ); |
| 2620 | mountClassInstance(instance, Component, resolvedProps, maskedContext); |
| 2621 | finishClassComponent( |
| 2622 | request, |
| 2623 | task, |
| 2624 | keyPath, |
| 2625 | instance, |
| 2626 | Component, |
| 2627 | resolvedProps, |
| 2628 | ); |
| 2629 | } |
| 2630 | |
| 2631 | const didWarnAboutBadClass: {[string]: boolean} = {}; |
| 2632 | const didWarnAboutContextTypes: {[string]: boolean} = {}; |
| 2633 | const didWarnAboutContextTypeOnFunctionComponent: {[string]: boolean} = {}; |
| 2634 | const didWarnAboutGetDerivedStateOnFunctionComponent: {[string]: boolean} = {}; |
| 2635 | let didWarnAboutReassigningProps = false; |
| 2636 | let didWarnAboutGenerators = false; |
| 2637 | let didWarnAboutMaps = false; |
| 2638 | |
| 2639 | function renderFunctionComponent( |
| 2640 | request: Request, |
| 2641 | task: Task, |
| 2642 | keyPath: KeyNode, |
| 2643 | Component: any, |
| 2644 | props: any, |
| 2645 | ): void { |
| 2646 | let legacyContext; |
| 2647 | if (!disableLegacyContext && !disableLegacyContextForFunctionComponents) { |
| 2648 | legacyContext = getMaskedContext(Component, task.legacyContext); |
| 2649 | } |
| 2650 | if (__DEV__) { |
| 2651 | if ( |
| 2652 | Component.prototype && |
| 2653 | typeof Component.prototype.render === 'function' |
| 2654 | ) { |
| 2655 | const componentName = getComponentNameFromType(Component) || 'Unknown'; |
| 2656 | |
| 2657 | if (!didWarnAboutBadClass[componentName]) { |
| 2658 | console.error( |
| 2659 | "The <%s /> component appears to have a render method, but doesn't extend React.Component. " + |
| 2660 | 'This is likely to cause errors. Change %s to extend React.Component instead.', |
| 2661 | componentName, |
| 2662 | componentName, |
| 2663 | ); |
| 2664 | didWarnAboutBadClass[componentName] = true; |
| 2665 | } |
| 2666 | } |
| 2667 | } |
| 2668 | |
| 2669 | const value = renderWithHooks( |
| 2670 | request, |
| 2671 | task, |
| 2672 | keyPath, |
| 2673 | Component, |
| 2674 | props, |
| 2675 | legacyContext, |
| 2676 | ); |
| 2677 | if (request.aborted) { |
| 2678 | // eslint-disable-next-line no-throw-literal |
| 2679 | throw null; |
| 2680 | } |
| 2681 | |
| 2682 | const hasId = checkDidRenderIdHook(); |
| 2683 | const actionStateCount = getActionStateCount(); |
| 2684 | const actionStateMatchingIndex = getActionStateMatchingIndex(); |
| 2685 | |
| 2686 | if (__DEV__) { |
| 2687 | if (Component.contextTypes) { |
| 2688 | const componentName = getComponentNameFromType(Component) || 'Unknown'; |
| 2689 | if (!didWarnAboutContextTypes[componentName]) { |
| 2690 | didWarnAboutContextTypes[componentName] = true; |
| 2691 | if (disableLegacyContext) { |
| 2692 | console.error( |
| 2693 | '%s uses the legacy contextTypes API which was removed in React 19. ' + |
| 2694 | 'Use React.createContext() with React.useContext() instead. ' + |
| 2695 | '(https://react.dev/link/legacy-context)', |
| 2696 | componentName, |
| 2697 | ); |
| 2698 | } else { |
| 2699 | console.error( |
| 2700 | '%s uses the legacy contextTypes API which will be removed soon. ' + |
| 2701 | 'Use React.createContext() with React.useContext() instead. ' + |
| 2702 | '(https://react.dev/link/legacy-context)', |
| 2703 | componentName, |
| 2704 | ); |
| 2705 | } |
| 2706 | } |
| 2707 | } |
| 2708 | } |
| 2709 | if (__DEV__) { |
| 2710 | validateFunctionComponentInDev(Component); |
| 2711 | } |
| 2712 | finishFunctionComponent( |
| 2713 | request, |
| 2714 | task, |
| 2715 | keyPath, |
| 2716 | value, |
| 2717 | hasId, |
| 2718 | actionStateCount, |
| 2719 | actionStateMatchingIndex, |
| 2720 | ); |
| 2721 | } |
| 2722 | |
| 2723 | function finishFunctionComponent( |
| 2724 | request: Request, |
| 2725 | task: Task, |
| 2726 | keyPath: KeyNode, |
| 2727 | children: ReactNodeList, |
| 2728 | hasId: boolean, |
| 2729 | actionStateCount: number, |
| 2730 | actionStateMatchingIndex: number, |
| 2731 | ) { |
| 2732 | let didEmitActionStateMarkers = false; |
| 2733 | if (actionStateCount !== 0 && request.formState !== null) { |
| 2734 | // For each useActionState hook, emit a marker that indicates whether we |
| 2735 | // rendered using the form state passed at the root. We only emit these |
| 2736 | // markers if form state is passed at the root. |
| 2737 | const segment = task.blockedSegment; |
| 2738 | if (segment === null) { |
| 2739 | // Implies we're in reumable mode. |
| 2740 | } else { |
| 2741 | didEmitActionStateMarkers = true; |
| 2742 | const target = segment.chunks; |
| 2743 | for (let i = 0; i < actionStateCount; i++) { |
| 2744 | if (i === actionStateMatchingIndex) { |
| 2745 | pushFormStateMarkerIsMatching(target); |
| 2746 | } else { |
| 2747 | pushFormStateMarkerIsNotMatching(target); |
| 2748 | } |
| 2749 | } |
| 2750 | } |
| 2751 | } |
| 2752 | |
| 2753 | const prevKeyPath = task.keyPath; |
| 2754 | task.keyPath = keyPath; |
| 2755 | if (hasId) { |
| 2756 | // This component materialized an id. We treat this as its own level, with |
| 2757 | // a single "child" slot. |
| 2758 | const prevTreeContext = task.treeContext; |
| 2759 | const totalChildren = 1; |
| 2760 | const index = 0; |
| 2761 | // Modify the id context. Because we'll need to reset this if something |
| 2762 | // suspends or errors, we'll use the non-destructive render path. |
| 2763 | task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); |
| 2764 | renderNode(request, task, children, -1); |
| 2765 | // Like the other contexts, this does not need to be in a finally block |
| 2766 | // because renderNode takes care of unwinding the stack. |
| 2767 | task.treeContext = prevTreeContext; |
| 2768 | } else if (didEmitActionStateMarkers) { |
| 2769 | // If there were useActionState hooks, we must use the non-destructive path |
| 2770 | // because this component is not a pure indirection; we emitted markers |
| 2771 | // to the stream. |
| 2772 | renderNode(request, task, children, -1); |
| 2773 | } else { |
| 2774 | // We're now successfully past this task, and we haven't modified the |
| 2775 | // context stack. We don't have to pop back to the previous task every |
| 2776 | // again, so we can use the destructive recursive form. |
| 2777 | renderNodeDestructive(request, task, children, -1); |
| 2778 | } |
| 2779 | task.keyPath = prevKeyPath; |
| 2780 | } |
| 2781 | |
| 2782 | function validateFunctionComponentInDev(Component: any): void { |
| 2783 | if (__DEV__) { |
| 2784 | if (Component && Component.childContextTypes) { |
| 2785 | console.error( |
| 2786 | 'childContextTypes cannot be defined on a function component.\n' + |
| 2787 | ' %s.childContextTypes = ...', |
| 2788 | Component.displayName || Component.name || 'Component', |
| 2789 | ); |
| 2790 | } |
| 2791 | |
| 2792 | if (typeof Component.getDerivedStateFromProps === 'function') { |
| 2793 | const componentName = getComponentNameFromType(Component) || 'Unknown'; |
| 2794 | |
| 2795 | if (!didWarnAboutGetDerivedStateOnFunctionComponent[componentName]) { |
| 2796 | console.error( |
| 2797 | '%s: Function components do not support getDerivedStateFromProps.', |
| 2798 | componentName, |
| 2799 | ); |
| 2800 | didWarnAboutGetDerivedStateOnFunctionComponent[componentName] = true; |
| 2801 | } |
| 2802 | } |
| 2803 | |
| 2804 | if ( |
| 2805 | typeof Component.contextType === 'object' && |
| 2806 | Component.contextType !== null |
| 2807 | ) { |
| 2808 | const componentName = getComponentNameFromType(Component) || 'Unknown'; |
| 2809 | |
| 2810 | if (!didWarnAboutContextTypeOnFunctionComponent[componentName]) { |
| 2811 | console.error( |
| 2812 | '%s: Function components do not support contextType.', |
| 2813 | componentName, |
| 2814 | ); |
| 2815 | didWarnAboutContextTypeOnFunctionComponent[componentName] = true; |
| 2816 | } |
| 2817 | } |
| 2818 | } |
| 2819 | } |
| 2820 | |
| 2821 | function renderForwardRef( |
| 2822 | request: Request, |
| 2823 | task: Task, |
| 2824 | keyPath: KeyNode, |
| 2825 | type: any, |
| 2826 | props: Object, |
| 2827 | ref: any, |
| 2828 | ): void { |
| 2829 | let propsWithoutRef; |
| 2830 | if ('ref' in props) { |
| 2831 | // `ref` is just a prop now, but `forwardRef` expects it to not appear in |
| 2832 | // the props object. This used to happen in the JSX runtime, but now we do |
| 2833 | // it here. |
| 2834 | propsWithoutRef = {} as {[string]: any}; |
| 2835 | for (const key in props) { |
| 2836 | // Since `ref` should only appear in props via the JSX transform, we can |
| 2837 | // assume that this is a plain object. So we don't need a |
| 2838 | // hasOwnProperty check. |
| 2839 | if (key !== 'ref') { |
| 2840 | propsWithoutRef[key] = props[key]; |
| 2841 | } |
| 2842 | } |
| 2843 | } else { |
| 2844 | propsWithoutRef = props; |
| 2845 | } |
| 2846 | |
| 2847 | const children = renderWithHooks( |
| 2848 | request, |
| 2849 | task, |
| 2850 | keyPath, |
| 2851 | type.render, |
| 2852 | propsWithoutRef, |
| 2853 | ref, |
| 2854 | ); |
| 2855 | const hasId = checkDidRenderIdHook(); |
| 2856 | const actionStateCount = getActionStateCount(); |
| 2857 | const actionStateMatchingIndex = getActionStateMatchingIndex(); |
| 2858 | finishFunctionComponent( |
| 2859 | request, |
| 2860 | task, |
| 2861 | keyPath, |
| 2862 | children, |
| 2863 | hasId, |
| 2864 | actionStateCount, |
| 2865 | actionStateMatchingIndex, |
| 2866 | ); |
| 2867 | } |
| 2868 | |
| 2869 | function renderMemo( |
| 2870 | request: Request, |
| 2871 | task: Task, |
| 2872 | keyPath: KeyNode, |
| 2873 | type: any, |
| 2874 | props: Object, |
| 2875 | ref: any, |
| 2876 | ): void { |
| 2877 | const innerType = type.type; |
| 2878 | renderElement(request, task, keyPath, innerType, props, ref); |
| 2879 | } |
| 2880 | |
| 2881 | function renderContextConsumer( |
| 2882 | request: Request, |
| 2883 | task: Task, |
| 2884 | keyPath: KeyNode, |
| 2885 | context: ReactContext<any>, |
| 2886 | props: Object, |
| 2887 | ): void { |
| 2888 | const render = props.children; |
| 2889 | |
| 2890 | if (__DEV__) { |
| 2891 | if (typeof render !== 'function') { |
| 2892 | console.error( |
| 2893 | 'A context consumer was rendered with multiple children, or a child ' + |
| 2894 | "that isn't a function. A context consumer expects a single child " + |
| 2895 | 'that is a function. If you did pass a function, make sure there ' + |
| 2896 | 'is no trailing or leading whitespace around it.', |
| 2897 | ); |
| 2898 | } |
| 2899 | } |
| 2900 | |
| 2901 | const newValue = readContext(context); |
| 2902 | const newChildren = render(newValue); |
| 2903 | |
| 2904 | const prevKeyPath = task.keyPath; |
| 2905 | task.keyPath = keyPath; |
| 2906 | renderNodeDestructive(request, task, newChildren, -1); |
| 2907 | task.keyPath = prevKeyPath; |
| 2908 | } |
| 2909 | |
| 2910 | function renderContextProvider( |
| 2911 | request: Request, |
| 2912 | task: Task, |
| 2913 | keyPath: KeyNode, |
| 2914 | context: ReactContext<any>, |
| 2915 | props: Object, |
| 2916 | ): void { |
| 2917 | const value = props.value; |
| 2918 | const children = props.children; |
| 2919 | let prevSnapshot; |
| 2920 | if (__DEV__) { |
| 2921 | prevSnapshot = task.context; |
| 2922 | } |
| 2923 | const prevKeyPath = task.keyPath; |
| 2924 | task.context = pushProvider(context, value); |
| 2925 | task.keyPath = keyPath; |
| 2926 | renderNodeDestructive(request, task, children, -1); |
| 2927 | task.context = popProvider(context); |
| 2928 | task.keyPath = prevKeyPath; |
| 2929 | if (__DEV__) { |
| 2930 | if (prevSnapshot !== task.context) { |
| 2931 | console.error( |
| 2932 | 'Popping the context provider did not return back to the original snapshot. This is a bug in React.', |
| 2933 | ); |
| 2934 | } |
| 2935 | } |
| 2936 | } |
| 2937 | |
| 2938 | function renderLazyComponent( |
| 2939 | request: Request, |
| 2940 | task: Task, |
| 2941 | keyPath: KeyNode, |
| 2942 | lazyComponent: LazyComponentType<any, any>, |
| 2943 | props: Object, |
| 2944 | ref: any, |
| 2945 | ): void { |
| 2946 | let Component; |
| 2947 | if (__DEV__) { |
| 2948 | Component = callLazyInitInDEV(lazyComponent); |
| 2949 | } else { |
| 2950 | const payload = lazyComponent._payload; |
| 2951 | const init = lazyComponent._init; |
| 2952 | Component = init(payload); |
| 2953 | } |
| 2954 | if (request.aborted) { |
| 2955 | // eslint-disable-next-line no-throw-literal |
| 2956 | throw null; |
| 2957 | } |
| 2958 | renderElement(request, task, keyPath, Component, props, ref); |
| 2959 | } |
| 2960 | |
| 2961 | function renderActivity( |
| 2962 | request: Request, |
| 2963 | task: Task, |
| 2964 | keyPath: KeyNode, |
| 2965 | props: ActivityProps, |
| 2966 | ): void { |
| 2967 | const segment = task.blockedSegment; |
| 2968 | if (segment === null) { |
| 2969 | // Replay |
| 2970 | const mode = props.mode; |
| 2971 | if (mode === 'hidden') { |
| 2972 | // A hidden Activity boundary is not server rendered. Prerendering happens |
| 2973 | // on the client. |
| 2974 | } else { |
| 2975 | // A visible Activity boundary has its children rendered inside the boundary. |
| 2976 | const prevKeyPath = task.keyPath; |
| 2977 | task.keyPath = keyPath; |
| 2978 | renderNode(request, task, props.children, -1); |
| 2979 | task.keyPath = prevKeyPath; |
| 2980 | } |
| 2981 | } else { |
| 2982 | // Render |
| 2983 | const mode = props.mode; |
| 2984 | if (mode === 'hidden') { |
| 2985 | // A hidden Activity boundary is not server rendered. Prerendering happens |
| 2986 | // on the client. |
| 2987 | } else { |
| 2988 | // An Activity boundary is delimited so that we can hydrate it separately. |
| 2989 | pushStartActivityBoundary(segment.chunks, request.renderState); |
| 2990 | segment.lastPushedText = false; |
| 2991 | // A visible Activity boundary has its children rendered inside the boundary. |
| 2992 | const prevKeyPath = task.keyPath; |
| 2993 | task.keyPath = keyPath; |
| 2994 | // We use the non-destructive form because if something suspends, we still |
| 2995 | // need to pop back up and finish the end comment. |
| 2996 | renderNode(request, task, props.children, -1); |
| 2997 | task.keyPath = prevKeyPath; |
| 2998 | pushEndActivityBoundary(segment.chunks, request.renderState); |
| 2999 | segment.lastPushedText = false; |
| 3000 | } |
| 3001 | } |
| 3002 | } |
| 3003 | |
| 3004 | function renderViewTransition( |
| 3005 | request: Request, |
| 3006 | task: Task, |
| 3007 | keyPath: KeyNode, |
| 3008 | props: ViewTransitionProps, |
| 3009 | ) { |
| 3010 | const prevContext = task.formatContext; |
| 3011 | const prevKeyPath = task.keyPath; |
| 3012 | // Get the name off props or generate an auto-generated one in case we need it. |
| 3013 | const autoName = getViewTransitionName( |
| 3014 | props, |
| 3015 | task.treeContext, |
| 3016 | request.resumableState, |
| 3017 | ); |
| 3018 | task.formatContext = getViewTransitionFormatContext( |
| 3019 | request.resumableState, |
| 3020 | prevContext, |
| 3021 | getViewTransitionClassName(props.default, props.update), |
| 3022 | getViewTransitionClassName(props.default, props.enter), |
| 3023 | getViewTransitionClassName(props.default, props.exit), |
| 3024 | getViewTransitionClassName(props.default, props.share), |
| 3025 | // Pass `undefined` (rather than the resolved class) when the prop is absent |
| 3026 | // so the format context can distinguish "no parentEnter/parentExit" (which |
| 3027 | // stops the relay) from an explicit "auto"/class (which continues it). |
| 3028 | enableViewTransitionParentEnterExit && props.parentEnter !== undefined |
| 3029 | ? getViewTransitionClassName(props.default, props.parentEnter) |
| 3030 | : undefined, |
| 3031 | enableViewTransitionParentEnterExit && props.parentExit !== undefined |
| 3032 | ? getViewTransitionClassName(props.default, props.parentExit) |
| 3033 | : undefined, |
| 3034 | // A ViewTransition with an onParentEnter/onParentExit handler but no class |
| 3035 | // still relays the activation to its descendants, so the relay must continue |
| 3036 | // through it even though the handler itself emits no annotation. |
| 3037 | enableViewTransitionParentEnterExit && props.onParentEnter != null, |
| 3038 | enableViewTransitionParentEnterExit && props.onParentExit != null, |
| 3039 | props.name, |
| 3040 | autoName, |
| 3041 | ); |
| 3042 | task.keyPath = keyPath; |
| 3043 | if (props.name != null && props.name !== 'auto') { |
| 3044 | renderNodeDestructive(request, task, props.children, -1); |
| 3045 | } else { |
| 3046 | // This will be auto-assigned a name which claims a "useId" slot. |
| 3047 | // This component materialized an id. We treat this as its own level, with |
| 3048 | // a single "child" slot. |
| 3049 | const prevTreeContext = task.treeContext; |
| 3050 | const totalChildren = 1; |
| 3051 | const index = 0; |
| 3052 | // Modify the id context. Because we'll need to reset this if something |
| 3053 | // suspends or errors, we'll use the non-destructive render path. |
| 3054 | task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); |
| 3055 | renderNode(request, task, props.children, -1); |
| 3056 | // Like the other contexts, this does not need to be in a finally block |
| 3057 | // because renderNode takes care of unwinding the stack. |
| 3058 | task.treeContext = prevTreeContext; |
| 3059 | } |
| 3060 | task.formatContext = prevContext; |
| 3061 | task.keyPath = prevKeyPath; |
| 3062 | } |
| 3063 | |
| 3064 | function renderElement( |
| 3065 | request: Request, |
| 3066 | task: Task, |
| 3067 | keyPath: KeyNode, |
| 3068 | type: any, |
| 3069 | props: Object, |
| 3070 | ref: any, |
| 3071 | ): void { |
| 3072 | if (typeof type === 'function') { |
| 3073 | if (shouldConstruct(type)) { |
| 3074 | renderClassComponent(request, task, keyPath, type, props); |
| 3075 | return; |
| 3076 | } else { |
| 3077 | renderFunctionComponent(request, task, keyPath, type, props); |
| 3078 | return; |
| 3079 | } |
| 3080 | } |
| 3081 | if (typeof type === 'string') { |
| 3082 | renderHostElement(request, task, keyPath, type, props); |
| 3083 | return; |
| 3084 | } |
| 3085 | |
| 3086 | switch (type) { |
| 3087 | // LegacyHidden acts the same as a fragment. This only works because we |
| 3088 | // currently assume that every instance of LegacyHidden is accompanied by a |
| 3089 | // host component wrapper. In the hidden mode, the host component is given a |
| 3090 | // `hidden` attribute, which ensures that the initial HTML is not visible. |
| 3091 | // To support the use of LegacyHidden as a true fragment, without an extra |
| 3092 | // DOM node, we would have to hide the initial HTML in some other way. |
| 3093 | // TODO: Delete in LegacyHidden. It's an unstable API only used in the |
| 3094 | // www build. As a migration step, we could add a special prop to Offscreen |
| 3095 | // that simulates the old behavior (no hiding, no change to effects). |
| 3096 | // $FlowFixMe[invalid-compare] |
| 3097 | case REACT_LEGACY_HIDDEN_TYPE: |
| 3098 | // $FlowFixMe[invalid-compare] -- falls through |
| 3099 | case REACT_STRICT_MODE_TYPE: |
| 3100 | // $FlowFixMe[invalid-compare] -- falls through |
| 3101 | case REACT_PROFILER_TYPE: |
| 3102 | // $FlowFixMe[invalid-compare] -- falls through |
| 3103 | case REACT_FRAGMENT_TYPE: { |
| 3104 | const prevKeyPath = task.keyPath; |
| 3105 | task.keyPath = keyPath; |
| 3106 | renderNodeDestructive(request, task, props.children, -1); |
| 3107 | task.keyPath = prevKeyPath; |
| 3108 | return; |
| 3109 | } |
| 3110 | // $FlowFixMe[invalid-compare] |
| 3111 | case REACT_ACTIVITY_TYPE: { |
| 3112 | renderActivity(request, task, keyPath, props); |
| 3113 | return; |
| 3114 | } |
| 3115 | // $FlowFixMe[invalid-compare] |
| 3116 | case REACT_SUSPENSE_LIST_TYPE: { |
| 3117 | renderSuspenseList(request, task, keyPath, props); |
| 3118 | return; |
| 3119 | } |
| 3120 | // $FlowFixMe[invalid-compare] |
| 3121 | case REACT_VIEW_TRANSITION_TYPE: { |
| 3122 | if (enableViewTransition) { |
| 3123 | renderViewTransition(request, task, keyPath, props); |
| 3124 | return; |
| 3125 | } |
| 3126 | // Fallthrough |
| 3127 | } |
| 3128 | // $FlowFixMe[invalid-compare] |
| 3129 | case REACT_SCOPE_TYPE: { |
| 3130 | if (enableScopeAPI) { |
| 3131 | const prevKeyPath = task.keyPath; |
| 3132 | task.keyPath = keyPath; |
| 3133 | renderNodeDestructive(request, task, props.children, -1); |
| 3134 | task.keyPath = prevKeyPath; |
| 3135 | return; |
| 3136 | } |
| 3137 | throw new Error('ReactDOMServer does not yet support scope components.'); |
| 3138 | } |
| 3139 | // $FlowFixMe[invalid-compare] |
| 3140 | case REACT_SUSPENSE_TYPE: { |
| 3141 | renderSuspenseBoundary(request, task, keyPath, props); |
| 3142 | return; |
| 3143 | } |
| 3144 | } |
| 3145 | |
| 3146 | // $FlowFixMe[invalid-compare] |
| 3147 | if (typeof type === 'object' && type !== null) { |
| 3148 | switch (type.$$typeof) { |
| 3149 | // $FlowFixMe[invalid-compare] |
| 3150 | case REACT_FORWARD_REF_TYPE: { |
| 3151 | renderForwardRef(request, task, keyPath, type, props, ref); |
| 3152 | return; |
| 3153 | } |
| 3154 | // $FlowFixMe[invalid-compare] |
| 3155 | case REACT_MEMO_TYPE: { |
| 3156 | renderMemo(request, task, keyPath, type, props, ref); |
| 3157 | return; |
| 3158 | } |
| 3159 | // $FlowFixMe[invalid-compare] |
| 3160 | case REACT_CONTEXT_TYPE: { |
| 3161 | const context = type; |
| 3162 | renderContextProvider(request, task, keyPath, context, props); |
| 3163 | return; |
| 3164 | } |
| 3165 | // $FlowFixMe[invalid-compare] |
| 3166 | case REACT_CONSUMER_TYPE: { |
| 3167 | const context: ReactContext<any> = (type as ReactConsumerType<any>) |
| 3168 | ._context; |
| 3169 | renderContextConsumer(request, task, keyPath, context, props); |
| 3170 | return; |
| 3171 | } |
| 3172 | // $FlowFixMe[invalid-compare] |
| 3173 | case REACT_LAZY_TYPE: { |
| 3174 | renderLazyComponent(request, task, keyPath, type, props, ref); |
| 3175 | return; |
| 3176 | } |
| 3177 | } |
| 3178 | } |
| 3179 | |
| 3180 | let info = ''; |
| 3181 | if (__DEV__) { |
| 3182 | if ( |
| 3183 | type === undefined || |
| 3184 | (typeof type === 'object' && |
| 3185 | // $FlowFixMe[invalid-compare] |
| 3186 | type !== null && |
| 3187 | Object.keys(type).length === 0) |
| 3188 | ) { |
| 3189 | info += |
| 3190 | ' You likely forgot to export your component from the file ' + |
| 3191 | "it's defined in, or you might have mixed up default and " + |
| 3192 | 'named imports.'; |
| 3193 | } |
| 3194 | } |
| 3195 | |
| 3196 | throw new Error( |
| 3197 | 'Element type is invalid: expected a string (for built-in ' + |
| 3198 | 'components) or a class/function (for composite components) ' + |
| 3199 | `but got: ${type == null ? type : typeof type}.${info}`, |
| 3200 | ); |
| 3201 | } |
| 3202 | |
| 3203 | function resumeNode( |
| 3204 | request: Request, |
| 3205 | task: ReplayTask, |
| 3206 | segmentId: number, |
| 3207 | node: ReactNodeList, |
| 3208 | childIndex: number, |
| 3209 | ): void { |
| 3210 | const prevReplay = task.replay; |
| 3211 | const blockedBoundary = task.blockedBoundary; |
| 3212 | const resumedSegment = createPendingSegment( |
| 3213 | request, |
| 3214 | 0, |
| 3215 | null, |
| 3216 | task.formatContext, |
| 3217 | false, |
| 3218 | false, |
| 3219 | ); |
| 3220 | resumedSegment.id = segmentId; |
| 3221 | resumedSegment.parentFlushed = true; |
| 3222 | try { |
| 3223 | // Convert the current ReplayTask to a RenderTask. |
| 3224 | const renderTask: RenderTask = task as any; |
| 3225 | renderTask.replay = null; |
| 3226 | renderTask.blockedSegment = resumedSegment; |
| 3227 | renderNode(request, task, node, childIndex); |
| 3228 | resumedSegment.status = COMPLETED; |
| 3229 | finishedSegment(request, blockedBoundary, resumedSegment); |
| 3230 | if (blockedBoundary === null) { |
| 3231 | request.completedRootSegment = resumedSegment; |
| 3232 | } else { |
| 3233 | queueCompletedSegment(blockedBoundary, resumedSegment); |
| 3234 | if (blockedBoundary.parentFlushed) { |
| 3235 | request.partialBoundaries.push(blockedBoundary); |
| 3236 | } |
| 3237 | } |
| 3238 | } finally { |
| 3239 | // Restore to a ReplayTask. |
| 3240 | task.replay = prevReplay; |
| 3241 | task.blockedSegment = null; |
| 3242 | } |
| 3243 | } |
| 3244 | |
| 3245 | function replayElement( |
| 3246 | request: Request, |
| 3247 | task: ReplayTask, |
| 3248 | keyPath: KeyNode, |
| 3249 | name: null | string, |
| 3250 | keyOrIndex: number | string, |
| 3251 | childIndex: number, |
| 3252 | type: any, |
| 3253 | props: Object, |
| 3254 | ref: any, |
| 3255 | replay: ReplaySet, |
| 3256 | ): void { |
| 3257 | // We're replaying. Find the path to follow. |
| 3258 | const replayNodes = replay.nodes; |
| 3259 | for (let i = 0; i < replayNodes.length; i++) { |
| 3260 | // Flow doesn't support refinement on tuples so we do it manually here. |
| 3261 | const node = replayNodes[i]; |
| 3262 | if (keyOrIndex !== node[1]) { |
| 3263 | continue; |
| 3264 | } |
| 3265 | if (node.length === 4) { |
| 3266 | // Matched a replayable path. |
| 3267 | // Let's double check that the component name matches as a precaution. |
| 3268 | if (name !== null && name !== node[0]) { |
| 3269 | throw new Error( |
| 3270 | 'Expected the resume to render <' + |
| 3271 | (node[0] as any) + |
| 3272 | '> in this slot but instead it rendered <' + |
| 3273 | name + |
| 3274 | '>. ' + |
| 3275 | "The tree doesn't match so React will fallback to client rendering.", |
| 3276 | ); |
| 3277 | } |
| 3278 | const childNodes = node[2]; |
| 3279 | const childSlots = node[3]; |
| 3280 | const currentNode = task.node; |
| 3281 | task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1}; |
| 3282 | try { |
| 3283 | renderElement(request, task, keyPath, type, props, ref); |
| 3284 | if ( |
| 3285 | task.replay.pendingTasks === 1 && |
| 3286 | task.replay.nodes.length > 0 |
| 3287 | // TODO check remaining slots |
| 3288 | ) { |
| 3289 | throw new Error( |
| 3290 | "Couldn't find all resumable slots by key/index during replaying. " + |
| 3291 | "The tree doesn't match so React will fallback to client rendering.", |
| 3292 | ); |
| 3293 | } |
| 3294 | task.replay.pendingTasks--; |
| 3295 | } catch (x) { |
| 3296 | if ( |
| 3297 | typeof x === 'object' && |
| 3298 | x !== null && |
| 3299 | (x === SuspenseException || |
| 3300 | typeof x.then === 'function' || |
| 3301 | // Rethrow so retryReplayTask can trampoline on stack overflow. |
| 3302 | x.message === 'Maximum call stack size exceeded') |
| 3303 | ) { |
| 3304 | // Suspend |
| 3305 | if (task.node === currentNode) { |
| 3306 | // This same element suspended so we need to pop the replay we just added. |
| 3307 | task.replay = replay; |
| 3308 | } else { |
| 3309 | // We finished rendering this node, so now we can consume this slot. |
| 3310 | replayNodes.splice(i, 1); |
| 3311 | } |
| 3312 | throw x; |
| 3313 | } |
| 3314 | task.replay.pendingTasks--; |
| 3315 | // Unlike regular render, we don't terminate the siblings if we error |
| 3316 | // during a replay. That's because this component didn't actually error |
| 3317 | // in the original prerender. What's unable to complete is the child |
| 3318 | // replay nodes which might be Suspense boundaries which are able to |
| 3319 | // absorb the error and we can still continue with siblings. |
| 3320 | const thrownInfo = getThrownInfo(task.componentStack); |
| 3321 | erroredReplay( |
| 3322 | request, |
| 3323 | task.blockedBoundary, |
| 3324 | request.aborted ? request.fatalError : x, |
| 3325 | thrownInfo, |
| 3326 | childNodes, |
| 3327 | childSlots, |
| 3328 | __DEV__ ? task.debugTask : null, |
| 3329 | ); |
| 3330 | } |
| 3331 | task.replay = replay; |
| 3332 | } else { |
| 3333 | // Let's double check that the component type matches. |
| 3334 | if (type !== REACT_SUSPENSE_TYPE) { |
| 3335 | const expectedType = 'Suspense'; |
| 3336 | throw new Error( |
| 3337 | 'Expected the resume to render <' + |
| 3338 | expectedType + |
| 3339 | '> in this slot but instead it rendered <' + |
| 3340 | (getComponentNameFromType(type) || 'Unknown') + |
| 3341 | '>. ' + |
| 3342 | "The tree doesn't match so React will fallback to client rendering.", |
| 3343 | ); |
| 3344 | } |
| 3345 | // Matched a replayable path. |
| 3346 | replaySuspenseBoundary( |
| 3347 | request, |
| 3348 | task, |
| 3349 | keyPath, |
| 3350 | props, |
| 3351 | node[5], |
| 3352 | node[2], |
| 3353 | node[3], |
| 3354 | node[4] === null ? [] : node[4][2], |
| 3355 | node[4] === null ? null : node[4][3], |
| 3356 | ); |
| 3357 | } |
| 3358 | // We finished rendering this node, so now we can consume this |
| 3359 | // slot. This must happen after in case we rerender this task. |
| 3360 | replayNodes.splice(i, 1); |
| 3361 | return; |
| 3362 | } |
| 3363 | // We didn't find any matching nodes. We assume that this element was already |
| 3364 | // rendered in the prelude and skip it. |
| 3365 | } |
| 3366 | |
| 3367 | function validateIterable( |
| 3368 | task: Task, |
| 3369 | iterable: Iterable<any>, |
| 3370 | childIndex: number, |
| 3371 | iterator: Iterator<any>, |
| 3372 | iteratorFn: () => ?Iterator<any>, |
| 3373 | ): void { |
| 3374 | if (__DEV__) { |
| 3375 | if (iterator === iterable) { |
| 3376 | // We don't support rendering Generators as props because it's a mutation. |
| 3377 | // See https://github.com/facebook/react/issues/12995 |
| 3378 | // We do support generators if they were created by a GeneratorFunction component |
| 3379 | // as its direct child since we can recreate those by rerendering the component |
| 3380 | // as needed. |
| 3381 | const isGeneratorComponent = |
| 3382 | childIndex === -1 && // Only the root child is valid |
| 3383 | task.componentStack !== null && |
| 3384 | typeof task.componentStack.type === 'function' && // FunctionComponent |
| 3385 | // $FlowFixMe[method-unbinding] |
| 3386 | Object.prototype.toString.call(task.componentStack.type) === |
| 3387 | '[object GeneratorFunction]' && |
| 3388 | // $FlowFixMe[method-unbinding] |
| 3389 | Object.prototype.toString.call(iterator) === '[object Generator]'; |
| 3390 | if (!isGeneratorComponent) { |
| 3391 | if (!didWarnAboutGenerators) { |
| 3392 | console.error( |
| 3393 | 'Using Iterators as children is unsupported and will likely yield ' + |
| 3394 | 'unexpected results because enumerating a generator mutates it. ' + |
| 3395 | 'You may convert it to an array with `Array.from()` or the ' + |
| 3396 | '`[...spread]` operator before rendering. You can also use an ' + |
| 3397 | 'Iterable that can iterate multiple times over the same items.', |
| 3398 | ); |
| 3399 | } |
| 3400 | didWarnAboutGenerators = true; |
| 3401 | } |
| 3402 | } else if ((iterable as any).entries === iteratorFn) { |
| 3403 | // Warn about using Maps as children |
| 3404 | if (!didWarnAboutMaps) { |
| 3405 | console.error( |
| 3406 | 'Using Maps as children is not supported. ' + |
| 3407 | 'Use an array of keyed ReactElements instead.', |
| 3408 | ); |
| 3409 | didWarnAboutMaps = true; |
| 3410 | } |
| 3411 | } |
| 3412 | } |
| 3413 | } |
| 3414 | |
| 3415 | function validateAsyncIterable( |
| 3416 | task: Task, |
| 3417 | iterable: AsyncIterable<any>, |
| 3418 | childIndex: number, |
| 3419 | iterator: AsyncIterator<any>, |
| 3420 | ): void { |
| 3421 | if (__DEV__) { |
| 3422 | if (iterator === iterable) { |
| 3423 | // We don't support rendering Generators as props because it's a mutation. |
| 3424 | // See https://github.com/facebook/react/issues/12995 |
| 3425 | // We do support generators if they were created by a GeneratorFunction component |
| 3426 | // as its direct child since we can recreate those by rerendering the component |
| 3427 | // as needed. |
| 3428 | const isGeneratorComponent = |
| 3429 | childIndex === -1 && // Only the root child is valid |
| 3430 | task.componentStack !== null && |
| 3431 | typeof task.componentStack.type === 'function' && // FunctionComponent |
| 3432 | // $FlowFixMe[method-unbinding] |
| 3433 | Object.prototype.toString.call(task.componentStack.type) === |
| 3434 | '[object AsyncGeneratorFunction]' && |
| 3435 | // $FlowFixMe[method-unbinding] |
| 3436 | Object.prototype.toString.call(iterator) === '[object AsyncGenerator]'; |
| 3437 | if (!isGeneratorComponent) { |
| 3438 | if (!didWarnAboutGenerators) { |
| 3439 | console.error( |
| 3440 | 'Using AsyncIterators as children is unsupported and will likely yield ' + |
| 3441 | 'unexpected results because enumerating a generator mutates it. ' + |
| 3442 | 'You can use an AsyncIterable that can iterate multiple times over ' + |
| 3443 | 'the same items.', |
| 3444 | ); |
| 3445 | } |
| 3446 | didWarnAboutGenerators = true; |
| 3447 | } |
| 3448 | } |
| 3449 | } |
| 3450 | } |
| 3451 | |
| 3452 | function warnOnFunctionType(invalidChild: Function) { |
| 3453 | if (__DEV__) { |
| 3454 | const name = invalidChild.displayName || invalidChild.name || 'Component'; |
| 3455 | console.error( |
| 3456 | 'Functions are not valid as a React child. This may happen if ' + |
| 3457 | 'you return %s instead of <%s /> from render. ' + |
| 3458 | 'Or maybe you meant to call this function rather than return it.', |
| 3459 | name, |
| 3460 | name, |
| 3461 | ); |
| 3462 | } |
| 3463 | } |
| 3464 | |
| 3465 | function warnOnSymbolType(invalidChild: symbol) { |
| 3466 | if (__DEV__) { |
| 3467 | // eslint-disable-next-line react-internal/safe-string-coercion |
| 3468 | const name = String(invalidChild); |
| 3469 | console.error('Symbols are not valid as a React child.\n' + ' %s', name); |
| 3470 | } |
| 3471 | } |
| 3472 | |
| 3473 | // This function by it self renders a node and consumes the task by mutating it |
| 3474 | // to update the current execution state. |
| 3475 | function renderNodeDestructive( |
| 3476 | request: Request, |
| 3477 | task: Task, |
| 3478 | node: ReactNodeList, |
| 3479 | childIndex: number, |
| 3480 | ): void { |
| 3481 | if (task.replay !== null && typeof task.replay.slots === 'number') { |
| 3482 | // TODO: Figure out a cheaper place than this hot path to do this check. |
| 3483 | const resumeSegmentID = task.replay.slots; |
| 3484 | resumeNode(request, task, resumeSegmentID, node, childIndex); |
| 3485 | return; |
| 3486 | } |
| 3487 | // Stash the node we're working on. We'll pick up from this task in case |
| 3488 | // something suspends. |
| 3489 | task.node = node; |
| 3490 | task.childIndex = childIndex; |
| 3491 | |
| 3492 | const previousComponentStack = task.componentStack; |
| 3493 | const previousDebugTask = __DEV__ ? task.debugTask : null; |
| 3494 | |
| 3495 | pushComponentStack(task); |
| 3496 | |
| 3497 | retryNode(request, task); |
| 3498 | |
| 3499 | task.componentStack = previousComponentStack; |
| 3500 | if (__DEV__) { |
| 3501 | task.debugTask = previousDebugTask; |
| 3502 | } |
| 3503 | } |
| 3504 | |
| 3505 | function retryNode(request: Request, task: Task): void { |
| 3506 | const node = task.node; |
| 3507 | const childIndex = task.childIndex; |
| 3508 | |
| 3509 | if (node === null) { |
| 3510 | return; |
| 3511 | } |
| 3512 | |
| 3513 | // Handle object types |
| 3514 | if (typeof node === 'object') { |
| 3515 | switch ((node as any).$$typeof) { |
| 3516 | case REACT_ELEMENT_TYPE: { |
| 3517 | const element: any = node; |
| 3518 | const type = element.type; |
| 3519 | const key: ReactKey = element.key; |
| 3520 | const props = element.props; |
| 3521 | |
| 3522 | // TODO: We should get the ref off the props object right before using |
| 3523 | // it. |
| 3524 | const refProp = props.ref; |
| 3525 | const ref = refProp !== undefined ? refProp : null; |
| 3526 | |
| 3527 | const debugTask: null | ConsoleTask = __DEV__ ? task.debugTask : null; |
| 3528 | |
| 3529 | const name = getComponentNameFromType(type); |
| 3530 | const keyOrIndex = |
| 3531 | key == null || key === REACT_OPTIMISTIC_KEY |
| 3532 | ? childIndex === -1 |
| 3533 | ? 0 |
| 3534 | : childIndex |
| 3535 | : key; |
| 3536 | const keyPath = [task.keyPath, name, keyOrIndex]; |
| 3537 | if (task.replay !== null) { |
| 3538 | if (debugTask) { |
| 3539 | debugTask.run( |
| 3540 | replayElement.bind( |
| 3541 | null, |
| 3542 | request, |
| 3543 | task, |
| 3544 | keyPath, |
| 3545 | name, |
| 3546 | keyOrIndex, |
| 3547 | childIndex, |
| 3548 | type, |
| 3549 | props, |
| 3550 | ref, |
| 3551 | task.replay, |
| 3552 | ), |
| 3553 | ); |
| 3554 | } else { |
| 3555 | replayElement( |
| 3556 | request, |
| 3557 | task, |
| 3558 | keyPath, |
| 3559 | name, |
| 3560 | keyOrIndex, |
| 3561 | childIndex, |
| 3562 | type, |
| 3563 | props, |
| 3564 | ref, |
| 3565 | task.replay, |
| 3566 | ); |
| 3567 | } |
| 3568 | // No matches found for this node. We assume it's already emitted in the |
| 3569 | // prelude and skip it during the replay. |
| 3570 | } else { |
| 3571 | // We're doing a plain render. |
| 3572 | if (debugTask) { |
| 3573 | debugTask.run( |
| 3574 | renderElement.bind( |
| 3575 | null, |
| 3576 | request, |
| 3577 | task, |
| 3578 | keyPath, |
| 3579 | type, |
| 3580 | props, |
| 3581 | ref, |
| 3582 | ), |
| 3583 | ); |
| 3584 | } else { |
| 3585 | renderElement(request, task, keyPath, type, props, ref); |
| 3586 | } |
| 3587 | } |
| 3588 | return; |
| 3589 | } |
| 3590 | case REACT_PORTAL_TYPE: |
| 3591 | throw new Error( |
| 3592 | 'Portals are not currently supported by the server renderer. ' + |
| 3593 | 'Render them conditionally so that they only appear on the client render.', |
| 3594 | ); |
| 3595 | case REACT_LAZY_TYPE: { |
| 3596 | const lazyNode: LazyComponentType<any, any> = node as any; |
| 3597 | let resolvedNode; |
| 3598 | if (__DEV__) { |
| 3599 | resolvedNode = callLazyInitInDEV(lazyNode); |
| 3600 | } else { |
| 3601 | const payload = lazyNode._payload; |
| 3602 | const init = lazyNode._init; |
| 3603 | resolvedNode = init(payload); |
| 3604 | } |
| 3605 | if (request.aborted) { |
| 3606 | // eslint-disable-next-line no-throw-literal |
| 3607 | throw null; |
| 3608 | } |
| 3609 | // Now we render the resolved node |
| 3610 | renderNodeDestructive(request, task, resolvedNode, childIndex); |
| 3611 | return; |
| 3612 | } |
| 3613 | } |
| 3614 | |
| 3615 | if (isArray(node)) { |
| 3616 | renderChildrenArray(request, task, node, childIndex); |
| 3617 | return; |
| 3618 | } |
| 3619 | |
| 3620 | const iteratorFn = getIteratorFn(node); |
| 3621 | if (iteratorFn) { |
| 3622 | const iterator = iteratorFn.call(node); |
| 3623 | if (iterator) { |
| 3624 | if (__DEV__) { |
| 3625 | validateIterable(task, node, childIndex, iterator, iteratorFn); |
| 3626 | } |
| 3627 | // We need to know how many total children are in this set, so that we |
| 3628 | // can allocate enough id slots to acommodate them. So we must exhaust |
| 3629 | // the iterator before we start recursively rendering the children. |
| 3630 | // TODO: This is not great but I think it's inherent to the id |
| 3631 | // generation algorithm. |
| 3632 | let step = iterator.next(); |
| 3633 | if (!step.done) { |
| 3634 | const children = []; |
| 3635 | do { |
| 3636 | children.push(step.value); |
| 3637 | step = iterator.next(); |
| 3638 | } while (!step.done); |
| 3639 | renderChildrenArray(request, task, children, childIndex); |
| 3640 | } |
| 3641 | return; |
| 3642 | } |
| 3643 | } |
| 3644 | |
| 3645 | if ( |
| 3646 | enableAsyncIterableChildren && |
| 3647 | typeof (node as any)[ASYNC_ITERATOR] === 'function' |
| 3648 | ) { |
| 3649 | const iterator: AsyncIterator<ReactNodeList> = (node as any)[ |
| 3650 | ASYNC_ITERATOR |
| 3651 | ](); |
| 3652 | if (iterator) { |
| 3653 | if (__DEV__) { |
| 3654 | validateAsyncIterable(task, node as any, childIndex, iterator); |
| 3655 | } |
| 3656 | // TODO: Update the task.node to be the iterator to avoid asking |
| 3657 | // for new iterators, but we currently warn for rendering these |
| 3658 | // so needs some refactoring to deal with the warning. |
| 3659 | |
| 3660 | // Restore the thenable state before resuming. |
| 3661 | const prevThenableState = task.thenableState; |
| 3662 | task.thenableState = null; |
| 3663 | prepareToUseThenableState(prevThenableState); |
| 3664 | |
| 3665 | // We need to know how many total children are in this set, so that we |
| 3666 | // can allocate enough id slots to acommodate them. So we must exhaust |
| 3667 | // the iterator before we start recursively rendering the children. |
| 3668 | // TODO: This is not great but I think it's inherent to the id |
| 3669 | // generation algorithm. |
| 3670 | const children = []; |
| 3671 | |
| 3672 | let done = false; |
| 3673 | |
| 3674 | // $FlowFixMe[invalid-compare] |
| 3675 | if (iterator === node) { |
| 3676 | // If it's an iterator we need to continue reading where we left |
| 3677 | // off. We can do that by reading the first few rows from the previous |
| 3678 | // thenable state. |
| 3679 | // $FlowFixMe[underconstrained-implicit-instantiation] |
| 3680 | let step = readPreviousThenableFromState(); |
| 3681 | while (step !== undefined) { |
| 3682 | if (step.done) { |
| 3683 | done = true; |
| 3684 | break; |
| 3685 | } |
| 3686 | children.push(step.value); |
| 3687 | step = readPreviousThenableFromState(); |
| 3688 | } |
| 3689 | } |
| 3690 | |
| 3691 | if (!done) { |
| 3692 | let step = unwrapThenable(iterator.next()); |
| 3693 | while (!step.done) { |
| 3694 | children.push(step.value); |
| 3695 | step = unwrapThenable(iterator.next()); |
| 3696 | } |
| 3697 | } |
| 3698 | renderChildrenArray(request, task, children, childIndex); |
| 3699 | return; |
| 3700 | } |
| 3701 | } |
| 3702 | |
| 3703 | // Usables are a valid React node type. When React encounters a Usable in |
| 3704 | // a child position, it unwraps it using the same algorithm as `use`. For |
| 3705 | // example, for promises, React will throw an exception to unwind the |
| 3706 | // stack, then replay the component once the promise resolves. |
| 3707 | // |
| 3708 | // A difference from `use` is that React will keep unwrapping the value |
| 3709 | // until it reaches a non-Usable type. |
| 3710 | // |
| 3711 | // e.g. Usable<Usable<Usable<T>>> should resolve to T |
| 3712 | const maybeUsable: Object = node; |
| 3713 | if (typeof maybeUsable.then === 'function') { |
| 3714 | // Clear any previous thenable state that was created by the unwrapping. |
| 3715 | task.thenableState = null; |
| 3716 | const thenable: Thenable<ReactNodeList> = maybeUsable as any; |
| 3717 | const result = renderNodeDestructive( |
| 3718 | request, |
| 3719 | task, |
| 3720 | unwrapThenable(thenable), |
| 3721 | childIndex, |
| 3722 | ); |
| 3723 | return result; |
| 3724 | } |
| 3725 | |
| 3726 | if (maybeUsable.$$typeof === REACT_CONTEXT_TYPE) { |
| 3727 | const context: ReactContext<ReactNodeList> = maybeUsable as any; |
| 3728 | return renderNodeDestructive( |
| 3729 | request, |
| 3730 | task, |
| 3731 | readContext(context), |
| 3732 | childIndex, |
| 3733 | ); |
| 3734 | } |
| 3735 | |
| 3736 | // $FlowFixMe[method-unbinding] |
| 3737 | const childString = Object.prototype.toString.call(node); |
| 3738 | |
| 3739 | throw new Error( |
| 3740 | `Objects are not valid as a React child (found: ${ |
| 3741 | childString === '[object Object]' |
| 3742 | ? 'object with keys {' + Object.keys(node).join(', ') + '}' |
| 3743 | : childString |
| 3744 | }). ` + |
| 3745 | 'If you meant to render a collection of children, use an array ' + |
| 3746 | 'instead.', |
| 3747 | ); |
| 3748 | } |
| 3749 | |
| 3750 | if (typeof node === 'string') { |
| 3751 | const segment = task.blockedSegment; |
| 3752 | if (segment === null) { |
| 3753 | // We assume a text node doesn't have a representation in the replay set, |
| 3754 | // since it can't postpone. If it does, it'll be left unmatched and error. |
| 3755 | } else { |
| 3756 | segment.lastPushedText = pushTextInstance( |
| 3757 | segment.chunks, |
| 3758 | node, |
| 3759 | request.renderState, |
| 3760 | segment.lastPushedText, |
| 3761 | ); |
| 3762 | } |
| 3763 | return; |
| 3764 | } |
| 3765 | |
| 3766 | if (typeof node === 'number' || typeof node === 'bigint') { |
| 3767 | const segment = task.blockedSegment; |
| 3768 | if (segment === null) { |
| 3769 | // We assume a text node doesn't have a representation in the replay set, |
| 3770 | // since it can't postpone. If it does, it'll be left unmatched and error. |
| 3771 | } else { |
| 3772 | segment.lastPushedText = pushTextInstance( |
| 3773 | segment.chunks, |
| 3774 | '' + node, |
| 3775 | request.renderState, |
| 3776 | segment.lastPushedText, |
| 3777 | ); |
| 3778 | } |
| 3779 | return; |
| 3780 | } |
| 3781 | |
| 3782 | if (__DEV__) { |
| 3783 | if (typeof node === 'function') { |
| 3784 | warnOnFunctionType(node); |
| 3785 | } |
| 3786 | if (typeof node === 'symbol') { |
| 3787 | warnOnSymbolType(node); |
| 3788 | } |
| 3789 | } |
| 3790 | } |
| 3791 | |
| 3792 | function replayFragment( |
| 3793 | request: Request, |
| 3794 | task: ReplayTask, |
| 3795 | children: Array<any>, |
| 3796 | childIndex: number, |
| 3797 | ): void { |
| 3798 | // If we're supposed follow this array, we'd expect to see a ReplayNode matching |
| 3799 | // this fragment. |
| 3800 | const replay = task.replay; |
| 3801 | const replayNodes = replay.nodes; |
| 3802 | for (let j = 0; j < replayNodes.length; j++) { |
| 3803 | const node = replayNodes[j]; |
| 3804 | if (node[1] !== childIndex) { |
| 3805 | continue; |
| 3806 | } |
| 3807 | // Matched a replayable path. |
| 3808 | const childNodes = node[2]; |
| 3809 | const childSlots = node[3]; |
| 3810 | task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1}; |
| 3811 | try { |
| 3812 | renderChildrenArray(request, task, children, -1); |
| 3813 | if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) { |
| 3814 | throw new Error( |
| 3815 | "Couldn't find all resumable slots by key/index during replaying. " + |
| 3816 | "The tree doesn't match so React will fallback to client rendering.", |
| 3817 | ); |
| 3818 | } |
| 3819 | task.replay.pendingTasks--; |
| 3820 | } catch (x) { |
| 3821 | if ( |
| 3822 | typeof x === 'object' && |
| 3823 | x !== null && |
| 3824 | (x === SuspenseException || typeof x.then === 'function') |
| 3825 | ) { |
| 3826 | // Suspend |
| 3827 | throw x; |
| 3828 | } |
| 3829 | task.replay.pendingTasks--; |
| 3830 | // Unlike regular render, we don't terminate the siblings if we error |
| 3831 | // during a replay. That's because this component didn't actually error |
| 3832 | // in the original prerender. What's unable to complete is the child |
| 3833 | // replay nodes which might be Suspense boundaries which are able to |
| 3834 | // absorb the error and we can still continue with siblings. |
| 3835 | // This is an error, stash the component stack if it is null. |
| 3836 | const thrownInfo = getThrownInfo(task.componentStack); |
| 3837 | erroredReplay( |
| 3838 | request, |
| 3839 | task.blockedBoundary, |
| 3840 | request.aborted ? request.fatalError : x, |
| 3841 | thrownInfo, |
| 3842 | childNodes, |
| 3843 | childSlots, |
| 3844 | __DEV__ ? task.debugTask : null, |
| 3845 | ); |
| 3846 | } |
| 3847 | task.replay = replay; |
| 3848 | // We finished rendering this node, so now we can consume this |
| 3849 | // slot. This must happen after in case we rerender this task. |
| 3850 | replayNodes.splice(j, 1); |
| 3851 | break; |
| 3852 | } |
| 3853 | } |
| 3854 | |
| 3855 | function warnForMissingKey(request: Request, task: Task, child: mixed): void { |
| 3856 | if (__DEV__) { |
| 3857 | if ( |
| 3858 | child === null || |
| 3859 | typeof child !== 'object' || |
| 3860 | (child.$$typeof !== REACT_ELEMENT_TYPE && |
| 3861 | child.$$typeof !== REACT_PORTAL_TYPE) |
| 3862 | ) { |
| 3863 | return; |
| 3864 | } |
| 3865 | |
| 3866 | if ( |
| 3867 | !child._store || |
| 3868 | ((child._store.validated || child.key != null) && |
| 3869 | child._store.validated !== 2) |
| 3870 | ) { |
| 3871 | return; |
| 3872 | } |
| 3873 | |
| 3874 | if (typeof child._store !== 'object') { |
| 3875 | throw new Error( |
| 3876 | 'React Component in warnForMissingKey should have a _store. ' + |
| 3877 | 'This error is likely caused by a bug in React. Please file an issue.', |
| 3878 | ); |
| 3879 | } |
| 3880 | |
| 3881 | // $FlowFixMe[cannot-write] unable to narrow type from mixed to writable object |
| 3882 | child._store.validated = 1; |
| 3883 | |
| 3884 | let didWarnForKey = request.didWarnForKey; |
| 3885 | if (didWarnForKey == null) { |
| 3886 | didWarnForKey = request.didWarnForKey = new WeakSet(); |
| 3887 | } |
| 3888 | const parentStackFrame = task.componentStack; |
| 3889 | if (parentStackFrame === null || didWarnForKey.has(parentStackFrame)) { |
| 3890 | // We already warned for other children in this parent. |
| 3891 | return; |
| 3892 | } |
| 3893 | didWarnForKey.add(parentStackFrame); |
| 3894 | |
| 3895 | const componentName = getComponentNameFromType(child.type); |
| 3896 | const childOwner = child._owner; |
| 3897 | const parentOwner = parentStackFrame.owner; |
| 3898 | |
| 3899 | let currentComponentErrorInfo = ''; |
| 3900 | if (parentOwner && typeof parentOwner.type !== 'undefined') { |
| 3901 | const name = getComponentNameFromType(parentOwner.type); |
| 3902 | if (name) { |
| 3903 | currentComponentErrorInfo = |
| 3904 | '\n\nCheck the render method of `' + name + '`.'; |
| 3905 | } |
| 3906 | } |
| 3907 | if (!currentComponentErrorInfo) { |
| 3908 | if (componentName) { |
| 3909 | currentComponentErrorInfo = `\n\nCheck the top-level render call using <${componentName}>.`; |
| 3910 | } |
| 3911 | } |
| 3912 | |
| 3913 | // Usually the current owner is the offender, but if it accepts children as a |
| 3914 | // property, it may be the creator of the child that's responsible for |
| 3915 | // assigning it a key. |
| 3916 | let childOwnerAppendix = ''; |
| 3917 | if (childOwner != null && parentOwner !== childOwner) { |
| 3918 | let ownerName = null; |
| 3919 | if (typeof childOwner.type !== 'undefined') { |
| 3920 | ownerName = getComponentNameFromType(childOwner.type); |
| 3921 | } else if (typeof childOwner.name === 'string') { |
| 3922 | ownerName = childOwner.name; |
| 3923 | } |
| 3924 | if (ownerName) { |
| 3925 | // Give the component that originally created this child. |
| 3926 | childOwnerAppendix = ` It was passed a child from ${ownerName}.`; |
| 3927 | } |
| 3928 | } |
| 3929 | |
| 3930 | // We create a fake component stack for the child to log the stack trace from. |
| 3931 | const previousComponentStack = task.componentStack; |
| 3932 | const stackFrame = createComponentStackFromType( |
| 3933 | task.componentStack, |
| 3934 | (child as any).type, |
| 3935 | (child as any)._owner, |
| 3936 | (child as any)._debugStack, |
| 3937 | ); |
| 3938 | task.componentStack = stackFrame; |
| 3939 | console.error( |
| 3940 | 'Each child in a list should have a unique "key" prop.' + |
| 3941 | '%s%s See https://react.dev/link/warning-keys for more information.', |
| 3942 | currentComponentErrorInfo, |
| 3943 | childOwnerAppendix, |
| 3944 | ); |
| 3945 | task.componentStack = previousComponentStack; |
| 3946 | } |
| 3947 | } |
| 3948 | |
| 3949 | function renderChildrenArray( |
| 3950 | request: Request, |
| 3951 | task: Task, |
| 3952 | children: Array<any>, |
| 3953 | childIndex: number, |
| 3954 | ): void { |
| 3955 | const prevKeyPath = task.keyPath; |
| 3956 | const previousComponentStack = task.componentStack; |
| 3957 | let previousDebugTask = null; |
| 3958 | if (__DEV__) { |
| 3959 | previousDebugTask = task.debugTask; |
| 3960 | // We read debugInfo from task.node instead of children because it might have been an |
| 3961 | // unwrapped iterable so we read from the original node. |
| 3962 | pushServerComponentStack(task, (task.node as any)._debugInfo); |
| 3963 | } |
| 3964 | if (childIndex !== -1) { |
| 3965 | task.keyPath = [task.keyPath, 'Fragment', childIndex]; |
| 3966 | if (task.replay !== null) { |
| 3967 | replayFragment( |
| 3968 | request, |
| 3969 | // $FlowFixMe[incompatible-type]: Refined. |
| 3970 | task, |
| 3971 | children, |
| 3972 | childIndex, |
| 3973 | ); |
| 3974 | task.keyPath = prevKeyPath; |
| 3975 | if (__DEV__) { |
| 3976 | task.componentStack = previousComponentStack; |
| 3977 | task.debugTask = previousDebugTask; |
| 3978 | } |
| 3979 | return; |
| 3980 | } |
| 3981 | } |
| 3982 | |
| 3983 | const prevTreeContext = task.treeContext; |
| 3984 | const totalChildren = children.length; |
| 3985 | |
| 3986 | if (task.replay !== null) { |
| 3987 | // Replay |
| 3988 | // First we need to check if we have any resume slots at this level. |
| 3989 | const resumeSlots = task.replay.slots; |
| 3990 | if (resumeSlots !== null && typeof resumeSlots === 'object') { |
| 3991 | for (let i = 0; i < totalChildren; i++) { |
| 3992 | const node = children[i]; |
| 3993 | task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i); |
| 3994 | // We need to use the non-destructive form so that we can safely pop back |
| 3995 | // up and render the sibling if something suspends. |
| 3996 | const resumeSegmentID = resumeSlots[i]; |
| 3997 | // TODO: If this errors we should still continue with the next sibling. |
| 3998 | if (typeof resumeSegmentID === 'number') { |
| 3999 | resumeNode(request, task, resumeSegmentID, node, i); |
| 4000 | // We finished rendering this node, so now we can consume this |
| 4001 | // slot. This must happen after in case we rerender this task. |
| 4002 | delete resumeSlots[i]; |
| 4003 | } else { |
| 4004 | renderNode(request, task, node, i); |
| 4005 | } |
| 4006 | } |
| 4007 | task.treeContext = prevTreeContext; |
| 4008 | task.keyPath = prevKeyPath; |
| 4009 | if (__DEV__) { |
| 4010 | task.componentStack = previousComponentStack; |
| 4011 | task.debugTask = previousDebugTask; |
| 4012 | } |
| 4013 | return; |
| 4014 | } |
| 4015 | } |
| 4016 | |
| 4017 | for (let i = 0; i < totalChildren; i++) { |
| 4018 | const node = children[i]; |
| 4019 | if (__DEV__) { |
| 4020 | warnForMissingKey(request, task, node); |
| 4021 | } |
| 4022 | task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i); |
| 4023 | // We need to use the non-destructive form so that we can safely pop back |
| 4024 | // up and render the sibling if something suspends. |
| 4025 | renderNode(request, task, node, i); |
| 4026 | } |
| 4027 | |
| 4028 | // Because this context is always set right before rendering every child, we |
| 4029 | // only need to reset it to the previous value at the very end. |
| 4030 | task.treeContext = prevTreeContext; |
| 4031 | task.keyPath = prevKeyPath; |
| 4032 | if (__DEV__) { |
| 4033 | task.componentStack = previousComponentStack; |
| 4034 | task.debugTask = previousDebugTask; |
| 4035 | } |
| 4036 | } |
| 4037 | |
| 4038 | function trackPostponedBoundary( |
| 4039 | request: Request, |
| 4040 | trackedPostpones: PostponedHoles, |
| 4041 | boundary: SuspenseBoundary, |
| 4042 | ): ReplaySuspenseBoundary { |
| 4043 | boundary.status = POSTPONED; |
| 4044 | // We need to eagerly assign it an ID because we'll need to refer to |
| 4045 | // it before flushing and we know that we can't inline it. |
| 4046 | boundary.rootSegmentID = request.nextSegmentId++; |
| 4047 | |
| 4048 | const tracked = boundary.tracked; |
| 4049 | if (tracked === null) { |
| 4050 | throw new Error( |
| 4051 | 'It should not be possible to postpone at the root. This is a bug in React.', |
| 4052 | ); |
| 4053 | } |
| 4054 | |
| 4055 | const boundaryKeyPath = tracked.contentKeyPath; |
| 4056 | if (boundaryKeyPath === null) { |
| 4057 | throw new Error( |
| 4058 | 'It should not be possible to postpone at the root. This is a bug in React.', |
| 4059 | ); |
| 4060 | } |
| 4061 | |
| 4062 | const fallbackReplayNode = tracked.fallbackNode; |
| 4063 | |
| 4064 | const children: Array<ReplayNode> = []; |
| 4065 | const boundaryNode: void | ReplayNode = |
| 4066 | trackedPostpones.workingMap.get(boundaryKeyPath); |
| 4067 | if (boundaryNode === undefined) { |
| 4068 | const suspenseBoundary: ReplaySuspenseBoundary = [ |
| 4069 | boundaryKeyPath[1], |
| 4070 | boundaryKeyPath[2], |
| 4071 | children, |
| 4072 | null, |
| 4073 | fallbackReplayNode, |
| 4074 | boundary.rootSegmentID, |
| 4075 | ]; |
| 4076 | trackedPostpones.workingMap.set(boundaryKeyPath, suspenseBoundary); |
| 4077 | addToReplayParent(suspenseBoundary, boundaryKeyPath[0], trackedPostpones); |
| 4078 | return suspenseBoundary; |
| 4079 | } else { |
| 4080 | // Upgrade to ReplaySuspenseBoundary. |
| 4081 | const suspenseBoundary: ReplaySuspenseBoundary = boundaryNode as any; |
| 4082 | suspenseBoundary[4] = fallbackReplayNode; |
| 4083 | suspenseBoundary[5] = boundary.rootSegmentID; |
| 4084 | return suspenseBoundary; |
| 4085 | } |
| 4086 | } |
| 4087 | |
| 4088 | function trackPostpone( |
| 4089 | request: Request, |
| 4090 | trackedPostpones: PostponedHoles, |
| 4091 | task: Task, |
| 4092 | segment: Segment, |
| 4093 | ): void { |
| 4094 | segment.status = POSTPONED; |
| 4095 | |
| 4096 | const keyPath = task.keyPath; |
| 4097 | const boundary = task.blockedBoundary; |
| 4098 | |
| 4099 | if (boundary === null) { |
| 4100 | segment.id = request.nextSegmentId++; |
| 4101 | trackedPostpones.rootSlots = segment.id; |
| 4102 | if (request.completedRootSegment !== null) { |
| 4103 | // Postpone the root if this was a deeper segment. |
| 4104 | request.completedRootSegment.status = POSTPONED; |
| 4105 | } |
| 4106 | return; |
| 4107 | } |
| 4108 | |
| 4109 | // $FlowFixMe[invalid-compare] |
| 4110 | if (boundary !== null && boundary.status === PENDING) { |
| 4111 | const boundaryNode = trackPostponedBoundary( |
| 4112 | request, |
| 4113 | trackedPostpones, |
| 4114 | boundary, |
| 4115 | ); |
| 4116 | if ( |
| 4117 | boundary.tracked !== null && |
| 4118 | boundary.tracked.contentKeyPath === keyPath && |
| 4119 | task.childIndex === -1 |
| 4120 | ) { |
| 4121 | // Assign ID |
| 4122 | if (segment.id === -1) { |
| 4123 | if (segment.parentFlushed) { |
| 4124 | // If this segment's parent was already flushed, it means we really just |
| 4125 | // skipped the parent and this segment is now the root. |
| 4126 | segment.id = boundary.rootSegmentID; |
| 4127 | } else { |
| 4128 | segment.id = request.nextSegmentId++; |
| 4129 | } |
| 4130 | } |
| 4131 | // We postponed directly inside the Suspense boundary so we mark this for resuming. |
| 4132 | boundaryNode[3] = segment.id; |
| 4133 | return; |
| 4134 | } |
| 4135 | // Otherwise, fall through to add the child node. |
| 4136 | } |
| 4137 | |
| 4138 | // We know that this will leave a hole so we might as well assign an ID now. |
| 4139 | // We might have one already if we had a parent that gave us its ID. |
| 4140 | if (segment.id === -1) { |
| 4141 | // $FlowFixMe[invalid-compare] |
| 4142 | if (segment.parentFlushed && boundary !== null) { |
| 4143 | // If this segment's parent was already flushed, it means we really just |
| 4144 | // skipped the parent and this segment is now the root. |
| 4145 | segment.id = boundary.rootSegmentID; |
| 4146 | } else { |
| 4147 | segment.id = request.nextSegmentId++; |
| 4148 | } |
| 4149 | } |
| 4150 | |
| 4151 | if (task.childIndex === -1) { |
| 4152 | // Resume starting from directly inside the previous parent element. |
| 4153 | if (keyPath === null) { |
| 4154 | trackedPostpones.rootSlots = segment.id; |
| 4155 | } else { |
| 4156 | const workingMap = trackedPostpones.workingMap; |
| 4157 | let resumableNode = workingMap.get(keyPath); |
| 4158 | if (resumableNode === undefined) { |
| 4159 | resumableNode = [ |
| 4160 | keyPath[1], |
| 4161 | keyPath[2], |
| 4162 | [] as Array<ReplayNode>, |
| 4163 | segment.id, |
| 4164 | ]; |
| 4165 | addToReplayParent(resumableNode, keyPath[0], trackedPostpones); |
| 4166 | } else { |
| 4167 | resumableNode[3] = segment.id; |
| 4168 | } |
| 4169 | } |
| 4170 | } else { |
| 4171 | let slots; |
| 4172 | if (keyPath === null) { |
| 4173 | slots = trackedPostpones.rootSlots; |
| 4174 | if (slots === null) { |
| 4175 | slots = trackedPostpones.rootSlots = {} as {[index: number]: number}; |
| 4176 | } else if (typeof slots === 'number') { |
| 4177 | throw new Error( |
| 4178 | 'It should not be possible to postpone both at the root of an element ' + |
| 4179 | 'as well as a slot below. This is a bug in React.', |
| 4180 | ); |
| 4181 | } |
| 4182 | } else { |
| 4183 | const workingMap = trackedPostpones.workingMap; |
| 4184 | let resumableNode = workingMap.get(keyPath); |
| 4185 | if (resumableNode === undefined) { |
| 4186 | slots = {} as {[index: number]: number}; |
| 4187 | resumableNode = [ |
| 4188 | keyPath[1], |
| 4189 | keyPath[2], |
| 4190 | [] as Array<ReplayNode>, |
| 4191 | slots, |
| 4192 | ] as ReplayNode; |
| 4193 | workingMap.set(keyPath, resumableNode); |
| 4194 | addToReplayParent(resumableNode, keyPath[0], trackedPostpones); |
| 4195 | } else { |
| 4196 | slots = resumableNode[3]; |
| 4197 | if (slots === null) { |
| 4198 | slots = resumableNode[3] = {} as {[index: number]: number}; |
| 4199 | } else if (typeof slots === 'number') { |
| 4200 | throw new Error( |
| 4201 | 'It should not be possible to postpone both at the root of an element ' + |
| 4202 | 'as well as a slot below. This is a bug in React.', |
| 4203 | ); |
| 4204 | } |
| 4205 | } |
| 4206 | } |
| 4207 | slots[task.childIndex] = segment.id; |
| 4208 | } |
| 4209 | } |
| 4210 | |
| 4211 | // In case a boundary errors, we need to stop tracking it because we won't |
| 4212 | // resume it. |
| 4213 | function untrackBoundary(request: Request, boundary: SuspenseBoundary) { |
| 4214 | const trackedPostpones = request.trackedPostpones; |
| 4215 | if (trackedPostpones === null) { |
| 4216 | return; |
| 4217 | } |
| 4218 | const tracked = boundary.tracked; |
| 4219 | if (tracked === null) { |
| 4220 | return; |
| 4221 | } |
| 4222 | const boundaryKeyPath = tracked.contentKeyPath; |
| 4223 | if (boundaryKeyPath === null) { |
| 4224 | return; |
| 4225 | } |
| 4226 | const boundaryNode: void | ReplayNode = |
| 4227 | trackedPostpones.workingMap.get(boundaryKeyPath); |
| 4228 | if (boundaryNode === undefined) { |
| 4229 | return; |
| 4230 | } |
| 4231 | |
| 4232 | // Downgrade to plain ReplayNode since we won't replay through it. |
| 4233 | // $FlowFixMe[cannot-write]: We intentionally downgrade this to the other tuple. |
| 4234 | boundaryNode.length = 4; |
| 4235 | // Remove any resumable slots. |
| 4236 | boundaryNode[2] = []; |
| 4237 | boundaryNode[3] = null; |
| 4238 | |
| 4239 | // TODO: We should really just remove the boundary from all parent paths too so |
| 4240 | // we don't replay the path to it. |
| 4241 | } |
| 4242 | |
| 4243 | function spawnNewSuspendedReplayTask( |
| 4244 | request: Request, |
| 4245 | task: ReplayTask, |
| 4246 | thenableState: ThenableState | null, |
| 4247 | ): ReplayTask { |
| 4248 | return createReplayTask( |
| 4249 | request, |
| 4250 | thenableState, |
| 4251 | task.replay, |
| 4252 | task.node, |
| 4253 | task.childIndex, |
| 4254 | task.blockedBoundary, |
| 4255 | task.hoistableState, |
| 4256 | task.abortSet, |
| 4257 | task.keyPath, |
| 4258 | task.formatContext, |
| 4259 | task.context, |
| 4260 | task.treeContext, |
| 4261 | task.row, |
| 4262 | task.componentStack, |
| 4263 | !disableLegacyContext ? task.legacyContext : emptyContextObject, |
| 4264 | __DEV__ ? task.debugTask : null, |
| 4265 | ); |
| 4266 | } |
| 4267 | |
| 4268 | function spawnNewSuspendedRenderTask( |
| 4269 | request: Request, |
| 4270 | task: RenderTask, |
| 4271 | thenableState: ThenableState | null, |
| 4272 | ): RenderTask { |
| 4273 | // Something suspended, we'll need to create a new segment and resolve it later. |
| 4274 | const segment = task.blockedSegment; |
| 4275 | const insertionIndex = segment.chunks.length; |
| 4276 | const newSegment = createPendingSegment( |
| 4277 | request, |
| 4278 | insertionIndex, |
| 4279 | null, |
| 4280 | task.formatContext, |
| 4281 | // Adopt the parent segment's leading text embed |
| 4282 | segment.lastPushedText, |
| 4283 | // Assume we are text embedded at the trailing edge |
| 4284 | true, |
| 4285 | ); |
| 4286 | segment.children.push(newSegment); |
| 4287 | // Reset lastPushedText for current Segment since the new Segment "consumed" it |
| 4288 | segment.lastPushedText = false; |
| 4289 | return createRenderTask( |
| 4290 | request, |
| 4291 | thenableState, |
| 4292 | task.node, |
| 4293 | task.childIndex, |
| 4294 | task.blockedBoundary, |
| 4295 | newSegment, |
| 4296 | task.blockedPreamble, |
| 4297 | task.hoistableState, |
| 4298 | task.abortSet, |
| 4299 | task.keyPath, |
| 4300 | task.formatContext, |
| 4301 | task.context, |
| 4302 | task.treeContext, |
| 4303 | task.row, |
| 4304 | task.componentStack, |
| 4305 | !disableLegacyContext ? task.legacyContext : emptyContextObject, |
| 4306 | __DEV__ ? task.debugTask : null, |
| 4307 | ); |
| 4308 | } |
| 4309 | |
| 4310 | // This is a non-destructive form of rendering a node. If it suspends it spawns |
| 4311 | // a new task and restores the context of this task to what it was before. |
| 4312 | function renderNode( |
| 4313 | request: Request, |
| 4314 | task: Task, |
| 4315 | node: ReactNodeList, |
| 4316 | childIndex: number, |
| 4317 | ): void { |
| 4318 | // Snapshot the current context in case something throws to interrupt the |
| 4319 | // process. |
| 4320 | const previousFormatContext = task.formatContext; |
| 4321 | const previousLegacyContext = !disableLegacyContext |
| 4322 | ? task.legacyContext |
| 4323 | : emptyContextObject; |
| 4324 | const previousContext = task.context; |
| 4325 | const previousKeyPath = task.keyPath; |
| 4326 | const previousTreeContext = task.treeContext; |
| 4327 | const previousComponentStack = task.componentStack; |
| 4328 | const previousDebugTask = __DEV__ ? task.debugTask : null; |
| 4329 | let x; |
| 4330 | // Store how much we've pushed at this point so we can reset it in case something |
| 4331 | // suspended partially through writing something. |
| 4332 | const segment = task.blockedSegment; |
| 4333 | if (segment === null) { |
| 4334 | // Replay |
| 4335 | task = task as any as ReplayTask; // Refined |
| 4336 | const previousReplaySet: ReplaySet = task.replay; |
| 4337 | try { |
| 4338 | return renderNodeDestructive(request, task, node, childIndex); |
| 4339 | } catch (thrownValue) { |
| 4340 | resetHooksState(); |
| 4341 | |
| 4342 | x = |
| 4343 | thrownValue === SuspenseException |
| 4344 | ? // This is a special type of exception used for Suspense. For historical |
| 4345 | // reasons, the rest of the Suspense implementation expects the thrown |
| 4346 | // value to be a thenable, because before `use` existed that was the |
| 4347 | // (unstable) API for suspending. This implementation detail can change |
| 4348 | // later, once we deprecate the old API in favor of `use`. |
| 4349 | getSuspendedThenable() |
| 4350 | : thrownValue; |
| 4351 | |
| 4352 | if (request.aborted) { |
| 4353 | // We are aborting so we can just bubble up to the task by falling through |
| 4354 | // $FlowFixMe[invalid-compare] |
| 4355 | } else if (typeof x === 'object' && x !== null) { |
| 4356 | // $FlowFixMe[method-unbinding] |
| 4357 | if (typeof x.then === 'function') { |
| 4358 | const wakeable: Wakeable = x as any; |
| 4359 | const thenableState = |
| 4360 | thrownValue === SuspenseException |
| 4361 | ? getThenableStateAfterSuspending() |
| 4362 | : null; |
| 4363 | const newTask = spawnNewSuspendedReplayTask( |
| 4364 | request, |
| 4365 | // $FlowFixMe[incompatible-type]: Refined. |
| 4366 | task, |
| 4367 | thenableState, |
| 4368 | ); |
| 4369 | const ping = newTask.ping; |
| 4370 | wakeable.then(ping.resolve, ping.reject); |
| 4371 | |
| 4372 | // Restore the context. We assume that this will be restored by the inner |
| 4373 | // functions in case nothing throws so we don't use "finally" here. |
| 4374 | task.formatContext = previousFormatContext; |
| 4375 | if (!disableLegacyContext) { |
| 4376 | task.legacyContext = previousLegacyContext; |
| 4377 | } |
| 4378 | task.context = previousContext; |
| 4379 | task.keyPath = previousKeyPath; |
| 4380 | task.treeContext = previousTreeContext; |
| 4381 | task.componentStack = previousComponentStack; |
| 4382 | task.replay = previousReplaySet; |
| 4383 | if (__DEV__) { |
| 4384 | task.debugTask = previousDebugTask; |
| 4385 | } |
| 4386 | // Restore all active ReactContexts to what they were before. |
| 4387 | switchContext(previousContext); |
| 4388 | return; |
| 4389 | } |
| 4390 | if (x.message === 'Maximum call stack size exceeded') { |
| 4391 | // This was a stack overflow. We do a lot of recursion in React by default for |
| 4392 | // performance but it can lead to stack overflows in extremely deep trees. |
| 4393 | // We do have the ability to create a trampoile if this happens which makes |
| 4394 | // this kind of zero-cost. |
| 4395 | const thenableState = |
| 4396 | thrownValue === SuspenseException |
| 4397 | ? getThenableStateAfterSuspending() |
| 4398 | : null; |
| 4399 | const newTask = spawnNewSuspendedReplayTask( |
| 4400 | request, |
| 4401 | // $FlowFixMe[incompatible-type]: Refined. |
| 4402 | task, |
| 4403 | thenableState, |
| 4404 | ); |
| 4405 | |
| 4406 | // Immediately schedule the task for retrying. |
| 4407 | request.pingedTasks.push(newTask); |
| 4408 | |
| 4409 | // Restore the context. We assume that this will be restored by the inner |
| 4410 | // functions in case nothing throws so we don't use "finally" here. |
| 4411 | task.formatContext = previousFormatContext; |
| 4412 | if (!disableLegacyContext) { |
| 4413 | task.legacyContext = previousLegacyContext; |
| 4414 | } |
| 4415 | task.context = previousContext; |
| 4416 | task.keyPath = previousKeyPath; |
| 4417 | task.treeContext = previousTreeContext; |
| 4418 | task.componentStack = previousComponentStack; |
| 4419 | task.replay = previousReplaySet; |
| 4420 | if (__DEV__) { |
| 4421 | task.debugTask = previousDebugTask; |
| 4422 | } |
| 4423 | // Restore all active ReactContexts to what they were before. |
| 4424 | switchContext(previousContext); |
| 4425 | return; |
| 4426 | } |
| 4427 | } |
| 4428 | |
| 4429 | // TODO: Abort any undiscovered Suspense boundaries in the ReplayNode. |
| 4430 | } |
| 4431 | } else { |
| 4432 | // Render |
| 4433 | const childrenLength = segment.children.length; |
| 4434 | const chunkLength = segment.chunks.length; |
| 4435 | try { |
| 4436 | return renderNodeDestructive(request, task, node, childIndex); |
| 4437 | } catch (thrownValue) { |
| 4438 | resetHooksState(); |
| 4439 | |
| 4440 | // Reset the write pointers to where we started. |
| 4441 | segment.children.length = childrenLength; |
| 4442 | segment.chunks.length = chunkLength; |
| 4443 | |
| 4444 | x = |
| 4445 | thrownValue === SuspenseException |
| 4446 | ? // This is a special type of exception used for Suspense. For historical |
| 4447 | // reasons, the rest of the Suspense implementation expects the thrown |
| 4448 | // value to be a thenable, because before `use` existed that was the |
| 4449 | // (unstable) API for suspending. This implementation detail can change |
| 4450 | // later, once we deprecate the old API in favor of `use`. |
| 4451 | getSuspendedThenable() |
| 4452 | : thrownValue; |
| 4453 | |
| 4454 | if (request.aborted) { |
| 4455 | // We are aborting so we can just bubble up to the task by falling through |
| 4456 | // $FlowFixMe[invalid-compare] |
| 4457 | } else if (typeof x === 'object' && x !== null) { |
| 4458 | // $FlowFixMe[method-unbinding] |
| 4459 | if (typeof x.then === 'function') { |
| 4460 | const wakeable: Wakeable = x as any; |
| 4461 | const thenableState = |
| 4462 | thrownValue === SuspenseException |
| 4463 | ? getThenableStateAfterSuspending() |
| 4464 | : null; |
| 4465 | const newTask = spawnNewSuspendedRenderTask( |
| 4466 | request, |
| 4467 | // $FlowFixMe[incompatible-type]: Refined. |
| 4468 | task, |
| 4469 | thenableState, |
| 4470 | ); |
| 4471 | const ping = newTask.ping; |
| 4472 | wakeable.then(ping.resolve, ping.reject); |
| 4473 | |
| 4474 | // Restore the context. We assume that this will be restored by the inner |
| 4475 | // functions in case nothing throws so we don't use "finally" here. |
| 4476 | task.formatContext = previousFormatContext; |
| 4477 | if (!disableLegacyContext) { |
| 4478 | task.legacyContext = previousLegacyContext; |
| 4479 | } |
| 4480 | task.context = previousContext; |
| 4481 | task.keyPath = previousKeyPath; |
| 4482 | task.treeContext = previousTreeContext; |
| 4483 | task.componentStack = previousComponentStack; |
| 4484 | if (__DEV__) { |
| 4485 | task.debugTask = previousDebugTask; |
| 4486 | } |
| 4487 | // Restore all active ReactContexts to what they were before. |
| 4488 | switchContext(previousContext); |
| 4489 | return; |
| 4490 | } |
| 4491 | if (x.message === 'Maximum call stack size exceeded') { |
| 4492 | // This was a stack overflow. We do a lot of recursion in React by default for |
| 4493 | // performance but it can lead to stack overflows in extremely deep trees. |
| 4494 | // We do have the ability to create a trampoile if this happens which makes |
| 4495 | // this kind of zero-cost. |
| 4496 | const thenableState = |
| 4497 | thrownValue === SuspenseException |
| 4498 | ? getThenableStateAfterSuspending() |
| 4499 | : null; |
| 4500 | const newTask = spawnNewSuspendedRenderTask( |
| 4501 | request, |
| 4502 | // $FlowFixMe[incompatible-type]: Refined. |
| 4503 | task, |
| 4504 | thenableState, |
| 4505 | ); |
| 4506 | |
| 4507 | // Immediately schedule the task for retrying. |
| 4508 | request.pingedTasks.push(newTask); |
| 4509 | |
| 4510 | // Restore the context. We assume that this will be restored by the inner |
| 4511 | // functions in case nothing throws so we don't use "finally" here. |
| 4512 | task.formatContext = previousFormatContext; |
| 4513 | if (!disableLegacyContext) { |
| 4514 | task.legacyContext = previousLegacyContext; |
| 4515 | } |
| 4516 | task.context = previousContext; |
| 4517 | task.keyPath = previousKeyPath; |
| 4518 | task.treeContext = previousTreeContext; |
| 4519 | task.componentStack = previousComponentStack; |
| 4520 | if (__DEV__) { |
| 4521 | task.debugTask = previousDebugTask; |
| 4522 | } |
| 4523 | // Restore all active ReactContexts to what they were before. |
| 4524 | switchContext(previousContext); |
| 4525 | return; |
| 4526 | } |
| 4527 | } |
| 4528 | } |
| 4529 | } |
| 4530 | |
| 4531 | // Restore the context. We assume that this will be restored by the inner |
| 4532 | // functions in case nothing throws so we don't use "finally" here. |
| 4533 | task.formatContext = previousFormatContext; |
| 4534 | if (!disableLegacyContext) { |
| 4535 | task.legacyContext = previousLegacyContext; |
| 4536 | } |
| 4537 | task.context = previousContext; |
| 4538 | task.keyPath = previousKeyPath; |
| 4539 | task.treeContext = previousTreeContext; |
| 4540 | // We intentionally do not restore the component stack on the error pathway |
| 4541 | // Whatever handles the error needs to use this stack which is the location of the |
| 4542 | // error. We must restore the stack wherever we handle this |
| 4543 | |
| 4544 | // Restore all active ReactContexts to what they were before. |
| 4545 | switchContext(previousContext); |
| 4546 | |
| 4547 | throw x; |
| 4548 | } |
| 4549 | |
| 4550 | function erroredReplay( |
| 4551 | request: Request, |
| 4552 | boundary: Root | SuspenseBoundary, |
| 4553 | error: mixed, |
| 4554 | errorInfo: ThrownInfo, |
| 4555 | replayNodes: ReplayNode[], |
| 4556 | resumeSlots: ResumeSlots, |
| 4557 | debugTask: null | ConsoleTask, |
| 4558 | ): void { |
| 4559 | // Erroring during a replay doesn't actually cause an error by itself because |
| 4560 | // that component has already rendered. What causes the error is the resumable |
| 4561 | // points that we did not yet finish which will be below the point of the reset. |
| 4562 | // For example, if we're replaying a path to a Suspense boundary that is not done |
| 4563 | // that doesn't error the parent Suspense boundary. |
| 4564 | // This might be a bit strange that the error in a parent gets thrown at a child. |
| 4565 | // We log it only once and reuse the digest. |
| 4566 | const errorDigest = logRecoverableError(request, error, errorInfo, debugTask); |
| 4567 | abortRemainingReplayNodes( |
| 4568 | request, |
| 4569 | boundary, |
| 4570 | replayNodes, |
| 4571 | resumeSlots, |
| 4572 | error, |
| 4573 | errorDigest, |
| 4574 | errorInfo, |
| 4575 | false, |
| 4576 | ); |
| 4577 | } |
| 4578 | |
| 4579 | function erroredTask( |
| 4580 | request: Request, |
| 4581 | boundary: Root | SuspenseBoundary, |
| 4582 | row: null | SuspenseListRow, |
| 4583 | error: mixed, |
| 4584 | errorInfo: ThrownInfo, |
| 4585 | debugTask: null | ConsoleTask, |
| 4586 | ) { |
| 4587 | if (row !== null) { |
| 4588 | if (--row.pendingTasks === 0) { |
| 4589 | finishSuspenseListRow(request, row); |
| 4590 | } |
| 4591 | } |
| 4592 | |
| 4593 | request.allPendingTasks--; |
| 4594 | |
| 4595 | // We don't handle halts here because we only halt when prerendering and |
| 4596 | // when prerendering we should be finishing tasks not erroring them when |
| 4597 | // they halt or postpone |
| 4598 | if (boundary === null) { |
| 4599 | // Recoverables can remain silent when a Suspense boundary lets us emit a |
| 4600 | // shell and defer its content to a downstream renderer. At the root there |
| 4601 | // is no shell to stream, so this is a fatal error and must be reported like |
| 4602 | // any other root error. |
| 4603 | if (isRecoverableError(error)) { |
| 4604 | // This recoverable reached the root without a Suspense boundary, so |
| 4605 | // report it using the fatal diagnostic while leaving the original intact. |
| 4606 | const fatalRecoverableError = cloneRecoverableErrorAsFatal(error as any); |
| 4607 | logRecoverableError(request, fatalRecoverableError, errorInfo, debugTask); |
| 4608 | fatalError(request, fatalRecoverableError, errorInfo, debugTask); |
| 4609 | } else { |
| 4610 | logRecoverableError(request, error, errorInfo, debugTask); |
| 4611 | fatalError(request, error, errorInfo, debugTask); |
| 4612 | } |
| 4613 | // The shell fatally errored, so the render can never complete. Return before |
| 4614 | // the completeAll check below so we don't fire onAllReady for a render that |
| 4615 | // produced nothing. This mirrors finishAbortedTask, which also returns after |
| 4616 | // a fatalError on the root. |
| 4617 | return; |
| 4618 | } else { |
| 4619 | const errorDigest = logRecoverableError( |
| 4620 | request, |
| 4621 | error, |
| 4622 | errorInfo, |
| 4623 | debugTask, |
| 4624 | ); |
| 4625 | boundary.pendingTasks--; |
| 4626 | if (boundary.status !== CLIENT_RENDERED) { |
| 4627 | boundary.status = CLIENT_RENDERED; |
| 4628 | encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, false); |
| 4629 | untrackBoundary(request, boundary); |
| 4630 | |
| 4631 | const boundaryRow = boundary.row; |
| 4632 | if (boundaryRow !== null) { |
| 4633 | // Unblock the SuspenseListRow that was blocked by this boundary. |
| 4634 | // finishSuspenseListRow → unblockSuspenseListRow → finishedTask reenters |
| 4635 | // and decrements allPendingTasks. Pin the counter above zero so those |
| 4636 | // nested calls can't trip completeAll before this outer frame's own |
| 4637 | // zero check at the end. |
| 4638 | request.allPendingTasks++; |
| 4639 | if (--boundaryRow.pendingTasks === 0) { |
| 4640 | finishSuspenseListRow(request, boundaryRow); |
| 4641 | } |
| 4642 | request.allPendingTasks--; |
| 4643 | } |
| 4644 | |
| 4645 | // Regardless of what happens next, this boundary won't be displayed, |
| 4646 | // so we can flush it, if the parent already flushed. |
| 4647 | if (boundary.parentFlushed) { |
| 4648 | // We don't have a preference where in the queue this goes since it's likely |
| 4649 | // to error on the client anyway. However, intentionally client-rendered |
| 4650 | // boundaries should be flushed earlier so that they can start on the client. |
| 4651 | // We reuse the same queue for errors. |
| 4652 | request.clientRenderedBoundaries.push(boundary); |
| 4653 | } |
| 4654 | |
| 4655 | if ( |
| 4656 | request.pendingRootTasks === 0 && |
| 4657 | request.trackedPostpones === null && |
| 4658 | boundary.preamble !== null |
| 4659 | ) { |
| 4660 | // The root is complete and this boundary may contribute part of the preamble. |
| 4661 | // We eagerly attempt to prepare the preamble here because we expect most requests |
| 4662 | // to have few boundaries which contribute preambles and it allow us to do this |
| 4663 | // preparation work during the work phase rather than the when flushing. |
| 4664 | preparePreamble(request); |
| 4665 | } |
| 4666 | } |
| 4667 | } |
| 4668 | |
| 4669 | if (request.allPendingTasks === 0) { |
| 4670 | completeAll(request); |
| 4671 | } |
| 4672 | } |
| 4673 | |
| 4674 | function abortTaskSoft(this: Request, task: Task): void { |
| 4675 | // This aborts task without aborting the parent boundary that it blocks. |
| 4676 | // It's used for when we didn't need this task to complete the tree. |
| 4677 | // If task was needed, then it should use abortTask instead. |
| 4678 | const request: Request = this; |
| 4679 | const boundary = task.blockedBoundary; |
| 4680 | const segment = task.blockedSegment; |
| 4681 | if (segment !== null) { |
| 4682 | segment.status = ABORTED; |
| 4683 | finishedTask(request, boundary, task.row, segment); |
| 4684 | } |
| 4685 | } |
| 4686 | |
| 4687 | function abortRemainingSuspenseBoundary( |
| 4688 | request: Request, |
| 4689 | rootSegmentID: number, |
| 4690 | error: mixed, |
| 4691 | errorDigest: ?string, |
| 4692 | errorInfo: ThrownInfo, |
| 4693 | wasAborted: boolean, |
| 4694 | ): void { |
| 4695 | const resumedBoundary = createSuspenseBoundary( |
| 4696 | request, |
| 4697 | null, |
| 4698 | new Set(), |
| 4699 | null, |
| 4700 | false, |
| 4701 | ); |
| 4702 | resumedBoundary.parentFlushed = true; |
| 4703 | // We restore the same id of this boundary as was used during prerender. |
| 4704 | resumedBoundary.rootSegmentID = rootSegmentID; |
| 4705 | |
| 4706 | resumedBoundary.status = CLIENT_RENDERED; |
| 4707 | encodeErrorForBoundary( |
| 4708 | resumedBoundary, |
| 4709 | errorDigest, |
| 4710 | error, |
| 4711 | errorInfo, |
| 4712 | wasAborted, |
| 4713 | ); |
| 4714 | |
| 4715 | if (resumedBoundary.parentFlushed) { |
| 4716 | request.clientRenderedBoundaries.push(resumedBoundary); |
| 4717 | } |
| 4718 | } |
| 4719 | |
| 4720 | function abortRemainingReplayNodes( |
| 4721 | request: Request, |
| 4722 | boundary: Root | SuspenseBoundary, |
| 4723 | nodes: Array<ReplayNode>, |
| 4724 | slots: ResumeSlots, |
| 4725 | error: mixed, |
| 4726 | errorDigest: ?string, |
| 4727 | errorInfo: ThrownInfo, |
| 4728 | aborted: boolean, |
| 4729 | ): void { |
| 4730 | for (let i = 0; i < nodes.length; i++) { |
| 4731 | const node = nodes[i]; |
| 4732 | if (node.length === 4) { |
| 4733 | abortRemainingReplayNodes( |
| 4734 | request, |
| 4735 | boundary, |
| 4736 | node[2], |
| 4737 | node[3], |
| 4738 | error, |
| 4739 | errorDigest, |
| 4740 | errorInfo, |
| 4741 | aborted, |
| 4742 | ); |
| 4743 | } else { |
| 4744 | const boundaryNode: ReplaySuspenseBoundary = node; |
| 4745 | const rootSegmentID = boundaryNode[5]; |
| 4746 | abortRemainingSuspenseBoundary( |
| 4747 | request, |
| 4748 | rootSegmentID, |
| 4749 | error, |
| 4750 | errorDigest, |
| 4751 | errorInfo, |
| 4752 | aborted, |
| 4753 | ); |
| 4754 | } |
| 4755 | } |
| 4756 | // Empty the set, since we've cleared it now. |
| 4757 | nodes.length = 0; |
| 4758 | |
| 4759 | if (slots !== null) { |
| 4760 | // We had something still to resume in the parent boundary. We must trigger |
| 4761 | // the error on the parent boundary since it's not able to complete. |
| 4762 | if (boundary === null) { |
| 4763 | throw new Error( |
| 4764 | 'We should not have any resumable nodes in the shell. ' + |
| 4765 | 'This is a bug in React.', |
| 4766 | ); |
| 4767 | } else if (boundary.status !== CLIENT_RENDERED) { |
| 4768 | boundary.status = CLIENT_RENDERED; |
| 4769 | encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, aborted); |
| 4770 | if (boundary.parentFlushed) { |
| 4771 | request.clientRenderedBoundaries.push(boundary); |
| 4772 | } |
| 4773 | } |
| 4774 | // Empty the set |
| 4775 | if (typeof slots === 'object') { |
| 4776 | for (const index in slots) { |
| 4777 | delete slots[index as any]; |
| 4778 | } |
| 4779 | } |
| 4780 | } |
| 4781 | } |
| 4782 | |
| 4783 | function abortTask(task: Task, request: Request): void { |
| 4784 | // Mark pending tasks as aborted synchronously so any work that was already |
| 4785 | // scheduled cannot begin after abort is called. |
| 4786 | if (task === request.currentTask) { |
| 4787 | // This is a currently rendering Task. The render itself will abort the task. |
| 4788 | return; |
| 4789 | } |
| 4790 | const boundary = task.blockedBoundary; |
| 4791 | const segment = task.blockedSegment; |
| 4792 | if (segment !== null) { |
| 4793 | segment.status = ABORTED; |
| 4794 | } |
| 4795 | |
| 4796 | if (__DEV__ && enableAsyncDebugInfo) { |
| 4797 | // Capture async debug information at the point abort begins. The task may |
| 4798 | // receive more data before finishAbort runs and no longer suspend at the |
| 4799 | // call site we need to report. |
| 4800 | let node: any = task.node; |
| 4801 | if (node !== null && typeof node === 'object') { |
| 4802 | let debugInfo = node._debugInfo; |
| 4803 | while ( |
| 4804 | typeof node === 'object' && |
| 4805 | node !== null && |
| 4806 | node.$$typeof === REACT_LAZY_TYPE |
| 4807 | ) { |
| 4808 | const payload = node._payload; |
| 4809 | if (payload.status === 'fulfilled') { |
| 4810 | node = payload.value; |
| 4811 | continue; |
| 4812 | } |
| 4813 | break; |
| 4814 | } |
| 4815 | if ( |
| 4816 | typeof node === 'object' && |
| 4817 | node !== null && |
| 4818 | (isArray(node) || |
| 4819 | typeof node[ASYNC_ITERATOR] === 'function' || |
| 4820 | // $FlowFixMe[invalid-compare] |
| 4821 | node.$$typeof === REACT_ELEMENT_TYPE || |
| 4822 | // $FlowFixMe[invalid-compare] |
| 4823 | node.$$typeof === REACT_LAZY_TYPE) && |
| 4824 | isArray(node._debugInfo) |
| 4825 | ) { |
| 4826 | debugInfo = node._debugInfo; |
| 4827 | } |
| 4828 | pushHaltedAwaitOnComponentStack(task, debugInfo); |
| 4829 | if (task.thenableState !== null) { |
| 4830 | pushSuspendedCallSiteOnComponentStack(request, task); |
| 4831 | } |
| 4832 | } |
| 4833 | } |
| 4834 | |
| 4835 | if (boundary !== null) { |
| 4836 | boundary.fallbackAbortableTasks.forEach(fallbackTask => |
| 4837 | abortTask(fallbackTask, request), |
| 4838 | ); |
| 4839 | } |
| 4840 | } |
| 4841 | |
| 4842 | function finishAbortedTask(task: Task, request: Request, error: mixed): void { |
| 4843 | // Report and complete a task that was synchronously claimed by abortTask. |
| 4844 | // A currently rendering task remains responsible for unwinding itself. |
| 4845 | if (task === request.currentTask) { |
| 4846 | return; |
| 4847 | } |
| 4848 | const boundary = task.blockedBoundary; |
| 4849 | const segment = task.blockedSegment; |
| 4850 | if (segment !== null) { |
| 4851 | if (segment.status !== ABORTED) { |
| 4852 | return; |
| 4853 | } |
| 4854 | } |
| 4855 | |
| 4856 | const errorInfo = getThrownInfo(task.componentStack); |
| 4857 | // Only errors materialized by use() or abort() carry this internal brand. |
| 4858 | // Throwing the browser() token directly is still an application error. |
| 4859 | const isRecoverableReason = isRecoverableError(error); |
| 4860 | |
| 4861 | if (boundary === null) { |
| 4862 | const replay: null | ReplaySet = task.replay; |
| 4863 | if (replay === null) { |
| 4864 | // We didn't complete the root so we have nothing to show. We can close |
| 4865 | // the request; |
| 4866 | if ( |
| 4867 | !isRecoverableReason && |
| 4868 | request.trackedPostpones !== null && |
| 4869 | segment !== null |
| 4870 | ) { |
| 4871 | const trackedPostpones = request.trackedPostpones; |
| 4872 | // We are aborting a prerender and must treat the shell as halted |
| 4873 | // We log the error but we still resolve the prerender |
| 4874 | logRecoverableError(request, error, errorInfo, task.debugTask); |
| 4875 | trackPostpone(request, trackedPostpones, task, segment); |
| 4876 | finishedTask(request, null, task.row, segment); |
| 4877 | } else if (isRecoverableReason) { |
| 4878 | // This root task cannot recover from the abort. Report a fatal clone, |
| 4879 | // but keep the original branded reason on the request for other tasks. |
| 4880 | const fatalRecoverableError = cloneRecoverableErrorAsFatal( |
| 4881 | error as any, |
| 4882 | ); |
| 4883 | logRecoverableError( |
| 4884 | request, |
| 4885 | fatalRecoverableError, |
| 4886 | errorInfo, |
| 4887 | task.debugTask, |
| 4888 | ); |
| 4889 | if (request.status !== CLOSING && request.status !== CLOSED) { |
| 4890 | fatalError(request, fatalRecoverableError, errorInfo, task.debugTask); |
| 4891 | } |
| 4892 | } else { |
| 4893 | logRecoverableError(request, error, errorInfo, task.debugTask); |
| 4894 | if (request.status !== CLOSING && request.status !== CLOSED) { |
| 4895 | fatalError(request, error, errorInfo, task.debugTask); |
| 4896 | } |
| 4897 | } |
| 4898 | return; |
| 4899 | } |
| 4900 | if (request.status !== CLOSING && request.status !== CLOSED) { |
| 4901 | // If the shell aborts during a replay, that's not a fatal error. Instead |
| 4902 | // we should be able to recover by client rendering all the root boundaries in |
| 4903 | // the ReplaySet. |
| 4904 | replay.pendingTasks--; |
| 4905 | if (replay.pendingTasks === 0 && replay.nodes.length > 0) { |
| 4906 | const errorDigest = logRecoverableError( |
| 4907 | request, |
| 4908 | error, |
| 4909 | errorInfo, |
| 4910 | null, |
| 4911 | ); |
| 4912 | abortRemainingReplayNodes( |
| 4913 | request, |
| 4914 | null, |
| 4915 | replay.nodes, |
| 4916 | replay.slots, |
| 4917 | error, |
| 4918 | errorDigest, |
| 4919 | errorInfo, |
| 4920 | true, |
| 4921 | ); |
| 4922 | } |
| 4923 | request.pendingRootTasks--; |
| 4924 | if (request.pendingRootTasks === 0) { |
| 4925 | completeShell(request); |
| 4926 | } |
| 4927 | } |
| 4928 | } else { |
| 4929 | // We construct an errorInfo from the boundary's componentStack so the error in dev will indicate which |
| 4930 | // boundary the message is referring to |
| 4931 | const trackedPostpones = request.trackedPostpones; |
| 4932 | if (boundary.status !== CLIENT_RENDERED) { |
| 4933 | if ( |
| 4934 | !isRecoverableReason && |
| 4935 | trackedPostpones !== null && |
| 4936 | segment !== null |
| 4937 | ) { |
| 4938 | // We are aborting a prerender and must halt this boundary. |
| 4939 | // We treat this like other postpones during prerendering |
| 4940 | logRecoverableError(request, error, errorInfo, task.debugTask); |
| 4941 | trackPostpone(request, trackedPostpones, task, segment); |
| 4942 | // If this boundary was still pending then we haven't already cancelled its fallbacks. |
| 4943 | // We'll need to abort the fallbacks, which will also error that parent boundary. |
| 4944 | boundary.fallbackAbortableTasks.forEach(fallbackTask => |
| 4945 | finishAbortedTask(fallbackTask, request, error), |
| 4946 | ); |
| 4947 | boundary.fallbackAbortableTasks.clear(); |
| 4948 | return finishedTask(request, boundary, task.row, segment); |
| 4949 | } |
| 4950 | boundary.status = CLIENT_RENDERED; |
| 4951 | // We are aborting a render or resume which should put boundaries |
| 4952 | // into an explicitly client rendered state |
| 4953 | const errorDigest = logRecoverableError( |
| 4954 | request, |
| 4955 | error, |
| 4956 | errorInfo, |
| 4957 | task.debugTask, |
| 4958 | ); |
| 4959 | encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, true); |
| 4960 | |
| 4961 | untrackBoundary(request, boundary); |
| 4962 | |
| 4963 | if (boundary.parentFlushed) { |
| 4964 | request.clientRenderedBoundaries.push(boundary); |
| 4965 | } |
| 4966 | } |
| 4967 | |
| 4968 | boundary.pendingTasks--; |
| 4969 | |
| 4970 | const boundaryRow = boundary.row; |
| 4971 | if (boundaryRow !== null) { |
| 4972 | // Unblock the SuspenseListRow that was blocked by this boundary. |
| 4973 | if (--boundaryRow.pendingTasks === 0) { |
| 4974 | finishSuspenseListRow(request, boundaryRow); |
| 4975 | } |
| 4976 | } |
| 4977 | |
| 4978 | // If this boundary was still pending then we haven't already cancelled its fallbacks. |
| 4979 | // We'll need to abort the fallbacks, which will also error that parent boundary. |
| 4980 | boundary.fallbackAbortableTasks.forEach(fallbackTask => |
| 4981 | finishAbortedTask(fallbackTask, request, error), |
| 4982 | ); |
| 4983 | boundary.fallbackAbortableTasks.clear(); |
| 4984 | } |
| 4985 | |
| 4986 | const row = task.row; |
| 4987 | if (row !== null) { |
| 4988 | if (--row.pendingTasks === 0) { |
| 4989 | finishSuspenseListRow(request, row); |
| 4990 | } |
| 4991 | } |
| 4992 | |
| 4993 | request.allPendingTasks--; |
| 4994 | if (request.allPendingTasks === 0) { |
| 4995 | completeAll(request); |
| 4996 | } |
| 4997 | } |
| 4998 | |
| 4999 | function finishAbortedTaskDEV( |
| 5000 | task: Task, |
Showing first 5,000 of 6,706 lines.
View raw