16
16
*/
17
17
18
18
import { AppOptions , app } from './firebase-namespace-api' ;
19
- import { credential , GoogleOAuthAccessToken } from './credential/index' ;
19
+ import { credential } from './credential/index' ;
20
20
import { getApplicationDefault } from './credential/credential-internal' ;
21
21
import * as validator from './utils/validator' ;
22
22
import { deepCopy } from './utils/deep-copy' ;
@@ -39,6 +39,8 @@ import { RemoteConfig } from './remote-config/remote-config';
39
39
import Credential = credential . Credential ;
40
40
import Database = database . Database ;
41
41
42
+ const TOKEN_EXPIRY_THRESHOLD_MILLIS = 5 * 60 * 1000 ;
43
+
42
44
/**
43
45
* Type representing a callback which is called every time an app lifecycle event occurs.
44
46
*/
@@ -57,129 +59,80 @@ export interface FirebaseAccessToken {
57
59
* Internals of a FirebaseApp instance.
58
60
*/
59
61
export class FirebaseAppInternals {
60
- private isDeleted_ = false ;
61
62
private cachedToken_ : FirebaseAccessToken ;
62
- private cachedTokenPromise_ : Promise < FirebaseAccessToken > | null ;
63
63
private tokenListeners_ : Array < ( token : string ) => void > ;
64
- private tokenRefreshTimeout_ : NodeJS . Timer ;
65
64
66
65
constructor ( private credential_ : Credential ) {
67
66
this . tokenListeners_ = [ ] ;
68
67
}
69
68
70
- /**
71
- * Gets an auth token for the associated app.
72
- *
73
- * @param {boolean } forceRefresh Whether or not to force a token refresh.
74
- * @return {Promise<FirebaseAccessToken> } A Promise that will be fulfilled with the current or
75
- * new token.
76
- */
77
- public getToken ( forceRefresh ?: boolean ) : Promise < FirebaseAccessToken > {
78
- const expired = this . cachedToken_ && this . cachedToken_ . expirationTime < Date . now ( ) ;
79
- if ( this . cachedTokenPromise_ && ! forceRefresh && ! expired ) {
80
- return this . cachedTokenPromise_
81
- . catch ( ( error ) => {
82
- // Update the cached token promise to avoid caching errors. Set it to resolve with the
83
- // cached token if we have one (and return that promise since the token has still not
84
- // expired).
85
- if ( this . cachedToken_ ) {
86
- this . cachedTokenPromise_ = Promise . resolve ( this . cachedToken_ ) ;
87
- return this . cachedTokenPromise_ ;
88
- }
89
-
90
- // Otherwise, set the cached token promise to null so that it will force a refresh next
91
- // time getToken() is called.
92
- this . cachedTokenPromise_ = null ;
93
-
94
- // And re-throw the caught error.
95
- throw error ;
96
- } ) ;
97
- } else {
98
- // Clear the outstanding token refresh timeout. This is a noop if the timeout is undefined.
99
- clearTimeout ( this . tokenRefreshTimeout_ ) ;
100
-
101
- // this.credential_ may be an external class; resolving it in a promise helps us
102
- // protect against exceptions and upgrades the result to a promise in all cases.
103
- this . cachedTokenPromise_ = Promise . resolve ( this . credential_ . getAccessToken ( ) )
104
- . then ( ( result : GoogleOAuthAccessToken ) => {
105
- // Since the developer can provide the credential implementation, we want to weakly verify
106
- // the return type until the type is properly exported.
107
- if ( ! validator . isNonNullObject ( result ) ||
108
- typeof result . expires_in !== 'number' ||
109
- typeof result . access_token !== 'string' ) {
110
- throw new FirebaseAppError (
111
- AppErrorCodes . INVALID_CREDENTIAL ,
112
- `Invalid access token generated: "${ JSON . stringify ( result ) } ". Valid access ` +
113
- 'tokens must be an object with the "expires_in" (number) and "access_token" ' +
114
- '(string) properties.' ,
115
- ) ;
116
- }
117
-
118
- const token : FirebaseAccessToken = {
119
- accessToken : result . access_token ,
120
- expirationTime : Date . now ( ) + ( result . expires_in * 1000 ) ,
121
- } ;
122
-
123
- const hasAccessTokenChanged = ( this . cachedToken_ && this . cachedToken_ . accessToken !== token . accessToken ) ;
124
- const hasExpirationChanged = ( this . cachedToken_ && this . cachedToken_ . expirationTime !== token . expirationTime ) ;
125
- if ( ! this . cachedToken_ || hasAccessTokenChanged || hasExpirationChanged ) {
126
- this . cachedToken_ = token ;
127
- this . tokenListeners_ . forEach ( ( listener ) => {
128
- listener ( token . accessToken ) ;
129
- } ) ;
130
- }
131
-
132
- // Establish a timeout to proactively refresh the token every minute starting at five
133
- // minutes before it expires. Once a token refresh succeeds, no further retries are
134
- // needed; if it fails, retry every minute until the token expires (resulting in a total
135
- // of four retries: at 4, 3, 2, and 1 minutes).
136
- let refreshTimeInSeconds = ( result . expires_in - ( 5 * 60 ) ) ;
137
- let numRetries = 4 ;
138
-
139
- // In the rare cases the token is short-lived (that is, it expires in less than five
140
- // minutes from when it was fetched), establish the timeout to refresh it after the
141
- // current minute ends and update the number of retries that should be attempted before
142
- // the token expires.
143
- if ( refreshTimeInSeconds <= 0 ) {
144
- refreshTimeInSeconds = result . expires_in % 60 ;
145
- numRetries = Math . floor ( result . expires_in / 60 ) - 1 ;
146
- }
147
-
148
- // The token refresh timeout keeps the Node.js process alive, so only create it if this
149
- // instance has not already been deleted.
150
- if ( numRetries && ! this . isDeleted_ ) {
151
- this . setTokenRefreshTimeout ( refreshTimeInSeconds * 1000 , numRetries ) ;
152
- }
153
-
154
- return token ;
155
- } )
156
- . catch ( ( error ) => {
157
- let errorMessage = ( typeof error === 'string' ) ? error : error . message ;
158
-
159
- errorMessage = 'Credential implementation provided to initializeApp() via the ' +
160
- '"credential" property failed to fetch a valid Google OAuth2 access token with the ' +
161
- `following error: "${ errorMessage } ".` ;
162
-
163
- if ( errorMessage . indexOf ( 'invalid_grant' ) !== - 1 ) {
164
- errorMessage += ' There are two likely causes: (1) your server time is not properly ' +
165
- 'synced or (2) your certificate key file has been revoked. To solve (1), re-sync the ' +
166
- 'time on your server. To solve (2), make sure the key ID for your key file is still ' +
167
- 'present at https://console.firebase.google.com/iam-admin/serviceaccounts/project. If ' +
168
- 'not, generate a new key file at ' +
169
- 'https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk.' ;
170
- }
171
-
172
- throw new FirebaseAppError ( AppErrorCodes . INVALID_CREDENTIAL , errorMessage ) ;
173
- } ) ;
174
-
175
- return this . cachedTokenPromise_ ;
69
+ public getToken ( forceRefresh = false ) : Promise < FirebaseAccessToken > {
70
+ if ( forceRefresh || this . shouldRefresh ( ) ) {
71
+ return this . refreshToken ( ) ;
176
72
}
73
+
74
+ return Promise . resolve ( this . cachedToken_ ) ;
75
+ }
76
+
77
+ private refreshToken ( ) : Promise < FirebaseAccessToken > {
78
+ return Promise . resolve ( this . credential_ . getAccessToken ( ) )
79
+ . then ( ( result ) => {
80
+ // Since the developer can provide the credential implementation, we want to weakly verify
81
+ // the return type until the type is properly exported.
82
+ if ( ! validator . isNonNullObject ( result ) ||
83
+ typeof result . expires_in !== 'number' ||
84
+ typeof result . access_token !== 'string' ) {
85
+ throw new FirebaseAppError (
86
+ AppErrorCodes . INVALID_CREDENTIAL ,
87
+ `Invalid access token generated: "${ JSON . stringify ( result ) } ". Valid access ` +
88
+ 'tokens must be an object with the "expires_in" (number) and "access_token" ' +
89
+ '(string) properties.' ,
90
+ ) ;
91
+ }
92
+
93
+ const token = {
94
+ accessToken : result . access_token ,
95
+ expirationTime : Date . now ( ) + ( result . expires_in * 1000 ) ,
96
+ } ;
97
+ if ( ! this . cachedToken_
98
+ || this . cachedToken_ . accessToken !== token . accessToken
99
+ || this . cachedToken_ . expirationTime !== token . expirationTime ) {
100
+ this . cachedToken_ = token ;
101
+ this . tokenListeners_ . forEach ( ( listener ) => {
102
+ listener ( token . accessToken ) ;
103
+ } ) ;
104
+ }
105
+
106
+ return token ;
107
+ } )
108
+ . catch ( ( error ) => {
109
+ let errorMessage = ( typeof error === 'string' ) ? error : error . message ;
110
+
111
+ errorMessage = 'Credential implementation provided to initializeApp() via the ' +
112
+ '"credential" property failed to fetch a valid Google OAuth2 access token with the ' +
113
+ `following error: "${ errorMessage } ".` ;
114
+
115
+ if ( errorMessage . indexOf ( 'invalid_grant' ) !== - 1 ) {
116
+ errorMessage += ' There are two likely causes: (1) your server time is not properly ' +
117
+ 'synced or (2) your certificate key file has been revoked. To solve (1), re-sync the ' +
118
+ 'time on your server. To solve (2), make sure the key ID for your key file is still ' +
119
+ 'present at https://console.firebase.google.com/iam-admin/serviceaccounts/project. If ' +
120
+ 'not, generate a new key file at ' +
121
+ 'https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk.' ;
122
+ }
123
+
124
+ throw new FirebaseAppError ( AppErrorCodes . INVALID_CREDENTIAL , errorMessage ) ;
125
+ } ) ;
126
+ }
127
+
128
+ private shouldRefresh ( ) : boolean {
129
+ return ! this . cachedToken_ || ( this . cachedToken_ . expirationTime - Date . now ( ) ) <= TOKEN_EXPIRY_THRESHOLD_MILLIS ;
177
130
}
178
131
179
132
/**
180
133
* Adds a listener that is called each time a token changes.
181
134
*
182
- * @param { function(string) } listener The listener that will be called with each new token.
135
+ * @param listener The listener that will be called with each new token.
183
136
*/
184
137
public addAuthTokenListener ( listener : ( token : string ) => void ) : void {
185
138
this . tokenListeners_ . push ( listener ) ;
@@ -191,42 +144,11 @@ export class FirebaseAppInternals {
191
144
/**
192
145
* Removes a token listener.
193
146
*
194
- * @param { function(string) } listener The listener to remove.
147
+ * @param listener The listener to remove.
195
148
*/
196
149
public removeAuthTokenListener ( listener : ( token : string ) => void ) : void {
197
150
this . tokenListeners_ = this . tokenListeners_ . filter ( ( other ) => other !== listener ) ;
198
151
}
199
-
200
- /**
201
- * Deletes the FirebaseAppInternals instance.
202
- */
203
- public delete ( ) : void {
204
- this . isDeleted_ = true ;
205
-
206
- // Clear the token refresh timeout so it doesn't keep the Node.js process alive.
207
- clearTimeout ( this . tokenRefreshTimeout_ ) ;
208
- }
209
-
210
- /**
211
- * Establishes timeout to refresh the Google OAuth2 access token used by the SDK.
212
- *
213
- * @param {number } delayInMilliseconds The delay to use for the timeout.
214
- * @param {number } numRetries The number of times to retry fetching a new token if the prior fetch
215
- * failed.
216
- */
217
- private setTokenRefreshTimeout ( delayInMilliseconds : number , numRetries : number ) : void {
218
- this . tokenRefreshTimeout_ = setTimeout ( ( ) => {
219
- this . getToken ( /* forceRefresh */ true )
220
- . catch ( ( ) => {
221
- // Ignore the error since this might just be an intermittent failure. If we really cannot
222
- // refresh the token, an error will be logged once the existing token expires and we try
223
- // to fetch a fresh one.
224
- if ( numRetries > 0 ) {
225
- this . setTokenRefreshTimeout ( 60 * 1000 , numRetries - 1 ) ;
226
- }
227
- } ) ;
228
- } , delayInMilliseconds ) ;
229
- }
230
152
}
231
153
232
154
/**
@@ -419,8 +341,6 @@ export class FirebaseApp implements app.App {
419
341
this . checkDestroyed_ ( ) ;
420
342
this . firebaseInternals_ . removeApp ( this . name_ ) ;
421
343
422
- this . INTERNAL . delete ( ) ;
423
-
424
344
return Promise . all ( Object . keys ( this . services_ ) . map ( ( serviceName ) => {
425
345
const service = this . services_ [ serviceName ] ;
426
346
if ( isStateful ( service ) ) {
0 commit comments