@@ -37,9 +37,11 @@ export type FirebaseAccessToken = {
3737 * Internals of a FirebaseApp instance.
3838 */
3939export class FirebaseAppInternals {
40+ private isDeleted_ = false ;
4041 private cachedToken_ : FirebaseAccessToken ;
4142 private cachedTokenPromise_ : Promise < FirebaseAccessToken > ;
4243 private tokenListeners_ : Array < ( token : string ) => void > ;
44+ private tokenRefreshTimeout_ : NodeJS . Timer ;
4345
4446 constructor ( private credential_ : Credential ) {
4547 this . tokenListeners_ = [ ] ;
@@ -54,8 +56,27 @@ export class FirebaseAppInternals {
5456 public getToken ( forceRefresh ?: boolean ) : Promise < FirebaseAccessToken > {
5557 const expired = this . cachedToken_ && this . cachedToken_ . expirationTime < Date . now ( ) ;
5658 if ( this . cachedTokenPromise_ && ! forceRefresh && ! expired ) {
57- return this . cachedTokenPromise_ ;
59+ return this . cachedTokenPromise_
60+ . catch ( ( error ) => {
61+ // Update the cached token promise to avoid caching errors. Set it to resolve with the
62+ // cached token if we have one (and return that promise since the token has still not
63+ // expired).
64+ if ( this . cachedToken_ ) {
65+ this . cachedTokenPromise_ = Promise . resolve ( this . cachedToken_ ) ;
66+ return this . cachedTokenPromise_ ;
67+ }
68+
69+ // Otherwise, set the cached token promise to null so that it will force a refresh next
70+ // time getToken() is called.
71+ this . cachedTokenPromise_ = null ;
72+
73+ // And re-throw the caught error.
74+ throw error ;
75+ } ) ;
5876 } else {
77+ // Clear the outstanding token refresh timeout. This is a noop if the timeout is undefined.
78+ clearTimeout ( this . tokenRefreshTimeout_ ) ;
79+
5980 // this.credential_ may be an external class; resolving it in a promise helps us
6081 // protect against exceptions and upgrades the result to a promise in all cases.
6182 this . cachedTokenPromise_ = Promise . resolve ( this . credential_ . getAccessToken ( ) )
@@ -87,17 +108,31 @@ export class FirebaseAppInternals {
87108 } ) ;
88109 }
89110
111+ // Establish a timeout to proactively refresh the token every minute starting at five
112+ // minutes before it expires. Once a token refresh succeeds, no further retries are
113+ // needed; if it fails, retry every minute until the token expires (resulting in a total
114+ // of four retries: at 4, 3, 2, and 1 minutes).
115+ let refreshTimeInSeconds = ( result . expires_in - ( 5 * 60 ) ) ;
116+ let numRetries = 4 ;
117+
118+ // In the rare cases the token is short-lived (that is, it expires in less than five
119+ // minutes from when it was fetched), establish the timeout to refresh it after the
120+ // current minute ends and update the number of retries that should be attempted before
121+ // the token expires.
122+ if ( refreshTimeInSeconds <= 0 ) {
123+ refreshTimeInSeconds = result . expires_in % 60 ;
124+ numRetries = Math . floor ( result . expires_in / 60 ) - 1 ;
125+ }
126+
127+ // The token refresh timeout keeps the Node.js process alive, so only create it if this
128+ // instance has not already been deleted.
129+ if ( numRetries && ! this . isDeleted_ ) {
130+ this . setTokenRefreshTimeout ( refreshTimeInSeconds * 1000 , numRetries ) ;
131+ }
132+
90133 return token ;
91134 } )
92135 . catch ( ( error ) => {
93- // Update the cached token promise to avoid caching errors. Set it to resolve with the
94- // cached token if we have one; otherwise, set it to null.
95- if ( this . cachedToken_ ) {
96- this . cachedTokenPromise_ = Promise . resolve ( this . cachedToken_ ) ;
97- } else {
98- this . cachedTokenPromise_ = null ;
99- }
100-
101136 let errorMessage = ( typeof error === 'string' ) ? error : error . message ;
102137
103138 errorMessage = 'Credential implementation provided to initializeApp() via the ' +
@@ -140,6 +175,37 @@ export class FirebaseAppInternals {
140175 public removeAuthTokenListener ( listener : ( token : string ) => void ) {
141176 this . tokenListeners_ = this . tokenListeners_ . filter ( ( other ) => other !== listener ) ;
142177 }
178+
179+ /**
180+ * Deletes the FirebaseAppInternals instance.
181+ */
182+ public delete ( ) : void {
183+ this . isDeleted_ = true ;
184+
185+ // Clear the token refresh timeout so it doesn't keep the Node.js process alive.
186+ clearTimeout ( this . tokenRefreshTimeout_ ) ;
187+ }
188+
189+ /**
190+ * Establishes timeout to refresh the Google OAuth2 access token used by the SDK.
191+ *
192+ * @param {number } delayInMilliseconds The delay to use for the timeout.
193+ * @param {number } numRetries The number of times to retry fetching a new token if the prior fetch
194+ * failed.
195+ */
196+ private setTokenRefreshTimeout ( delayInMilliseconds : number , numRetries : number ) : void {
197+ this . tokenRefreshTimeout_ = setTimeout ( ( ) => {
198+ this . getToken ( /* forceRefresh */ true )
199+ . catch ( ( error ) => {
200+ // Ignore the error since this might just be an intermittent failure. If we really cannot
201+ // refresh the token, an error will be logged once the existing token expires and we try
202+ // to fetch a fresh one.
203+ if ( numRetries > 0 ) {
204+ this . setTokenRefreshTimeout ( 60 * 1000 , numRetries - 1 ) ;
205+ }
206+ } ) ;
207+ } , delayInMilliseconds ) ;
208+ }
143209}
144210
145211
@@ -278,6 +344,8 @@ export class FirebaseApp {
278344 this . checkDestroyed_ ( ) ;
279345 this . firebaseInternals_ . removeApp ( this . name_ ) ;
280346
347+ this . INTERNAL . delete ( ) ;
348+
281349 return Promise . all ( Object . keys ( this . services_ ) . map ( ( serviceName ) => {
282350 return this . services_ [ serviceName ] . INTERNAL . delete ( ) ;
283351 } ) ) . then ( ( ) => {
0 commit comments