@@ -81,10 +81,6 @@ export async function getToken(
81
81
const cachedToken = await state . cachedTokenPromise ;
82
82
if ( cachedToken && isValid ( cachedToken ) ) {
83
83
token = cachedToken ;
84
-
85
- setState ( app , { ...state , token } ) ;
86
- // notify all listeners with the cached token
87
- notifyTokenListeners ( app , { token : token . token } ) ;
88
84
}
89
85
}
90
86
@@ -95,16 +91,28 @@ export async function getToken(
95
91
} ;
96
92
}
97
93
94
+ let waitedForInFlightRequest = false ;
95
+
98
96
/**
99
97
* DEBUG MODE
100
98
* If debug mode is set, and there is no cached token, fetch a new App
101
99
* Check token using the debug token, and return it directly.
102
100
*/
103
101
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 ;
108
116
// Write debug token to indexedDB.
109
117
await writeTokenToStorage ( app , tokenFromDebugExchange ) ;
110
118
// Write debug token to state.
@@ -116,10 +124,19 @@ export async function getToken(
116
124
* request a new token
117
125
*/
118
126
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 ;
123
140
} catch ( e ) {
124
141
if ( ( e as FirebaseError ) . code === AppCheckError . THROTTLED ) {
125
142
// Warn if throttled, but do not treat it as an error.
@@ -147,7 +164,9 @@ export async function getToken(
147
164
await writeTokenToStorage ( app , token ) ;
148
165
}
149
166
150
- notifyTokenListeners ( app , interopTokenResult ) ;
167
+ if ( ! waitedForInFlightRequest ) {
168
+ notifyTokenListeners ( app , interopTokenResult ) ;
169
+ }
151
170
return interopTokenResult ;
152
171
}
153
172
@@ -169,8 +188,6 @@ export function addTokenListener(
169
188
tokenObservers : [ ...state . tokenObservers , tokenObserver ]
170
189
} ;
171
190
172
- let cacheCheckPromise = Promise . resolve ( ) ;
173
-
174
191
// Invoke the listener async immediately if there is a valid token
175
192
// in memory.
176
193
if ( state . token && isValid ( state . token ) ) {
@@ -180,26 +197,21 @@ export function addTokenListener(
180
197
. catch ( ( ) => {
181
198
/* we don't care about exceptions thrown in listeners */
182
199
} ) ;
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
- } ) ;
196
200
}
197
201
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 ( ( ) => {
203
215
if ( ! newState . tokenRefresher ) {
204
216
const tokenRefresher = createTokenRefresher ( appCheck ) ;
205
217
newState . tokenRefresher = tokenRefresher ;
@@ -295,7 +307,7 @@ function createTokenRefresher(appCheck: AppCheckService): Refresher {
295
307
) ;
296
308
}
297
309
298
- function notifyTokenListeners (
310
+ export function notifyTokenListeners (
299
311
app : FirebaseApp ,
300
312
token : AppCheckTokenResult
301
313
) : void {
0 commit comments