@@ -23,8 +23,10 @@ import { Provider } from '@firebase/component';
2323
2424import { User } from '../auth/user' ;
2525import { hardAssert , debugAssert } from '../util/assert' ;
26+ import { AsyncQueue } from '../util/async_queue' ;
2627import { Code , FirestoreError } from '../util/error' ;
2728import { logDebug } from '../util/log' ;
29+ import { Deferred } from '../util/promise' ;
2830
2931// TODO(mikelehen): This should be split into multiple files and probably
3032// moved to an auth/ folder to match other platforms.
@@ -78,7 +80,7 @@ export class OAuthToken implements Token {
7880 * token and may need to invalidate other state if the current user has also
7981 * changed.
8082 */
81- export type CredentialChangeListener = ( user : User ) => void ;
83+ export type CredentialChangeListener = ( user : User ) => Promise < void > ;
8284
8385/**
8486 * Provides methods for getting the uid and token for the current user and
@@ -98,8 +100,13 @@ export interface CredentialsProvider {
98100 * Specifies a listener to be notified of credential changes
99101 * (sign-in / sign-out, token changes). It is immediately called once with the
100102 * initial user.
103+ *
104+ * The change listener is invoked on the provided AsyncQueue.
101105 */
102- setChangeListener ( changeListener : CredentialChangeListener ) : void ;
106+ setChangeListener (
107+ asyncQueue : AsyncQueue ,
108+ changeListener : CredentialChangeListener
109+ ) : void ;
103110
104111 /** Removes the previously-set change listener. */
105112 removeChangeListener ( ) : void ;
@@ -120,14 +127,17 @@ export class EmptyCredentialsProvider implements CredentialsProvider {
120127
121128 invalidateToken ( ) : void { }
122129
123- setChangeListener ( changeListener : CredentialChangeListener ) : void {
130+ setChangeListener (
131+ asyncQueue : AsyncQueue ,
132+ changeListener : CredentialChangeListener
133+ ) : void {
124134 debugAssert (
125135 ! this . changeListener ,
126136 'Can only call setChangeListener() once.'
127137 ) ;
128138 this . changeListener = changeListener ;
129139 // Fire with initial user.
130- changeListener ( User . UNAUTHENTICATED ) ;
140+ asyncQueue . enqueueRetryable ( ( ) => changeListener ( User . FIRST_PARTY ) ) ;
131141 }
132142
133143 removeChangeListener ( ) : void {
@@ -155,14 +165,17 @@ export class EmulatorCredentialsProvider implements CredentialsProvider {
155165
156166 invalidateToken ( ) : void { }
157167
158- setChangeListener ( changeListener : CredentialChangeListener ) : void {
168+ setChangeListener (
169+ asyncQueue : AsyncQueue ,
170+ changeListener : CredentialChangeListener
171+ ) : void {
159172 debugAssert (
160173 ! this . changeListener ,
161174 'Can only call setChangeListener() once.'
162175 ) ;
163176 this . changeListener = changeListener ;
164177 // Fire with initial user.
165- changeListener ( this . token . user ) ;
178+ asyncQueue . enqueueRetryable ( ( ) => changeListener ( this . token . user ) ) ;
166179 }
167180
168181 removeChangeListener ( ) : void {
@@ -175,11 +188,20 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
175188 * The auth token listener registered with FirebaseApp, retained here so we
176189 * can unregister it.
177190 */
178- private tokenListener : ( ( token : string | null ) => void ) | null = null ;
191+ private tokenListener : ( ) => void ;
179192
180193 /** Tracks the current User. */
181194 private currentUser : User = User . UNAUTHENTICATED ;
182- private receivedInitialUser : boolean = false ;
195+
196+ /**
197+ * Promise that allows blocking on the next `tokenChange` event. The Promise
198+ * is reassigned in `awaitTokenAndRaiseInitialEvent()` to allow blocking on
199+ * an a lazily loaded Auth instance. In this case, `this.receivedUser`
200+ * resolves once when the SDK first detects that there is no synchronous
201+ * Auth initialization, and then gets re-created and resolves again once Auth
202+ * is initialized.
203+ */
204+ private receivedUser = new Deferred ( ) ;
183205
184206 /**
185207 * Counter used to detect if the token changed while a getToken request was
@@ -188,44 +210,55 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
188210 private tokenCounter = 0 ;
189211
190212 /** The listener registered with setChangeListener(). */
191- private changeListener : CredentialChangeListener | null = null ;
213+ private changeListener : CredentialChangeListener = ( ) => Promise . resolve ( ) ;
214+
215+ private invokeChangeListener = false ;
192216
193217 private forceRefresh = false ;
194218
195- private auth : FirebaseAuthInternal | null ;
219+ private auth : FirebaseAuthInternal | null = null ;
220+
221+ private asyncQueue : AsyncQueue | null = null ;
196222
197223 constructor ( authProvider : Provider < FirebaseAuthInternalName > ) {
198224 this . tokenListener = ( ) => {
199225 this . tokenCounter ++ ;
226+ this . receivedUser . resolve ( ) ;
200227 this . currentUser = this . getUser ( ) ;
201- this . receivedInitialUser = true ;
202- if ( this . changeListener ) {
203- this . changeListener ( this . currentUser ) ;
228+ if ( this . invokeChangeListener ) {
229+ this . asyncQueue ! . enqueueRetryable ( ( ) =>
230+ this . changeListener ( this . currentUser )
231+ ) ;
204232 }
205233 } ;
206234
207- this . tokenCounter = 0 ;
208-
209- this . auth = authProvider . getImmediate ( { optional : true } ) ;
235+ const registerAuth = ( auth : FirebaseAuthInternal ) : void => {
236+ logDebug ( 'FirebaseCredentialsProvider' , 'Auth detected' ) ;
237+ this . auth = auth ;
238+ this . awaitTokenAndRaiseInitialEvent ( ) ;
239+ this . auth . addAuthTokenListener ( this . tokenListener ) ;
240+ } ;
210241
211- if ( this . auth ) {
212- this . auth . addAuthTokenListener ( this . tokenListener ! ) ;
213- } else {
214- // if auth is not available, invoke tokenListener once with null token
215- this . tokenListener ( null ) ;
216- authProvider . get ( ) . then (
217- auth => {
218- this . auth = auth ;
219- if ( this . tokenListener ) {
220- // tokenListener can be removed by removeChangeListener()
221- this . auth . addAuthTokenListener ( this . tokenListener ) ;
222- }
223- } ,
224- ( ) => {
225- /* this.authProvider.get() never rejects */
242+ authProvider . onInit ( auth => registerAuth ( auth ) ) ;
243+
244+ // Our users can initialize Auth right after Firestore, so we give it
245+ // a chance to register itself with the component framework before we
246+ // determine whether to start up in unauthenticated mode.
247+ setTimeout ( ( ) => {
248+ if ( ! this . auth ) {
249+ const auth = authProvider . getImmediate ( { optional : true } ) ;
250+ if ( auth ) {
251+ registerAuth ( auth ) ;
252+ } else if ( this . invokeChangeListener ) {
253+ // If auth is still not available, invoke tokenListener once with null
254+ // token
255+ logDebug ( 'FirebaseCredentialsProvider' , 'Auth not yet detected' ) ;
256+ this . asyncQueue ! . enqueueRetryable ( ( ) =>
257+ this . changeListener ( this . currentUser )
258+ ) ;
226259 }
227- ) ;
228- }
260+ }
261+ } , 0 ) ;
229262 }
230263
231264 getToken ( ) : Promise < Token | null > {
@@ -273,25 +306,21 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
273306 this . forceRefresh = true ;
274307 }
275308
276- setChangeListener ( changeListener : CredentialChangeListener ) : void {
277- debugAssert (
278- ! this . changeListener ,
279- 'Can only call setChangeListener() once.'
280- ) ;
309+ setChangeListener (
310+ asyncQueue : AsyncQueue ,
311+ changeListener : CredentialChangeListener
312+ ) : void {
313+ debugAssert ( ! this . asyncQueue , 'Can only call setChangeListener() once.' ) ;
314+ this . invokeChangeListener = true ;
315+ this . asyncQueue = asyncQueue ;
281316 this . changeListener = changeListener ;
282-
283- // Fire the initial event
284- if ( this . receivedInitialUser ) {
285- changeListener ( this . currentUser ) ;
286- }
287317 }
288318
289319 removeChangeListener ( ) : void {
290320 if ( this . auth ) {
291321 this . auth . removeAuthTokenListener ( this . tokenListener ! ) ;
292322 }
293- this . tokenListener = null ;
294- this . changeListener = null ;
323+ this . changeListener = ( ) => Promise . resolve ( ) ;
295324 }
296325
297326 // Auth.getUid() can return null even with a user logged in. It is because
@@ -306,6 +335,27 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
306335 ) ;
307336 return new User ( currentUid ) ;
308337 }
338+
339+ /**
340+ * Blocks the AsyncQueue until the next user is available. This is invoked
341+ * on SDK start to wait for the first user token (or `null` if Auth is not yet
342+ * loaded). If Auth is loaded after Firestore,
343+ * `awaitTokenAndRaiseInitialEvent()` is also used to block Firestore until
344+ * Auth is fully initialized.
345+ *
346+ * This function also invokes the change listener immediately after the token
347+ * is available.
348+ */
349+ private awaitTokenAndRaiseInitialEvent ( ) : void {
350+ if ( this . invokeChangeListener ) {
351+ this . invokeChangeListener = false ; // Prevent double-firing of the listener
352+ this . asyncQueue ! . enqueueRetryable ( async ( ) => {
353+ await this . receivedUser . promise ;
354+ await this . changeListener ( this . currentUser ) ;
355+ this . invokeChangeListener = true ;
356+ } ) ;
357+ }
358+ }
309359}
310360
311361// Manual type definition for the subset of Gapi we use.
@@ -368,9 +418,12 @@ export class FirstPartyCredentialsProvider implements CredentialsProvider {
368418 ) ;
369419 }
370420
371- setChangeListener ( changeListener : CredentialChangeListener ) : void {
421+ setChangeListener (
422+ asyncQueue : AsyncQueue ,
423+ changeListener : CredentialChangeListener
424+ ) : void {
372425 // Fire with initial uid.
373- changeListener ( User . FIRST_PARTY ) ;
426+ asyncQueue . enqueueRetryable ( ( ) => changeListener ( User . FIRST_PARTY ) ) ;
374427 }
375428
376429 removeChangeListener ( ) : void { }
0 commit comments