diff --git a/src/app/firebase-app.ts b/src/app/firebase-app.ts index 4dd97cb33d..a0ee93e1fe 100644 --- a/src/app/firebase-app.ts +++ b/src/app/firebase-app.ts @@ -39,19 +39,21 @@ export interface FirebaseAccessToken { */ export class FirebaseAppInternals { private cachedToken_: FirebaseAccessToken; + private promiseToCachedToken_: Promise; private tokenListeners_: Array<(token: string) => void>; + private isRefreshing: boolean; // eslint-disable-next-line @typescript-eslint/naming-convention constructor(private credential_: Credential) { this.tokenListeners_ = []; + this.isRefreshing = false; } public getToken(forceRefresh = false): Promise { if (forceRefresh || this.shouldRefresh()) { - return this.refreshToken(); + this.promiseToCachedToken_ = this.refreshToken(); } - - return Promise.resolve(this.cachedToken_); + return this.promiseToCachedToken_ } public getCachedToken(): FirebaseAccessToken | null { @@ -59,6 +61,7 @@ export class FirebaseAppInternals { } private refreshToken(): Promise { + this.isRefreshing = true; return Promise.resolve(this.credential_.getAccessToken()) .then((result) => { // Since the developer can provide the credential implementation, we want to weakly verify @@ -108,11 +111,15 @@ export class FirebaseAppInternals { } throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage); - }); + }) + .finally(() => { + this.isRefreshing = false; + }) } private shouldRefresh(): boolean { - return !this.cachedToken_ || (this.cachedToken_.expirationTime - Date.now()) <= TOKEN_EXPIRY_THRESHOLD_MILLIS; + return (!this.cachedToken_ || (this.cachedToken_.expirationTime - Date.now()) <= TOKEN_EXPIRY_THRESHOLD_MILLIS) + && !this.isRefreshing; } /** diff --git a/test/integration/messaging.spec.ts b/test/integration/messaging.spec.ts index d11b035861..d564e06f66 100644 --- a/test/integration/messaging.spec.ts +++ b/test/integration/messaging.spec.ts @@ -278,7 +278,7 @@ describe('admin.messaging', () => { }); }); - xit('sendToDeviceGroup() returns a response with success count', () => { + it('sendToDeviceGroup() returns a response with success count', () => { return getMessaging().sendToDeviceGroup(notificationKey, payload, options) .then((response) => { expect(typeof response.successCount).to.equal('number'); diff --git a/test/unit/app/firebase-app.spec.ts b/test/unit/app/firebase-app.spec.ts index 2211f4f457..df4fc84a67 100644 --- a/test/unit/app/firebase-app.spec.ts +++ b/test/unit/app/firebase-app.spec.ts @@ -808,6 +808,16 @@ describe('FirebaseApp', () => { }); }); + it('only refreshes the token once for concurrent calls', () => { + const promise1 = mockApp.INTERNAL.getToken(); + const promise2 = mockApp.INTERNAL.getToken(); + expect(getTokenStub).to.have.been.calledOnce; + return Promise.all([promise1, promise2]).then((tokens) => { + expect(tokens[0]).to.equal(tokens[1]); + expect(getTokenStub).to.have.been.calledOnce; + }) + }); + it('Includes the original error in exception', () => { getTokenStub.restore(); const mockError = new FirebaseAppError(