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