@samitouri / QOS-React-1 / commits / a5fc797db1

[Fizz] Replay Postponed Paths (#27379)

This forks Task into ReplayTask and RenderTask. A RenderTask is the normal mode and it has a segment to write into. A ReplayTask doesn't have a segment to write into because that has already been written but instead it has a ReplayState which keeps track of the next set of paths to follow. Once we hit a "Resume" node we convert it into a RenderTask and continue rendering from there. We can resume at either an Element position or a Slot position. An Element pointing to a component doesn't mean we resume that component, it means we resume in the child position directly below that component. Slots are slots inside arrays. Instead of statically forking most paths, I kept using the same path and checked for the existence of a segment or replay state dynamically at runtime. However, there's still quite a bit of forking here like retryRenderTask and retryReplayTask. Even in the new forks there's a lot of duplication like resumeSuspenseBoundary, replaySuspenseBoundary and renderSuspenseBoundary. There's opportunity to simplify this a bit.

Sebastian Markbåge committed Sep 15, 2023 at 15:24 UTC a5fc797db14c6e05d4d5c4dbb22a0dd70d41f5d5
4 files changed +1211 -219
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+1 -1
@@ -6365,6 +6365,6 @@ describe('ReactDOMFizzServer', () => {
6365 resumed.pipe(writable);
6366 });
6367
6368 - // TODO: expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
6368 + expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
6369 });
6370 });
packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
+133 -4
@@ -105,7 +105,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
105 }
106 const temp = document.createElement('div');
107 temp.innerHTML = result;
108 - insertNodesAndExecuteScripts(temp, container, null);
108 + await insertNodesAndExecuteScripts(temp, container, null);
109 }
110
111 // @gate experimental
@@ -490,7 +490,51 @@ describe('ReactDOMFizzStaticBrowser', () => {
490
491 await readIntoContainer(resumed);
492
493 - // TODO: expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
493 + expect(getVisibleChildren(container)).toEqual(
494 + <div>{['Hello', 'World']}</div>,
495 + );
496 + });
497 +
498 + // @gate enablePostpone
499 + it('supports postponing in prerender and resuming with a prefix', async () => {
500 + let prerendering = true;
501 + function Postpone() {
502 + if (prerendering) {
503 + React.unstable_postpone();
504 + }
505 + return 'World';
506 + }
507 +
508 + function App() {
509 + return (
510 + <div>
511 + <Suspense fallback="Loading...">
512 + Hello
513 + <Postpone />
514 + </Suspense>
515 + </div>
516 + );
517 + }
518 +
519 + const prerendered = await ReactDOMFizzStatic.prerender(<App />);
520 + expect(prerendered.postponed).not.toBe(null);
521 +
522 + prerendering = false;
523 +
524 + const resumed = await ReactDOMFizzServer.resume(
525 + <App />,
526 + prerendered.postponed,
527 + );
528 +
529 + await readIntoContainer(prerendered.prelude);
530 +
531 + expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
532 +
533 + await readIntoContainer(resumed);
534 +
535 + expect(getVisibleChildren(container)).toEqual(
536 + <div>{['Hello', 'World']}</div>,
537 + );
538 });
539
540 // @gate enablePostpone
@@ -500,7 +544,51 @@ describe('ReactDOMFizzStaticBrowser', () => {
544 React.unstable_postpone();
545 });
546
547 + function App() {
548 + return (
549 + <div>
550 + <Suspense fallback="Loading...">
551 + Hi
552 + {prerendering ? Hole : 'Hello'}
553 + </Suspense>
554 + </div>
555 + );
556 + }
557 +
558 + const prerendered = await ReactDOMFizzStatic.prerender(<App />);
559 + expect(prerendered.postponed).not.toBe(null);
560 +
561 + prerendering = false;
562 +
563 + const resumed = await ReactDOMFizzServer.resume(
564 + <App />,
565 + prerendered.postponed,
566 + );
567 +
568 + await readIntoContainer(prerendered.prelude);
569 +
570 + expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
571 +
572 + await readIntoContainer(resumed);
573 +
574 + expect(getVisibleChildren(container)).toEqual(
575 + <div>
576 + {'Hi'}
577 + {'Hello'}
578 + </div>,
579 + );
580 + });
581 +
582 + // @gate enablePostpone
583 + it('supports postponing in a nested array', async () => {
584 + let prerendering = true;
585 + const Hole = React.lazy(async () => {
586 + React.unstable_postpone();
587 + });
588 function Postpone() {
589 + if (prerendering) {
590 + React.unstable_postpone();
591 + }
592 return 'Hello';
593 }
594
@@ -509,7 +597,48 @@ describe('ReactDOMFizzStaticBrowser', () => {
597 <div>
598 <Suspense fallback="Loading...">
599 Hi
512 - {prerendering ? Hole : <Postpone />}
600 + {[<Postpone key="key" />, prerendering ? Hole : 'World']}
601 + </Suspense>
602 + </div>
603 + );
604 + }
605 +
606 + const prerendered = await ReactDOMFizzStatic.prerender(<App />);
607 + expect(prerendered.postponed).not.toBe(null);
608 +
609 + prerendering = false;
610 +
611 + const resumed = await ReactDOMFizzServer.resume(
612 + <App />,
613 + prerendered.postponed,
614 + );
615 +
616 + await readIntoContainer(prerendered.prelude);
617 +
618 + expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
619 +
620 + await readIntoContainer(resumed);
621 +
622 + expect(getVisibleChildren(container)).toEqual(
623 + <div>{['Hi', 'Hello', 'World']}</div>,
624 + );
625 + });
626 +
627 + // @gate enablePostpone
628 + it('supports postponing in lazy as a direct child', async () => {
629 + let prerendering = true;
630 + const Hole = React.lazy(async () => {
631 + React.unstable_postpone();
632 + });
633 + function Postpone() {
634 + return prerendering ? Hole : 'Hello';
635 + }
636 +
637 + function App() {
638 + return (
639 + <div>
640 + <Suspense fallback="Loading...">
641 + <Postpone key="key" />
642 </Suspense>
643 </div>
644 );
@@ -531,7 +660,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
660
661 await readIntoContainer(resumed);
662
534 - // TODO: expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
663 + expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
664 });
665
666 // @gate enablePostpone
packages/react-server/src/ReactFizzServer.js
+1072 -213
@@ -168,7 +168,8 @@ export type KeyNode = [
168 const REPLAY_NODE = 0;
169 const REPLAY_SUSPENSE_BOUNDARY = 1;
170 const RESUME_ELEMENT = 2;
171 -const RESUME_SLOT = 3;
171 +const RESUME_SUSPENSE_BOUNDARY = 3;
172 +const RESUME_SLOT = 4;
173
174 type ReplaySuspenseBoundary = [
175 1, // REPLAY_SUSPENSE_BOUNDARY
@@ -188,6 +189,14 @@ type ReplayNode =
189 ]
190 | ReplaySuspenseBoundary;
191
192 +type ResumeSuspenseBoundary = [
193 + 3, // RESUME_SUSPENSE_BOUNDARY
194 + string | null /* name */,
195 + string | number /* key */,
196 + SuspenseBoundaryID /* id */,
197 + number /* rootSegmentID */,
198 +];
199 +
200 type ResumeElement = [
201 2, // RESUME_ELEMENT
202 string | null /* name */,
@@ -196,12 +205,16 @@ type ResumeElement = [
205 ];
206
207 type ResumeSlot = [
199 - 3, // RESUME_SLOT
208 + 4, // RESUME_SLOT
209 number /* index */,
210 number /* segment id */,
211 ];
212
204 -type ResumableNode = ReplayNode | ResumeElement | ResumeSlot;
213 +type ResumableNode =
214 + | ReplayNode
215 + | ResumeElement
216 + | ResumeSuspenseBoundary
217 + | ResumeSlot;
218
219 type PostponedHoles = {
220 workingMap: Map<KeyNode, ReplayNode>,
@@ -230,7 +243,8 @@ type SuspenseBoundary = {
243 keyPath: Root | KeyNode,
244 };
245
233 -export type Task = {
246 +type RenderTask = {
247 + replay: null,
248 node: ReactNodeList,
249 childIndex: number,
250 ping: () => void,
@@ -246,6 +260,32 @@ export type Task = {
260 thenableState: null | ThenableState,
261 };
262
263 +type ReplaySet = {
264 + nodes: Array<ResumableNode>, // the possible paths to follow down the replaying
265 + pendingTasks: number, // tracks the number of tasks currently tracking this set of nodes
266 + // if pending tasks reach zero but there are still nodes left, it means we couldn't find
267 + // them all in the tree, so we need to abort and client render the boundary.
268 +};
269 +
270 +type ReplayTask = {
271 + replay: ReplaySet,
272 + node: ReactNodeList,
273 + childIndex: number,
274 + ping: () => void,
275 + blockedBoundary: Root | SuspenseBoundary,
276 + blockedSegment: null, // we don't write to anything when we replay
277 + abortSet: Set<Task>, // the abortable set that this task belongs to
278 + keyPath: Root | KeyNode, // the path of all parent keys currently rendering
279 + formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML)
280 + legacyContext: LegacyContext, // the current legacy context that this task is executing in
281 + context: ContextSnapshot, // the current new context that this task is executing in
282 + treeContext: TreeContext, // the current tree context that this task is executing in
283 + componentStack: null | ComponentStackNode, // DEV-only component stack
284 + thenableState: null | ThenableState,
285 +};
286 +
287 +export type Task = RenderTask | ReplayTask;
288 +
289 const PENDING = 0;
290 const COMPLETED = 1;
291 const FLUSHED = 2;
@@ -400,7 +440,7 @@ export function createRequest(
440 );
441 // There is no parent so conceptually, we're unblocked to flush this segment.
442 rootSegment.parentFlushed = true;
403 - const rootTask = createTask(
443 + const rootTask = createRenderTask(
444 request,
445 null,
446 children,
@@ -490,25 +530,13 @@ export function resumeRequest(
530 onFatalError: onFatalError === undefined ? noop : onFatalError,
531 formState: null,
532 };
493 - // This segment represents the root fallback.
494 - const rootSegment = createPendingSegment(
495 - request,
496 - 0,
497 - null,
498 - postponedState.rootFormatContext,
499 - // Root segments are never embedded in Text on either edge
500 - false,
501 - false,
502 - );
503 - // There is no parent so conceptually, we're unblocked to flush this segment.
504 - rootSegment.parentFlushed = true;
505 - const rootTask = createTask(
533 + const rootTask = createReplayTask(
534 request,
535 null,
536 + {nodes: postponedState.resumablePath, pendingTasks: 0},
537 children,
538 -1,
539 null,
511 - rootSegment,
540 abortSet,
541 null,
542 postponedState.rootFormatContext,
@@ -560,7 +588,7 @@ function createSuspenseBoundary(
588 };
589 }
590
563 -function createTask(
591 +function createRenderTask(
592 request: Request,
593 thenableState: ThenableState | null,
594 node: ReactNodeList,
@@ -573,14 +601,15 @@ function createTask(
601 legacyContext: LegacyContext,
602 context: ContextSnapshot,
603 treeContext: TreeContext,
576 -): Task {
604 +): RenderTask {
605 request.allPendingTasks++;
606 if (blockedBoundary === null) {
607 request.pendingRootTasks++;
608 } else {
609 blockedBoundary.pendingTasks++;
610 }
583 - const task: Task = ({
611 + const task: RenderTask = ({
612 + replay: null,
613 node,
614 childIndex,
615 ping: () => pingTask(request, task),
@@ -601,6 +630,49 @@ function createTask(
630 return task;
631 }
632
633 +function createReplayTask(
634 + request: Request,
635 + thenableState: ThenableState | null,
636 + replay: ReplaySet,
637 + node: ReactNodeList,
638 + childIndex: number,
639 + blockedBoundary: Root | SuspenseBoundary,
640 + abortSet: Set<Task>,
641 + keyPath: Root | KeyNode,
642 + formatContext: FormatContext,
643 + legacyContext: LegacyContext,
644 + context: ContextSnapshot,
645 + treeContext: TreeContext,
646 +): ReplayTask {
647 + request.allPendingTasks++;
648 + if (blockedBoundary === null) {
649 + request.pendingRootTasks++;
650 + } else {
651 + blockedBoundary.pendingTasks++;
652 + }
653 + replay.pendingTasks++;
654 + const task: ReplayTask = ({
655 + replay,
656 + node,
657 + childIndex,
658 + ping: () => pingTask(request, task),
659 + blockedBoundary,
660 + blockedSegment: null,
661 + abortSet,
662 + keyPath,
663 + formatContext,
664 + legacyContext,
665 + context,
666 + treeContext,
667 + thenableState,
668 + }: any);
669 + if (__DEV__) {
670 + task.componentStack = null;
671 + }
672 + abortSet.add(task);
673 + return task;
674 +}
675 +
676 function createPendingSegment(
677 request: Request,
678 index: number,
@@ -739,10 +811,19 @@ function fatalError(request: Request, error: mixed): void {
811
812 function renderSuspenseBoundary(
813 request: Request,
742 - task: Task,
814 + someTask: Task,
815 keyPath: Root | KeyNode,
816 props: Object,
817 ): void {
818 + if (someTask.replay !== null) {
819 + throw new Error(
820 + 'Did not expect to see a Suspense boundary in this slot. ' +
821 + "The tree doesn't match so React will fallback to client rendering.",
822 + );
823 + }
824 + // $FlowFixMe: Refined.
825 + const task: RenderTask = someTask;
826 +
827 pushBuiltInComponentStackInDEV(task, 'Suspense');
828
829 const prevKeyPath = task.keyPath;
@@ -866,7 +947,7 @@ function renderSuspenseBoundary(
947
948 // We create suspended task for the fallback because we don't want to actually work
949 // on it yet in case we finish the main content, so we queue for later.
869 - const suspendedFallbackTask = createTask(
950 + const suspendedFallbackTask = createRenderTask(
951 request,
952 null,
953 fallback,
@@ -891,6 +972,203 @@ function renderSuspenseBoundary(
972 popComponentStackInDEV(task);
973 }
974
975 +function replaySuspenseBoundary(
976 + request: Request,
977 + task: ReplayTask,
978 + props: Object,
979 + replayNode: ReplaySuspenseBoundary,
980 +): void {
981 + pushBuiltInComponentStackInDEV(task, 'Suspense');
982 +
983 + const previousReplaySet: ReplaySet = task.replay;
984 +
985 + const parentBoundary = task.blockedBoundary;
986 +
987 + const content: ReactNodeList = props.children;
988 +
989 + const fallbackAbortSet: Set<Task> = new Set();
990 + const resumedBoundary = createSuspenseBoundary(
991 + request,
992 + fallbackAbortSet,
993 + task.keyPath,
994 + );
995 + resumedBoundary.parentFlushed = true;
996 + // We restore the same id of this boundary as was used during prerender.
997 + resumedBoundary.id = replayNode[4];
998 + resumedBoundary.rootSegmentID = replayNode[5];
999 +
1000 + // We can reuse the current context and task to render the content immediately without
1001 + // context switching. We just need to temporarily switch which boundary and replay node
1002 + // we're writing to. If something suspends, it'll spawn new suspended task with that context.
1003 + task.blockedBoundary = resumedBoundary;
1004 + task.replay = {nodes: replayNode[3], pendingTasks: 1};
1005 + if (enableFloat) {
1006 + // Does this even matter for replaying?
1007 + setCurrentlyRenderingBoundaryResourcesTarget(
1008 + request.renderState,
1009 + resumedBoundary.resources,
1010 + );
1011 + }
1012 + try {
1013 + // We use the safe form because we don't handle suspending here. Only error handling.
1014 + renderNode(request, task, content, -1);
1015 + if (
1016 + resumedBoundary.pendingTasks === 0 &&
1017 + resumedBoundary.status === PENDING
1018 + ) {
1019 + resumedBoundary.status = COMPLETED;
1020 + request.completedBoundaries.push(resumedBoundary);
1021 + }
1022 + if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) {
1023 + throw new Error(
1024 + "Couldn't find all resumable slots by key/index during replaying. " +
1025 + "The tree doesn't match so React will fallback to client rendering.",
1026 + );
1027 + }
1028 + task.replay.pendingTasks--;
1029 + } catch (error) {
1030 + resumedBoundary.status = CLIENT_RENDERED;
1031 + let errorDigest;
1032 + if (
1033 + enablePostpone &&
1034 + typeof error === 'object' &&
1035 + error !== null &&
1036 + error.$$typeof === REACT_POSTPONE_TYPE
1037 + ) {
1038 + const postponeInstance: Postpone = (error: any);
1039 + logPostpone(request, postponeInstance.message);
1040 + // TODO: Figure out a better signal than a magic digest value.
1041 + errorDigest = 'POSTPONE';
1042 + } else {
1043 + errorDigest = logRecoverableError(request, error);
1044 + }
1045 + resumedBoundary.errorDigest = errorDigest;
1046 + if (__DEV__) {
1047 + captureBoundaryErrorDetailsDev(resumedBoundary, error);
1048 + }
1049 +
1050 + task.replay.pendingTasks--;
1051 +
1052 + // We don't need to decrement any task numbers because we didn't spawn any new task.
1053 + // We don't need to schedule any task because we know the parent has written yet.
1054 + // We do need to fallthrough to create the fallback though.
1055 + } finally {
1056 + if (enableFloat) {
1057 + setCurrentlyRenderingBoundaryResourcesTarget(
1058 + request.renderState,
1059 + parentBoundary ? parentBoundary.resources : null,
1060 + );
1061 + }
1062 + task.blockedBoundary = parentBoundary;
1063 + task.replay = previousReplaySet;
1064 + }
1065 + // TODO: Should this be in the finally?
1066 + popComponentStackInDEV(task);
1067 +}
1068 +
1069 +function resumeSuspenseBoundary(
1070 + request: Request,
1071 + task: ReplayTask,
1072 + props: Object,
1073 + replayNode: ResumeSuspenseBoundary,
1074 +): void {
1075 + pushBuiltInComponentStackInDEV(task, 'Suspense');
1076 +
1077 + const previousReplaySet: ReplaySet = task.replay;
1078 +
1079 + const parentBoundary = task.blockedBoundary;
1080 +
1081 + const content: ReactNodeList = props.children;
1082 +
1083 + const fallbackAbortSet: Set<Task> = new Set();
1084 + const resumedBoundary = createSuspenseBoundary(
1085 + request,
1086 + fallbackAbortSet,
1087 + task.keyPath,
1088 + );
1089 + resumedBoundary.parentFlushed = true;
1090 + // We restore the same id of this boundary as was used during prerender.
1091 + resumedBoundary.id = replayNode[3];
1092 + resumedBoundary.rootSegmentID = replayNode[4];
1093 +
1094 + const resumedSegment = createPendingSegment(
1095 + request,
1096 + 0,
1097 + null,
1098 + task.formatContext,
1099 + false,
1100 + false,
1101 + );
1102 + resumedSegment.parentFlushed = true;
1103 + resumedSegment.id = replayNode[4];
1104 +
1105 + // We can reuse the current context and task to render the content immediately without
1106 + // context switching. We just need to temporarily switch which boundary and replay node
1107 + // we're writing to. If something suspends, it'll spawn new suspended task with that context.
1108 + task.blockedBoundary = resumedBoundary;
1109 + if (enableFloat) {
1110 + // Does this even matter for replaying?
1111 + setCurrentlyRenderingBoundaryResourcesTarget(
1112 + request.renderState,
1113 + resumedBoundary.resources,
1114 + );
1115 + }
1116 + try {
1117 + // Convert the current ReplayTask to a RenderTask.
1118 + const renderTask: RenderTask = (task: any);
1119 + renderTask.replay = null;
1120 + renderTask.blockedSegment = resumedSegment;
1121 + // We use the safe form because we don't handle suspending here. Only error handling.
1122 + renderNode(request, task, content, -1);
1123 + resumedSegment.status = COMPLETED;
1124 + queueCompletedSegment(resumedBoundary, resumedSegment);
1125 + if (
1126 + resumedBoundary.pendingTasks === 0 &&
1127 + resumedBoundary.status === PENDING
1128 + ) {
1129 + resumedBoundary.status = COMPLETED;
1130 + request.completedBoundaries.push(resumedBoundary);
1131 + }
1132 + } catch (error) {
1133 + resumedBoundary.status = CLIENT_RENDERED;
1134 + let errorDigest;
1135 + if (
1136 + enablePostpone &&
1137 + typeof error === 'object' &&
1138 + error !== null &&
1139 + error.$$typeof === REACT_POSTPONE_TYPE
1140 + ) {
1141 + const postponeInstance: Postpone = (error: any);
1142 + logPostpone(request, postponeInstance.message);
1143 + // TODO: Figure out a better signal than a magic digest value.
1144 + errorDigest = 'POSTPONE';
1145 + } else {
1146 + errorDigest = logRecoverableError(request, error);
1147 + }
1148 + resumedBoundary.errorDigest = errorDigest;
1149 + if (__DEV__) {
1150 + captureBoundaryErrorDetailsDev(resumedBoundary, error);
1151 + }
1152 +
1153 + // We don't need to decrement any task numbers because we didn't spawn any new task.
1154 + // We don't need to schedule any task because we know the parent has written yet.
1155 + // We do need to fallthrough to create the fallback though.
1156 + } finally {
1157 + if (enableFloat) {
1158 + setCurrentlyRenderingBoundaryResourcesTarget(
1159 + request.renderState,
1160 + parentBoundary ? parentBoundary.resources : null,
1161 + );
1162 + }
1163 + task.blockedBoundary = parentBoundary;
1164 + // Restore to a ReplayTask
1165 + task.blockedSegment = null;
1166 + task.replay = previousReplaySet;
1167 + }
1168 + // TODO: Should this be in the finally?
1169 + popComponentStackInDEV(task);
1170 +}
1171 +
1172 function renderBackupSuspenseBoundary(
1173 request: Request,
1174 task: Task,
@@ -901,13 +1179,18 @@ function renderBackupSuspenseBoundary(
1179
1180 const content = props.children;
1181 const segment = task.blockedSegment;
904 -
905 - pushStartCompletedSuspenseBoundary(segment.chunks);
1182 const prevKeyPath = task.keyPath;
1183 task.keyPath = keyPath;
908 - renderNode(request, task, content, -1);
1184 + if (segment === null) {
1185 + // Replay
1186 + renderNode(request, task, content, -1);
1187 + } else {
1188 + // Render
1189 + pushStartCompletedSuspenseBoundary(segment.chunks);
1190 + renderNode(request, task, content, -1);
1191 + pushEndCompletedSuspenseBoundary(segment.chunks);
1192 + }
1193 task.keyPath = prevKeyPath;
910 - pushEndCompletedSuspenseBoundary(segment.chunks);
1194
1195 popComponentStackInDEV(task);
1196 }
@@ -921,38 +1204,56 @@ function renderHostElement(
1204 ): void {
1205 pushBuiltInComponentStackInDEV(task, type);
1206 const segment = task.blockedSegment;
1207 + if (segment === null) {
1208 + // Replay
1209 + const children = props.children; // TODO: Make this a Config for replaying.
1210 + const prevContext = task.formatContext;
1211 + const prevKeyPath = task.keyPath;
1212 + task.formatContext = getChildFormatContext(prevContext, type, props);
1213 + task.keyPath = keyPath;
1214
925 - const children = pushStartInstance(
926 - segment.chunks,
927 - type,
928 - props,
929 - request.resumableState,
930 - request.renderState,
931 - task.formatContext,
932 - segment.lastPushedText,
933 - );
934 - segment.lastPushedText = false;
935 - const prevContext = task.formatContext;
936 - const prevKeyPath = task.keyPath;
937 - task.formatContext = getChildFormatContext(prevContext, type, props);
938 - task.keyPath = keyPath;
1215 + // We use the non-destructive form because if something suspends, we still
1216 + // need to pop back up and finish this subtree of HTML.
1217 + renderNode(request, task, children, -1);
1218 +
1219 + // We expect that errors will fatal the whole task and that we don't need
1220 + // the correct context. Therefore this is not in a finally.
1221 + task.formatContext = prevContext;
1222 + task.keyPath = prevKeyPath;
1223 + } else {
1224 + // Render
1225 + const children = pushStartInstance(
1226 + segment.chunks,
1227 + type,
1228 + props,
1229 + request.resumableState,
1230 + request.renderState,
1231 + task.formatContext,
1232 + segment.lastPushedText,
1233 + );
1234 + segment.lastPushedText = false;
1235 + const prevContext = task.formatContext;
1236 + const prevKeyPath = task.keyPath;
1237 + task.formatContext = getChildFormatContext(prevContext, type, props);
1238 + task.keyPath = keyPath;
1239
940 - // We use the non-destructive form because if something suspends, we still
941 - // need to pop back up and finish this subtree of HTML.
942 - renderNode(request, task, children, -1);
1240 + // We use the non-destructive form because if something suspends, we still
1241 + // need to pop back up and finish this subtree of HTML.
1242 + renderNode(request, task, children, -1);
1243
944 - // We expect that errors will fatal the whole task and that we don't need
945 - // the correct context. Therefore this is not in a finally.
946 - task.formatContext = prevContext;
947 - task.keyPath = prevKeyPath;
948 - pushEndInstance(
949 - segment.chunks,
950 - type,
951 - props,
952 - request.resumableState,
953 - prevContext,
954 - );
955 - segment.lastPushedText = false;
1244 + // We expect that errors will fatal the whole task and that we don't need
1245 + // the correct context. Therefore this is not in a finally.
1246 + task.formatContext = prevContext;
1247 + task.keyPath = prevKeyPath;
1248 + pushEndInstance(
1249 + segment.chunks,
1250 + type,
1251 + props,
1252 + request.resumableState,
1253 + prevContext,
1254 + );
1255 + segment.lastPushedText = false;
1256 + }
1257 popComponentStackInDEV(task);
1258 }
1259
@@ -1645,9 +1946,234 @@ function renderElement(
1946 );
1947 }
1948
1648 -// $FlowFixMe[missing-local-annot]
1649 -function validateIterable(iterable, iteratorFn: Function): void {
1650 - if (__DEV__) {
1949 +function resumeNode(
1950 + request: Request,
1951 + task: ReplayTask,
1952 + segmentId: number,
1953 + node: ReactNodeList,
1954 + childIndex: number,
1955 +): void {
1956 + const prevReplay = task.replay;
1957 + const blockedBoundary = task.blockedBoundary;
1958 + const resumedSegment = createPendingSegment(
1959 + request,
1960 + 0,
1961 + null,
1962 + task.formatContext,
1963 + false,
1964 + false,
1965 + );
1966 + resumedSegment.id = segmentId;
1967 + resumedSegment.parentFlushed = true;
1968 + try {
1969 + // Convert the current ReplayTask to a RenderTask.
1970 + const renderTask: RenderTask = (task: any);
1971 + renderTask.replay = null;
1972 + renderTask.blockedSegment = resumedSegment;
1973 + renderNode(request, task, node, childIndex);
1974 + resumedSegment.status = COMPLETED;
1975 + if (blockedBoundary === null) {
1976 + request.completedRootSegment = resumedSegment;
1977 + } else {
1978 + queueCompletedSegment(blockedBoundary, resumedSegment);
1979 + if (blockedBoundary.parentFlushed) {
1980 + request.partialBoundaries.push(blockedBoundary);
1981 + }
1982 + }
1983 + } finally {
1984 + // Restore to a ReplayTask.
1985 + task.replay = prevReplay;
1986 + task.blockedSegment = null;
1987 + }
1988 +}
1989 +
1990 +function resumeElement(
1991 + request: Request,
1992 + task: ReplayTask,
1993 + keyPath: Root | KeyNode,
1994 + segmentId: number,
1995 + prevThenableState: ThenableState | null,
1996 + type: any,
1997 + props: Object,
1998 + ref: any,
1999 +): void {
2000 + const prevReplay = task.replay;
2001 + const blockedBoundary = task.blockedBoundary;
2002 + const resumedSegment = createPendingSegment(
2003 + request,
2004 + 0,
2005 + null,
2006 + task.formatContext,
2007 + false,
2008 + false,
2009 + );
2010 + resumedSegment.id = segmentId;
2011 + resumedSegment.parentFlushed = true;
2012 + try {
2013 + // Convert the current ReplayTask to a RenderTask.
2014 + const renderTask: RenderTask = (task: any);
2015 + renderTask.replay = null;
2016 + renderTask.blockedSegment = resumedSegment;
2017 + renderElement(request, task, keyPath, prevThenableState, type, props, ref);
2018 + resumedSegment.status = COMPLETED;
2019 + if (blockedBoundary === null) {
2020 + request.completedRootSegment = resumedSegment;
2021 + } else {
2022 + queueCompletedSegment(blockedBoundary, resumedSegment);
2023 + if (blockedBoundary.parentFlushed) {
2024 + request.partialBoundaries.push(blockedBoundary);
2025 + }
2026 + }
2027 + } finally {
2028 + // Restore to a ReplayTask.
2029 + task.replay = prevReplay;
2030 + task.blockedSegment = null;
2031 + }
2032 +}
2033 +
2034 +function replayElement(
2035 + request: Request,
2036 + task: ReplayTask,
2037 + keyPath: Root | KeyNode,
2038 + prevThenableState: ThenableState | null,
2039 + name: null | string,
2040 + keyOrIndex: number | string,
2041 + childIndex: number,
2042 + type: any,
2043 + props: Object,
2044 + ref: any,
2045 + replay: ReplaySet,
2046 +): void {
2047 + // We're replaying. Find the path to follow.
2048 + const replayNodes = replay.nodes;
2049 + for (let i = 0; i < replayNodes.length; i++) {
2050 + // Flow doesn't support refinement on tuples so we do it manually here.
2051 + const candidate: any = replayNodes[i];
2052 + switch (candidate[0]) {
2053 + case REPLAY_NODE: {
2054 + const node: ReplayNode = candidate;
2055 + if (keyOrIndex === node[2]) {
2056 + // Let's double check that the component name matches as a precaution.
2057 + if (name !== null && name !== node[1]) {
2058 + throw new Error(
2059 + 'Expected to see a component of type "' +
2060 + name +
2061 + '" in this slot. ' +
2062 + "The tree doesn't match so React will fallback to client rendering.",
2063 + );
2064 + }
2065 + // Matched a replayable path.
2066 + task.replay = {nodes: node[3], pendingTasks: 1};
2067 + try {
2068 + renderElement(
2069 + request,
2070 + task,
2071 + keyPath,
2072 + prevThenableState,
2073 + type,
2074 + props,
2075 + ref,
2076 + );
2077 + // We finished rendering this node, so now we can consume this
2078 + // slot. This must happen after in case we rerender this task.
2079 + replayNodes.splice(i, 1);
2080 + } finally {
2081 + task.replay.pendingTasks--;
2082 + if (
2083 + task.replay.pendingTasks === 0 &&
2084 + task.replay.nodes.length > 0
2085 + ) {
2086 + throw new Error(
2087 + "Couldn't find all resumable slots by key/index during replaying. " +
2088 + "The tree doesn't match so React will fallback to client rendering.",
2089 + );
2090 + }
2091 + task.replay = replay;
2092 + }
2093 + }
2094 + continue;
2095 + }
2096 + case REPLAY_SUSPENSE_BOUNDARY: {
2097 + const node: ReplaySuspenseBoundary = candidate;
2098 + if (keyOrIndex === node[2]) {
2099 + // Let's double check that the component type matches.
2100 + if (type !== REACT_SUSPENSE_TYPE) {
2101 + throw new Error(
2102 + 'Expected to see a Suspense boundary in this slot. ' +
2103 + "The tree doesn't match so React will fallback to client rendering.",
2104 + );
2105 + }
2106 + // Matched a replayable path.
2107 + replaySuspenseBoundary(request, task, props, node);
2108 + // We finished rendering this node, so now we can consume this
2109 + // slot. This must happen after in case we rerender this task.
2110 + replayNodes.splice(i, 1);
2111 + }
2112 + continue;
2113 + }
2114 + case RESUME_ELEMENT: {
2115 + const node: ResumeElement = candidate;
2116 + if (keyOrIndex === node[2]) {
2117 + // Let's double check that the component name matches as a precaution.
2118 + if (name !== node[1]) {
2119 + throw new Error(
2120 + 'Expected to see a component of type "' +
2121 + (name || 'unknown') +
2122 + '" in this slot. ' +
2123 + "The tree doesn't match so React will fallback to client rendering.",
2124 + );
2125 + }
2126 + // Matched a resumable element.
2127 +
2128 + const segmentId = node[3];
2129 +
2130 + resumeElement(
2131 + request,
2132 + task,
2133 + keyPath,
2134 + segmentId,
2135 + prevThenableState,
2136 + type,
2137 + props,
2138 + ref,
2139 + );
2140 +
2141 + // We finished rendering this node, so now we can consume this
2142 + // slot. This must happen after in case we rerender this task.
2143 + replayNodes.splice(i, 1);
2144 + }
2145 + continue;
2146 + }
2147 + case RESUME_SUSPENSE_BOUNDARY: {
2148 + const node: ResumeSuspenseBoundary = candidate;
2149 + if (keyOrIndex === node[2]) {
2150 + // Let's double check that the component name matches as a precaution.
2151 + if (type !== REACT_SUSPENSE_TYPE) {
2152 + throw new Error(
2153 + 'Expected to see a Suspense boundary in this slot. ' +
2154 + "The tree doesn't match so React will fallback to client rendering.",
2155 + );
2156 + }
2157 + // Matched a resumable suspense boundary.
2158 + resumeSuspenseBoundary(request, task, props, node);
2159 +
2160 + // We finished rendering this node, so now we can consume this
2161 + // slot. This must happen after in case we rerender this task.
2162 + replayNodes.splice(i, 1);
2163 + }
2164 + continue;
2165 + }
2166 + // For RESUME_SLOT we ignore them here and assume we've handled them
2167 + // separately already.
2168 + }
2169 + }
2170 + // We didn't find any matching nodes. We assume that this element was already
2171 + // rendered in the prelude and skip it.
2172 +}
2173 +
2174 +// $FlowFixMe[missing-local-annot]
2175 +function validateIterable(iterable, iteratorFn: Function): void {
2176 + if (__DEV__) {
2177 // We don't support rendering Generators because it's a mutation.
2178 // See https://github.com/facebook/react/issues/12995
2179 if (
@@ -1748,20 +2274,37 @@ function renderNodeDestructiveImpl(
2274 const props = element.props;
2275 const ref = element.ref;
2276 const name = getComponentNameFromType(type);
1751 - const keyPath = [
1752 - task.keyPath,
1753 - name,
1754 - key == null ? (childIndex === -1 ? 0 : childIndex) : key,
1755 - ];
1756 - renderElement(
1757 - request,
1758 - task,
1759 - keyPath,
1760 - prevThenableState,
1761 - type,
1762 - props,
1763 - ref,
1764 - );
2277 + const keyOrIndex =
2278 + key == null ? (childIndex === -1 ? 0 : childIndex) : key;
2279 + const keyPath = [task.keyPath, name, keyOrIndex];
2280 + if (task.replay !== null) {
2281 + replayElement(
2282 + request,
2283 + task,
2284 + keyPath,
2285 + prevThenableState,
2286 + name,
2287 + keyOrIndex,
2288 + childIndex,
2289 + type,
2290 + props,
2291 + ref,
2292 + task.replay,
2293 + );
2294 + // No matches found for this node. We assume it's already emitted in the
2295 + // prelude and skip it during the replay.
2296 + } else {
2297 + // We're doing a plain render.
2298 + renderElement(
2299 + request,
2300 + task,
2301 + keyPath,
2302 + prevThenableState,
2303 + type,
2304 + props,
2305 + ref,
2306 + );
2307 + }
2308 return;
2309 }
2310 case REACT_PORTAL_TYPE:
@@ -1882,23 +2425,33 @@ function renderNodeDestructiveImpl(
2425
2426 if (typeof node === 'string') {
2427 const segment = task.blockedSegment;
1885 - segment.lastPushedText = pushTextInstance(
1886 - task.blockedSegment.chunks,
1887 - node,
1888 - request.renderState,
1889 - segment.lastPushedText,
1890 - );
2428 + if (segment === null) {
2429 + // We assume a text node doesn't have a representation in the replay set,
2430 + // since it can't postpone. If it does, it'll be left unmatched and error.
2431 + } else {
2432 + segment.lastPushedText = pushTextInstance(
2433 + segment.chunks,
2434 + node,
2435 + request.renderState,
2436 + segment.lastPushedText,
2437 + );
2438 + }
2439 return;
2440 }
2441
2442 if (typeof node === 'number') {
2443 const segment = task.blockedSegment;
1896 - segment.lastPushedText = pushTextInstance(
1897 - task.blockedSegment.chunks,
1898 - '' + node,
1899 - request.renderState,
1900 - segment.lastPushedText,
1901 - );
2444 + if (segment === null) {
2445 + // We assume a text node doesn't have a representation in the replay set,
2446 + // since it can't postpone. If it does, it'll be left unmatched and error.
2447 + } else {
2448 + segment.lastPushedText = pushTextInstance(
2449 + segment.chunks,
2450 + '' + node,
2451 + request.renderState,
2452 + segment.lastPushedText,
2453 + );
2454 + }
2455 return;
2456 }
2457
@@ -1918,13 +2471,85 @@ function renderChildrenArray(
2471 task: Task,
2472 children: Array<any>,
2473 childIndex: number,
1921 -) {
2474 +): void {
2475 const prevKeyPath = task.keyPath;
2476 if (childIndex !== -1) {
1924 - task.keyPath = [task.keyPath, '', childIndex];
2477 + task.keyPath = [task.keyPath, 'Fragment', childIndex];
2478 + if (task.replay !== null) {
2479 + // If we're supposed follow this array, we'd expect to see a ReplayNode matching
2480 + // this fragment.
2481 + const replayTask: ReplayTask = task;
2482 + const replay = task.replay;
2483 + const replayNodes = replay.nodes;
2484 + for (let j = 0; j < replayNodes.length; j++) {
2485 + const replayNode = replayNodes[j];
2486 + if (replayNode[0] !== REPLAY_NODE) {
2487 + continue;
2488 + }
2489 + const node: ReplayNode = (replayNode: any);
2490 + if (node[2] !== childIndex) {
2491 + continue;
2492 + }
2493 + // Matched a replayable path.
2494 + replayTask.replay = {nodes: node[3], pendingTasks: 1};
2495 + try {
2496 + renderChildrenArray(request, task, children, -1);
2497 + } finally {
2498 + replayTask.replay.pendingTasks--;
2499 + if (
2500 + replayTask.replay.pendingTasks === 0 &&
2501 + replayTask.replay.nodes.length > 0
2502 + ) {
2503 + throw new Error(
2504 + "Couldn't find all resumable slots by key/index during replaying. " +
2505 + "The tree doesn't match so React will fallback to client rendering.",
2506 + );
2507 + }
2508 + replayTask.replay = replay;
2509 + }
2510 + // We finished rendering this node, so now we can consume this
2511 + // slot. This must happen after in case we rerender this task.
2512 + replayNodes.splice(j, 1);
2513 + break;
2514 + }
2515 + task.keyPath = prevKeyPath;
2516 + return;
2517 + }
2518 }
2519 const prevTreeContext = task.treeContext;
2520 const totalChildren = children.length;
2521 +
2522 + if (task.replay !== null) {
2523 + // Replay
2524 + // First we need to check if we have any resume slots at this level.
2525 + // TODO: This could be simpler if we just stored RESUME_SLOT in a separate set.
2526 + let hadOtherReplayNodes = false;
2527 + const replayNodes = task.replay.nodes;
2528 + for (let j = 0; j < replayNodes.length; ) {
2529 + const replayNode = replayNodes[j];
2530 + if (replayNode[0] !== RESUME_SLOT) {
2531 + hadOtherReplayNodes = true;
2532 + j++; // skip
2533 + continue;
2534 + }
2535 + const resumeSlot: ResumeSlot = (replayNode: any);
2536 + const i = resumeSlot[1]; // The index of the child to resume.
2537 + const segmentId = resumeSlot[2];
2538 + task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
2539 + resumeNode(request, task, segmentId, children[i], i);
2540 + // We finished rendering this node, so now we can consume this
2541 + // slot. This must happen after in case we rerender this task.
2542 + replayNodes.splice(j, 1);
2543 + }
2544 + // If had non-resume slot nodes, we need to also try to match them below.
2545 + if (!hadOtherReplayNodes) {
2546 + // If we didn't, we can bail early.
2547 + task.treeContext = prevTreeContext;
2548 + task.keyPath = prevKeyPath;
2549 + return;
2550 + }
2551 + }
2552 +
2553 for (let i = 0; i < totalChildren; i++) {
2554 const node = children[i];
2555 task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
@@ -1932,6 +2557,7 @@ function renderChildrenArray(
2557 // up and render the sibling if something suspends.
2558 renderNode(request, task, node, i);
2559 }
2560 +
2561 // Because this context is always set right before rendering every child, we
2562 // only need to reset it to the previous value at the very end.
2563 task.treeContext = prevTreeContext;
@@ -1945,8 +2571,13 @@ function trackPostpone(
2571 segment: Segment,
2572 ): void {
2573 segment.status = POSTPONED;
1948 - // We know that this will leave a hole so we might as well assign an ID now.
1949 - segment.id = request.nextSegmentId++;
2574 +
2575 + const keyPath = task.keyPath;
2576 + if (keyPath === null) {
2577 + throw new Error(
2578 + 'It should not be possible to postpone at the root. This is a bug in React.',
2579 + );
2580 + }
2581
2582 const boundary = task.blockedBoundary;
2583 if (boundary !== null && boundary.status === PENDING) {
@@ -1965,29 +2596,52 @@ function trackPostpone(
2596 'It should not be possible to postpone at the root. This is a bug in React.',
2597 );
2598 }
1968 - const children: Array<ResumableNode> = [];
1969 - const boundaryNode: ReplaySuspenseBoundary = [
1970 - REPLAY_SUSPENSE_BOUNDARY,
1971 - boundaryKeyPath[1],
1972 - boundaryKeyPath[2],
1973 - children,
1974 - boundary.id,
1975 - boundary.rootSegmentID,
1976 - ];
1977 - trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode);
1978 - addToReplayParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
2599 +
2600 + if (boundaryKeyPath === keyPath && task.childIndex === -1) {
2601 + // Since we postponed directly in the Suspense boundary we can't have written anything
2602 + // to its segment. Therefore this will end up becoming the root segment.
2603 + segment.id = boundary.rootSegmentID;
2604 + // We postponed directly inside the Suspense boundary so we mark this for resuming.
2605 + const boundaryNode: ResumeSuspenseBoundary = [
2606 + RESUME_SUSPENSE_BOUNDARY,
2607 + boundaryKeyPath[1],
2608 + boundaryKeyPath[2],
2609 + boundary.id,
2610 + boundary.rootSegmentID,
2611 + ];
2612 + addToReplayParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
2613 + return;
2614 + } else {
2615 + const children: Array<ResumableNode> = [];
2616 + const boundaryNode: ReplaySuspenseBoundary = [
2617 + REPLAY_SUSPENSE_BOUNDARY,
2618 + boundaryKeyPath[1],
2619 + boundaryKeyPath[2],
2620 + children,
2621 + boundary.id,
2622 + boundary.rootSegmentID,
2623 + ];
2624 + trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode);
2625 + addToReplayParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
2626 + // Fall through to add the child node.
2627 + }
2628 }
2629
1981 - const keyPath = task.keyPath;
1982 - if (keyPath === null) {
1983 - throw new Error(
1984 - 'It should not be possible to postpone at the root. This is a bug in React.',
1985 - );
2630 + // We know that this will leave a hole so we might as well assign an ID now.
2631 + // We might have one already if we had a parent that gave us its ID.
2632 + if (segment.id === -1) {
2633 + if (segment.parentFlushed && boundary !== null) {
2634 + // If this segment's parent was already flushed, it means we really just
2635 + // skipped the parent and this segment is now the root.
2636 + segment.id = boundary.rootSegmentID;
2637 + } else {
2638 + segment.id = request.nextSegmentId++;
2639 + }
2640 }
2641
2642 if (task.childIndex === -1) {
1989 - // Resume at the position before the first array
1990 - const resumableElement = [
2643 + // Resume starting from directly inside the previous parent element.
2644 + const resumableElement: ResumeElement = [
2645 RESUME_ELEMENT,
2646 keyPath[1],
2647 keyPath[2],
@@ -2003,7 +2657,7 @@ function trackPostpone(
2657
2658 function injectPostponedHole(
2659 request: Request,
2006 - task: Task,
2660 + task: RenderTask,
2661 reason: string,
2662 ): Segment {
2663 logPostpone(request, reason);
@@ -2026,9 +2680,41 @@ function injectPostponedHole(
2680 return newSegment;
2681 }
2682
2029 -function spawnNewSuspendedTask(
2683 +function spawnNewSuspendedReplayTask(
2684 request: Request,
2031 - task: Task,
2685 + task: ReplayTask,
2686 + thenableState: ThenableState | null,
2687 + x: Wakeable,
2688 +): void {
2689 + const newTask = createReplayTask(
2690 + request,
2691 + thenableState,
2692 + task.replay,
2693 + task.node,
2694 + task.childIndex,
2695 + task.blockedBoundary,
2696 + task.abortSet,
2697 + task.keyPath,
2698 + task.formatContext,
2699 + task.legacyContext,
2700 + task.context,
2701 + task.treeContext,
2702 + );
2703 +
2704 + if (__DEV__) {
2705 + if (task.componentStack !== null) {
2706 + // We pop one task off the stack because the node that suspended will be tried again,
2707 + // which will add it back onto the stack.
2708 + newTask.componentStack = task.componentStack.parent;
2709 + }
2710 + }
2711 + const ping = newTask.ping;
2712 + x.then(ping, ping);
2713 +}
2714 +
2715 +function spawnNewSuspendedRenderTask(
2716 + request: Request,
2717 + task: RenderTask,
2718 thenableState: ThenableState | null,
2719 x: Wakeable,
2720 ): void {
@@ -2048,7 +2734,7 @@ function spawnNewSuspendedTask(
2734 segment.children.push(newSegment);
2735 // Reset lastPushedText for current Segment since the new Segment "consumed" it
2736 segment.lastPushedText = false;
2051 - const newTask = createTask(
2737 + const newTask = createRenderTask(
2738 request,
2739 thenableState,
2740 task.node,
@@ -2082,12 +2768,6 @@ function renderNode(
2768 node: ReactNodeList,
2769 childIndex: number,
2770 ): void {
2085 - // Store how much we've pushed at this point so we can reset it in case something
2086 - // suspended partially through writing something.
2087 - const segment = task.blockedSegment;
2088 - const childrenLength = segment.children.length;
2089 - const chunkLength = segment.chunks.length;
2090 -
2771 // Snapshot the current context in case something throws to interrupt the
2772 // process.
2773 const previousFormatContext = task.formatContext;
@@ -2099,102 +2779,164 @@ function renderNode(
2779 if (__DEV__) {
2780 previousComponentStack = task.componentStack;
2781 }
2102 - try {
2103 - return renderNodeDestructive(request, task, null, node, childIndex);
2104 - } catch (thrownValue) {
2105 - resetHooksState();
2782 + let x;
2783 + // Store how much we've pushed at this point so we can reset it in case something
2784 + // suspended partially through writing something.
2785 + const segment = task.blockedSegment;
2786 + if (segment === null) {
2787 + // Replay
2788 + try {
2789 + return renderNodeDestructive(request, task, null, node, childIndex);
2790 + } catch (thrownValue) {
2791 + resetHooksState();
2792 +
2793 + x =
2794 + thrownValue === SuspenseException
2795 + ? // This is a special type of exception used for Suspense. For historical
2796 + // reasons, the rest of the Suspense implementation expects the thrown
2797 + // value to be a thenable, because before `use` existed that was the
2798 + // (unstable) API for suspending. This implementation detail can change
2799 + // later, once we deprecate the old API in favor of `use`.
2800 + getSuspendedThenable()
2801 + : thrownValue;
2802 +
2803 + if (typeof x === 'object' && x !== null) {
2804 + // $FlowFixMe[method-unbinding]
2805 + if (typeof x.then === 'function') {
2806 + const wakeable: Wakeable = (x: any);
2807 + const thenableState = getThenableStateAfterSuspending();
2808 + spawnNewSuspendedReplayTask(
2809 + request,
2810 + // $FlowFixMe: Refined.
2811 + task,
2812 + thenableState,
2813 + wakeable,
2814 + );
2815
2107 - // Reset the write pointers to where we started.
2108 - segment.children.length = childrenLength;
2109 - segment.chunks.length = chunkLength;
2816 + // Restore the context. We assume that this will be restored by the inner
2817 + // functions in case nothing throws so we don't use "finally" here.
2818 + task.formatContext = previousFormatContext;
2819 + task.legacyContext = previousLegacyContext;
2820 + task.context = previousContext;
2821 + task.keyPath = previousKeyPath;
2822 + task.treeContext = previousTreeContext;
2823 + // Restore all active ReactContexts to what they were before.
2824 + switchContext(previousContext);
2825 + if (__DEV__) {
2826 + task.componentStack = previousComponentStack;
2827 + }
2828 + return;
2829 + }
2830 + }
2831
2111 - const x =
2112 - thrownValue === SuspenseException
2113 - ? // This is a special type of exception used for Suspense. For historical
2114 - // reasons, the rest of the Suspense implementation expects the thrown
2115 - // value to be a thenable, because before `use` existed that was the
2116 - // (unstable) API for suspending. This implementation detail can change
2117 - // later, once we deprecate the old API in favor of `use`.
2118 - getSuspendedThenable()
2119 - : thrownValue;
2832 + // TODO: Abort any undiscovered Suspense boundaries in the ResumableNode.
2833 + }
2834 + } else {
2835 + // Render
2836 + const childrenLength = segment.children.length;
2837 + const chunkLength = segment.chunks.length;
2838 + try {
2839 + return renderNodeDestructive(request, task, null, node, childIndex);
2840 + } catch (thrownValue) {
2841 + resetHooksState();
2842 +
2843 + // Reset the write pointers to where we started.
2844 + segment.children.length = childrenLength;
2845 + segment.chunks.length = chunkLength;
2846 +
2847 + x =
2848 + thrownValue === SuspenseException
2849 + ? // This is a special type of exception used for Suspense. For historical
2850 + // reasons, the rest of the Suspense implementation expects the thrown
2851 + // value to be a thenable, because before `use` existed that was the
2852 + // (unstable) API for suspending. This implementation detail can change
2853 + // later, once we deprecate the old API in favor of `use`.
2854 + getSuspendedThenable()
2855 + : thrownValue;
2856 +
2857 + if (typeof x === 'object' && x !== null) {
2858 + // $FlowFixMe[method-unbinding]
2859 + if (typeof x.then === 'function') {
2860 + const wakeable: Wakeable = (x: any);
2861 + const thenableState = getThenableStateAfterSuspending();
2862 + spawnNewSuspendedRenderTask(
2863 + request,
2864 + // $FlowFixMe: Refined.
2865 + task,
2866 + thenableState,
2867 + wakeable,
2868 + );
2869
2121 - if (typeof x === 'object' && x !== null) {
2122 - // $FlowFixMe[method-unbinding]
2123 - if (typeof x.then === 'function') {
2124 - const wakeable: Wakeable = (x: any);
2125 - const thenableState = getThenableStateAfterSuspending();
2126 - spawnNewSuspendedTask(request, task, thenableState, wakeable);
2127 -
2128 - // Restore the context. We assume that this will be restored by the inner
2129 - // functions in case nothing throws so we don't use "finally" here.
2130 - task.formatContext = previousFormatContext;
2131 - task.legacyContext = previousLegacyContext;
2132 - task.context = previousContext;
2133 - task.keyPath = previousKeyPath;
2134 - task.treeContext = previousTreeContext;
2135 - // Restore all active ReactContexts to what they were before.
2136 - switchContext(previousContext);
2137 - if (__DEV__) {
2138 - task.componentStack = previousComponentStack;
2870 + // Restore the context. We assume that this will be restored by the inner
2871 + // functions in case nothing throws so we don't use "finally" here.
2872 + task.formatContext = previousFormatContext;
2873 + task.legacyContext = previousLegacyContext;
2874 + task.context = previousContext;
2875 + task.keyPath = previousKeyPath;
2876 + task.treeContext = previousTreeContext;
2877 + // Restore all active ReactContexts to what they were before.
2878 + switchContext(previousContext);
2879 + if (__DEV__) {
2880 + task.componentStack = previousComponentStack;
2881 + }
2882 + return;
2883 }
2140 - return;
2141 - }
2142 - if (
2143 - enablePostpone &&
2144 - request.trackedPostpones !== null &&
2145 - x.$$typeof === REACT_POSTPONE_TYPE &&
2146 - task.blockedBoundary !== null // TODO: Support holes in the shell
2147 - ) {
2148 - // If we're tracking postpones, we inject a hole here and continue rendering
2149 - // sibling. Similar to suspending. If we're not tracking, we treat it more like
2150 - // an error. Notably this doesn't spawn a new task since nothing will fill it
2151 - // in during this prerender.
2152 - const postponeInstance: Postpone = (x: any);
2153 - const trackedPostpones = request.trackedPostpones;
2154 - const postponedSegment = injectPostponedHole(
2155 - request,
2156 - task,
2157 - postponeInstance.message,
2158 - );
2159 - trackPostpone(request, trackedPostpones, task, postponedSegment);
2160 -
2161 - // Restore the context. We assume that this will be restored by the inner
2162 - // functions in case nothing throws so we don't use "finally" here.
2163 - task.formatContext = previousFormatContext;
2164 - task.legacyContext = previousLegacyContext;
2165 - task.context = previousContext;
2166 - task.keyPath = previousKeyPath;
2167 - task.treeContext = previousTreeContext;
2168 - // Restore all active ReactContexts to what they were before.
2169 - switchContext(previousContext);
2170 - if (__DEV__) {
2171 - task.componentStack = previousComponentStack;
2884 + if (
2885 + enablePostpone &&
2886 + request.trackedPostpones !== null &&
2887 + x.$$typeof === REACT_POSTPONE_TYPE &&
2888 + task.blockedBoundary !== null // TODO: Support holes in the shell
2889 + ) {
2890 + // If we're tracking postpones, we inject a hole here and continue rendering
2891 + // sibling. Similar to suspending. If we're not tracking, we treat it more like
2892 + // an error. Notably this doesn't spawn a new task since nothing will fill it
2893 + // in during this prerender.
2894 + const postponeInstance: Postpone = (x: any);
2895 + const trackedPostpones = request.trackedPostpones;
2896 + const postponedSegment = injectPostponedHole(
2897 + request,
2898 + ((task: any): RenderTask), // We don't use ReplayTasks in prerenders.
2899 + postponeInstance.message,
2900 + );
2901 + trackPostpone(request, trackedPostpones, task, postponedSegment);
2902 +
2903 + // Restore the context. We assume that this will be restored by the inner
2904 + // functions in case nothing throws so we don't use "finally" here.
2905 + task.formatContext = previousFormatContext;
2906 + task.legacyContext = previousLegacyContext;
2907 + task.context = previousContext;
2908 + task.keyPath = previousKeyPath;
2909 + task.treeContext = previousTreeContext;
2910 + // Restore all active ReactContexts to what they were before.
2911 + switchContext(previousContext);
2912 + if (__DEV__) {
2913 + task.componentStack = previousComponentStack;
2914 + }
2915 + return;
2916 }
2173 - return;
2917 }
2918 }
2176 - // Restore the context. We assume that this will be restored by the inner
2177 - // functions in case nothing throws so we don't use "finally" here.
2178 - task.formatContext = previousFormatContext;
2179 - task.legacyContext = previousLegacyContext;
2180 - task.context = previousContext;
2181 - task.keyPath = previousKeyPath;
2182 - task.treeContext = previousTreeContext;
2183 - // Restore all active ReactContexts to what they were before.
2184 - switchContext(previousContext);
2185 - if (__DEV__) {
2186 - task.componentStack = previousComponentStack;
2187 - }
2188 - // We assume that we don't need the correct context.
2189 - // Let's terminate the rest of the tree and don't render any siblings.
2190 - throw x;
2919 }
2920 + // Restore the context. We assume that this will be restored by the inner
2921 + // functions in case nothing throws so we don't use "finally" here.
2922 + task.formatContext = previousFormatContext;
2923 + task.legacyContext = previousLegacyContext;
2924 + task.context = previousContext;
2925 + task.keyPath = previousKeyPath;
2926 + task.treeContext = previousTreeContext;
2927 + // Restore all active ReactContexts to what they were before.
2928 + switchContext(previousContext);
2929 + if (__DEV__) {
2930 + task.componentStack = previousComponentStack;
2931 + }
2932 + // We assume that we don't need the correct context.
2933 + // Let's terminate the rest of the tree and don't render any siblings.
2934 + throw x;
2935 }
2936
2937 function erroredTask(
2938 request: Request,
2939 boundary: Root | SuspenseBoundary,
2197 - segment: Segment,
2940 error: mixed,
2941 ) {
2942 // Report the error to a global handler.
@@ -2213,6 +2955,9 @@ function erroredTask(
2955 errorDigest = logRecoverableError(request, error);
2956 }
2957 if (boundary === null) {
2958 + // TODO: If the shell errors during a replay, that's not a fatal error. Instead
2959 + // we should be able to recover by client rendering all the root boundaries in
2960 + // the ReplaySet and any already matched.
2961 fatalError(request, error);
2962 } else {
2963 boundary.pendingTasks--;
@@ -2249,8 +2994,17 @@ function abortTaskSoft(this: Request, task: Task): void {
2994 const request: Request = this;
2995 const boundary = task.blockedBoundary;
2996 const segment = task.blockedSegment;
2252 - segment.status = ABORTED;
2253 - finishedTask(request, boundary, segment);
2997 + if (segment !== null) {
2998 + segment.status = ABORTED;
2999 + finishedTask(request, boundary, segment);
3000 + }
3001 +}
3002 +
3003 +function abortRemainingResumableNodes(
3004 + nodes: Array<ResumableNode>,
3005 + error: mixed,
3006 +): void {
3007 + // TODO: Abort any undiscovered Suspense boundaries in the ReplaySet.
3008 }
3009
3010 function abortTask(task: Task, request: Request, error: mixed): void {
@@ -2258,7 +3012,16 @@ function abortTask(task: Task, request: Request, error: mixed): void {
3012 // client rendered mode.
3013 const boundary = task.blockedBoundary;
3014 const segment = task.blockedSegment;
2261 - segment.status = ABORTED;
3015 + if (segment === null) {
3016 + // $FlowFixMe: Refined.
3017 + const replay: ReplaySet = task.replay;
3018 + replay.pendingTasks--;
3019 + if (replay.pendingTasks === 0) {
3020 + abortRemainingResumableNodes(replay.nodes, error);
3021 + }
3022 + } else {
3023 + segment.status = ABORTED;
3024 + }
3025
3026 if (boundary === null) {
3027 request.allPendingTasks--;
@@ -2318,9 +3081,7 @@ function queueCompletedSegment(
3081 if (
3082 segment.chunks.length === 0 &&
3083 segment.children.length === 1 &&
2321 - segment.children[0].boundary === null &&
2322 - // Typically the id would not be assigned yet but if it's a postponed segment it might be.
2323 - segment.children[0].id === -1
3084 + segment.children[0].boundary === null
3085 ) {
3086 // This is an empty segment. There's nothing to write, so we can instead transfer the ID
3087 // to the child. That way any existing references point to the child.
@@ -2339,10 +3100,10 @@ function queueCompletedSegment(
3100 function finishedTask(
3101 request: Request,
3102 boundary: Root | SuspenseBoundary,
2342 - segment: Segment,
3103 + segment: null | Segment,
3104 ) {
3105 if (boundary === null) {
2345 - if (segment.parentFlushed) {
3106 + if (segment !== null && segment.parentFlushed) {
3107 if (request.completedRootSegment !== null) {
3108 throw new Error(
3109 'There can only be one root segment. This is a bug in React.',
@@ -2367,7 +3128,7 @@ function finishedTask(
3128 boundary.status = COMPLETED;
3129 }
3130 // This must have been the last segment we were waiting on. This boundary is now complete.
2370 - if (segment.parentFlushed) {
3131 + if (segment !== null && segment.parentFlushed) {
3132 // Our parent segment already flushed, so we need to schedule this segment to be emitted.
3133 // If it is a segment that was aborted, we'll write other content instead so we don't need
3134 // to emit it.
@@ -2387,7 +3148,7 @@ function finishedTask(
3148 boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request);
3149 boundary.fallbackAbortableTasks.clear();
3150 } else {
2390 - if (segment.parentFlushed) {
3151 + if (segment !== null && segment.parentFlushed) {
3152 // Our parent already flushed, so we need to schedule this segment to be emitted.
3153 // If it is a segment that was aborted, we'll write other content instead so we don't need
3154 // to emit it.
@@ -2425,6 +3186,27 @@ function retryTask(request: Request, task: Task): void {
3186 );
3187 }
3188 const segment = task.blockedSegment;
3189 + if (segment === null) {
3190 + retryReplayTask(
3191 + request,
3192 + // $FlowFixMe: Refined.
3193 + task,
3194 + );
3195 + } else {
3196 + retryRenderTask(
3197 + request,
3198 + // $FlowFixMe: Refined.
3199 + task,
3200 + segment,
3201 + );
3202 + }
3203 +}
3204 +
3205 +function retryRenderTask(
3206 + request: Request,
3207 + task: RenderTask,
3208 + segment: Segment,
3209 +): void {
3210 if (segment.status !== PENDING) {
3211 // We completed this by other means before we had a chance to retry it.
3212 return;
@@ -2513,7 +3295,82 @@ function retryTask(request: Request, task: Task): void {
3295 }
3296 task.abortSet.delete(task);
3297 segment.status = ERRORED;
2516 - erroredTask(request, task.blockedBoundary, segment, x);
3298 + erroredTask(request, task.blockedBoundary, x);
3299 + return;
3300 + } finally {
3301 + if (enableFloat) {
3302 + setCurrentlyRenderingBoundaryResourcesTarget(request.renderState, null);
3303 + }
3304 + if (__DEV__) {
3305 + currentTaskInDEV = prevTaskInDEV;
3306 + }
3307 + }
3308 +}
3309 +
3310 +function retryReplayTask(request: Request, task: ReplayTask): void {
3311 + if (task.replay.pendingTasks === 0) {
3312 + // There are no pending tasks working on this set, so we must have aborted.
3313 + return;
3314 + }
3315 +
3316 + // We restore the context to what it was when we suspended.
3317 + // We don't restore it after we leave because it's likely that we'll end up
3318 + // needing a very similar context soon again.
3319 + switchContext(task.context);
3320 + let prevTaskInDEV = null;
3321 + if (__DEV__) {
3322 + prevTaskInDEV = currentTaskInDEV;
3323 + currentTaskInDEV = task;
3324 + }
3325 +
3326 + try {
3327 + // We call the destructive form that mutates this task. That way if something
3328 + // suspends again, we can reuse the same task instead of spawning a new one.
3329 +
3330 + // Reset the task's thenable state before continuing, so that if a later
3331 + // component suspends we can reuse the same task object. If the same
3332 + // component suspends again, the thenable state will be restored.
3333 + const prevThenableState = task.thenableState;
3334 + task.thenableState = null;
3335 +
3336 + renderNodeDestructive(request, task, prevThenableState, task.node, -1);
3337 +
3338 + if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) {
3339 + throw new Error(
3340 + "Couldn't find all resumable slots by key/index during replaying. " +
3341 + "The tree doesn't match so React will fallback to client rendering.",
3342 + );
3343 + }
3344 + task.replay.pendingTasks--;
3345 +
3346 + task.abortSet.delete(task);
3347 + finishedTask(request, task.blockedBoundary, null);
3348 + } catch (thrownValue) {
3349 + resetHooksState();
3350 +
3351 + const x =
3352 + thrownValue === SuspenseException
3353 + ? // This is a special type of exception used for Suspense. For historical
3354 + // reasons, the rest of the Suspense implementation expects the thrown
3355 + // value to be a thenable, because before `use` existed that was the
3356 + // (unstable) API for suspending. This implementation detail can change
3357 + // later, once we deprecate the old API in favor of `use`.
3358 + getSuspendedThenable()
3359 + : thrownValue;
3360 +
3361 + if (typeof x === 'object' && x !== null) {
3362 + // $FlowFixMe[method-unbinding]
3363 + if (typeof x.then === 'function') {
3364 + // Something suspended again, let's pick it back up later.
3365 + const ping = task.ping;
3366 + x.then(ping, ping);
3367 + task.thenableState = getThenableStateAfterSuspending();
3368 + return;
3369 + }
3370 + }
3371 + task.replay.pendingTasks--;
3372 + task.abortSet.delete(task);
3373 + erroredTask(request, task.blockedBoundary, x);
3374 return;
3375 } finally {
3376 if (enableFloat) {
@@ -3011,7 +3868,9 @@ function flushCompletedQueues(
3868 if (
3869 !enablePostpone ||
3870 request.trackedPostpones === null ||
3014 - request.trackedPostpones.root.length === 0
3871 + // We check the working map instead of the root because the root could've
3872 + // been mutated at this point if it was passed straight through to resume().
3873 + request.trackedPostpones.workingMap.size === 0
3874 ) {
3875 writePostamble(destination, request.resumableState);
3876 }
scripts/error-codes/codes.json
+5 -1
@@ -471,5 +471,9 @@
471 "483": "Hooks are not supported inside an async component. This error is often caused by accidentally adding `'use client'` to a module that was originally written for the server.",
472 "484": "A Server Component was postponed. The reason is omitted in production builds to avoid leaking sensitive details.",
473 "485": "Cannot update form state while rendering.",
474 - "486": "It should not be possible to postpone at the root. This is a bug in React."
474 + "486": "It should not be possible to postpone at the root. This is a bug in React.",
475 + "487": "Did not expect to see a Suspense boundary in this slot. The tree doesn't match so React will fallback to client rendering.",
476 + "488": "Couldn't find all resumable slots by key/index during replaying. The tree doesn't match so React will fallback to client rendering.",
477 + "489": "Expected to see a component of type \"%s\" in this slot. The tree doesn't match so React will fallback to client rendering.",
478 + "490": "Expected to see a Suspense boundary in this slot. The tree doesn't match so React will fallback to client rendering."
479 }
\ No newline at end of file