@@ -17,8 +17,7 @@ import type {
1717import ReactSharedInternals from 'shared/ReactSharedInternals' ;
1818const { ReactCurrentActQueue} = ReactSharedInternals ;
1919
20- // TODO: Sparse arrays are bad for performance.
21- export opaque type ThenableState = Array < Thenable < any > | void > ;
20+ export opaque type ThenableState = Array < Thenable < any >> ;
2221
2322let thenableState : ThenableState | null = null ;
2423
@@ -62,15 +61,30 @@ export function isThenableStateResolved(thenables: ThenableState): boolean {
6261 return true ;
6362}
6463
65- export function trackUsedThenable < T > ( thenable : Thenable < T > , index : number ) {
64+ function noop ( ) : void { }
65+
66+ export function trackUsedThenable < T > ( thenable : Thenable < T > , index : number ) : T {
6667 if ( __DEV__ && ReactCurrentActQueue . current !== null ) {
6768 ReactCurrentActQueue . didUsePromise = true ;
6869 }
6970
7071 if ( thenableState === null ) {
7172 thenableState = [ thenable ] ;
7273 } else {
73- thenableState [ index ] = thenable ;
74+ const previous = thenableState [ index ] ;
75+ if ( previous === undefined ) {
76+ thenableState . push ( thenable ) ;
77+ } else {
78+ if ( previous !== thenable ) {
79+ // Reuse the previous thenable, and drop the new one. We can assume
80+ // they represent the same value, because components are idempotent.
81+
82+ // Avoid an unhandled rejection errors for the Promises that we'll
83+ // intentionally ignore.
84+ thenable . then ( noop , noop ) ;
85+ thenable = previous ;
86+ }
87+ }
7488 }
7589
7690 // We use an expando to track the status and result of a thenable so that we
@@ -80,52 +94,48 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
8094 // If the thenable doesn't have a status, set it to "pending" and attach
8195 // a listener that will update its status and result when it resolves.
8296 switch ( thenable . status ) {
83- case 'fulfilled' :
84- case 'rejected' :
85- // A thenable that already resolved shouldn't have been thrown, so this is
86- // unexpected. Suggests a mistake in a userspace data library. Don't track
87- // this thenable, because if we keep trying it will likely infinite loop
88- // without ever resolving.
89- // TODO: Log a warning?
90- break ;
97+ case 'fulfilled' : {
98+ const fulfilledValue : T = thenable . value ;
99+ return fulfilledValue ;
100+ }
101+ case 'rejected' : {
102+ const rejectedError = thenable . reason ;
103+ throw rejectedError ;
104+ }
91105 default : {
92106 if ( typeof thenable . status === 'string' ) {
93107 // Only instrument the thenable if the status if not defined. If
94108 // it's defined, but an unknown value, assume it's been instrumented by
95109 // some custom userspace implementation. We treat it as "pending".
96- break ;
110+ } else {
111+ const pendingThenable : PendingThenable < mixed > = (thenable: any);
112+ pendingThenable.status = 'pending';
113+ pendingThenable.then(
114+ fulfilledValue => {
115+ if ( thenable . status === 'pending' ) {
116+ const fulfilledThenable : FulfilledThenable < mixed > = ( thenable : any ) ;
117+ fulfilledThenable . status = 'fulfilled' ;
118+ fulfilledThenable . value = fulfilledValue ;
119+ }
120+ } ,
121+ ( error : mixed ) => {
122+ if ( thenable . status === 'pending' ) {
123+ const rejectedThenable : RejectedThenable < mixed > = ( thenable : any ) ;
124+ rejectedThenable . status = 'rejected' ;
125+ rejectedThenable . reason = error ;
126+ }
127+ } ,
128+ ) ;
97129 }
98- const pendingThenable : PendingThenable < mixed > = (thenable: any);
99- pendingThenable.status = 'pending';
100- pendingThenable.then(
101- fulfilledValue => {
102- if ( thenable . status === 'pending' ) {
103- const fulfilledThenable : FulfilledThenable < mixed > = ( thenable : any ) ;
104- fulfilledThenable . status = 'fulfilled' ;
105- fulfilledThenable . value = fulfilledValue ;
106- }
107- } ,
108- ( error : mixed ) => {
109- if ( thenable . status === 'pending' ) {
110- const rejectedThenable : RejectedThenable < mixed > = ( thenable : any ) ;
111- rejectedThenable . status = 'rejected' ;
112- rejectedThenable . reason = error ;
113- }
114- } ,
115- ) ;
116- break ;
117- }
118- }
119- }
120130
121- export function getPreviouslyUsedThenableAtIndex < T > (
122- index: number,
123- ): Thenable< T > | null {
124- if ( thenableState !== null ) {
125- const thenable = thenableState [ index ] ;
126- if ( thenable !== undefined ) {
127- return thenable ;
131+ // Suspend.
132+ // TODO: Throwing here is an implementation detail that allows us to
133+ // unwind the call stack. But we shouldn't allow it to leak into
134+ // userspace. Throw an opaque placeholder value instead of the
135+ // actual thenable. If it doesn't get captured by the work loop, log
136+ // a warning, because that means something in userspace must have
137+ // caught it.
138+ throw thenable ;
128139 }
129140 }
130- return null;
131141}
0 commit comments