394
completedBoundaries: Array<SuspenseBoundary>, // Completed but not yet fully flushed boundaries to show.
395
partialBoundaries: Array<SuspenseBoundary>, // Partially completed boundaries that can flush its segments early.
396
trackedPostpones: null | PostponedHoles, // Gets set to non-null while we want to track postponed holes. I.e. during a prerender.
397
+ // While prerendering a postponed request that produced a real shell, this
398
+ // holds the PostponedState returned by getPostponedState. getPostponedState
399
+ // snapshots nextSegmentId before the (pull-driven) prelude flush runs, but the
400
+ // flush outlines completed boundaries and advances nextSegmentId past that
401
+ // snapshot. We finalize the snapshot from flushCompletedQueues so the resumed
402
+ // render allocates segment ids strictly above the shell's; otherwise the shell
403
+ // and resume emit duplicate B:/S: ids once concatenated. Stays null for live
404
+ // renders and resumes.
405
+ postponedState: null | PostponedState,
406
// onError is called when an error happens anywhere in the tree. It might recover.
407
// The return string is used in production primarily to avoid leaking internals, secondarily to save bytes.
408
// Returning null/undefined will cause a default error message in production
562
this.completedBoundaries = [] as Array<SuspenseBoundary>;
563
this.partialBoundaries = [] as Array<SuspenseBoundary>;
564
this.trackedPostpones = null;
565
+ this.postponedState = null;
566
this.onError = onError === undefined ? defaultErrorHandler : onError;
567
this.onAllReady = onAllReady === undefined ? noop : onAllReady;
568
this.onShellReady = onShellReady === undefined ? noop : onShellReady;
6156
largeBoundaries.splice(0, i);
6157
} finally {
6158
flushingPartialBoundaries = false;
6159
+ const postponedState = request.postponedState;
6160
+ if (postponedState !== null) {
6161
+ // The shell flush above may have outlined completed boundaries, advancing
6162
+ // nextSegmentId past the value getPostponedState snapshotted before the
6163
+ // flush. Re-sync the postponed state to the post-flush high-water mark so
6164
+ // the resume allocates segment ids strictly above the shell's and can't
6165
+ // emit duplicate B:/S: ids. nextSegmentId only increases, so later flush
6166
+ // passes refine this monotonically.
6167
+ // TODO: Could be too late if the postponed state was already serialized
6168
+ // by API consumers. Accessing postponed state before the prelude has flushed
6169
+ // should be forbidden in the API.
6170
+ postponedState.nextSegmentId = request.nextSegmentId;
6171
+ }
6172
if (
6173
request.allPendingTasks === 0 &&
6174
request.clientRenderedBoundaries.length === 0 &&
6446
}
6447
let replaySlots: ResumeSlots;
6448
let nextSegmentId: number;
6426
- if (
6427
- request.completedRootSegment !== null &&
6428
- // The Root postponed
6429
- (request.completedRootSegment.status === POSTPONED ||
6430
- // Or the Preamble was not available
6431
- request.completedPreambleSegments === null)
6432
- ) {
6449
+ // True only when we postponed with a real shell (as opposed to postponing the
6450
+ // root itself). Only in that case does the prelude flush outline completed
6451
+ // boundaries and advance nextSegmentId past the value we capture here.
6452
+ const hasFlushableShell =
6453
+ request.completedRootSegment === null ||
6454
+ // The Root did not postpone
6455
+ (request.completedRootSegment.status !== POSTPONED &&
6456
+ // the Preamble was available
6457
+ request.completedPreambleSegments !== null);
6458
+ if (!hasFlushableShell) {
6459
nextSegmentId = 0;
6460
// We need to ensure that on resume we retry the root. We use a number
6461
// type for the replaySlots to signify this (see resumeRequest).
6469
replaySlots = trackedPostpones.rootSlots;
6470
completeResumableState(request.resumableState);
6471
}
6446
- return {
6472
+ const postponedState: PostponedState = {
6473
nextSegmentId,
6474
rootFormatContext: request.rootFormatContext,
6475
progressiveChunkSize: request.progressiveChunkSize,
6477
replayNodes: trackedPostpones.rootNodes,
6478
replaySlots,
6479
};
6480
+ if (hasFlushableShell) {
6481
+ // The prelude hasn't flushed yet — it's pull-driven and runs after this
6482
+ // (see completeAll -> onAllReady). Flushing the shell outlines completed
6483
+ // boundaries, bumping request.nextSegmentId beyond the snapshot above. Hold
6484
+ // a reference so flushCompletedQueues can finalize nextSegmentId to the
6485
+ // post-flush high-water mark. Otherwise the resume reuses the shell's
6486
+ // segment ids and the concatenated document has duplicate B:/S: ids, which
6487
+ // cross-wires React's boundary-completion scripts ($RC).
6488
+ request.postponedState = postponedState;
6489
+ }
6490
+ return postponedState;
6491
}