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
- );
819
+ // If we're replaying through this pass, it means we're replaying through
820
+ // an already completed Suspense boundary. It's too late to do anything about it
821
+ // so we can just render through it.
822
+ const prevKeyPath = someTask.keyPath;
823
+ someTask.keyPath = keyPath;
824
+ const content: ReactNodeList = props.children;
825
+ try {
826
+ renderNode(request, someTask, content, -1);
827
+ } finally {
828
+ someTask.keyPath = prevKeyPath;
829
+ }
830
+ return;
831
}
832
// $FlowFixMe: Refined.
833
const task: RenderTask = someTask;
983
function replaySuspenseBoundary(
984
request: Request,
985
task: ReplayTask,
986
+ keyPath: Root | KeyNode,
987
props: Object,
988
replayNode: ReplaySuspenseBoundary,
989
): void {
990
pushBuiltInComponentStackInDEV(task, 'Suspense');
991
992
+ const prevKeyPath = task.keyPath;
993
const previousReplaySet: ReplaySet = task.replay;
994
995
const parentBoundary = task.blockedBoundary;
1013
task.blockedBoundary = resumedBoundary;
1014
task.replay = {nodes: replayNode[3], pendingTasks: 1};
1015
if (enableFloat) {
1006
- // Does this even matter for replaying?
1016
setCurrentlyRenderingBoundaryResourcesTarget(
1017
request.renderState,
1018
resumedBoundary.resources,
1058
1059
task.replay.pendingTasks--;
1060
1061
+ // The parent already flushed in the prerender so we need to schedule this to be emitted.
1062
+ request.clientRenderedBoundaries.push(resumedBoundary);
1063
+
1064
// We don't need to decrement any task numbers because we didn't spawn any new task.
1065
// We don't need to schedule any task because we know the parent has written yet.
1066
// We do need to fallthrough to create the fallback though.
1073
}
1074
task.blockedBoundary = parentBoundary;
1075
task.replay = previousReplaySet;
1076
+ task.keyPath = prevKeyPath;
1077
}
1078
// TODO: Should this be in the finally?
1079
popComponentStackInDEV(task);
1082
function resumeSuspenseBoundary(
1083
request: Request,
1084
task: ReplayTask,
1085
+ keyPath: Root | KeyNode,
1086
props: Object,
1087
replayNode: ResumeSuspenseBoundary,
1088
): void {
1089
pushBuiltInComponentStackInDEV(task, 'Suspense');
1090
1091
+ const prevKeyPath = task.keyPath;
1092
const previousReplaySet: ReplaySet = task.replay;
1093
1094
const parentBoundary = task.blockedBoundary;
1128
resumedBoundary.resources,
1129
);
1130
}
1131
+ task.keyPath = keyPath;
1132
try {
1133
// Convert the current ReplayTask to a RenderTask.
1134
const renderTask: RenderTask = (task: any);
1166
captureBoundaryErrorDetailsDev(resumedBoundary, error);
1167
}
1168
1169
+ // The parent already flushed in the prerender so we need to schedule this to be emitted.
1170
+ request.clientRenderedBoundaries.push(resumedBoundary);
1171
+
1172
// We don't need to decrement any task numbers because we didn't spawn any new task.
1173
// We don't need to schedule any task because we know the parent has written yet.
1174
// We do need to fallthrough to create the fallback though.
1183
// Restore to a ReplayTask
1184
task.blockedSegment = null;
1185
task.replay = previousReplaySet;
1186
+ task.keyPath = prevKeyPath;
1187
}
1188
// TODO: Should this be in the finally?
1189
popComponentStackInDEV(task);
2083
);
2084
}
2085
// Matched a replayable path.
2066
- task.replay = {nodes: node[3], pendingTasks: 1};
2086
+ const childNodes = node[3];
2087
+ task.replay = {nodes: childNodes, pendingTasks: 1};
2088
try {
2089
renderElement(
2090
request,
2095
props,
2096
ref,
2097
);
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--;
2098
if (
2083
- task.replay.pendingTasks === 0 &&
2099
+ task.replay.pendingTasks === 1 &&
2100
task.replay.nodes.length > 0
2101
) {
2102
throw new Error(
2104
"The tree doesn't match so React will fallback to client rendering.",
2105
);
2106
}
2107
+ } catch (x) {
2108
+ if (
2109
+ typeof x === 'object' &&
2110
+ x !== null &&
2111
+ (x === SuspenseException || typeof x.then === 'function')
2112
+ ) {
2113
+ // Suspend
2114
+ throw x;
2115
+ }
2116
+ // Unlike regular render, we don't terminate the siblings if we error
2117
+ // during a replay. That's because this component didn't actually error
2118
+ // in the original prerender. What's unable to complete is the child
2119
+ // replay nodes which might be Suspense boundaries which are able to
2120
+ // absorb the error and we can still continue with siblings.
2121
+ erroredReplay(request, task.blockedBoundary, x, childNodes);
2122
+ } finally {
2123
+ task.replay.pendingTasks--;
2124
task.replay = replay;
2125
}
2126
+ // We finished rendering this node, so now we can consume this
2127
+ // slot. This must happen after in case we rerender this task.
2128
+ replayNodes.splice(i, 1);
2129
}
2130
continue;
2131
}
2140
);
2141
}
2142
// Matched a replayable path.
2107
- replaySuspenseBoundary(request, task, props, node);
2143
+ replaySuspenseBoundary(request, task, keyPath, props, node);
2144
// We finished rendering this node, so now we can consume this
2145
// slot. This must happen after in case we rerender this task.
2146
replayNodes.splice(i, 1);
2191
);
2192
}
2193
// Matched a resumable suspense boundary.
2158
- resumeSuspenseBoundary(request, task, props, node);
2194
+ resumeSuspenseBoundary(request, task, keyPath, props, node);
2195
2196
// We finished rendering this node, so now we can consume this
2197
// slot. This must happen after in case we rerender this task.
2502
}
2503
}
2504
2505
+function replayFragment(
2506
+ request: Request,
2507
+ task: ReplayTask,
2508
+ children: Array<any>,
2509
+ childIndex: number,
2510
+): void {
2511
+ // If we're supposed follow this array, we'd expect to see a ReplayNode matching
2512
+ // this fragment.
2513
+ const replay = task.replay;
2514
+ const replayNodes = replay.nodes;
2515
+ for (let j = 0; j < replayNodes.length; j++) {
2516
+ const replayNode = replayNodes[j];
2517
+ if (replayNode[0] !== REPLAY_NODE) {
2518
+ continue;
2519
+ }
2520
+ const node: ReplayNode = (replayNode: any);
2521
+ if (node[2] !== childIndex) {
2522
+ continue;
2523
+ }
2524
+ // Matched a replayable path.
2525
+ const childNodes = node[3];
2526
+ task.replay = {nodes: childNodes, pendingTasks: 1};
2527
+ try {
2528
+ renderChildrenArray(request, task, children, -1);
2529
+ if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) {
2530
+ throw new Error(
2531
+ "Couldn't find all resumable slots by key/index during replaying. " +
2532
+ "The tree doesn't match so React will fallback to client rendering.",
2533
+ );
2534
+ }
2535
+ } catch (x) {
2536
+ if (
2537
+ typeof x === 'object' &&
2538
+ x !== null &&
2539
+ (x === SuspenseException || typeof x.then === 'function')
2540
+ ) {
2541
+ // Suspend
2542
+ throw x;
2543
+ }
2544
+ // Unlike regular render, we don't terminate the siblings if we error
2545
+ // during a replay. That's because this component didn't actually error
2546
+ // in the original prerender. What's unable to complete is the child
2547
+ // replay nodes which might be Suspense boundaries which are able to
2548
+ // absorb the error and we can still continue with siblings.
2549
+ erroredReplay(request, task.blockedBoundary, x, childNodes);
2550
+ } finally {
2551
+ task.replay.pendingTasks--;
2552
+ task.replay = replay;
2553
+ }
2554
+ // We finished rendering this node, so now we can consume this
2555
+ // slot. This must happen after in case we rerender this task.
2556
+ replayNodes.splice(j, 1);
2557
+ break;
2558
+ }
2559
+}
2560
+
2561
function renderChildrenArray(
2562
request: Request,
2563
task: Task,
2568
if (childIndex !== -1) {
2569
task.keyPath = [task.keyPath, 'Fragment', childIndex];
2570
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
- }
2571
+ replayFragment(
2572
+ request,
2573
+ // $FlowFixMe: Refined.
2574
+ task,
2575
+ children,
2576
+ childIndex,
2577
+ );
2578
task.keyPath = prevKeyPath;
2579
return;
2580
}
2997
throw x;
2998
}
2999
3000
+function erroredReplay(
3001
+ request: Request,
3002
+ boundary: Root | SuspenseBoundary,
3003
+ error: mixed,
3004
+ replayNodes: ResumableNode[],
3005
+): void {
3006
+ // Erroring during a replay doesn't actually cause an error by itself because
3007
+ // that component has already rendered. What causes the error is the resumable
3008
+ // points that we did not yet finish which will be below the point of the reset.
3009
+ // For example, if we're replaying a path to a Suspense boundary that is not done
3010
+ // that doesn't error the parent Suspense boundary.
3011
+ // This might be a bit strange that the error in a parent gets thrown at a child.
3012
+ // We log it only once and reuse the digest.
3013
+ let errorDigest;
3014
+ if (
3015
+ enablePostpone &&
3016
+ typeof error === 'object' &&
3017
+ error !== null &&
3018
+ error.$$typeof === REACT_POSTPONE_TYPE
3019
+ ) {
3020
+ const postponeInstance: Postpone = (error: any);
3021
+ logPostpone(request, postponeInstance.message);
3022
+ // TODO: Figure out a better signal than a magic digest value.
3023
+ errorDigest = 'POSTPONE';
3024
+ } else {
3025
+ errorDigest = logRecoverableError(request, error);
3026
+ }
3027
+ abortRemainingResumableNodes(
3028
+ request,
3029
+ boundary,
3030
+ replayNodes,
3031
+ error,
3032
+ errorDigest,
3033
+ );
3034
+}
3035
+
3036
function erroredTask(
3037
request: Request,
3038
boundary: Root | SuspenseBoundary,
3054
errorDigest = logRecoverableError(request, error);
3055
}
3056
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.
3057
fatalError(request, error);
3058
} else {
3059
boundary.pendingTasks--;
3096
}
3097
}
3098
3099
+function abortRemainingSuspenseBoundary(
3100
+ request: Request,
3101
+ id: SuspenseBoundaryID,
3102
+ rootSegmentID: number,
3103
+ error: mixed,
3104
+ errorDigest: ?string,
3105
+): void {
3106
+ const resumedBoundary = createSuspenseBoundary(
3107
+ request,
3108
+ new Set(),
3109
+ null, // The keyPath doesn't matter at this point so we don't bother rebuilding it.
3110
+ );
3111
+ resumedBoundary.parentFlushed = true;
3112
+ // We restore the same id of this boundary as was used during prerender.
3113
+ resumedBoundary.id = id;
3114
+ resumedBoundary.rootSegmentID = rootSegmentID;
3115
+
3116
+ resumedBoundary.status = CLIENT_RENDERED;
3117
+ resumedBoundary.errorDigest = errorDigest;
3118
+ if (__DEV__) {
3119
+ const errorPrefix = 'The server did not finish this Suspense boundary: ';
3120
+ let errorMessage;
3121
+ if (error && typeof error.message === 'string') {
3122
+ errorMessage = errorPrefix + error.message;
3123
+ } else {
3124
+ // eslint-disable-next-line react-internal/safe-string-coercion
3125
+ errorMessage = errorPrefix + String(error);
3126
+ }
3127
+ const previousTaskInDev = currentTaskInDEV;
3128
+ currentTaskInDEV = null;
3129
+ try {
3130
+ captureBoundaryErrorDetailsDev(resumedBoundary, errorMessage);
3131
+ } finally {
3132
+ currentTaskInDEV = previousTaskInDev;
3133
+ }
3134
+ }
3135
+ if (resumedBoundary.parentFlushed) {
3136
+ request.clientRenderedBoundaries.push(resumedBoundary);
3137
+ }
3138
+}
3139
+
3140
function abortRemainingResumableNodes(
3141
+ request: Request,
3142
+ boundary: Root | SuspenseBoundary,
3143
nodes: Array<ResumableNode>,
3144
error: mixed,
3145
+ errorDigest: ?string,
3146
): void {
3007
- // TODO: Abort any undiscovered Suspense boundaries in the ReplaySet.
3147
+ for (let i = 0; i < nodes.length; i++) {
3148
+ const node: any = nodes[i];
3149
+ switch (node[0]) {
3150
+ case REPLAY_NODE: {
3151
+ abortRemainingResumableNodes(
3152
+ request,
3153
+ boundary,
3154
+ node[3],
3155
+ error,
3156
+ errorDigest,
3157
+ );
3158
+ continue;
3159
+ }
3160
+ case REPLAY_SUSPENSE_BOUNDARY: {
3161
+ const boundaryNode: ReplaySuspenseBoundary = node;
3162
+ const id = boundaryNode[4];
3163
+ const rootSegmentID = boundaryNode[5];
3164
+ abortRemainingSuspenseBoundary(
3165
+ request,
3166
+ id,
3167
+ rootSegmentID,
3168
+ error,
3169
+ errorDigest,
3170
+ );
3171
+ continue;
3172
+ }
3173
+ case RESUME_SUSPENSE_BOUNDARY: {
3174
+ const boundaryNode: ResumeSuspenseBoundary = node;
3175
+ const id = boundaryNode[3];
3176
+ const rootSegmentID = boundaryNode[4];
3177
+ abortRemainingSuspenseBoundary(
3178
+ request,
3179
+ id,
3180
+ rootSegmentID,
3181
+ error,
3182
+ errorDigest,
3183
+ );
3184
+ continue;
3185
+ }
3186
+ case RESUME_ELEMENT:
3187
+ case RESUME_SLOT: {
3188
+ // We had something still to resume in the parent boundary. We must trigger
3189
+ // the error on the parent boundary since it's not able to complete.
3190
+ if (boundary === null) {
3191
+ throw new Error(
3192
+ 'We should not have any resumable nodes in the shell. ' +
3193
+ 'This is a bug in React.',
3194
+ );
3195
+ } else if (boundary.status !== CLIENT_RENDERED) {
3196
+ boundary.status = CLIENT_RENDERED;
3197
+ boundary.errorDigest = errorDigest;
3198
+ if (__DEV__) {
3199
+ captureBoundaryErrorDetailsDev(boundary, error);
3200
+ }
3201
+ if (boundary.parentFlushed) {
3202
+ request.clientRenderedBoundaries.push(boundary);
3203
+ }
3204
+ }
3205
+ continue;
3206
+ }
3207
+ }
3208
+ }
3209
+ // Empty the set, since we've cleared it now.
3210
+ nodes.length = 0;
3211
}
3212
3213
function abortTask(task: Task, request: Request, error: mixed): void {
3215
// client rendered mode.
3216
const boundary = task.blockedBoundary;
3217
const segment = task.blockedSegment;
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 {
3218
+ if (segment !== null) {
3219
segment.status = ABORTED;
3220
}
3221
3222
if (boundary === null) {
3223
request.allPendingTasks--;
3028
- // We didn't complete the root so we have nothing to show. We can close
3029
- // the request;
3224
if (request.status !== CLOSING && request.status !== CLOSED) {
3031
- logRecoverableError(request, error);
3032
- fatalError(request, error);
3225
+ const replay: null | ReplaySet = task.replay;
3226
+ if (replay === null) {
3227
+ // We didn't complete the root so we have nothing to show. We can close
3228
+ // the request;
3229
+ logRecoverableError(request, error);
3230
+ fatalError(request, error);
3231
+ } else {
3232
+ // If the shell aborts during a replay, that's not a fatal error. Instead
3233
+ // we should be able to recover by client rendering all the root boundaries in
3234
+ // the ReplaySet.
3235
+ replay.pendingTasks--;
3236
+ if (replay.pendingTasks === 0 && replay.nodes.length > 0) {
3237
+ const errorDigest = logRecoverableError(request, error);
3238
+ abortRemainingResumableNodes(
3239
+ request,
3240
+ null,
3241
+ replay.nodes,
3242
+ error,
3243
+ errorDigest,
3244
+ );
3245
+ }
3246
+ }
3247
}
3248
} else {
3249
boundary.pendingTasks--;
3250
if (boundary.status !== CLIENT_RENDERED) {
3251
boundary.status = CLIENT_RENDERED;
3038
- boundary.errorDigest = request.onError(error);
3252
+ boundary.errorDigest = logRecoverableError(request, error);
3253
if (__DEV__) {
3254
const errorPrefix =
3255
'The server did not finish this Suspense boundary: ';
3359
// We can now cancel any pending task on the fallback since we won't need to show it anymore.
3360
// This needs to happen after we read the parentFlushed flags because aborting can finish
3361
// work which can trigger user code, which can start flushing, which can change those flags.
3148
- boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request);
3149
- boundary.fallbackAbortableTasks.clear();
3362
+ // If the boundary was POSTPONED, we still need to finish the fallback first.
3363
+ if (boundary.status === COMPLETED) {
3364
+ boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request);
3365
+ boundary.fallbackAbortableTasks.clear();
3366
+ }
3367
} else {
3368
if (segment !== null && segment.parentFlushed) {
3369
// Our parent already flushed, so we need to schedule this segment to be emitted.
3587
}
3588
task.replay.pendingTasks--;
3589
task.abortSet.delete(task);
3373
- erroredTask(request, task.blockedBoundary, x);
3590
+ erroredReplay(request, task.blockedBoundary, x, task.replay.nodes);
3591
+ request.allPendingTasks--;
3592
+ if (request.allPendingTasks === 0) {
3593
+ const onAllReady = request.onAllReady;
3594
+ onAllReady();
3595
+ }
3596
return;
3597
} finally {
3598
if (enableFloat) {