Skip to content

Commit 6e61787

Browse files
committed
Get rid of duplicate listener calls
1 parent c26ce0b commit 6e61787

File tree

3 files changed

+52
-35
lines changed

3 files changed

+52
-35
lines changed

packages/app-check/src/api.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import {
3232
getToken as getTokenInternal,
3333
addTokenListener,
3434
removeTokenListener,
35-
isValid
35+
isValid,
36+
notifyTokenListeners
3637
} from './internal-api';
3738
import { readTokenFromStorage } from './storage';
3839
import { getDebugToken, initializeDebugMode, isDebugMode } from './debug';
@@ -129,6 +130,9 @@ function _activate(
129130
newState.cachedTokenPromise = readTokenFromStorage(app).then(cachedToken => {
130131
if (cachedToken && isValid(cachedToken)) {
131132
setState(app, { ...getState(app), token: cachedToken });
133+
console.log('notifying token listeners');
134+
// notify all listeners with the cached token
135+
notifyTokenListeners(app, { token: cachedToken.token });
132136
}
133137
return cachedToken;
134138
});

packages/app-check/src/internal-api.ts

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,6 @@ export async function getToken(
8181
const cachedToken = await state.cachedTokenPromise;
8282
if (cachedToken && isValid(cachedToken)) {
8383
token = cachedToken;
84-
85-
setState(app, { ...state, token });
86-
// notify all listeners with the cached token
87-
notifyTokenListeners(app, { token: token.token });
8884
}
8985
}
9086

@@ -95,16 +91,28 @@ export async function getToken(
9591
};
9692
}
9793

94+
let waitedForInFlightRequest = false;
95+
9896
/**
9997
* DEBUG MODE
10098
* If debug mode is set, and there is no cached token, fetch a new App
10199
* Check token using the debug token, and return it directly.
102100
*/
103101
if (isDebugMode()) {
104-
const tokenFromDebugExchange: AppCheckTokenInternal = await exchangeToken(
105-
getExchangeDebugTokenRequest(app, await getDebugToken()),
106-
appCheck.platformLoggerProvider
107-
);
102+
// Avoid making another call to the exchange endpoint if one is in flight.
103+
if (!state.exchangeTokenPromise) {
104+
state.exchangeTokenPromise = exchangeToken(
105+
getExchangeDebugTokenRequest(app, await getDebugToken()),
106+
appCheck.platformLoggerProvider
107+
).then(token => {
108+
state.exchangeTokenPromise = undefined;
109+
return token;
110+
});
111+
} else {
112+
waitedForInFlightRequest = true;
113+
}
114+
const tokenFromDebugExchange: AppCheckTokenInternal =
115+
await state.exchangeTokenPromise;
108116
// Write debug token to indexedDB.
109117
await writeTokenToStorage(app, tokenFromDebugExchange);
110118
// Write debug token to state.
@@ -116,10 +124,19 @@ export async function getToken(
116124
* request a new token
117125
*/
118126
try {
119-
// state.provider is populated in initializeAppCheck()
120-
// ensureActivated() at the top of this function checks that
121-
// initializeAppCheck() has been called.
122-
token = await state.provider!.getToken();
127+
// Avoid making another call to the exchange endpoint if one is in flight.
128+
if (!state.exchangeTokenPromise) {
129+
// state.provider is populated in initializeAppCheck()
130+
// ensureActivated() at the top of this function checks that
131+
// initializeAppCheck() has been called.
132+
state.exchangeTokenPromise = state.provider!.getToken().then(token => {
133+
state.exchangeTokenPromise = undefined;
134+
return token;
135+
});
136+
} else {
137+
waitedForInFlightRequest = true;
138+
}
139+
token = await state.exchangeTokenPromise;
123140
} catch (e) {
124141
if ((e as FirebaseError).code === AppCheckError.THROTTLED) {
125142
// Warn if throttled, but do not treat it as an error.
@@ -147,7 +164,9 @@ export async function getToken(
147164
await writeTokenToStorage(app, token);
148165
}
149166

150-
notifyTokenListeners(app, interopTokenResult);
167+
if (!waitedForInFlightRequest) {
168+
notifyTokenListeners(app, interopTokenResult);
169+
}
151170
return interopTokenResult;
152171
}
153172

@@ -169,8 +188,6 @@ export function addTokenListener(
169188
tokenObservers: [...state.tokenObservers, tokenObserver]
170189
};
171190

172-
let cacheCheckPromise = Promise.resolve();
173-
174191
// Invoke the listener async immediately if there is a valid token
175192
// in memory.
176193
if (state.token && isValid(state.token)) {
@@ -180,26 +197,21 @@ export function addTokenListener(
180197
.catch(() => {
181198
/* we don't care about exceptions thrown in listeners */
182199
});
183-
} else if (state.token == null) {
184-
// Only check cache if there was no token. If the token was invalid,
185-
// skip this and rely on exchange endpoint.
186-
cacheCheckPromise = state
187-
.cachedTokenPromise! // Storage token promise. Always populated in `activate()`.
188-
.then(cachedToken => {
189-
if (cachedToken && isValid(cachedToken)) {
190-
listener({ token: cachedToken.token });
191-
}
192-
})
193-
.catch(() => {
194-
/** Ignore errors in listeners. */
195-
});
196200
}
197201

198-
// Wait for any cached token promise to resolve before starting the token
199-
// refresher. The refresher checks to see if there is an existing token
200-
// in state and calls the exchange endpoint if not. We should first let the
201-
// IndexedDB check have a chance to populate state if it can.
202-
void cacheCheckPromise.then(() => {
202+
/**
203+
* Wait for any cached token promise to resolve before starting the token
204+
* refresher. The refresher checks to see if there is an existing token
205+
* in state and calls the exchange endpoint if not. We should first let the
206+
* IndexedDB check have a chance to populate state if it can.
207+
*
208+
* We want to call the listener if the cached token check returns something
209+
* but cachedTokenPromise handler already will notify all listeners on the
210+
* first fetch, and we don't want duplicate calls to the listener.
211+
*/
212+
213+
// state.cachedTokenPromise is always populated in `activate()`.
214+
void state.cachedTokenPromise!.then(() => {
203215
if (!newState.tokenRefresher) {
204216
const tokenRefresher = createTokenRefresher(appCheck);
205217
newState.tokenRefresher = tokenRefresher;
@@ -295,7 +307,7 @@ function createTokenRefresher(appCheck: AppCheckService): Refresher {
295307
);
296308
}
297309

298-
function notifyTokenListeners(
310+
export function notifyTokenListeners(
299311
app: FirebaseApp,
300312
token: AppCheckTokenResult
301313
): void {

packages/app-check/src/state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface AppCheckState {
3030
provider?: AppCheckProvider;
3131
token?: AppCheckTokenInternal;
3232
cachedTokenPromise?: Promise<AppCheckTokenInternal | undefined>;
33+
exchangeTokenPromise?: Promise<AppCheckTokenInternal>;
3334
tokenRefresher?: Refresher;
3435
reCAPTCHAState?: ReCAPTCHAState;
3536
isTokenAutoRefreshEnabled?: boolean;

0 commit comments

Comments
 (0)