Skip to content

Commit 4ec654d

Browse files
authored
firebase-ios-sdk/1499: Restart streams on any token change. (#1120)
* firebase-ios-sdk/1499: Restart streams on any token change. See firebase/firebase-ios-sdk#1499 This reworks our UserListener into a CredentialChangeListener which fires on any token change, even if the User did not change. This allows us to restart our streams (but not switch mutation queues, etc.) on token changes.
1 parent fc37f4c commit 4ec654d

File tree

5 files changed

+79
-92
lines changed

5 files changed

+79
-92
lines changed

packages/firestore/src/api/credentials.ts

Lines changed: 50 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { User } from '../auth/user';
18-
import { assert, fail } from '../util/assert';
18+
import { assert } from '../util/assert';
1919
import { Code, FirestoreError } from '../util/error';
2020
import { FirebaseApp } from '@firebase/app-types';
2121
import { _FirebaseApp } from '@firebase/app-types/private';
@@ -63,9 +63,11 @@ export class OAuthToken implements Token {
6363
}
6464

6565
/**
66-
* A Listener for user change events.
66+
* A Listener for credential change events. The listener should fetch a new
67+
* token and may need to invalidate other state if the current user has also
68+
* changed.
6769
*/
68-
export type UserListener = (user: User) => void;
70+
export type CredentialChangeListener = (user: User) => void;
6971

7072
/**
7173
* Provides methods for getting the uid and token for the current user and
@@ -82,23 +84,24 @@ export interface CredentialsProvider {
8284
invalidateToken(): void;
8385

8486
/**
85-
* Specifies a listener to be notified of user changes (sign-in / sign-out).
86-
* It immediately called once with the initial user.
87+
* Specifies a listener to be notified of credential changes
88+
* (sign-in / sign-out, token changes). It is immediately called once with the
89+
* initial user.
8790
*/
88-
setUserChangeListener(listener: UserListener): void;
91+
setChangeListener(changeListener: CredentialChangeListener): void;
8992

90-
/** Removes the previously-set user change listener. */
91-
removeUserChangeListener(): void;
93+
/** Removes the previously-set change listener. */
94+
removeChangeListener(): void;
9295
}
9396

