Skip to content

Defer check whether auth is available #4845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 5, 2021

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/firestore/.idea/runConfigurations/Unit_Tests.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

134 changes: 87 additions & 47 deletions packages/firestore/src/api/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import { Provider } from '@firebase/component';

import { User } from '../auth/user';
import { hardAssert, debugAssert } from '../util/assert';
import { AsyncQueue } from '../util/async_queue';
import { Code, FirestoreError } from '../util/error';
import { logDebug } from '../util/log';
import { Deferred } from '../util/promise';

// TODO(mikelehen): This should be split into multiple files and probably
// moved to an auth/ folder to match other platforms.
Expand Down Expand Up @@ -78,7 +80,7 @@ export class OAuthToken implements Token {
* token and may need to invalidate other state if the current user has also
* changed.
*/
export type CredentialChangeListener = (user: User) => void;
export type CredentialChangeListener = (user: User) => Promise<void>;

/**
* Provides methods for getting the uid and token for the current user and
Expand All @@ -98,8 +100,13 @@ export interface CredentialsProvider {
* Specifies a listener to be notified of credential changes
* (sign-in / sign-out, token changes). It is immediately called once with the
* initial user.
*
* The change listener is invoked on the provided AsyncQueue.
*/
setChangeListener(changeListener: CredentialChangeListener): void;
setChangeListener(
asyncQueue: AsyncQueue,
changeListener: CredentialChangeListener
): void;

/** Removes the previously-set change listener. */
removeChangeListener(): void;
Expand All @@ -120,14 +127,17 @@ export class EmptyCredentialsProvider implements CredentialsProvider {

invalidateToken(): void {}

setChangeListener(changeListener: CredentialChangeListener): void {
setChangeListener(
asyncQueue: AsyncQueue,
changeListener: CredentialChangeListener
): void {
debugAssert(
!this.changeListener,
'Can only call setChangeListener() once.'
);
this.changeListener = changeListener;
// Fire with initial user.
changeListener(User.UNAUTHENTICATED);
asyncQueue.enqueueRetryable(() => changeListener(User.UNAUTHENTICATED));
}

removeChangeListener(): void {
Expand Down Expand Up @@ -155,14 +165,17 @@ export class EmulatorCredentialsProvider implements CredentialsProvider {

invalidateToken(): void {}

setChangeListener(changeListener: CredentialChangeListener): void {
setChangeListener(
asyncQueue: AsyncQueue,
changeListener: CredentialChangeListener
): void {
debugAssert(
!this.changeListener,
'Can only call setChangeListener() once.'
);
this.changeListener = changeListener;
// Fire with initial user.
changeListener(this.token.user);
asyncQueue.enqueueRetryable(() => changeListener(this.token.user));
}

removeChangeListener(): void {
Expand All @@ -175,11 +188,13 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
* The auth token listener registered with FirebaseApp, retained here so we
* can unregister it.
*/
private tokenListener: ((token: string | null) => void) | null = null;
private tokenListener: () => void;

/** Tracks the current User. */
private currentUser: User = User.UNAUTHENTICATED;
private receivedInitialUser: boolean = false;

/** Promise that allows blocking on the first `tokenListener` event. */
private receivedInitialUser = new Deferred();

/**
* Counter used to detect if the token changed while a getToken request was
Expand All @@ -188,44 +203,55 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
private tokenCounter = 0;

/** The listener registered with setChangeListener(). */
private changeListener: CredentialChangeListener | null = null;
private changeListener: CredentialChangeListener = () => Promise.resolve();

private invokeChangeListener = false;

private forceRefresh = false;

private auth: FirebaseAuthInternal | null;
private auth: FirebaseAuthInternal | null = null;

private asyncQueue: AsyncQueue | null = null;

constructor(authProvider: Provider<FirebaseAuthInternalName>) {
this.tokenListener = () => {
this.tokenCounter++;
this.currentUser = this.getUser();
this.receivedInitialUser = true;
if (this.changeListener) {
this.changeListener(this.currentUser);
this.receivedInitialUser.resolve();
if (this.invokeChangeListener) {
this.asyncQueue!.enqueueRetryable(() =>
this.changeListener(this.currentUser)
);
}
};

this.tokenCounter = 0;

this.auth = authProvider.getImmediate({ optional: true });
const registerAuth = (auth: FirebaseAuthInternal): void => {
logDebug('FirebaseCredentialsProvider', 'Auth detected');
this.auth = auth;
this.awaitTokenAndRaiseInitialEvent();
this.auth.addAuthTokenListener(this.tokenListener);
};

if (this.auth) {
this.auth.addAuthTokenListener(this.tokenListener!);
} else {
// if auth is not available, invoke tokenListener once with null token
this.tokenListener(null);
authProvider.get().then(
auth => {
this.auth = auth;
if (this.tokenListener) {
// tokenListener can be removed by removeChangeListener()
this.auth.addAuthTokenListener(this.tokenListener);
}
},
() => {
/* this.authProvider.get() never rejects */
authProvider.onInit(auth => registerAuth(auth));

// Our users can initialize Auth right after Firestore, so we give it
// a chance to register itself with the component framework before we
// determine whether to start up in unauthenticated mode.
setTimeout(() => {
if (!this.auth) {
const auth = authProvider.getImmediate({ optional: true });
if (auth) {
registerAuth(auth);
} else if (this.invokeChangeListener) {
// If auth is still not available, invoke tokenListener once with null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment needs update.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

// token
logDebug('FirebaseCredentialsProvider', 'Auth not yet detected');
this.asyncQueue!.enqueueRetryable(() =>
this.changeListener(this.currentUser)
);
}
);
}
}
}, 0);
}

getToken(): Promise<Token | null> {
Expand Down Expand Up @@ -273,25 +299,21 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
this.forceRefresh = true;
}

setChangeListener(changeListener: CredentialChangeListener): void {
debugAssert(
!this.changeListener,
'Can only call setChangeListener() once.'
);
setChangeListener(
asyncQueue: AsyncQueue,
changeListener: CredentialChangeListener
): void {
debugAssert(!this.asyncQueue, 'Can only call setChangeListener() once.');
this.invokeChangeListener = true;
this.asyncQueue = asyncQueue;
this.changeListener = changeListener;

// Fire the initial event
if (this.receivedInitialUser) {
changeListener(this.currentUser);
}
}

removeChangeListener(): void {
if (this.auth) {
this.auth.removeAuthTokenListener(this.tokenListener!);
}
this.tokenListener = null;
this.changeListener = null;
this.changeListener = () => Promise.resolve();
}

// Auth.getUid() can return null even with a user logged in. It is because
Expand All @@ -306,6 +328,21 @@ export class FirebaseCredentialsProvider implements CredentialsProvider {
);
return new User(currentUid);
}

/**
* Blocks the AsyncQueue until the next user is available. This function also
* invokes `this.changeListener` immediately once the token is available.
*/
private awaitTokenAndRaiseInitialEvent(): void {
if (this.invokeChangeListener) {
this.invokeChangeListener = false; // Prevent double-firing of the listener
this.asyncQueue!.enqueueRetryable(async () => {
await this.receivedInitialUser.promise;
await this.changeListener(this.currentUser);
this.invokeChangeListener = true;
});
}
}
}

// Manual type definition for the subset of Gapi we use.
Expand Down Expand Up @@ -368,9 +405,12 @@ export class FirstPartyCredentialsProvider implements CredentialsProvider {
);
}

setChangeListener(changeListener: CredentialChangeListener): void {
setChangeListener(
asyncQueue: AsyncQueue,
changeListener: CredentialChangeListener
): void {
// Fire with initial uid.
changeListener(User.FIRST_PARTY);
asyncQueue.enqueueRetryable(() => changeListener(User.FIRST_PARTY));
}

removeChangeListener(): void {}
Expand Down
36 changes: 11 additions & 25 deletions packages/firestore/src/core/firestore_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ export const MAX_CONCURRENT_LIMBO_RESOLUTIONS = 100;
export class FirestoreClient {
private user = User.UNAUTHENTICATED;
private readonly clientId = AutoId.newId();
private credentialListener: CredentialChangeListener = () => {};
private readonly receivedInitialUser = new Deferred<void>();
private credentialListener: CredentialChangeListener = () =>
Promise.resolve();

offlineComponents?: OfflineComponentProvider;
onlineComponents?: OnlineComponentProvider;
Expand All @@ -116,17 +116,14 @@ export class FirestoreClient {
public asyncQueue: AsyncQueue,
private databaseInfo: DatabaseInfo
) {
this.credentials.setChangeListener(user => {
this.credentials.setChangeListener(asyncQueue, async user => {
logDebug(LOG_TAG, 'Received user=', user.uid);
await this.credentialListener(user);
this.user = user;
this.credentialListener(user);
this.receivedInitialUser.resolve();
});
}

async getConfiguration(): Promise<ComponentConfiguration> {
await this.receivedInitialUser.promise;

return {
asyncQueue: this.asyncQueue,
databaseInfo: this.databaseInfo,
Expand All @@ -137,12 +134,8 @@ export class FirestoreClient {
};
}

setCredentialChangeListener(listener: (user: User) => void): void {
setCredentialChangeListener(listener: (user: User) => Promise<void>): void {
this.credentialListener = listener;
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.receivedInitialUser.promise.then(() =>
this.credentialListener(this.user)
);
}

/**
Expand Down Expand Up @@ -198,15 +191,13 @@ export async function setOfflineComponentProvider(
await offlineComponentProvider.initialize(configuration);

let currentUser = configuration.initialUser;
client.setCredentialChangeListener(user => {
client.setCredentialChangeListener(async user => {
if (!currentUser.isEqual(user)) {
await localStoreHandleUserChange(
offlineComponentProvider.localStore,
user
);
currentUser = user;
client.asyncQueue.enqueueRetryable(async () => {
await localStoreHandleUserChange(
offlineComponentProvider.localStore,
user
);
});
}
});

Expand Down Expand Up @@ -236,12 +227,7 @@ export async function setOnlineComponentProvider(
// The CredentialChangeListener of the online component provider takes
// precedence over the offline component provider.
client.setCredentialChangeListener(user =>
client.asyncQueue.enqueueRetryable(() =>
remoteStoreHandleCredentialChange(
onlineComponentProvider.remoteStore,
user
)
)
remoteStoreHandleCredentialChange(onlineComponentProvider.remoteStore, user)
);
client.onlineComponents = onlineComponentProvider;
}
Expand Down
Loading