[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