154
// Linked list representing the identity of a component given the component/tag name and key.
155
// The name might be minified but we assume that it's going to be the same generated name. Typically
156
// because it's just the same compiled output in practice.
157
-type KeyNode =
158
- | null
159
- | [KeyNode /* parent */, string | null /* name */, string | number /* key */];
157
+type KeyNode = [
158
+ Root | KeyNode /* parent */,
159
+ string | null /* name */,
160
+ string | number /* key */,
161
+];
162
+
163
+const REPLAY_NODE = 0;
164
+const REPLAY_SUSPENSE_BOUNDARY = 1;
165
+const RESUME_SEGMENT = 2;
166
+
167
+type ResumableParentNode =
168
+ | [
169
+ 0, // REPLAY_NODE
170
+ string | null /* name */,
171
+ string | number /* key */,
172
+ Array<ResumableNode> /* children */,
173
+ ]
174
+ | [
175
+ 1, // REPLAY_SUSPENSE_BOUNDARY
176
+ string | null /* name */,
177
+ string | number /* key */,
178
+ Array<ResumableNode> /* children */,
179
+ SuspenseBoundaryID,
180
+ ];
181
+type ResumableNode =
182
+ | ResumableParentNode
183
+ | [
184
+ 2, // RESUME_SEGMENT
185
+ string | null /* name */,
186
+ string | number /* key */,
187
+ number /* segment id */,
188
+ ];
189
+
190
+type PostponedHoles = {
191
+ workingMap: Map<KeyNode, ResumableParentNode>,
192
+ root: Array<ResumableNode>,
193
+};
194
195
type LegacyContext = {
196
[key: string]: any,
197
};
198
199
+const CLIENT_RENDERED = 4; // if it errors or infinitely suspends
200
+
201
type SuspenseBoundary = {
202
+ status: 0 | 1 | 4 | 5,
203
id: SuspenseBoundaryID,
204
rootSegmentID: number,
205
errorDigest: ?string, // the error hash if it errors
206
errorMessage?: string, // the error string if it errors
207
errorComponentStack?: string, // the error component stack if it errors
171
- forceClientRender: boolean, // if it errors or infinitely suspends
208
parentFlushed: boolean,
209
pendingTasks: number, // when it reaches zero we can show this boundary's content
210
completedSegments: Array<Segment>, // completed but not yet flushed segments.
211
byteSize: number, // used to determine whether to inline children boundaries.
212
fallbackAbortableTasks: Set<Task>, // used to cancel task on the fallback if the boundary completes or gets canceled.
213
resources: BoundaryResources,
214
+ keyPath: Root | KeyNode,
215
};
216
217
export type Task = {
220
blockedBoundary: Root | SuspenseBoundary,
221
blockedSegment: Segment, // the segment we'll write to
222
abortSet: Set<Task>, // the abortable set that this task belongs to
186
- keyPath: KeyNode, // the path of all parent keys currently rendering
223
+ keyPath: Root | KeyNode, // the path of all parent keys currently rendering
224
legacyContext: LegacyContext, // the current legacy context that this task is executing in
225
context: ContextSnapshot, // the current new context that this task is executing in
226
treeContext: TreeContext, // the current tree context that this task is executing in
233
const FLUSHED = 2;
234
const ABORTED = 3;
235
const ERRORED = 4;
236
+const POSTPONED = 5;
237
238
type Root = null;
239
240
type Segment = {
203
- status: 0 | 1 | 2 | 3 | 4,
241
+ status: 0 | 1 | 2 | 3 | 4 | 5,
242
parentFlushed: boolean, // typically a segment will be flushed by its parent, except if its parent was already flushed
243
id: number, // starts as 0 and is lazily assigned if the parent flushes early
244
+index: number, // the index within the parent's chunks or 0 at the root
262
flushScheduled: boolean,
263
+resumableState: ResumableState,
264
+renderState: RenderState,
265
+ +rootFormatContext: FormatContext,
266
+progressiveChunkSize: number,
267
status: 0 | 1 | 2,
268
fatalError: mixed,
276
clientRenderedBoundaries: Array<SuspenseBoundary>, // Errored or client rendered but not yet flushed.
277
completedBoundaries: Array<SuspenseBoundary>, // Completed but not yet fully flushed boundaries to show.
278
partialBoundaries: Array<SuspenseBoundary>, // Partially completed boundaries that can flush its segments early.
279
+ trackedPostpones: null | PostponedHoles, // Gets set to non-null while we want to track postponed holes. I.e. during a prerender.
280
// onError is called when an error happens anywhere in the tree. It might recover.
281
// The return string is used in production primarily to avoid leaking internals, secondarily to save bytes.
282
// Returning null/undefined will cause a defualt error message in production
342
flushScheduled: false,
343
resumableState,
344
renderState,
345
+ rootFormatContext,
346
progressiveChunkSize:
347
progressiveChunkSize === undefined
348
? DEFAULT_PROGRESSIVE_CHUNK_SIZE
358
clientRenderedBoundaries: ([]: Array<SuspenseBoundary>),
359
completedBoundaries: ([]: Array<SuspenseBoundary>),
360
partialBoundaries: ([]: Array<SuspenseBoundary>),
361
+ trackedPostpones: null,
362
onError: onError === undefined ? defaultErrorHandler : onError,
363
onPostpone: onPostpone === undefined ? noop : onPostpone,
364
onAllReady: onAllReady === undefined ? noop : onAllReady,
417
function createSuspenseBoundary(
418
request: Request,
419
fallbackAbortableTasks: Set<Task>,
420
+ keyPath: Root | KeyNode,
421
): SuspenseBoundary {
422
return {
423
+ status: PENDING,
424
id: UNINITIALIZED_SUSPENSE_BOUNDARY_ID,
425
rootSegmentID: -1,
426
parentFlushed: false,
427
pendingTasks: 0,
384
- forceClientRender: false,
428
completedSegments: [],
429
byteSize: 0,
430
fallbackAbortableTasks,
431
errorDigest: null,
432
resources: createBoundaryResources(),
433
+ keyPath,
434
};
435
}
436
441
blockedBoundary: Root | SuspenseBoundary,
442
blockedSegment: Segment,
443
abortSet: Set<Task>,
400
- keyPath: KeyNode,
444
+ keyPath: Root | KeyNode,
445
legacyContext: LegacyContext,
446
context: ContextSnapshot,
447
treeContext: TreeContext,
624
const content: ReactNodeList = props.children;
625
626
const fallbackAbortSet: Set<Task> = new Set();
583
- const newBoundary = createSuspenseBoundary(request, fallbackAbortSet);
627
+ const newBoundary = createSuspenseBoundary(
628
+ request,
629
+ fallbackAbortSet,
630
+ task.keyPath,
631
+ );
632
const insertionIndex = parentSegment.chunks.length;
633
// The children of the boundary segment is actually the fallback.
634
const boundarySegment = createPendingSegment(
685
);
686
contentRootSegment.status = COMPLETED;
687
queueCompletedSegment(newBoundary, contentRootSegment);
640
- if (newBoundary.pendingTasks === 0) {
688
+ if (newBoundary.pendingTasks === 0 && newBoundary.status === PENDING) {
689
+ newBoundary.status = COMPLETED;
690
// This must have been the last segment we were waiting on. This boundary is now complete.
691
// Therefore we won't need the fallback. We early return so that we don't have to create
692
// the fallback.
695
}
696
} catch (error) {
697
contentRootSegment.status = ERRORED;
649
- newBoundary.forceClientRender = true;
698
+ newBoundary.status = CLIENT_RENDERED;
699
let errorDigest;
700
if (
701
enablePostpone &&
1673
}
1674
}
1675
1676
+function trackPostpone(
1677
+ request: Request,
1678
+ trackedPostpones: PostponedHoles,
1679
+ task: Task,
1680
+ segment: Segment,
1681
+): void {
1682
+ segment.status = POSTPONED;
1683
+ // We know that this will leave a hole so we might as well assign an ID now.
1684
+ segment.id = request.nextSegmentId++;
1685
+
1686
+ const boundary = task.blockedBoundary;
1687
+ if (boundary !== null && boundary.status === PENDING) {
1688
+ boundary.status = POSTPONED;
1689
+ // We need to eagerly assign it an ID because we'll need to refer to
1690
+ // it before flushing and we know that we can't inline it.
1691
+ boundary.id = assignSuspenseBoundaryID(
1692
+ request.renderState,
1693
+ request.resumableState,
1694
+ );
1695
+
1696
+ const boundaryKeyPath = boundary.keyPath;
1697
+ if (boundaryKeyPath === null) {
1698
+ throw new Error(
1699
+ 'It should not be possible to postpone at the root. This is a bug in React.',
1700
+ );
1701
+ }
1702
+ const children: Array<ResumableNode> = [];
1703
+ const boundaryNode: ResumableParentNode = [
1704
+ REPLAY_SUSPENSE_BOUNDARY,
1705
+ boundaryKeyPath[1],
1706
+ boundaryKeyPath[2],
1707
+ children,
1708
+ boundary.id,
1709
+ ];
1710
+ trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode);
1711
+ addToResumableParent(boundaryNode, boundaryKeyPath, trackedPostpones);
1712
+ }
1713
+
1714
+ const keyPath = task.keyPath;
1715
+ if (keyPath === null) {
1716
+ throw new Error(
1717
+ 'It should not be possible to postpone at the root. This is a bug in React.',
1718
+ );
1719
+ }
1720
+
1721
+ const segmentNode: ResumableNode = [
1722
+ RESUME_SEGMENT,
1723
+ keyPath[1],
1724
+ keyPath[2],
1725
+ segment.id,
1726
+ ];
1727
+ addToResumableParent(segmentNode, keyPath, trackedPostpones);
1728
+}
1729
+
1730
+function injectPostponedHole(
1731
+ request: Request,
1732
+ task: Task,
1733
+ reason: string,
1734
+): Segment {
1735
+ logPostpone(request, reason);
1736
+ // Something suspended, we'll need to create a new segment and resolve it later.
1737
+ const segment = task.blockedSegment;
1738
+ const insertionIndex = segment.chunks.length;
1739
+ const newSegment = createPendingSegment(
1740
+ request,
1741
+ insertionIndex,
1742
+ null,
1743
+ segment.formatContext,
1744
+ // Adopt the parent segment's leading text embed
1745
+ segment.lastPushedText,
1746
+ // Assume we are text embedded at the trailing edge
1747
+ true,
1748
+ );
1749
+ segment.children.push(newSegment);
1750
+ // Reset lastPushedText for current Segment since the new Segment "consumed" it
1751
+ segment.lastPushedText = false;
1752
+ return segment;
1753
+}
1754
+
1755
function spawnNewSuspendedTask(
1756
request: Request,
1757
task: Task,
1841
getSuspendedThenable()
1842
: thrownValue;
1843
1716
- // $FlowFixMe[method-unbinding]
1717
- if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
1718
- const wakeable: Wakeable = (x: any);
1719
- const thenableState = getThenableStateAfterSuspending();
1720
- spawnNewSuspendedTask(request, task, thenableState, wakeable);
1721
-
1722
- // Restore the context. We assume that this will be restored by the inner
1723
- // functions in case nothing throws so we don't use "finally" here.
1724
- task.blockedSegment.formatContext = previousFormatContext;
1725
- task.legacyContext = previousLegacyContext;
1726
- task.context = previousContext;
1727
- task.keyPath = previousKeyPath;
1728
- // Restore all active ReactContexts to what they were before.
1729
- switchContext(previousContext);
1730
- if (__DEV__) {
1731
- task.componentStack = previousComponentStack;
1844
+ if (typeof x === 'object' && x !== null) {
1845
+ // $FlowFixMe[method-unbinding]
1846
+ if (typeof x.then === 'function') {
1847
+ const wakeable: Wakeable = (x: any);
1848
+ const thenableState = getThenableStateAfterSuspending();
1849
+ spawnNewSuspendedTask(request, task, thenableState, wakeable);
1850
+
1851
+ // Restore the context. We assume that this will be restored by the inner
1852
+ // functions in case nothing throws so we don't use "finally" here.
1853
+ task.blockedSegment.formatContext = previousFormatContext;
1854
+ task.legacyContext = previousLegacyContext;
1855
+ task.context = previousContext;
1856
+ task.keyPath = previousKeyPath;
1857
+ // Restore all active ReactContexts to what they were before.
1858
+ switchContext(previousContext);
1859
+ if (__DEV__) {
1860
+ task.componentStack = previousComponentStack;
1861
+ }
1862
+ return;
1863
}
1733
- return;
1734
- } else {
1735
- // Restore the context. We assume that this will be restored by the inner
1736
- // functions in case nothing throws so we don't use "finally" here.
1737
- task.blockedSegment.formatContext = previousFormatContext;
1738
- task.legacyContext = previousLegacyContext;
1739
- task.context = previousContext;
1740
- task.keyPath = previousKeyPath;
1741
- // Restore all active ReactContexts to what they were before.
1742
- switchContext(previousContext);
1743
- if (__DEV__) {
1744
- task.componentStack = previousComponentStack;
1864
+ if (
1865
+ enablePostpone &&
1866
+ request.trackedPostpones !== null &&
1867
+ x.$$typeof === REACT_POSTPONE_TYPE &&
1868
+ task.blockedBoundary !== null // TODO: Support holes in the shell
1869
+ ) {
1870
+ // If we're tracking postpones, we inject a hole here and continue rendering
1871
+ // sibling. Similar to suspending. If we're not tracking, we treat it more like
1872
+ // an error. Notably this doesn't spawn a new task since nothing will fill it
1873
+ // in during this prerender.
1874
+ const postponeInstance: Postpone = (x: any);
1875
+ const trackedPostpones = request.trackedPostpones;
1876
+ const postponedSegment = injectPostponedHole(
1877
+ request,
1878
+ task,
1879
+ postponeInstance.message,
1880
+ );
1881
+ trackPostpone(request, trackedPostpones, task, postponedSegment);
1882
+
1883
+ // Restore the context. We assume that this will be restored by the inner
1884
+ // functions in case nothing throws so we don't use "finally" here.
1885
+ task.blockedSegment.formatContext = previousFormatContext;
1886
+ task.legacyContext = previousLegacyContext;
1887
+ task.context = previousContext;
1888
+ task.keyPath = previousKeyPath;
1889
+ // Restore all active ReactContexts to what they were before.
1890
+ switchContext(previousContext);
1891
+ if (__DEV__) {
1892
+ task.componentStack = previousComponentStack;
1893
+ }
1894
+ return;
1895
}
1746
- // We assume that we don't need the correct context.
1747
- // Let's terminate the rest of the tree and don't render any siblings.
1748
- throw x;
1896
}
1897
+ // Restore the context. We assume that this will be restored by the inner
1898
+ // functions in case nothing throws so we don't use "finally" here.
1899
+ task.blockedSegment.formatContext = previousFormatContext;
1900
+ task.legacyContext = previousLegacyContext;
1901
+ task.context = previousContext;
1902
+ task.keyPath = previousKeyPath;
1903
+ // Restore all active ReactContexts to what they were before.
1904
+ switchContext(previousContext);
1905
+ if (__DEV__) {
1906
+ task.componentStack = previousComponentStack;
1907
+ }
1908
+ // We assume that we don't need the correct context.
1909
+ // Let's terminate the rest of the tree and don't render any siblings.
1910
+ throw x;
1911
}
1912
}
1913
1936
fatalError(request, error);
1937
} else {
1938
boundary.pendingTasks--;
1778
- if (!boundary.forceClientRender) {
1779
- boundary.forceClientRender = true;
1939
+ if (boundary.status !== CLIENT_RENDERED) {
1940
+ boundary.status = CLIENT_RENDERED;
1941
boundary.errorDigest = errorDigest;
1942
if (__DEV__) {
1943
captureBoundaryErrorDetailsDev(boundary, error);
1990
}
1991
} else {
1992
boundary.pendingTasks--;
1832
-
1833
- if (!boundary.forceClientRender) {
1834
- boundary.forceClientRender = true;
1993
+ if (boundary.status !== CLIENT_RENDERED) {
1994
+ boundary.status = CLIENT_RENDERED;
1995
boundary.errorDigest = request.onError(error);
1996
if (__DEV__) {
1997
const errorPrefix =
2078
}
2079
} else {
2080
boundary.pendingTasks--;
1921
- if (boundary.forceClientRender) {
2081
+ if (boundary.status === CLIENT_RENDERED) {
2082
// This already errored.
2083
} else if (boundary.pendingTasks === 0) {
2084
+ if (boundary.status === PENDING) {
2085
+ boundary.status = COMPLETED;
2086
+ }
2087
// This must have been the last segment we were waiting on. This boundary is now complete.
2088
if (segment.parentFlushed) {
2089
// Our parent segment already flushed, so we need to schedule this segment to be emitted.
2197
getSuspendedThenable()
2198
: thrownValue;
2199
2037
- // $FlowFixMe[method-unbinding]
2038
- if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
2039
- // Something suspended again, let's pick it back up later.
2040
- const ping = task.ping;
2041
- x.then(ping, ping);
2042
- task.thenableState = getThenableStateAfterSuspending();
2043
- } else {
2044
- task.abortSet.delete(task);
2045
- segment.status = ERRORED;
2046
- erroredTask(request, task.blockedBoundary, segment, x);
2200
+ if (typeof x === 'object' && x !== null) {
2201
+ // $FlowFixMe[method-unbinding]
2202
+ if (typeof x.then === 'function') {
2203
+ // Something suspended again, let's pick it back up later.
2204
+ const ping = task.ping;
2205
+ x.then(ping, ping);
2206
+ task.thenableState = getThenableStateAfterSuspending();
2207
+ return;
2208
+ } else if (
2209
+ enablePostpone &&
2210
+ request.trackedPostpones !== null &&
2211
+ x.$$typeof === REACT_POSTPONE_TYPE &&
2212
+ task.blockedBoundary !== null // TODO: Support holes in the shell
2213
+ ) {
2214
+ // If we're tracking postpones, we mark this segment as postponed and finish
2215
+ // the task without filling it in. If we're not tracking, we treat it more like
2216
+ // an error.
2217
+ const trackedPostpones = request.trackedPostpones;
2218
+ task.abortSet.delete(task);
2219
+ const postponeInstance: Postpone = (x: any);
2220
+ logPostpone(request, postponeInstance.message);
2221
+ trackPostpone(request, trackedPostpones, task, segment);
2222
+ finishedTask(request, task.blockedBoundary, segment);
2223
+ }
2224
}
2225
+ task.abortSet.delete(task);
2226
+ segment.status = ERRORED;
2227
+ erroredTask(request, task.blockedBoundary, segment, x);
2228
+ return;
2229
} finally {
2230
if (enableFloat) {
2231
setCurrentlyRenderingBoundaryResourcesTarget(request.renderState, null);
2307
case PENDING: {
2308
// We're emitting a placeholder for this segment to be filled in later.
2309
// Therefore we'll need to assign it an ID - to refer to it by.
2129
- const segmentID = (segment.id = request.nextSegmentId++);
2310
+ segment.id = request.nextSegmentId++;
2311
+ // Fallthrough
2312
+ }
2313
+ case POSTPONED: {
2314
+ const segmentID = segment.id;
2315
// When this segment finally completes it won't be embedded in text since it will flush separately
2316
segment.lastPushedText = false;
2317
segment.textEmbedded = false;
2359
// Not a suspense boundary.
2360
return flushSubtree(request, destination, segment);
2361
}
2362
+
2363
boundary.parentFlushed = true;
2364
// This segment is a Suspense boundary. We need to decide whether to
2365
// emit the content or the fallback now.
2180
- if (boundary.forceClientRender) {
2366
+ if (boundary.status === CLIENT_RENDERED) {
2367
// Emit a client rendered suspense boundary wrapper.
2368
// We never queue the inner boundary so we'll never emit its content or partial segments.
2369
2381
destination,
2382
request.renderState,
2383
);
2198
- } else if (boundary.pendingTasks > 0) {
2384
+ } else if (boundary.status !== COMPLETED) {
2385
+ if (boundary.status === PENDING) {
2386
+ boundary.id = assignSuspenseBoundaryID(
2387
+ request.renderState,
2388
+ request.resumableState,
2389
+ );
2390
+ }
2391
// This boundary is still loading. Emit a pending suspense boundary wrapper.
2392
2393
// Assign an ID to refer to the future content by.
2398
}
2399
2400
/// This is the first time we should have referenced this ID.
2209
- const id = (boundary.id = assignSuspenseBoundaryID(
2210
- request.renderState,
2211
- request.resumableState,
2212
- ));
2401
+ const id = boundary.id;
2402
2403
writeStartPendingSuspenseBoundary(destination, request.renderState, id);
2404
2711
) {
2712
request.flushScheduled = false;
2713
if (enableFloat) {
2525
- writePostamble(destination, request.resumableState);
2714
+ // We write the trailing tags but only if don't have any data to resume.
2715
+ // If we need to resume we'll write the postamble in the resume instead.
2716
+ if (
2717
+ !enablePostpone ||
2718
+ request.trackedPostpones === null ||
2719
+ request.trackedPostpones.root.length === 0
2720
+ ) {
2721
+ writePostamble(destination, request.resumableState);
2722
+ }
2723
}
2724
completeWriting(destination);
2725
flushBuffered(destination);
2739
}
2740
}
2741
2545
-export function startWork(request: Request): void {
2742
+export function startRender(request: Request): void {
2743
request.flushScheduled = request.destination !== null;
2744
if (supportsRequestStorage) {
2745
scheduleWork(() => requestStorage.run(request, performWork, request));
2748
}
2749
}
2750
2751
+export function startPrerender(request: Request): void {
2752
+ // Start tracking postponed holes during this render.
2753
+ request.trackedPostpones = {workingMap: new Map(), root: []};
2754
+ startRender(request);
2755
+}
2756
+
2757
function enqueueFlush(request: Request): void {
2758
if (
2759
request.flushScheduled === false &&
2820
return request.resumableState;
2821
}
2822
2823
+function addToResumableParent(
2824
+ node: ResumableNode,
2825
+ keyPath: KeyNode,
2826
+ trackedPostpones: PostponedHoles,
2827
+): void {
2828
+ const parentKeyPath = keyPath[0];
2829
+ if (parentKeyPath === null) {
2830
+ trackedPostpones.root.push(node);
2831
+ } else {
2832
+ const workingMap = trackedPostpones.workingMap;
2833
+ let parentNode = workingMap.get(parentKeyPath);
2834
+ if (parentNode === undefined) {
2835
+ parentNode = ([
2836
+ REPLAY_NODE,
2837
+ parentKeyPath[1],
2838
+ parentKeyPath[2],
2839
+ ([]: Array<ResumableNode>),
2840
+ ]: ResumableParentNode);
2841
+ workingMap.set(parentKeyPath, parentNode);
2842
+ addToResumableParent(parentNode, parentKeyPath, trackedPostpones);
2843
+ }
2844
+ parentNode[3].push(node);
2845
+ }
2846
+}
2847
+
2848
export type PostponedState = {
2849
nextSegmentId: number,
2850
rootFormatContext: FormatContext,
2851
progressiveChunkSize: number,
2852
resumableState: ResumableState,
2853
+ resumablePath: Array<ResumableNode>,
2854
};
2855
2856
// Returns the state of a postponed request or null if nothing was postponed.
2857
export function getPostponedState(request: Request): null | PostponedState {
2629
- return null;
2858
+ const trackedPostpones = request.trackedPostpones;
2859
+ if (trackedPostpones === null || trackedPostpones.root.length === 0) {
2860
+ return null;
2861
+ }
2862
+ return {
2863
+ nextSegmentId: request.nextSegmentId,
2864
+ rootFormatContext: request.rootFormatContext,
2865
+ progressiveChunkSize: request.progressiveChunkSize,
2866
+ resumableState: request.resumableState,
2867
+ resumablePath: trackedPostpones.root,
2868
+ };
2869
}