Skip to content

Commit cd668c3

Browse files
Always block on Auth
1 parent b03423b commit cd668c3

File tree

9 files changed

+126
-104
lines changed

9 files changed

+126
-104
lines changed

packages/firestore-compat/src/index.console.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
FirestoreError
2424
} from '@firebase/firestore';
2525

26+
import { EmptyCredentialsProvider } from './src/api/credentials';
2627
import {
2728
Firestore as FirestoreCompat,
2829
MemoryPersistenceProvider
@@ -55,7 +56,7 @@ export class Firestore extends FirestoreCompat {
5556
databaseIdFromFirestoreDatabase(firestoreDatabase),
5657
new FirestoreExp(
5758
databaseIdFromFirestoreDatabase(firestoreDatabase),
58-
authProvider
59+
new EmptyCredentialsProvider()
5960
),
6061
new MemoryPersistenceProvider()
6162
);

packages/firestore/lite/register.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { Component, ComponentType } from '@firebase/component';
2424

2525
import { version } from '../package.json';
26+
import { LiteCredentialsProvider } from '../src/api/credentials';
2627
import { setSDKVersion } from '../src/core/version';
2728
import { Firestore } from '../src/lite-api/database';
2829
import { FirestoreSettings } from '../src/lite-api/settings';
@@ -42,7 +43,7 @@ export function registerFirestore(): void {
4243
const app = container.getProvider('app-exp').getImmediate()!;
4344
const firestoreInstance = new Firestore(
4445
app,
45-
container.getProvider('auth-internal')
46+
new LiteCredentialsProvider(container.getProvider('auth-internal'))
4647
);
4748
if (settings) {
4849
firestoreInstance._setSettings(settings);

packages/firestore/src/api/credentials.ts

+105-73
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ export type CredentialChangeListener = (user: User) => Promise<void>;
8787
* listening for changes.
8888
*/
8989
export interface CredentialsProvider {
90+
/**
91+
* Starts the credentials provider and specifies a listener to be notified of
92+
* credential changes (sign-in / sign-out, token changes). It is immediately
93+
* called once with the initial user.
94+
*
95+
* The change listener is invoked on the provided AsyncQueue.
96+
*/
97+
start(asyncQueue: AsyncQueue, changeListener: CredentialChangeListener): void;
98+
9099
/** Requests a token for the current user. */
91100
getToken(): Promise<Token | null>;
92101

@@ -96,53 +105,26 @@ export interface CredentialsProvider {
96105
*/
97106
invalidateToken(): void;
98107

99-
/**
100-
* Specifies a listener to be notified of credential changes
101-
* (sign-in / sign-out, token changes). It is immediately called once with the
102-
* initial user.
103-
*
104-
* The change listener is invoked on the provided AsyncQueue.
105-
*/
106-
setChangeListener(
107-
asyncQueue: AsyncQueue,
108-
changeListener: CredentialChangeListener
109-
): void;
110-
111-
/** Removes the previously-set change listener. */
112-
removeChangeListener(): void;
108+
shutdown(): void;
113109
}
114110

115111
/** A CredentialsProvider that always yields an empty token. */
116112
export class EmptyCredentialsProvider implements CredentialsProvider {
117-
/**
118-
* Stores the listener registered with setChangeListener()
119-
* This isn't actually necessary since the UID never changes, but we use this
120-
* to verify the listen contract is adhered to in tests.
121-
*/
122-
private changeListener: CredentialChangeListener | null = null;
123-
124113
getToken(): Promise<Token | null> {
125114
return Promise.resolve<Token | null>(null);
126115
}
127116

128117
invalidateToken(): void {}
129118

130-
setChangeListener(
119+
start(
131120
asyncQueue: AsyncQueue,
132121
changeListener: CredentialChangeListener
133122
): void {
134-
debugAssert(
135-
!this.changeListener,
136-
'Can only call setChangeListener() once.'
137-
);
138-
this.changeListener = changeListener;
139123
// Fire with initial user.
140124
asyncQueue.enqueueRetryable(() => changeListener(User.UNAUTHENTICATED));
141125
}
142126

143-
removeChangeListener(): void {
144-
this.changeListener = null;
145-
}
127+
shutdown(): void {}
146128
}
147129

148130
/**
@@ -165,7 +147,7 @@ export class EmulatorCredentialsProvider implements CredentialsProvider {
165147

166148
invalidateToken(): void {}
167149

168-
setChangeListener(
150+
start(
169151
asyncQueue: AsyncQueue,
170152
changeListener: CredentialChangeListener
171153
): void {
@@ -178,80 +160,146 @@ export class EmulatorCredentialsProvider implements CredentialsProvider {
178160
asyncQueue.enqueueRetryable(() => changeListener(this.token.user));
179161
}
180162

181-
removeChangeListener(): void {
163+
shutdown(): void {
182164
this.changeListener = null;
183165
}
184166
}
185167

168+
/** Credential provider for the Lite SDK. */
169+
export class LiteCredentialsProvider implements CredentialsProvider {
170+
private auth: FirebaseAuthInternal | null = null;
171+
172+
constructor(authProvider: Provider<FirebaseAuthInternalName>) {
173+
authProvider.onInit(auth => {
174+
this.auth = auth;
175+
});
176+
}
177+
178+
getToken(): Promise<Token | null> {
179+
if (!this.auth) {
180+
return Promise.resolve(null);
181+
}
182+
183+
return this.auth.getToken().then(tokenData => {
184+
if (tokenData) {
185+
hardAssert(
186+
typeof tokenData.accessToken === 'string',
187+
'Invalid tokenData returned from getToken():' + tokenData
188+
);
189+
return new OAuthToken(
190+
tokenData.accessToken,
191+
new User(this.auth!.getUid())
192+
);
193+
} else {
194+
return null;
195+
}
196+
});
197+
}
198+
199+
invalidateToken(): void {}
200+
201+
start(
202+
asyncQueue: AsyncQueue,
203+
changeListener: CredentialChangeListener
204+
): void {}
205+
206+
shutdown(): void {}
207+
}
208+
186209
export class FirebaseCredentialsProvider implements CredentialsProvider {
187210
/**
188211
* The auth token listener registered with FirebaseApp, retained here so we
189212
* can unregister it.
190213
*/
191-
private tokenListener: () => void;
214+
private tokenListener!: () => void;
192215

193216
/** Tracks the current User. */
194217
private currentUser: User = User.UNAUTHENTICATED;
195218

196-
/** Promise that allows blocking on the initialization of Firebase Auth. */
197-
private authDeferred = new Deferred();
198-
199219
/**
200220
* Counter used to detect if the token changed while a getToken request was
201221
* outstanding.
202222
*/
203223
private tokenCounter = 0;
204224

205-
/** The listener registered with setChangeListener(). */
206-
private changeListener?: CredentialChangeListener;
207-
208225
private forceRefresh = false;
209226

210227
private auth: FirebaseAuthInternal | null = null;
211228

212-
private asyncQueue: AsyncQueue | null = null;
229+
constructor(private authProvider: Provider<FirebaseAuthInternalName>) {}
230+
231+
start(
232+
asyncQueue: AsyncQueue,
233+
changeListener: CredentialChangeListener
234+
): void {
235+
let lastTokenId = -1;
236+
237+
// A change listener that prevents double-firing for the same token change.
238+
const guardedChangeListener: (user: User) => Promise<void> = user => {
239+
if (this.tokenCounter !== lastTokenId) {
240+
lastTokenId = this.tokenCounter;
241+
return changeListener(user);
242+
} else {
243+
return Promise.resolve();
244+
}
245+
};
246+
247+
// A promise that can be waited on to block on the next token change.
248+
// This promise is re-created after each change.
249+
let nextToken = new Deferred<void>();
213250

214-
constructor(authProvider: Provider<FirebaseAuthInternalName>) {
215251
this.tokenListener = () => {
216252
this.tokenCounter++;
217253
this.currentUser = this.getUser();
218-
this.authDeferred.resolve();
219-
if (this.changeListener) {
220-
this.asyncQueue!.enqueueRetryable(() =>
221-
this.changeListener!(this.currentUser)
222-
);
223-
}
254+
nextToken.resolve();
255+
nextToken = new Deferred<void>();
256+
asyncQueue.enqueueRetryable(() =>
257+
guardedChangeListener(this.currentUser)
258+
);
224259
};
225260

226261
const registerAuth = (auth: FirebaseAuthInternal): void => {
227-
logDebug('FirebaseCredentialsProvider', 'Auth detected');
228-
this.auth = auth;
229-
this.auth.addAuthTokenListener(this.tokenListener);
262+
asyncQueue.enqueueRetryable(async () => {
263+
logDebug('FirebaseCredentialsProvider', 'Auth detected');
264+
this.auth = auth;
265+
this.auth.addAuthTokenListener(this.tokenListener);
266+
267+
// Call the change listener inline to block on the user change.
268+
await nextToken.promise;
269+
await guardedChangeListener(this.currentUser);
270+
});
230271
};
231272

232-
authProvider.onInit(auth => registerAuth(auth));
273+
this.authProvider.onInit(auth => registerAuth(auth));
233274

234275
// Our users can initialize Auth right after Firestore, so we give it
235276
// a chance to register itself with the component framework before we
236277
// determine whether to start up in unauthenticated mode.
237278
setTimeout(() => {
238279
if (!this.auth) {
239-
const auth = authProvider.getImmediate({ optional: true });
280+
const auth = this.authProvider.getImmediate({ optional: true });
240281
if (auth) {
241282
registerAuth(auth);
242283
} else {
243284
// If auth is still not available, proceed with `null` user
244285
logDebug('FirebaseCredentialsProvider', 'Auth not yet detected');
245-
this.authDeferred.resolve();
286+
nextToken.resolve();
287+
nextToken = new Deferred<void>();
246288
}
247289
}
248290
}, 0);
291+
292+
asyncQueue.enqueueRetryable(async () => {
293+
// Call the change listener inline to block on the user change.
294+
await nextToken.promise;
295+
await guardedChangeListener(this.currentUser);
296+
});
249297
}
250298

251299
getToken(): Promise<Token | null> {
252300
debugAssert(
253301
this.tokenListener != null,
254-
'getToken cannot be called after listener removed.'
302+
'FirebaseCredentialsProvider not started.'
255303
);
256304

257305
// Take note of the current value of the tokenCounter so that this method
@@ -293,26 +341,10 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
293341
this.forceRefresh = true;
294342
}
295343

296-
setChangeListener(
297-
asyncQueue: AsyncQueue,
298-
changeListener: CredentialChangeListener
299-
): void {
300-
debugAssert(!this.asyncQueue, 'Can only call setChangeListener() once.');
301-
this.asyncQueue = asyncQueue;
302-
303-
// Blocks the AsyncQueue until the next user is available.
304-
this.asyncQueue!.enqueueRetryable(async () => {
305-
await this.authDeferred.promise;
306-
await changeListener(this.currentUser);
307-
this.changeListener = changeListener;
308-
});
309-
}
310-
311-
removeChangeListener(): void {
344+
shutdown(): void {
312345
if (this.auth) {
313346
this.auth.removeAuthTokenListener(this.tokenListener!);
314347
}
315-
this.changeListener = () => Promise.resolve();
316348
}
317349

318350
// Auth.getUid() can return null even with a user logged in. It is because
@@ -389,15 +421,15 @@ export class FirstPartyCredentialsProvider implements CredentialsProvider {
389421
);
390422
}
391423

392-
setChangeListener(
424+
start(
393425
asyncQueue: AsyncQueue,
394426
changeListener: CredentialChangeListener
395427
): void {
396428
// Fire with initial uid.
397429
asyncQueue.enqueueRetryable(() => changeListener(User.FIRST_PARTY));
398430
}
399431

400-
removeChangeListener(): void {}
432+
shutdown(): void {}
401433

402434
invalidateToken(): void {}
403435
}

packages/firestore/src/api/database.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import {
2222
FirebaseApp,
2323
getApp
2424
} from '@firebase/app-exp';
25-
import { FirebaseAuthInternalName } from '@firebase/auth-interop-types';
26-
import { Provider } from '@firebase/component';
2725
import { deepEqual } from '@firebase/util';
2826

2927
import {
@@ -60,6 +58,7 @@ import { cast } from '../util/input_validation';
6058
import { Deferred } from '../util/promise';
6159

6260
import { LoadBundleTask } from './bundle';
61+
import {CredentialsProvider} from "./credentials";
6362
import { PersistenceSettings, FirestoreSettings } from './settings';
6463
export {
6564
connectFirestoreEmulator,
@@ -103,9 +102,9 @@ export class Firestore extends LiteFirestore {
103102
/** @hideconstructor */
104103
constructor(
105104
databaseIdOrApp: DatabaseId | FirebaseApp,
106-
authProvider: Provider<FirebaseAuthInternalName>
105+
credentialsProvider: CredentialsProvider
107106
) {
108-
super(databaseIdOrApp, authProvider);
107+
super(databaseIdOrApp, credentialsProvider);
109108
this._persistenceKey =
110109
'name' in databaseIdOrApp ? databaseIdOrApp.name : '[DEFAULT]';
111110
}

packages/firestore/src/core/firestore_client.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export class FirestoreClient {
116116
public asyncQueue: AsyncQueue,
117117
private databaseInfo: DatabaseInfo
118118
) {
119-
this.credentials.setChangeListener(asyncQueue, async user => {
119+
this.credentials.start(asyncQueue, async user => {
120120
logDebug(LOG_TAG, 'Received user=', user.uid);
121121
await this.credentialListener(user);
122122
this.user = user;
@@ -163,10 +163,10 @@ export class FirestoreClient {
163163
await this.offlineComponents.terminate();
164164
}
165165

166-
// `removeChangeListener` must be called after shutting down the
167-
// RemoteStore as it will prevent the RemoteStore from retrieving
168-
// auth tokens.
169-
this.credentials.removeChangeListener();
166+
// The credentials provider must be terminated after shutting down the
167+
// RemoteStore as it will prevent the RemoteStore from retrieving auth
168+
// tokens.
169+
this.credentials.shutdown();
170170
deferred.resolve();
171171
} catch (e) {
172172
const firestoreError = wrapInUserErrorIfRecoverable(

0 commit comments

Comments
 (0)