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
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 */,
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>,
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,
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;
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,
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,
588
};
589
}
590
563
-function createTask(
591
+function createRenderTask(
592
request: Request,
593
thenableState: ThenableState | null,
594
node: ReactNodeList,
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),
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,
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;
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,
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,
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
}
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
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 (
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:
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
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);
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;
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) {
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],
2657
2658
function injectPostponedHole(
2659
request: Request,
2006
- task: Task,
2660
+ task: RenderTask,
2661
reason: string,
2662
): Segment {
2663
logPostpone(request, reason);
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 {
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,
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;
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.
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--;
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 {
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--;
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.
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.',
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.
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.
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;
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) {
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
}