9497
/** A CredentialsProvider that always yields an empty token. */
9598
export class EmptyCredentialsProvider implements CredentialsProvider {
9699
/**
97-
* Stores the User listener registered with setUserChangeListener()
100+
* Stores the listener registered with setChangeListener()
98101
* This isn't actually necessary since the UID never changes, but we use this
99102
* to verify the listen contract is adhered to in tests.
100103
*/
101-
private userListener: UserListener | null = null;
104+
private changeListener: CredentialChangeListener | null = null;
102105

103106
constructor() {}
104107

@@ -108,19 +111,19 @@ export class EmptyCredentialsProvider implements CredentialsProvider {
108111

109112
invalidateToken(): void {}
110113

111-
setUserChangeListener(listener: UserListener): void {
112-
assert(!this.userListener, 'Can only call setUserChangeListener() once.');
113-
this.userListener = listener;
114+
setChangeListener(changeListener: CredentialChangeListener): void {
115+
assert(!this.changeListener, 'Can only call setChangeListener() once.');
116+
this.changeListener = changeListener;
114117
// Fire with initial user.
115-
listener(User.UNAUTHENTICATED);
118+
changeListener(User.UNAUTHENTICATED);
116119
}
117120

118-
removeUserChangeListener(): void {
121+
removeChangeListener(): void {
119122
assert(
120-
this.userListener !== null,
121-
'removeUserChangeListener() when no listener registered'
123+
this.changeListener !== null,
124+
'removeChangeListener() when no listener registered'
122125
);
123-
this.userListener = null;
126+
this.changeListener = null;
124127
}
125128
}
126129

@@ -135,31 +138,26 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
135138
private currentUser: User;
136139

137140
/**
138-
* Counter used to detect if the user changed while a getToken request was
141+
* Counter used to detect if the token changed while a getToken request was
139142
* outstanding.
140143
*/
141-
private userCounter = 0;
144+
private tokenCounter = 0;
142145

143-
/** The User listener registered with setUserChangeListener(). */
144-
private userListener: UserListener | null = null;
146+
/** The listener registered with setChangeListener(). */
147+
private changeListener: CredentialChangeListener | null = null;
145148

146149
private forceRefresh = false;
147150

148151
constructor(private readonly app: FirebaseApp) {
149-
// We listen for token changes but all we really care about is knowing when
150-
// the uid may have changed.
151152
this.tokenListener = () => {
152-
const newUser = this.getUser();
153-
if (!this.currentUser || !newUser.isEqual(this.currentUser)) {
154-
this.currentUser = newUser;
155-
this.userCounter++;
156-
if (this.userListener) {
157-
this.userListener(this.currentUser);
158-
}
153+
this.tokenCounter++;
154+
this.currentUser = this.getUser();
155+
if (this.changeListener) {
156+
this.changeListener(this.currentUser);
159157
}
160158
};
161159

162-
this.userCounter = 0;
160+
this.tokenCounter = 0;
163161

164162
// Will fire at least once where we set this.currentUser
165163
(this.app as _FirebaseApp).INTERNAL.addAuthTokenListener(
@@ -173,21 +171,21 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
173171
'getToken cannot be called after listener removed.'
174172
);
175173

176-
// Take note of the current value of the userCounter so that this method can
177-
// fail (with an ABORTED error) if there is a user change while the request
178-
// is outstanding.
179-
const initialUserCounter = this.userCounter;
174+
// Take note of the current value of the tokenCounter so that this method
175+
// can fail (with an ABORTED error) if there is a token change while the
176+
// request is outstanding.
177+
const initialTokenCounter = this.tokenCounter;
180178
const forceRefresh = this.forceRefresh;
181179
this.forceRefresh = false;
182180
return (this.app as _FirebaseApp).INTERNAL.getToken(forceRefresh).then(
183181
tokenData => {
184-
// Cancel the request since the user changed while the request was
185-
// outstanding so the response is likely for a previous user (which
182+
// Cancel the request since the token changed while the request was
183+
// outstanding so the response is potentially for a previous user (which
186184
// user, we can't be sure).
187-
if (this.userCounter !== initialUserCounter) {
185+
if (this.tokenCounter !== initialTokenCounter) {
188186
throw new FirestoreError(
189187
Code.ABORTED,
190-
'getToken aborted due to uid change.'
188+
'getToken aborted due to token change.'
191189
);
192190
} else {
193191
if (tokenData) {
@@ -208,40 +206,30 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
208206
this.forceRefresh = true;
209207
}
210208

211-
setUserChangeListener(listener: UserListener): void {
212-
assert(!this.userListener, 'Can only call setUserChangeListener() once.');
213-
this.userListener = listener;
209+
setChangeListener(changeListener: CredentialChangeListener): void {
210+
assert(!this.changeListener, 'Can only call setChangeListener() once.');
211+
this.changeListener = changeListener;
214212

215213
// Fire the initial event, but only if we received the initial user
216214
if (this.currentUser) {
217-
listener(this.currentUser);
215+
changeListener(this.currentUser);
218216
}
219217
}
220218

221-
removeUserChangeListener(): void {
222-
assert(
223-
this.tokenListener != null,
224-
'removeUserChangeListener() called twice'
225-
);
219+
removeChangeListener(): void {
220+
assert(this.tokenListener != null, 'removeChangeListener() called twice');
226221
assert(
227-
this.userListener !== null,
228-
'removeUserChangeListener() called when no listener registered'
222+
this.changeListener !== null,
223+
'removeChangeListener() called when no listener registered'
229224
);
230225
(this.app as _FirebaseApp).INTERNAL.removeAuthTokenListener(
231226
this.tokenListener!
232227
);
233228
this.tokenListener = null;
234-
this.userListener = null;
229+
this.changeListener = null;
235230
}
236231

237232
private getUser(): User {
238-
// TODO(mikelehen): Remove this check once we're shipping with firebase.js.
239-
if (typeof (this.app as _FirebaseApp).INTERNAL.getUid !== 'function') {
240-
fail(
241-
'This version of the Firestore SDK requires at least version' +
242-
' 3.7.0 of firebase.js.'
243-
);
244-
}
245233
const currentUid = (this.app as _FirebaseApp).INTERNAL.getUid();
246234
assert(
247235
currentUid === null || typeof currentUid === 'string',
@@ -304,12 +292,12 @@ export class FirstPartyCredentialsProvider implements CredentialsProvider {
304292

305293
// TODO(33108925): can someone switch users w/o a page refresh?
306294
// TODO(33110621): need to understand token/session lifecycle
307-
setUserChangeListener(listener: UserListener): void {
295+
setChangeListener(changeListener: CredentialChangeListener): void {
308296
// Fire with initial uid.
309-
listener(User.FIRST_PARTY);
297+
changeListener(User.FIRST_PARTY);
310298
}
311299

312-
removeUserChangeListener(): void {}
300+
removeChangeListener(): void {}
313301

314302
invalidateToken(): void {}
315303
}

packages/firestore/src/core/firestore_client.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,9 @@ export class FirestoreClient {
146146
*/
147147
start(persistenceSettings: PersistenceSettings): Promise<void> {
148148
// We defer our initialization until we get the current user from
149-
// setUserChangeListener(). We block the async queue until we got the
150-
// initial user and the initialization is completed. This will prevent
151-
// any scheduled work from happening before initialization is completed.
149+
// setChangeListener(). We block the async queue until we got the initial
150+
// user and the initialization is completed. This will prevent any scheduled
151+
// work from happening before initialization is completed.
152152
//
153153
// If initializationDone resolved then the FirestoreClient is in a usable
154154
// state.
@@ -163,7 +163,7 @@ export class FirestoreClient {
163163
const persistenceResult = new Deferred<void>();
164164

165165
let initialized = false;
166-
this.credentials.setUserChangeListener(user => {
166+
this.credentials.setChangeListener(user => {
167167
if (!initialized) {
168168
initialized = true;
169169

@@ -172,7 +172,7 @@ export class FirestoreClient {
172172
.then(initializationDone.resolve, initializationDone.reject);
173173
} else {
174174
this.asyncQueue.enqueueAndForget(() => {
175-
return this.handleUserChange(user);
175+
return this.handleCredentialChange(user);
176176
});
177177
}
178178
});
@@ -353,6 +353,7 @@ export class FirestoreClient {
353353
* implementation is available in this.persistence.
354354
*/
355355
private initializeRest(user: User): Promise<void> {
356+
debug(LOG_TAG, 'Initializing. user=', user.uid);
356357
return this.platform
357358
.loadConnection(this.databaseInfo)
358359
.then(async connection => {
@@ -420,11 +421,11 @@ export class FirestoreClient {
420421
});
421422
}
422423

423-
private handleUserChange(user: User): Promise<void> {
424+
private handleCredentialChange(user: User): Promise<void> {
424425
this.asyncQueue.verifyOperationInProgress();
425426

426-
debug(LOG_TAG, 'User Changed: ' + user.uid);
427-
return this.syncEngine.handleUserChange(user);
427+
debug(LOG_TAG, 'Credential Changed. Current user: ' + user.uid);
428+
return this.syncEngine.handleCredentialChange(user);
428429
}
429430

430431
/** Disables the network connection. Pending operations will not complete. */
@@ -445,10 +446,10 @@ export class FirestoreClient {
445446
options && options.purgePersistenceWithDataLoss
446447
);
447448

448-
// `removeUserChangeListener` must be called after shutting down the
449+
// `removeChangeListener` must be called after shutting down the
449450
// RemoteStore as it will prevent the RemoteStore from retrieving
450451
// auth tokens.
451-
this.credentials.removeUserChangeListener();
452+
this.credentials.removeChangeListener();
452453
});
453454
}
454455

packages/firestore/src/core/sync_engine.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -847,22 +847,22 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
847847
);
848848
}
849849

850-
handleUserChange(user: User): Promise<void> {
850+
async handleCredentialChange(user: User): Promise<void> {
851+
const userChanged = !this.currentUser.isEqual(user);
851852
this.currentUser = user;
852-
return this.localStore
853-
.handleUserChange(user)
854-
.then(result => {
855-
// TODO(multitab): Consider calling this only in the primary tab.
856-
this.sharedClientState.handleUserChange(
857-
user,
858-
result.removedBatchIds,
859-
result.addedBatchIds
860-
);
861-
return this.emitNewSnapsAndNotifyLocalStore(result.affectedDocuments);
862-
})
863-
.then(() => {
864-
return this.remoteStore.handleUserChange(user);
865-
});
853+
854+
if (userChanged) {
855+
const result = await this.localStore.handleUserChange(user);
856+
// TODO(multitab): Consider calling this only in the primary tab.
857+
this.sharedClientState.handleUserChange(
858+
user,
859+
result.removedBatchIds,
860+
result.addedBatchIds
861+
);
862+
await this.emitNewSnapsAndNotifyLocalStore(result.affectedDocuments);
863+
}
864+
865+
await this.remoteStore.handleCredentialChange();
866866
}
867867

868868
// PORTING NOTE: Multi-tab only

packages/firestore/src/remote/remote_store.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { User } from '../auth/user';
1817
import { SnapshotVersion } from '../core/snapshot_version';
1918
import { Transaction } from '../core/transaction';
2019
import { OnlineState, TargetId } from '../core/types';
@@ -682,13 +681,12 @@ export class RemoteStore implements TargetMetadataProvider {
682681
return new Transaction(this.datastore);
683682
}
684683

685-
async handleUserChange(user: User): Promise<void> {
686-
log.debug(LOG_TAG, 'RemoteStore changing users: uid=', user.uid);
687-
684+
async handleCredentialChange(): Promise<void> {
688685
if (this.canUseNetwork()) {
689686
// Tear down and re-create our network streams. This will ensure we get a fresh auth token
690687
// for the new user and re-fill the write pipeline with new mutations from the LocalStore
691688
// (since mutations are per-user).
689+
log.debug(LOG_TAG, 'RemoteStore restarting streams for new credential');
692690
this.networkEnabled = false;
693691
await this.disableNetworkInternal();
694692
this.onlineStateTracker.set(OnlineState.Unknown);

packages/firestore/test/unit/specs/spec_test_runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,7 @@ abstract class TestRunner {
897897
private doChangeUser(user: string | null): Promise<void> {
898898
this.user = new User(user);
899899
return this.queue.enqueue(() =>
900-
this.syncEngine.handleUserChange(this.user)
900+
this.syncEngine.handleCredentialChange(this.user)
901901
);
902902
}
903903

0 commit comments

Comments
 (0)