@samitouri / QOS-React-1 / commits / 47fed6961f

[Fizz] Simplify ReplayNode data structure (#27395)

The key is that instead of storing different tags of resumable points, we just store if a replay node has any resumable slots and if that's at the root `number` or if it has resumable slots by index. This is a simpler and more compact format because we don't have to separate the three Resume forms. This helps deal with Postpone in fallbacks because it doesn't just double all the cases.

Sebastian Markbåge committed Sep 21, 2023 at 13:49 UTC 47fed6961f79e8c8388f5f11fe1a531b194bb4af
2 files changed +265 -393
packages/react-server/src/ReactFizzServer.js
+263 -392
@@ -162,58 +162,32 @@ export type KeyNode = [
162 string | number /* key */,
163 ];
164
165 -const REPLAY_NODE = 0;
166 -const REPLAY_SUSPENSE_BOUNDARY = 1;
167 -const RESUME_ELEMENT = 2;
168 -const RESUME_SUSPENSE_BOUNDARY = 3;
169 -const RESUME_SLOT = 4;
165 +type ResumeSlots =
166 + | null // nothing to resume
167 + | number // resume with segment ID at the root position
168 + | {[index: number]: number}; // resume with segmentID at the index
169
170 type ReplaySuspenseBoundary = [
172 - 1, // REPLAY_SUSPENSE_BOUNDARY
171 string | null /* name */,
172 string | number /* key */,
175 - Array<ResumableNode> /* children */,
173 + Array<ReplayNode> /* keyed children */,
174 + ResumeSlots /* resumable slots */,
175 number /* rootSegmentID */,
176 ];
177
178 type ReplayNode =
179 | [
181 - 0, // REPLAY_NODE
180 string | null /* name */,
181 string | number /* key */,
184 - Array<ResumableNode> /* children */,
182 + Array<ReplayNode> /* keyed children */,
183 + ResumeSlots /* resumable slots */,
184 ]
185 | ReplaySuspenseBoundary;
186
188 -type ResumeSuspenseBoundary = [
189 - 3, // RESUME_SUSPENSE_BOUNDARY
190 - string | null /* name */,
191 - string | number /* key */,
192 - number /* rootSegmentID */,
193 -];
194 -
195 -type ResumeElement = [
196 - 2, // RESUME_ELEMENT
197 - string | null /* name */,
198 - string | number /* key */,
199 - number /* segment id */,
200 -];
201 -
202 -type ResumeSlot = [
203 - 4, // RESUME_SLOT
204 - number /* index */,
205 - number /* segment id */,
206 -];
207 -
208 -type ResumableNode =
209 - | ReplayNode
210 - | ResumeElement
211 - | ResumeSuspenseBoundary
212 - | ResumeSlot;
213 -
187 type PostponedHoles = {
188 workingMap: Map<KeyNode, ReplayNode>,
216 - root: Array<ResumableNode>,
189 + rootNodes: Array<ReplayNode>,
190 + rootSlots: ResumeSlots,
191 };
192
193 type LegacyContext = {
@@ -255,7 +229,8 @@ type RenderTask = {
229 };
230
231 type ReplaySet = {
258 - nodes: Array<ResumableNode>, // the possible paths to follow down the replaying
232 + nodes: Array<ReplayNode>, // the possible paths to follow down the replaying
233 + slots: ResumeSlots, // slots to resume
234 pendingTasks: number, // tracks the number of tasks currently tracking this set of nodes
235 // if pending tasks reach zero but there are still nodes left, it means we couldn't find
236 // them all in the tree, so we need to abort and client render the boundary.
@@ -479,7 +454,11 @@ export function createPrerenderRequest(
454 onPostpone,
455 );
456 // Start tracking postponed holes during this render.
482 - request.trackedPostpones = {workingMap: new Map(), root: []};
457 + request.trackedPostpones = {
458 + workingMap: new Map(),
459 + rootNodes: [],
460 + rootSlots: null,
461 + };
462 return request;
463 }
464
@@ -524,10 +503,15 @@ export function resumeRequest(
503 onFatalError: onFatalError === undefined ? noop : onFatalError,
504 formState: null,
505 };
506 + const replay: ReplaySet = {
507 + nodes: postponedState.replayNodes,
508 + slots: postponedState.replaySlots,
509 + pendingTasks: 0,
510 + };
511 const rootTask = createReplayTask(
512 request,
513 null,
530 - {nodes: postponedState.resumablePath, pendingTasks: 0},
514 + replay,
515 children,
516 -1,
517 null,
@@ -978,7 +962,9 @@ function replaySuspenseBoundary(
962 task: ReplayTask,
963 keyPath: Root | KeyNode,
964 props: Object,
981 - replayNode: ReplaySuspenseBoundary,
965 + id: number,
966 + childNodes: Array<ReplayNode>,
967 + childSlots: ResumeSlots,
968 ): void {
969 pushBuiltInComponentStackInDEV(task, 'Suspense');
970
@@ -997,13 +983,13 @@ function replaySuspenseBoundary(
983 );
984 resumedBoundary.parentFlushed = true;
985 // We restore the same id of this boundary as was used during prerender.
1000 - resumedBoundary.rootSegmentID = replayNode[4];
986 + resumedBoundary.rootSegmentID = id;
987
988 // We can reuse the current context and task to render the content immediately without
989 // context switching. We just need to temporarily switch which boundary and replay node
990 // we're writing to. If something suspends, it'll spawn new suspended task with that context.
991 task.blockedBoundary = resumedBoundary;
1006 - task.replay = {nodes: replayNode[3], pendingTasks: 1};
992 + task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};
993 if (enableFloat) {
994 setCurrentlyRenderingBoundaryResourcesTarget(
995 request.renderState,
@@ -1012,7 +998,11 @@ function replaySuspenseBoundary(
998 }
999 try {
1000 // We use the safe form because we don't handle suspending here. Only error handling.
1015 - renderNode(request, task, content, -1);
1001 + if (typeof childSlots === 'number') {
1002 + resumeNode(request, task, childSlots, content, -1);
1003 + } else {
1004 + renderNode(request, task, content, -1);
1005 + }
1006 if (
1007 resumedBoundary.pendingTasks === 0 &&
1008 resumedBoundary.status === PENDING
@@ -1071,116 +1061,6 @@ function replaySuspenseBoundary(
1061 popComponentStackInDEV(task);
1062 }
1063
1074 -function resumeSuspenseBoundary(
1075 - request: Request,
1076 - task: ReplayTask,
1077 - keyPath: Root | KeyNode,
1078 - props: Object,
1079 - replayNode: ResumeSuspenseBoundary,
1080 -): void {
1081 - pushBuiltInComponentStackInDEV(task, 'Suspense');
1082 -
1083 - const prevKeyPath = task.keyPath;
1084 - const previousReplaySet: ReplaySet = task.replay;
1085 -
1086 - const parentBoundary = task.blockedBoundary;
1087 -
1088 - const content: ReactNodeList = props.children;
1089 -
1090 - const fallbackAbortSet: Set<Task> = new Set();
1091 - const resumedBoundary = createSuspenseBoundary(
1092 - request,
1093 - fallbackAbortSet,
1094 - task.keyPath,
1095 - );
1096 - resumedBoundary.parentFlushed = true;
1097 - const id = replayNode[3];
1098 - // We restore the same id of this boundary as was used during prerender.
1099 - resumedBoundary.rootSegmentID = id;
1100 -
1101 - const resumedSegment = createPendingSegment(
1102 - request,
1103 - 0,
1104 - null,
1105 - task.formatContext,
1106 - false,
1107 - false,
1108 - );
1109 - resumedSegment.parentFlushed = true;
1110 - resumedSegment.id = id;
1111 -
1112 - // We can reuse the current context and task to render the content immediately without
1113 - // context switching. We just need to temporarily switch which boundary and replay node
1114 - // we're writing to. If something suspends, it'll spawn new suspended task with that context.
1115 - task.blockedBoundary = resumedBoundary;
1116 - if (enableFloat) {
1117 - // Does this even matter for replaying?
1118 - setCurrentlyRenderingBoundaryResourcesTarget(
1119 - request.renderState,
1120 - resumedBoundary.resources,
1121 - );
1122 - }
1123 - task.keyPath = keyPath;
1124 - try {
1125 - // Convert the current ReplayTask to a RenderTask.
1126 - const renderTask: RenderTask = (task: any);
1127 - renderTask.replay = null;
1128 - renderTask.blockedSegment = resumedSegment;
1129 - // We use the safe form because we don't handle suspending here. Only error handling.
1130 - renderNode(request, task, content, -1);
1131 - resumedSegment.status = COMPLETED;
1132 - queueCompletedSegment(resumedBoundary, resumedSegment);
1133 - if (
1134 - resumedBoundary.pendingTasks === 0 &&
1135 - resumedBoundary.status === PENDING
1136 - ) {
1137 - resumedBoundary.status = COMPLETED;
1138 - request.completedBoundaries.push(resumedBoundary);
1139 - }
1140 - } catch (error) {
1141 - resumedBoundary.status = CLIENT_RENDERED;
1142 - let errorDigest;
1143 - if (
1144 - enablePostpone &&
1145 - typeof error === 'object' &&
1146 - error !== null &&
1147 - error.$$typeof === REACT_POSTPONE_TYPE
1148 - ) {
1149 - const postponeInstance: Postpone = (error: any);
1150 - logPostpone(request, postponeInstance.message);
1151 - // TODO: Figure out a better signal than a magic digest value.
1152 - errorDigest = 'POSTPONE';
1153 - } else {
1154 - errorDigest = logRecoverableError(request, error);
1155 - }
1156 - resumedBoundary.errorDigest = errorDigest;
1157 - if (__DEV__) {
1158 - captureBoundaryErrorDetailsDev(resumedBoundary, error);
1159 - }
1160 -
1161 - // The parent already flushed in the prerender so we need to schedule this to be emitted.
1162 - request.clientRenderedBoundaries.push(resumedBoundary);
1163 -
1164 - // We don't need to decrement any task numbers because we didn't spawn any new task.
1165 - // We don't need to schedule any task because we know the parent has written yet.
1166 - // We do need to fallthrough to create the fallback though.
1167 - } finally {
1168 - if (enableFloat) {
1169 - setCurrentlyRenderingBoundaryResourcesTarget(
1170 - request.renderState,
1171 - parentBoundary ? parentBoundary.resources : null,
1172 - );
1173 - }
1174 - task.blockedBoundary = parentBoundary;
1175 - // Restore to a ReplayTask
1176 - task.blockedSegment = null;
1177 - task.replay = previousReplaySet;
1178 - task.keyPath = prevKeyPath;
1179 - }
1180 - // TODO: Should this be in the finally?
1181 - popComponentStackInDEV(task);
1182 -}
1183 -
1064 function renderBackupSuspenseBoundary(
1065 request: Request,
1066 task: Task,
@@ -2060,140 +1940,100 @@ function replayElement(
1940 const replayNodes = replay.nodes;
1941 for (let i = 0; i < replayNodes.length; i++) {
1942 // Flow doesn't support refinement on tuples so we do it manually here.
2063 - const candidate: any = replayNodes[i];
2064 - switch (candidate[0]) {
2065 - case REPLAY_NODE: {
2066 - const node: ReplayNode = candidate;
2067 - if (keyOrIndex === node[2]) {
2068 - // Let's double check that the component name matches as a precaution.
2069 - if (name !== null && name !== node[1]) {
2070 - throw new Error(
2071 - 'Expected to see a component of type "' +
2072 - name +
2073 - '" in this slot. ' +
2074 - "The tree doesn't match so React will fallback to client rendering.",
2075 - );
2076 - }
2077 - // Matched a replayable path.
2078 - const childNodes = node[3];
2079 - task.replay = {nodes: childNodes, pendingTasks: 1};
2080 - try {
2081 - renderElement(
2082 - request,
2083 - task,
2084 - keyPath,
2085 - prevThenableState,
2086 - type,
2087 - props,
2088 - ref,
2089 - );
2090 - if (
2091 - task.replay.pendingTasks === 1 &&
2092 - task.replay.nodes.length > 0
2093 - ) {
2094 - throw new Error(
2095 - "Couldn't find all resumable slots by key/index during replaying. " +
2096 - "The tree doesn't match so React will fallback to client rendering.",
2097 - );
2098 - }
2099 - } catch (x) {
2100 - if (
2101 - typeof x === 'object' &&
2102 - x !== null &&
2103 - (x === SuspenseException || typeof x.then === 'function')
2104 - ) {
2105 - // Suspend
2106 - throw x;
2107 - }
2108 - // Unlike regular render, we don't terminate the siblings if we error
2109 - // during a replay. That's because this component didn't actually error
2110 - // in the original prerender. What's unable to complete is the child
2111 - // replay nodes which might be Suspense boundaries which are able to
2112 - // absorb the error and we can still continue with siblings.
2113 - erroredReplay(request, task.blockedBoundary, x, childNodes);
2114 - } finally {
2115 - task.replay.pendingTasks--;
2116 - task.replay = replay;
2117 - }
2118 - // We finished rendering this node, so now we can consume this
2119 - // slot. This must happen after in case we rerender this task.
2120 - replayNodes.splice(i, 1);
2121 - }
2122 - continue;
2123 - }
2124 - case REPLAY_SUSPENSE_BOUNDARY: {
2125 - const node: ReplaySuspenseBoundary = candidate;
2126 - if (keyOrIndex === node[2]) {
2127 - // Let's double check that the component type matches.
2128 - if (type !== REACT_SUSPENSE_TYPE) {
2129 - throw new Error(
2130 - 'Expected to see a Suspense boundary in this slot. ' +
2131 - "The tree doesn't match so React will fallback to client rendering.",
2132 - );
2133 - }
2134 - // Matched a replayable path.
2135 - replaySuspenseBoundary(request, task, keyPath, props, node);
2136 - // We finished rendering this node, so now we can consume this
2137 - // slot. This must happen after in case we rerender this task.
2138 - replayNodes.splice(i, 1);
2139 - }
2140 - continue;
2141 - }
2142 - case RESUME_ELEMENT: {
2143 - const node: ResumeElement = candidate;
2144 - if (keyOrIndex === node[2]) {
2145 - // Let's double check that the component name matches as a precaution.
2146 - if (name !== node[1]) {
2147 - throw new Error(
2148 - 'Expected to see a component of type "' +
2149 - (name || 'unknown') +
2150 - '" in this slot. ' +
2151 - "The tree doesn't match so React will fallback to client rendering.",
2152 - );
2153 - }
1943 + const node = replayNodes[i];
1944 + if (keyOrIndex !== node[1]) {
1945 + continue;
1946 + }
1947 + // Let's double check that the component name matches as a precaution.
1948 + if (name !== null && name !== node[0]) {
1949 + throw new Error(
1950 + 'Expected to see a component of type "' +
1951 + name +
1952 + '" in this slot. ' +
1953 + "The tree doesn't match so React will fallback to client rendering.",
1954 + );
1955 + }
1956 + if (node.length === 4) {
1957 + // Matched a replayable path.
1958 + const childNodes = node[2];
1959 + const childSlots = node[3];
1960 + task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};
1961 + try {
1962 + if (typeof childSlots === 'number') {
1963 // Matched a resumable element.
2155 -
2156 - const segmentId = node[3];
2157 -
1964 resumeElement(
1965 request,
1966 task,
1967 keyPath,
2162 - segmentId,
1968 + childSlots,
1969 + prevThenableState,
1970 + type,
1971 + props,
1972 + ref,
1973 + );
1974 + } else {
1975 + renderElement(
1976 + request,
1977 + task,
1978 + keyPath,
1979 prevThenableState,
1980 type,
1981 props,
1982 ref,
1983 );
2168 -
2169 - // We finished rendering this node, so now we can consume this
2170 - // slot. This must happen after in case we rerender this task.
2171 - replayNodes.splice(i, 1);
1984 }
2173 - continue;
2174 - }
2175 - case RESUME_SUSPENSE_BOUNDARY: {
2176 - const node: ResumeSuspenseBoundary = candidate;
2177 - if (keyOrIndex === node[2]) {
2178 - // Let's double check that the component name matches as a precaution.
2179 - if (type !== REACT_SUSPENSE_TYPE) {
2180 - throw new Error(
2181 - 'Expected to see a Suspense boundary in this slot. ' +
2182 - "The tree doesn't match so React will fallback to client rendering.",
2183 - );
2184 - }
2185 - // Matched a resumable suspense boundary.
2186 - resumeSuspenseBoundary(request, task, keyPath, props, node);
2187 -
2188 - // We finished rendering this node, so now we can consume this
2189 - // slot. This must happen after in case we rerender this task.
2190 - replayNodes.splice(i, 1);
1985 + if (
1986 + task.replay.pendingTasks === 1 &&
1987 + task.replay.nodes.length > 0
1988 + // TODO check remaining slots
1989 + ) {
1990 + throw new Error(
1991 + "Couldn't find all resumable slots by key/index during replaying. " +
1992 + "The tree doesn't match so React will fallback to client rendering.",
1993 + );
1994 + }
1995 + } catch (x) {
1996 + if (
1997 + typeof x === 'object' &&
1998 + x !== null &&
1999 + (x === SuspenseException || typeof x.then === 'function')
2000 + ) {
2001 + // Suspend
2002 + throw x;
2003 }
2192 - continue;
2004 + // Unlike regular render, we don't terminate the siblings if we error
2005 + // during a replay. That's because this component didn't actually error
2006 + // in the original prerender. What's unable to complete is the child
2007 + // replay nodes which might be Suspense boundaries which are able to
2008 + // absorb the error and we can still continue with siblings.
2009 + erroredReplay(request, task.blockedBoundary, x, childNodes, childSlots);
2010 + } finally {
2011 + task.replay.pendingTasks--;
2012 + task.replay = replay;
2013 }
2194 - // For RESUME_SLOT we ignore them here and assume we've handled them
2195 - // separately already.
2014 + } else {
2015 + // Let's double check that the component type matches.
2016 + if (type !== REACT_SUSPENSE_TYPE) {
2017 + throw new Error(
2018 + 'Expected to see a Suspense boundary in this slot. ' +
2019 + "The tree doesn't match so React will fallback to client rendering.",
2020 + );
2021 + }
2022 + // Matched a replayable path.
2023 + replaySuspenseBoundary(
2024 + request,
2025 + task,
2026 + keyPath,
2027 + props,
2028 + node[4],
2029 + node[2],
2030 + node[3],
2031 + );
2032 }
2033 + // We finished rendering this node, so now we can consume this
2034 + // slot. This must happen after in case we rerender this task.
2035 + replayNodes.splice(i, 1);
2036 + return;
2037 }
2038 // We didn't find any matching nodes. We assume that this element was already
2039 // rendered in the prelude and skip it.
@@ -2505,17 +2345,14 @@ function replayFragment(
2345 const replay = task.replay;
2346 const replayNodes = replay.nodes;
2347 for (let j = 0; j < replayNodes.length; j++) {
2508 - const replayNode = replayNodes[j];
2509 - if (replayNode[0] !== REPLAY_NODE) {
2510 - continue;
2511 - }
2512 - const node: ReplayNode = (replayNode: any);
2513 - if (node[2] !== childIndex) {
2348 + const node = replayNodes[j];
2349 + if (node[1] !== childIndex) {
2350 continue;
2351 }
2352 // Matched a replayable path.
2517 - const childNodes = node[3];
2518 - task.replay = {nodes: childNodes, pendingTasks: 1};
2353 + const childNodes = node[2];
2354 + const childSlots = node[3];
2355 + task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};
2356 try {
2357 renderChildrenArray(request, task, children, -1);
2358 if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) {
@@ -2538,7 +2375,7 @@ function replayFragment(
2375 // in the original prerender. What's unable to complete is the child
2376 // replay nodes which might be Suspense boundaries which are able to
2377 // absorb the error and we can still continue with siblings.
2541 - erroredReplay(request, task.blockedBoundary, x, childNodes);
2378 + erroredReplay(request, task.blockedBoundary, x, childNodes, childSlots);
2379 } finally {
2380 task.replay.pendingTasks--;
2381 task.replay = replay;
@@ -2577,28 +2414,23 @@ function renderChildrenArray(
2414 if (task.replay !== null) {
2415 // Replay
2416 // First we need to check if we have any resume slots at this level.
2580 - // TODO: This could be simpler if we just stored RESUME_SLOT in a separate set.
2581 - let hadOtherReplayNodes = false;
2582 - const replayNodes = task.replay.nodes;
2583 - for (let j = 0; j < replayNodes.length; ) {
2584 - const replayNode = replayNodes[j];
2585 - if (replayNode[0] !== RESUME_SLOT) {
2586 - hadOtherReplayNodes = true;
2587 - j++; // skip
2588 - continue;
2417 + const resumeSlots = task.replay.slots;
2418 + if (resumeSlots !== null && typeof resumeSlots === 'object') {
2419 + for (let i = 0; i < totalChildren; i++) {
2420 + const node = children[i];
2421 + task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
2422 + // We need to use the non-destructive form so that we can safely pop back
2423 + // up and render the sibling if something suspends.
2424 + const resumeSegmentID = resumeSlots[i];
2425 + if (typeof resumeSegmentID === 'number') {
2426 + resumeNode(request, task, resumeSegmentID, node, i);
2427 + // We finished rendering this node, so now we can consume this
2428 + // slot. This must happen after in case we rerender this task.
2429 + delete resumeSlots[i];
2430 + } else {
2431 + renderNode(request, task, node, i);
2432 + }
2433 }
2590 - const resumeSlot: ResumeSlot = (replayNode: any);
2591 - const i = resumeSlot[1]; // The index of the child to resume.
2592 - const segmentId = resumeSlot[2];
2593 - task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
2594 - resumeNode(request, task, segmentId, children[i], i);
2595 - // We finished rendering this node, so now we can consume this
2596 - // slot. This must happen after in case we rerender this task.
2597 - replayNodes.splice(j, 1);
2598 - }
2599 - // If had non-resume slot nodes, we need to also try to match them below.
2600 - if (!hadOtherReplayNodes) {
2601 - // If we didn't, we can bail early.
2434 task.treeContext = prevTreeContext;
2435 task.keyPath = prevKeyPath;
2436 return;
@@ -2628,12 +2460,6 @@ function trackPostpone(
2460 segment.status = POSTPONED;
2461
2462 const keyPath = task.keyPath;
2631 - if (keyPath === null) {
2632 - throw new Error(
2633 - 'It should not be possible to postpone at the root. This is a bug in React.',
2634 - );
2635 - }
2636 -
2463 const boundary = task.blockedBoundary;
2464 if (boundary !== null && boundary.status === PENDING) {
2465 boundary.status = POSTPONED;
@@ -2648,26 +2474,27 @@ function trackPostpone(
2474 );
2475 }
2476
2477 + const children: Array<ReplayNode> = [];
2478 if (boundaryKeyPath === keyPath && task.childIndex === -1) {
2479 // Since we postponed directly in the Suspense boundary we can't have written anything
2480 // to its segment. Therefore this will end up becoming the root segment.
2481 segment.id = boundary.rootSegmentID;
2482 // We postponed directly inside the Suspense boundary so we mark this for resuming.
2656 - const boundaryNode: ResumeSuspenseBoundary = [
2657 - RESUME_SUSPENSE_BOUNDARY,
2483 + const boundaryNode: ReplaySuspenseBoundary = [
2484 boundaryKeyPath[1],
2485 boundaryKeyPath[2],
2486 + children,
2487 + boundary.rootSegmentID,
2488 boundary.rootSegmentID,
2489 ];
2490 addToReplayParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
2491 return;
2492 } else {
2665 - const children: Array<ResumableNode> = [];
2493 const boundaryNode: ReplaySuspenseBoundary = [
2667 - REPLAY_SUSPENSE_BOUNDARY,
2494 boundaryKeyPath[1],
2495 boundaryKeyPath[2],
2496 children,
2497 + null,
2498 boundary.rootSegmentID,
2499 ];
2500 trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode);
@@ -2690,17 +2517,55 @@ function trackPostpone(
2517
2518 if (task.childIndex === -1) {
2519 // Resume starting from directly inside the previous parent element.
2693 - const resumableElement: ResumeElement = [
2694 - RESUME_ELEMENT,
2695 - keyPath[1],
2696 - keyPath[2],
2697 - segment.id,
2698 - ];
2699 - addToReplayParent(resumableElement, keyPath[0], trackedPostpones);
2520 + if (keyPath === null) {
2521 + trackedPostpones.rootSlots = segment.id;
2522 + } else {
2523 + const resumableElement: ReplayNode = [
2524 + keyPath[1],
2525 + keyPath[2],
2526 + ([]: Array<ReplayNode>),
2527 + segment.id,
2528 + ];
2529 + addToReplayParent(resumableElement, keyPath[0], trackedPostpones);
2530 + }
2531 } else {
2701 - // Resume at the slot within the array
2702 - const resumableNode = [RESUME_SLOT, task.childIndex, segment.id];
2703 - addToReplayParent(resumableNode, keyPath, trackedPostpones);
2532 + let slots;
2533 + if (keyPath === null) {
2534 + slots = trackedPostpones.rootSlots;
2535 + if (slots === null) {
2536 + slots = trackedPostpones.rootSlots = ({}: {[index: number]: number});
2537 + } else if (typeof slots === 'number') {
2538 + throw new Error(
2539 + 'It should not be possible to postpone both at the root of an element ' +
2540 + 'as well as a slot below. This is a bug in React.',
2541 + );
2542 + }
2543 + } else {
2544 + const workingMap = trackedPostpones.workingMap;
2545 + let resumableNode = workingMap.get(keyPath);
2546 + if (resumableNode === undefined) {
2547 + slots = ({}: {[index: number]: number});
2548 + resumableNode = ([
2549 + keyPath[1],
2550 + keyPath[2],
2551 + ([]: Array<ReplayNode>),
2552 + slots,
2553 + ]: ReplayNode);
2554 + workingMap.set(keyPath, resumableNode);
2555 + addToReplayParent(resumableNode, keyPath[0], trackedPostpones);
2556 + } else {
2557 + slots = resumableNode[3];
2558 + if (slots === null) {
2559 + slots = resumableNode[3] = ({}: {[index: number]: number});
2560 + } else if (typeof slots === 'number') {
2561 + throw new Error(
2562 + 'It should not be possible to postpone both at the root of an element ' +
2563 + 'as well as a slot below. This is a bug in React.',
2564 + );
2565 + }
2566 + }
2567 + }
2568 + slots[task.childIndex] = segment.id;
2569 }
2570 }
2571
@@ -2878,7 +2743,7 @@ function renderNode(
2743 }
2744 }
2745
2881 - // TODO: Abort any undiscovered Suspense boundaries in the ResumableNode.
2746 + // TODO: Abort any undiscovered Suspense boundaries in the ReplayNode.
2747 }
2748 } else {
2749 // Render
@@ -2987,7 +2852,8 @@ function erroredReplay(
2852 request: Request,
2853 boundary: Root | SuspenseBoundary,
2854 error: mixed,
2990 - replayNodes: ResumableNode[],
2855 + replayNodes: ReplayNode[],
2856 + resumeSlots: ResumeSlots,
2857 ): void {
2858 // Erroring during a replay doesn't actually cause an error by itself because
2859 // that component has already rendered. What causes the error is the resumable
@@ -3010,10 +2876,11 @@ function erroredReplay(
2876 } else {
2877 errorDigest = logRecoverableError(request, error);
2878 }
3013 - abortRemainingResumableNodes(
2879 + abortRemainingReplayNodes(
2880 request,
2881 boundary,
2882 replayNodes,
2883 + resumeSlots,
2884 error,
2885 errorDigest,
2886 );
@@ -3121,73 +2988,64 @@ function abortRemainingSuspenseBoundary(
2988 }
2989 }
2990
3124 -function abortRemainingResumableNodes(
2991 +function abortRemainingReplayNodes(
2992 request: Request,
2993 boundary: Root | SuspenseBoundary,
3127 - nodes: Array<ResumableNode>,
2994 + nodes: Array<ReplayNode>,
2995 + slots: ResumeSlots,
2996 error: mixed,
2997 errorDigest: ?string,
2998 ): void {
2999 for (let i = 0; i < nodes.length; i++) {
3132 - const node: any = nodes[i];
3133 - switch (node[0]) {
3134 - case REPLAY_NODE: {
3135 - abortRemainingResumableNodes(
3136 - request,
3137 - boundary,
3138 - node[3],
3139 - error,
3140 - errorDigest,
3141 - );
3142 - continue;
3143 - }
3144 - case REPLAY_SUSPENSE_BOUNDARY: {
3145 - const boundaryNode: ReplaySuspenseBoundary = node;
3146 - const rootSegmentID = boundaryNode[4];
3147 - abortRemainingSuspenseBoundary(
3148 - request,
3149 - rootSegmentID,
3150 - error,
3151 - errorDigest,
3152 - );
3153 - continue;
3000 + const node = nodes[i];
3001 + if (node.length === 4) {
3002 + abortRemainingReplayNodes(
3003 + request,
3004 + boundary,
3005 + node[2],
3006 + node[3],
3007 + error,
3008 + errorDigest,
3009 + );
3010 + } else {
3011 + const boundaryNode: ReplaySuspenseBoundary = node;
3012 + const rootSegmentID = boundaryNode[4];
3013 + abortRemainingSuspenseBoundary(
3014 + request,
3015 + rootSegmentID,
3016 + error,
3017 + errorDigest,
3018 + );
3019 + }
3020 + }
3021 + // Empty the set, since we've cleared it now.
3022 + nodes.length = 0;
3023 +
3024 + if (slots !== null) {
3025 + // We had something still to resume in the parent boundary. We must trigger
3026 + // the error on the parent boundary since it's not able to complete.
3027 + if (boundary === null) {
3028 + throw new Error(
3029 + 'We should not have any resumable nodes in the shell. ' +
3030 + 'This is a bug in React.',
3031 + );
3032 + } else if (boundary.status !== CLIENT_RENDERED) {
3033 + boundary.status = CLIENT_RENDERED;
3034 + boundary.errorDigest = errorDigest;
3035 + if (__DEV__) {
3036 + captureBoundaryErrorDetailsDev(boundary, error);
3037 }
3155 - case RESUME_SUSPENSE_BOUNDARY: {
3156 - const boundaryNode: ResumeSuspenseBoundary = node;
3157 - const rootSegmentID = boundaryNode[3];
3158 - abortRemainingSuspenseBoundary(
3159 - request,
3160 - rootSegmentID,
3161 - error,
3162 - errorDigest,
3163 - );
3164 - continue;
3038 + if (boundary.parentFlushed) {
3039 + request.clientRenderedBoundaries.push(boundary);
3040 }
3166 - case RESUME_ELEMENT:
3167 - case RESUME_SLOT: {
3168 - // We had something still to resume in the parent boundary. We must trigger
3169 - // the error on the parent boundary since it's not able to complete.
3170 - if (boundary === null) {
3171 - throw new Error(
3172 - 'We should not have any resumable nodes in the shell. ' +
3173 - 'This is a bug in React.',
3174 - );
3175 - } else if (boundary.status !== CLIENT_RENDERED) {
3176 - boundary.status = CLIENT_RENDERED;
3177 - boundary.errorDigest = errorDigest;
3178 - if (__DEV__) {
3179 - captureBoundaryErrorDetailsDev(boundary, error);
3180 - }
3181 - if (boundary.parentFlushed) {
3182 - request.clientRenderedBoundaries.push(boundary);
3183 - }
3184 - }
3185 - continue;
3041 + }
3042 + // Empty the set
3043 + if (typeof slots === 'object') {
3044 + for (const index in slots) {
3045 + delete slots[(index: any)];
3046 }
3047 }
3048 }
3189 - // Empty the set, since we've cleared it now.
3190 - nodes.length = 0;
3049 }
3050
3051 function abortTask(task: Task, request: Request, error: mixed): void {
@@ -3215,10 +3073,11 @@ function abortTask(task: Task, request: Request, error: mixed): void {
3073 replay.pendingTasks--;
3074 if (replay.pendingTasks === 0 && replay.nodes.length > 0) {
3075 const errorDigest = logRecoverableError(request, error);
3218 - abortRemainingResumableNodes(
3076 + abortRemainingReplayNodes(
3077 request,
3078 null,
3079 replay.nodes,
3080 + replay.slots,
3081 error,
3082 errorDigest,
3083 );
@@ -3567,7 +3426,13 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
3426 }
3427 task.replay.pendingTasks--;
3428 task.abortSet.delete(task);
3570 - erroredReplay(request, task.blockedBoundary, x, task.replay.nodes);
3429 + erroredReplay(
3430 + request,
3431 + task.blockedBoundary,
3432 + x,
3433 + task.replay.nodes,
3434 + task.replay.slots,
3435 + );
3436 request.allPendingTasks--;
3437 if (request.allPendingTasks === 0) {
3438 const onAllReady = request.onAllReady;
@@ -4173,26 +4038,26 @@ export function getRenderState(request: Request): RenderState {
4038 }
4039
4040 function addToReplayParent(
4176 - node: ResumableNode,
4041 + node: ReplayNode,
4042 parentKeyPath: Root | KeyNode,
4043 trackedPostpones: PostponedHoles,
4044 ): void {
4045 if (parentKeyPath === null) {
4181 - trackedPostpones.root.push(node);
4046 + trackedPostpones.rootNodes.push(node);
4047 } else {
4048 const workingMap = trackedPostpones.workingMap;
4049 let parentNode = workingMap.get(parentKeyPath);
4050 if (parentNode === undefined) {
4051 parentNode = ([
4187 - REPLAY_NODE,
4052 parentKeyPath[1],
4053 parentKeyPath[2],
4190 - ([]: Array<ResumableNode>),
4054 + ([]: Array<ReplayNode>),
4055 + null,
4056 ]: ReplayNode);
4057 workingMap.set(parentKeyPath, parentNode);
4058 addToReplayParent(parentNode, parentKeyPath[0], trackedPostpones);
4059 }
4195 - parentNode[3].push(node);
4060 + parentNode[2].push(node);
4061 }
4062 }
4063
@@ -4201,13 +4066,18 @@ export type PostponedState = {
4066 rootFormatContext: FormatContext,
4067 progressiveChunkSize: number,
4068 resumableState: ResumableState,
4204 - resumablePath: Array<ResumableNode>,
4069 + replayNodes: Array<ReplayNode>,
4070 + replaySlots: ResumeSlots,
4071 };
4072
4073 // Returns the state of a postponed request or null if nothing was postponed.
4074 export function getPostponedState(request: Request): null | PostponedState {
4075 const trackedPostpones = request.trackedPostpones;
4210 - if (trackedPostpones === null || trackedPostpones.root.length === 0) {
4076 + if (
4077 + trackedPostpones === null ||
4078 + (trackedPostpones.rootNodes.length === 0 &&
4079 + trackedPostpones.rootSlots === null)
4080 + ) {
4081 return null;
4082 }
4083 return {
@@ -4215,6 +4085,7 @@ export function getPostponedState(request: Request): null | PostponedState {
4085 rootFormatContext: request.rootFormatContext,
4086 progressiveChunkSize: request.progressiveChunkSize,
4087 resumableState: request.resumableState,
4218 - resumablePath: trackedPostpones.root,
4088 + replayNodes: trackedPostpones.rootNodes,
4089 + replaySlots: trackedPostpones.rootSlots,
4090 };
4091 }
scripts/error-codes/codes.json
+2 -1
@@ -475,5 +475,6 @@
475 "487": "We should not have any resumable nodes in the shell. This is a bug in React.",
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."
478 + "490": "Expected to see a Suspense boundary in this slot. The tree doesn't match so React will fallback to client rendering.",
479 + "491": "It should not be possible to postpone both at the root of an element as well as a slot below. This is a bug in React."
480 }
\ No newline at end of file