@@ -126,6 +126,7 @@ import {
126
126
throwException ,
127
127
createRootErrorUpdate ,
128
128
createClassErrorUpdate ,
129
+ SuspendOnTask ,
129
130
} from './ReactFiberThrow' ;
130
131
import {
131
132
commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber ,
@@ -201,13 +202,14 @@ const LegacyUnbatchedContext = /* */ 0b001000;
201
202
const RenderContext = /* */ 0b010000 ;
202
203
const CommitContext = /* */ 0b100000 ;
203
204
204
- type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 ;
205
+ type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6 ;
205
206
const RootIncomplete = 0 ;
206
207
const RootFatalErrored = 1 ;
207
- const RootErrored = 2 ;
208
- const RootSuspended = 3 ;
209
- const RootSuspendedWithDelay = 4 ;
210
- const RootCompleted = 5 ;
208
+ const RootSuspendedOnTask = 2 ;
209
+ const RootErrored = 3 ;
210
+ const RootSuspended = 4 ;
211
+ const RootSuspendedWithDelay = 5 ;
212
+ const RootCompleted = 6 ;
211
213
212
214
export type Thenable = {
213
215
then ( resolve : ( ) => mixed , reject ?: ( ) => mixed ) : Thenable | void ,
@@ -238,7 +240,7 @@ let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
238
240
// The work left over by components that were visited during this render. Only
239
241
// includes unprocessed updates, not work in bailed out children.
240
242
let workInProgressRootNextUnprocessedUpdateTime : ExpirationTime = NoWork ;
241
-
243
+ let workInProgressRootRestartTime : ExpirationTime = NoWork ;
242
244
// If we're pinged while rendering we don't always restart immediately.
243
245
// This flag determines if it might be worthwhile to restart if an opportunity
244
246
// happens latere.
@@ -708,7 +710,12 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
708
710
throw fatalError ;
709
711
}
710
712
711
- if ( workInProgress !== null ) {
713
+ if ( workInProgressRootExitStatus === RootSuspendedOnTask ) {
714
+ // Can't finish rendering at this level. Exit early and restart at the
715
+ // specified time.
716
+ markRootSuspendedAtTime ( root , expirationTime ) ;
717
+ root . nextKnownPendingLevel = workInProgressRootRestartTime ;
718
+ } else if ( workInProgress !== null ) {
712
719
// There's still work left over. Exit without committing.
713
720
stopInterruptedWorkLoopTimer ( ) ;
714
721
} else {
@@ -749,7 +756,8 @@ function finishConcurrentRender(
749
756
750
757
switch ( exitStatus ) {
751
758
case RootIncomplete :
752
- case RootFatalErrored : {
759
+ case RootFatalErrored :
760
+ case RootSuspendedOnTask : {
753
761
invariant ( false , 'Root did not complete. This is a bug in React.' ) ;
754
762
}
755
763
// Flow knows about invariant, so it complains if I add a break
@@ -1036,7 +1044,12 @@ function performSyncWorkOnRoot(root) {
1036
1044
throw fatalError ;
1037
1045
}
1038
1046
1039
- if ( workInProgress !== null ) {
1047
+ if ( workInProgressRootExitStatus === RootSuspendedOnTask ) {
1048
+ // Can't finish rendering at this level. Exit early and restart at the
1049
+ // specified time.
1050
+ markRootSuspendedAtTime ( root , expirationTime ) ;
1051
+ root . nextKnownPendingLevel = workInProgressRootRestartTime ;
1052
+ } else if ( workInProgress !== null ) {
1040
1053
// This is a sync render, so we should have finished the whole tree.
1041
1054
invariant (
1042
1055
false ,
@@ -1263,6 +1276,7 @@ function prepareFreshStack(root, expirationTime) {
1263
1276
workInProgressRootLatestSuspenseTimeout = Sync ;
1264
1277
workInProgressRootCanSuspendUsingConfig = null ;
1265
1278
workInProgressRootNextUnprocessedUpdateTime = NoWork ;
1279
+ workInProgressRootRestartTime = NoWork ;
1266
1280
workInProgressRootHasPendingPing = false ;
1267
1281
1268
1282
if ( enableSchedulerTracing ) {
@@ -1283,6 +1297,20 @@ function handleError(root, thrownValue) {
1283
1297
resetHooks ( ) ;
1284
1298
resetCurrentDebugFiberInDEV ( ) ;
1285
1299
1300
+ // Check if this is a SuspendOnTask exception. This is the one type of
1301
+ // exception that is allowed to happen at the root.
1302
+ // TODO: I think instanceof is OK here? A brand check seems unnecessary
1303
+ // since this is always thrown by the renderer and not across realms
1304
+ // or packages.
1305
+ if ( thrownValue instanceof SuspendOnTask ) {
1306
+ // Can't finish rendering at this level. Exit early and restart at
1307
+ // the specified time.
1308
+ workInProgressRootExitStatus = RootSuspendedOnTask ;
1309
+ workInProgressRootRestartTime = thrownValue . retryTime ;
1310
+ workInProgress = null ;
1311
+ return ;
1312
+ }
1313
+
1286
1314
if ( workInProgress === null || workInProgress . return === null ) {
1287
1315
// Expected to be working on a non-root fiber. This is a fatal error
1288
1316
// because there's no ancestor that can handle it; the root is
@@ -2624,15 +2652,17 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
2624
2652
try {
2625
2653
return originalBeginWork ( current , unitOfWork , expirationTime ) ;
2626
2654
} catch ( originalError ) {
2655
+ // Filter out special exception types
2627
2656
if (
2628
2657
originalError !== null &&
2629
2658
typeof originalError === 'object' &&
2630
- typeof originalError . then === 'function'
2659
+ // Promise
2660
+ ( typeof originalError . then === 'function' ||
2661
+ // SuspendOnTask exception
2662
+ originalError instanceof SuspendOnTask )
2631
2663
) {
2632
- // Don't replay promises. Treat everything else like an error.
2633
2664
throw originalError ;
2634
2665
}
2635
-
2636
2666
// Keep this code in sync with handleError; any changes here must have
2637
2667
// corresponding changes there.
2638
2668
resetContextDependencies ( ) ;
0 commit comments