main
js 6,706 lines 225 KB
Raw
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