170
type ReplaySuspenseBoundary = [
171
string | null /* name */,
172
string | number /* key */,
173
- Array<ReplayNode> /* keyed children */,
174
- ResumeSlots /* resumable slots */,
173
+ Array<ReplayNode> /* content keyed children */,
174
+ ResumeSlots /* content resumable slots */,
175
+ null | ReplayNode /* fallback content */,
176
number /* rootSegmentID */,
177
];
178
209
byteSize: number, // used to determine whether to inline children boundaries.
210
fallbackAbortableTasks: Set<Task>, // used to cancel task on the fallback if the boundary completes or gets canceled.
211
resources: BoundaryResources,
211
- keyPath: Root | KeyNode,
212
+ trackedContentKeyPath: null | KeyNode, // used to track the path for replay nodes
213
+ trackedFallbackNode: null | ReplayNode, // used to track the fallback for replay nodes
214
};
215
216
type RenderTask = {
551
function createSuspenseBoundary(
552
request: Request,
553
fallbackAbortableTasks: Set<Task>,
552
- keyPath: Root | KeyNode,
554
): SuspenseBoundary {
555
return {
556
status: PENDING,
562
fallbackAbortableTasks,
563
errorDigest: null,
564
resources: createBoundaryResources(),
564
- keyPath,
565
+ trackedContentKeyPath: null,
566
+ trackedFallbackNode: null,
567
};
568
}
569
825
const content: ReactNodeList = props.children;
826
827
const fallbackAbortSet: Set<Task> = new Set();
826
- const newBoundary = createSuspenseBoundary(
827
- request,
828
- fallbackAbortSet,
829
- keyPath,
830
- );
828
+ const newBoundary = createSuspenseBoundary(request, fallbackAbortSet);
829
+ if (request.trackedPostpones !== null) {
830
+ newBoundary.trackedContentKeyPath = keyPath;
831
+ }
832
const insertionIndex = parentSegment.chunks.length;
833
// The children of the boundary segment is actually the fallback.
834
const boundarySegment = createPendingSegment(
931
task.keyPath = prevKeyPath;
932
}
933
934
+ const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]];
935
+ const trackedPostpones = request.trackedPostpones;
936
+ if (trackedPostpones !== null) {
937
+ // We create a detached replay node to track any postpones inside the fallback.
938
+ const fallbackReplayNode: ReplayNode = [
939
+ fallbackKeyPath[1],
940
+ fallbackKeyPath[2],
941
+ ([]: Array<ReplayNode>),
942
+ null,
943
+ ];
944
+ trackedPostpones.workingMap.set(fallbackKeyPath, fallbackReplayNode);
945
+ if (newBoundary.status === POSTPONED) {
946
+ // This must exist now.
947
+ const boundaryReplayNode: ReplaySuspenseBoundary =
948
+ (trackedPostpones.workingMap.get(keyPath): any);
949
+ boundaryReplayNode[4] = fallbackReplayNode;
950
+ } else {
951
+ // We might not inject it into the postponed tree, unless the content actually
952
+ // postpones too. We need to keep track of it until that happpens.
953
+ newBoundary.trackedFallbackNode = fallbackReplayNode;
954
+ }
955
+ }
956
// We create suspended task for the fallback because we don't want to actually work
957
// on it yet in case we finish the main content, so we queue for later.
958
const suspendedFallbackTask = createRenderTask(
963
parentBoundary,
964
boundarySegment,
965
fallbackAbortSet,
943
- // TODO: Should distinguish key path of fallback and primary tasks
944
- keyPath,
966
+ fallbackKeyPath,
967
task.formatContext,
968
task.legacyContext,
969
task.context,
987
id: number,
988
childNodes: Array<ReplayNode>,
989
childSlots: ResumeSlots,
990
+ fallbackNodes: Array<ReplayNode>,
991
+ fallbackSlots: ResumeSlots,
992
): void {
993
pushBuiltInComponentStackInDEV(task, 'Suspense');
994
998
const parentBoundary = task.blockedBoundary;
999
1000
const content: ReactNodeList = props.children;
1001
+ const fallback: ReactNodeList = props.fallback;
1002
1003
const fallbackAbortSet: Set<Task> = new Set();
979
- const resumedBoundary = createSuspenseBoundary(
980
- request,
981
- fallbackAbortSet,
982
- task.keyPath,
983
- );
1004
+ const resumedBoundary = createSuspenseBoundary(request, fallbackAbortSet);
1005
resumedBoundary.parentFlushed = true;
1006
// We restore the same id of this boundary as was used during prerender.
1007
resumedBoundary.rootSegmentID = id;
1024
} else {
1025
renderNode(request, task, content, -1);
1026
}
1006
- if (
1007
- resumedBoundary.pendingTasks === 0 &&
1008
- resumedBoundary.status === PENDING
1009
- ) {
1010
- resumedBoundary.status = COMPLETED;
1011
- request.completedBoundaries.push(resumedBoundary);
1012
- }
1027
if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) {
1028
throw new Error(
1029
"Couldn't find all resumable slots by key/index during replaying. " +
1031
);
1032
}
1033
task.replay.pendingTasks--;
1034
+ if (
1035
+ resumedBoundary.pendingTasks === 0 &&
1036
+ resumedBoundary.status === PENDING
1037
+ ) {
1038
+ resumedBoundary.status = COMPLETED;
1039
+ request.completedBoundaries.push(resumedBoundary);
1040
+ // This must have been the last segment we were waiting on. This boundary is now complete.
1041
+ // Therefore we won't need the fallback. We early return so that we don't have to create
1042
+ // the fallback.
1043
+ popComponentStackInDEV(task);
1044
+ return;
1045
+ }
1046
} catch (error) {
1047
resumedBoundary.status = CLIENT_RENDERED;
1048
let errorDigest;
1083
task.replay = previousReplaySet;
1084
task.keyPath = prevKeyPath;
1085
}
1086
+
1087
+ const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]];
1088
+
1089
+ let suspendedFallbackTask;
1090
+ // We create suspended task for the fallback because we don't want to actually work
1091
+ // on it yet in case we finish the main content, so we queue for later.
1092
+ if (typeof fallbackSlots === 'number') {
1093
+ // Resuming directly in the fallback.
1094
+ const resumedSegment = createPendingSegment(
1095
+ request,
1096
+ 0,
1097
+ null,
1098
+ task.formatContext,
1099
+ false,
1100
+ false,
1101
+ );
1102
+ resumedSegment.id = fallbackSlots;
1103
+ resumedSegment.parentFlushed = true;
1104
+ suspendedFallbackTask = createRenderTask(
1105
+ request,
1106
+ null,
1107
+ fallback,
1108
+ -1,
1109
+ parentBoundary,
1110
+ resumedSegment,
1111
+ fallbackAbortSet,
1112
+ fallbackKeyPath,
1113
+ task.formatContext,
1114
+ task.legacyContext,
1115
+ task.context,
1116
+ task.treeContext,
1117
+ );
1118
+ } else {
1119
+ const fallbackReplay = {
1120
+ nodes: fallbackNodes,
1121
+ slots: fallbackSlots,
1122
+ pendingTasks: 0,
1123
+ };
1124
+ suspendedFallbackTask = createReplayTask(
1125
+ request,
1126
+ null,
1127
+ fallbackReplay,
1128
+ fallback,
1129
+ -1,
1130
+ parentBoundary,
1131
+ fallbackAbortSet,
1132
+ fallbackKeyPath,
1133
+ task.formatContext,
1134
+ task.legacyContext,
1135
+ task.context,
1136
+ task.treeContext,
1137
+ );
1138
+ }
1139
+ if (__DEV__) {
1140
+ suspendedFallbackTask.componentStack = task.componentStack;
1141
+ }
1142
+ // TODO: This should be queued at a separate lower priority queue so that we only work
1143
+ // on preparing fallbacks if we don't have any more main content to task on.
1144
+ request.pingedTasks.push(suspendedFallbackTask);
1145
+
1146
// TODO: Should this be in the finally?
1147
popComponentStackInDEV(task);
1148
}
2111
task,
2112
keyPath,
2113
props,
2028
- node[4],
2114
+ node[5],
2115
node[2],
2116
node[3],
2117
+ node[4] === null ? [] : node[4][2],
2118
+ node[4] === null ? null : node[4][3],
2119
);
2120
}
2121
// We finished rendering this node, so now we can consume this
2555
// it before flushing and we know that we can't inline it.
2556
boundary.rootSegmentID = request.nextSegmentId++;
2557
2470
- const boundaryKeyPath = boundary.keyPath;
2558
+ const boundaryKeyPath = boundary.trackedContentKeyPath;
2559
if (boundaryKeyPath === null) {
2560
throw new Error(
2561
'It should not be possible to postpone at the root. This is a bug in React.',
2562
);
2563
}
2564
2565
+ const fallbackReplayNode = boundary.trackedFallbackNode;
2566
+
2567
const children: Array<ReplayNode> = [];
2568
if (boundaryKeyPath === keyPath && task.childIndex === -1) {
2569
// Since we postponed directly in the Suspense boundary we can't have written anything
2575
boundaryKeyPath[2],
2576
children,
2577
boundary.rootSegmentID,
2578
+ fallbackReplayNode,
2579
boundary.rootSegmentID,
2580
];
2581
+ trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode);
2582
addToReplayParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
2583
return;
2584
} else {
2590
boundaryKeyPath[2],
2591
children,
2592
null,
2593
+ fallbackReplayNode,
2594
boundary.rootSegmentID,
2595
];
2596
trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode);
2597
addToReplayParent(boundaryNode, boundaryKeyPath[0], trackedPostpones);
2598
} else {
2599
// Upgrade to ReplaySuspenseBoundary.
2507
- ((boundaryNode: any): ReplaySuspenseBoundary)[4] =
2508
- boundary.rootSegmentID;
2600
+ const suspenseBoundary: ReplaySuspenseBoundary = (boundaryNode: any);
2601
+ suspenseBoundary[4] = fallbackReplayNode;
2602
+ suspenseBoundary[5] = boundary.rootSegmentID;
2603
}
2604
// Fall through to add the child node.
2605
}
2622
if (keyPath === null) {
2623
trackedPostpones.rootSlots = segment.id;
2624
} else {
2531
- const resumableElement: ReplayNode = [
2532
- keyPath[1],
2533
- keyPath[2],
2534
- ([]: Array<ReplayNode>),
2535
- segment.id,
2536
- ];
2537
- addToReplayParent(resumableElement, keyPath[0], trackedPostpones);
2625
+ const workingMap = trackedPostpones.workingMap;
2626
+ let resumableNode = workingMap.get(keyPath);
2627
+ if (resumableNode === undefined) {
2628
+ resumableNode = [
2629
+ keyPath[1],
2630
+ keyPath[2],
2631
+ ([]: Array<ReplayNode>),
2632
+ segment.id,
2633
+ ];
2634
+ addToReplayParent(resumableNode, keyPath[0], trackedPostpones);
2635
+ } else {
2636
+ resumableNode[3] = segment.id;
2637
+ }
2638
}
2639
} else {
2640
let slots;
3063
error: mixed,
3064
errorDigest: ?string,
3065
): void {
2966
- const resumedBoundary = createSuspenseBoundary(
2967
- request,
2968
- new Set(),
2969
- null, // The keyPath doesn't matter at this point so we don't bother rebuilding it.
2970
- );
3066
+ const resumedBoundary = createSuspenseBoundary(request, new Set());
3067
resumedBoundary.parentFlushed = true;
3068
// We restore the same id of this boundary as was used during prerender.
3069
resumedBoundary.rootSegmentID = rootSegmentID;
3113
);
3114
} else {
3115
const boundaryNode: ReplaySuspenseBoundary = node;
3020
- const rootSegmentID = boundaryNode[4];
3116
+ const rootSegmentID = boundaryNode[5];
3117
abortRemainingSuspenseBoundary(
3118
request,
3119
rootSegmentID,
3931
destination,
3932
request.resumableState,
3933
request.renderState,
3838
- request.allPendingTasks === 0 &&
3839
- (request.trackedPostpones === null ||
3840
- request.trackedPostpones.workingMap.size === 0),
3934
+ request.allPendingTasks === 0 && request.trackedPostpones === null,
3935
);
3936
}
3937
4026
if (enableFloat) {
4027
// We write the trailing tags but only if don't have any data to resume.
4028
// If we need to resume we'll write the postamble in the resume instead.
3935
- if (
3936
- !enablePostpone ||
3937
- request.trackedPostpones === null ||
3938
- // We check the working map instead of the root because the root could've
3939
- // been mutated at this point if it was passed straight through to resume().
3940
- request.trackedPostpones.workingMap.size === 0
3941
- ) {
4029
+ if (!enablePostpone || request.trackedPostpones === null) {
4030
writePostamble(destination, request.resumableState);
4031
}
4032
}
4178
(trackedPostpones.rootNodes.length === 0 &&
4179
trackedPostpones.rootSlots === null)
4180
) {
4181
+ // Reset. Let the flushing behave as if we completed the whole document.
4182
+ request.trackedPostpones = null;
4183
return null;
4184
}
4185
return {