@@ -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,20 @@ 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 ( ( ) => {
141+ changeListener ( User . FIRST_PARTY ) ;
142+ return Promise . resolve ( ) ;
143+ } ) ;
131144 }
132145
133146 removeChangeListener ( ) : void {
@@ -175,11 +188,25 @@ 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 ) | null = null ;
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 re-assgined 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, and then gets re-created and resolves again once Auth is loaded.
202+ */
203+ private receivedUser = new Deferred ( ) ;
204+
205+ /**
206+ * Whether the initial token event has been raised. This can go back to
207+ * `false` if Firestore first starts without Auth and Auth is loaded later.
208+ */
209+ private initialEventRaised : boolean = false ;
183210
184211 /**
185212 * Counter used to detect if the token changed while a getToken request was
@@ -188,44 +215,62 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
188215 private tokenCounter = 0 ;
189216
190217 /** The listener registered with setChangeListener(). */
191- private changeListener : CredentialChangeListener | null = null ;
218+ private changeListener : CredentialChangeListener = ( ) => Promise . resolve ( ) ;
192219
193220 private forceRefresh = false ;
194221
195- private auth : FirebaseAuthInternal | null ;
222+ private auth : FirebaseAuthInternal | null = null ;
223+
224+ private asyncQueue : AsyncQueue | null = null ;
196225
197226 constructor ( authProvider : Provider < FirebaseAuthInternalName > ) {
198227 this . tokenListener = ( ) => {
199228 this . tokenCounter ++ ;
229+ this . receivedUser . resolve ( ) ;
200230 this . currentUser = this . getUser ( ) ;
201- this . receivedInitialUser = true ;
202- if ( this . changeListener ) {
203- this . changeListener ( this . currentUser ) ;
231+ if ( this . initialEventRaised && this . asyncQueue ) {
232+ // We only invoke the change listener here if the initial event has been
233+ // raised. The initial event itself is invoked synchronously in
234+ // `awaitTokenAndRaiseInitialEvent()`.
235+ this . asyncQueue . enqueueRetryable ( ( ) => {
236+ this . changeListener ( this . currentUser ) ;
237+ return Promise . resolve ( ) ;
238+ } ) ;
204239 }
205240 } ;
206241
207242 this . tokenCounter = 0 ;
208243
209- this . auth = authProvider . getImmediate ( { optional : true } ) ;
244+ const registerAuth = ( auth : FirebaseAuthInternal ) : void => {
245+ logDebug ( 'FirebaseCredentialsProvider' , 'Auth detected' ) ;
246+ this . auth = auth ;
247+ if ( this . tokenListener ) {
248+ // tokenListener can be removed by removeChangeListener()
249+ this . awaitTokenAndRaiseInitialEvent ( ) ;
250+ this . auth . addAuthTokenListener ( this . tokenListener ) ;
251+ }
252+ } ;
210253
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 */
254+ authProvider . onInit ( auth => registerAuth ( auth ) ) ;
255+
256+ // Our users can initialize Auth right after Firestore, so we give it
257+ // a chance to register itself with the component framework before we
258+ // determine whether to start up in unauthenticated mode.
259+ setTimeout ( ( ) => {
260+ if ( ! this . auth ) {
261+ const auth = authProvider . getImmediate ( { optional : true } ) ;
262+ if ( auth ) {
263+ registerAuth ( auth ) ;
264+ } else if ( this . tokenListener ) {
265+ // If auth is still not available, invoke tokenListener once with null
266+ // token
267+ logDebug ( 'FirebaseCredentialsProvider' , 'Auth not yet detected' ) ;
268+ this . tokenListener ( ) ;
226269 }
227- ) ;
228- }
270+ }
271+ } , 0 ) ;
272+
273+ this . awaitTokenAndRaiseInitialEvent ( ) ;
229274 }
230275
231276 getToken ( ) : Promise < Token | null > {
@@ -273,25 +318,21 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
273318 this . forceRefresh = true ;
274319 }
275320
276- setChangeListener ( changeListener : CredentialChangeListener ) : void {
277- debugAssert (
278- ! this . changeListener ,
279- 'Can only call setChangeListener() once.'
280- ) ;
321+ setChangeListener (
322+ asyncQueue : AsyncQueue ,
323+ changeListener : CredentialChangeListener
324+ ) : void {
325+ debugAssert ( ! this . asyncQueue , 'Can only call setChangeListener() once.' ) ;
326+ this . asyncQueue = asyncQueue ;
281327 this . changeListener = changeListener ;
282-
283- // Fire the initial event
284- if ( this . receivedInitialUser ) {
285- changeListener ( this . currentUser ) ;
286- }
287328 }
288329
289330 removeChangeListener ( ) : void {
290331 if ( this . auth ) {
291332 this . auth . removeAuthTokenListener ( this . tokenListener ! ) ;
292333 }
293334 this . tokenListener = null ;
294- this . changeListener = null ;
335+ this . changeListener = ( ) => Promise . resolve ( ) ;
295336 }
296337
297338 // Auth.getUid() can return null even with a user logged in. It is because
@@ -306,6 +347,33 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
306347 ) ;
307348 return new User ( currentUid ) ;
308349 }
350+
351+ /**
352+ * Blocks the AsyncQueue until the next user is available. This is invoked
353+ * on SDK start to wait for the first user token (or `null` if Auth is not yet
354+ * loaded). If Auth is loaded after Firestore,
355+ * `awaitTokenAndRaiseInitialEvent()` is also used to block Firestore until
356+ * Auth is fully initialized.
357+ *
358+ * This function also invokes the change listener synchronously once a token
359+ * is available.
360+ */
361+ private awaitTokenAndRaiseInitialEvent ( ) : void {
362+ this . initialEventRaised = false ;
363+ if ( this . asyncQueue ) {
364+ // Create a new deferred Promise that gets resolved when we receive the
365+ // next token. Ensure that all previous Promises also get resolved.
366+ const awaitToken = new Deferred < void > ( ) ;
367+ void awaitToken . promise . then ( ( ) => awaitToken . resolve ( ) ) ;
368+ this . receivedUser = awaitToken ;
369+
370+ this . asyncQueue . enqueueRetryable ( async ( ) => {
371+ await awaitToken . promise ;
372+ await this . changeListener ( this . currentUser ) ;
373+ } ) ;
374+ }
375+ this . initialEventRaised = true ;
376+ }
309377}
310378
311379// Manual type definition for the subset of Gapi we use.
@@ -368,9 +436,15 @@ export class FirstPartyCredentialsProvider implements CredentialsProvider {
368436 ) ;
369437 }
370438
371- setChangeListener ( changeListener : CredentialChangeListener ) : void {
439+ setChangeListener (
440+ asyncQueue : AsyncQueue ,
441+ changeListener : CredentialChangeListener
442+ ) : void {
372443 // Fire with initial uid.
373- changeListener ( User . FIRST_PARTY ) ;
444+ asyncQueue . enqueueRetryable ( ( ) => {
445+ changeListener ( User . FIRST_PARTY ) ;
446+ return Promise . resolve ( ) ;
447+ } ) ;
374448 }
375449
376450 removeChangeListener ( ) : void { }
0 commit comments