77 * @flow
88 */
99
10- import type { Wakeable } from 'shared/ReactTypes' ;
10+ import type {
11+ Thenable ,
12+ PendingThenable ,
13+ FulfilledThenable ,
14+ RejectedThenable ,
15+ } from 'shared/ReactTypes' ;
1116import type { Lane } from './ReactFiberLane' ;
12- import { requestTransitionLane } from './ReactFiberRootScheduler' ;
13-
14- interface AsyncActionImpl {
15- lane: Lane ;
16- listeners: Array < ( false ) => mixed > ;
17- count: number ;
18- then (
19- onFulfill : ( value : boolean ) = > mixed ,
20- onReject : ( error : mixed ) = > mixed ,
21- ) : void ;
22- }
23-
24- interface PendingAsyncAction extends AsyncActionImpl {
25- status: 'pending' ;
26- }
27-
28- interface FulfilledAsyncAction extends AsyncActionImpl {
29- status: 'fulfilled' ;
30- value: boolean ;
31- }
3217
33- interface RejectedAsyncAction extends AsyncActionImpl {
34- status: 'rejected' ;
35- reason: mixed ;
36- }
18+ import { requestTransitionLane } from './ReactFiberRootScheduler' ;
19+ import { NoLane } from './ReactFiberLane' ;
3720
38- type AsyncAction =
39- | PendingAsyncAction
40- | FulfilledAsyncAction
41- | RejectedAsyncAction ;
21+ // If there are multiple, concurrent async actions, they are entangled. All
22+ // transition updates that occur while the async action is still in progress
23+ // are treated as part of the action.
24+ //
25+ // The ideal behavior would be to treat each async function as an independent
26+ // action. However, without a mechanism like AsyncContext, we can't tell which
27+ // action an update corresponds to. So instead, we entangle them all into one.
4228
43- let currentAsyncAction : AsyncAction | null = null ;
29+ // The listeners to notify once the entangled scope completes.
30+ let currentEntangledListeners : Array < ( ) => mixed > | null = null ;
31+ // The number of pending async actions in the entangled scope.
32+ let currentEntangledPendingCount : number = 0 ;
33+ // The transition lane shared by all updates in the entangled scope.
34+ let currentEntangledLane : Lane = NoLane ;
4435
45- export function requestAsyncActionContext (
36+ export function requestAsyncActionContext < S > (
4637 actionReturnValue : mixed ,
47- ) : AsyncAction | false {
38+ finishedState : S ,
39+ ) : Thenable < S > | S {
4840 if (
4941 actionReturnValue !== null &&
5042 typeof actionReturnValue === 'object' &&
@@ -53,78 +45,131 @@ export function requestAsyncActionContext(
5345 // This is an async action.
5446 //
5547 // Return a thenable that resolves once the action scope (i.e. the async
56- // function passed to startTransition) has finished running. The fulfilled
57- // value is `false` to represent that the action is not pending.
58- const thenable : Wakeable = ( actionReturnValue : any ) ;
59- if ( currentAsyncAction === null ) {
48+ // function passed to startTransition) has finished running.
49+
50+ const thenable : Thenable < mixed > = ( actionReturnValue : any ) ;
51+ let entangledListeners ;
52+ if ( currentEntangledListeners === null ) {
6053 // There's no outer async action scope. Create a new one.
61- const asyncAction : AsyncAction = {
62- lane : requestTransitionLane ( ) ,
63- listeners : [ ] ,
64- count : 0 ,
65- status : 'pending' ,
66- value : false ,
67- reason : undefined ,
68- then ( resolve : boolean => mixed ) {
69- asyncAction . listeners . push ( resolve ) ;
70- } ,
71- } ;
72- attachPingListeners ( thenable , asyncAction ) ;
73- currentAsyncAction = asyncAction ;
74- return asyncAction ;
54+ entangledListeners = currentEntangledListeners = [ ] ;
55+ currentEntangledPendingCount = 0 ;
56+ currentEntangledLane = requestTransitionLane ( ) ;
7557 } else {
76- // Inherit the outer scope.
77- const asyncAction : AsyncAction = ( currentAsyncAction : any ) ;
78- attachPingListeners ( thenable , asyncAction ) ;
79- return asyncAction ;
58+ entangledListeners = currentEntangledListeners ;
8059 }
60+
61+ currentEntangledPendingCount ++ ;
62+ let resultStatus = 'pending' ;
63+ let rejectedReason ;
64+ thenable . then (
65+ ( ) => {
66+ resultStatus = 'fulfilled' ;
67+ pingEngtangledActionScope ( ) ;
68+ } ,
69+ error => {
70+ resultStatus = 'rejected' ;
71+ rejectedReason = error ;
72+ pingEngtangledActionScope ( ) ;
73+ } ,
74+ ) ;
75+
76+ // Create a thenable that represents the result of this action, but doesn't
77+ // resolve until the entire entangled scope has finished.
78+ //
79+ // Expressed using promises:
80+ // const [thisResult] = await Promise.all([thisAction, entangledAction]);
81+ // return thisResult;
82+ const resultThenable = createResultThenable < S > ( entangledListeners ) ;
83+
84+ // Attach a listener to fill in the result.
85+ entangledListeners . push ( ( ) => {
86+ switch ( resultStatus ) {
87+ case 'fulfilled' : {
88+ const fulfilledThenable : FulfilledThenable < S > = ( resultThenable : any ) ;
89+ fulfilledThenable . status = 'fulfilled' ;
90+ fulfilledThenable . value = finishedState ;
91+ break ;
92+ }
93+ case 'rejected' : {
94+ const rejectedThenable : RejectedThenable < S > = (resultThenable: any);
95+ rejectedThenable.status = 'rejected';
96+ rejectedThenable.reason = rejectedReason;
97+ break;
98+ }
99+ case 'pending ':
100+ default : {
101+ // The listener above should have been called first, so `resultStatus`
102+ // should already be set to the correct value.
103+ throw new Error (
104+ 'Thenable should have already resolved. This ' +
105+ 'is a bug in React.' ,
106+ ) ;
107+ }
108+ }
109+ } ) ;
110+
111+ return resultThenable ;
81112 } else {
82113 // This is not an async action, but it may be part of an outer async action.
83- if ( currentAsyncAction === null ) {
84- // There's no outer async action scope.
85- return false ;
114+ if ( currentEntangledListeners === null ) {
115+ return finishedState ;
86116 } else {
87- // Inherit the outer scope.
88- return currentAsyncAction ;
117+ // Return a thenable that does not resolve until the entangled actions
118+ // have finished.
119+ const entangledListeners = currentEntangledListeners ;
120+ const resultThenable = createResultThenable < S > ( entangledListeners ) ;
121+ entangledListeners . push ( ( ) => {
122+ const fulfilledThenable : FulfilledThenable < S > = (resultThenable: any);
123+ fulfilledThenable.status = 'fulfilled';
124+ fulfilledThenable.value = finishedState;
125+ } ) ;
126+ return resultThenable ;
89127 }
90128 }
91129}
92130
93- export function peekAsyncActionContext ( ) : AsyncAction | null {
94- return currentAsyncAction ;
131+ function pingEngtangledActionScope ( ) {
132+ if (
133+ currentEntangledListeners !== null &&
134+ -- currentEntangledPendingCount === 0
135+ ) {
136+ // All the actions have finished. Close the entangled async action scope
137+ // and notify all the listeners.
138+ const listeners = currentEntangledListeners ;
139+ currentEntangledListeners = null ;
140+ currentEntangledLane = NoLane ;
141+ for ( let i = 0 ; i < listeners . length ; i ++ ) {
142+ const listener = listeners [ i ] ;
143+ listener ( ) ;
144+ }
145+ }
95146}
96147
97- function attachPingListeners ( thenable : Wakeable , asyncAction : AsyncAction ) {
98- asyncAction . count ++ ;
99- thenable . then (
100- ( ) => {
101- if ( -- asyncAction . count === 0 ) {
102- const fulfilledAsyncAction : FulfilledAsyncAction = ( asyncAction : any ) ;
103- fulfilledAsyncAction . status = 'fulfilled' ;
104- completeAsyncActionScope ( asyncAction ) ;
105- }
106- } ,
107- ( error : mixed ) => {
108- if ( -- asyncAction . count === 0 ) {
109- const rejectedAsyncAction : RejectedAsyncAction = ( asyncAction : any ) ;
110- rejectedAsyncAction . status = 'rejected' ;
111- rejectedAsyncAction . reason = error ;
112- completeAsyncActionScope ( asyncAction ) ;
113- }
148+ function createResultThenable < S > (
149+ entangledListeners : Array < ( ) => mixed > ,
150+ ) : Thenable < S > {
151+ // Waits for the entangled async action to complete, then resolves to the
152+ // result of an individual action.
153+ const resultThenable : PendingThenable < S > = {
154+ status : 'pending' ,
155+ value : null ,
156+ reason : null ,
157+ then ( resolve : S => mixed ) {
158+ // This is a bit of a cheat. `resolve` expects a value of type `S` to be
159+ // passed, but because we're instrumenting the `status` field ourselves,
160+ // and we know this thenable will only be used by React, we also know
161+ // the value isn't actually needed. So we add the resolve function
162+ // directly to the entangled listeners.
163+ //
164+ // This is also why we don't need to check if the thenable is still
165+ // pending; the Suspense implementation already performs that check.
166+ const ping : ( ) => mixed = ( resolve : any ) ;
167+ entangledListeners . push ( ping ) ;
114168 } ,
115- ) ;
116- return asyncAction ;
169+ } ;
170+ return resultThenable ;
117171}
118172
119- function completeAsyncActionScope ( action : AsyncAction ) {
120- if ( currentAsyncAction === action ) {
121- currentAsyncAction = null ;
122- }
123-
124- const listeners = action . listeners ;
125- action . listeners = [ ] ;
126- for ( let i = 0 ; i < listeners . length ; i ++ ) {
127- const listener = listeners [ i ] ;
128- listener ( false ) ;
129- }
173+ export function peekEntangledActionLane ( ) : Lane {
174+ return currentEntangledLane ;
130175}
0 commit comments