@@ -14,6 +14,10 @@ module.exports = class TokenManager {
1414 */
1515 constructor ( options ) {
1616 this . tokenInfo = { }
17+
18+ // while a token is being loaded, this promise will be defined, yet unsettled
19+ this . tokenLoadingPromise = undefined
20+
1721 this . iamUrl = options . iamUrl || 'https://iam.cloud.ibm.com/identity/token'
1822 if ( options . iamApikey ) {
1923 this . iamApikey = options . iamApikey
@@ -24,23 +28,48 @@ module.exports = class TokenManager {
2428 /**
2529 * This function sends an access token back through a Promise. The source of the token
2630 * is determined by the following logic:
27- * 1. If this class is managing tokens and does not yet have one, make a request for one
28- * 2. If this class is managing tokens and the token has expired, refresh it
29- * 3. If this class is managing tokens and has a valid token stored, send it
31+ * 1. If the token is expired (that is, we already have one, but it is no longer valid, or about to time out), we
32+ * load a new one
33+ * 2. If the token is not expired, we obviously have a valid token, so just resolve with it's value
34+ * 3. If we haven't got a token at all, but a loading is already in process, we wait for the loading promise to settle
35+ * and depending on the result
36+ * 3a) use the newly returned and cached token
37+ * 3b) in case of error, trigger a fresh loading attempt
38+ * 4. If there is no token available and also no loading in progress, trigger the token loading
3039 *
3140 * @returns {Promise } - resolved with token value
3241 */
3342 getToken ( ) {
3443 return new Promise ( ( resolve , reject ) => {
35- if ( ! this . tokenInfo . access_token || this . isTokenExpired ( ) ) {
36- // 1. request an initial token or 2. refresh an expired token
37- return this . requestToken ( ) . then ( tokenResponse => {
38- this . saveTokenInfo ( tokenResponse )
39- resolve ( this . tokenInfo . access_token )
40- } ) . catch ( error => reject ( error ) )
41- } else {
42- // 3. use valid managed token
44+ const loadToken = ( ) => {
45+ this . loadToken ( )
46+ . then ( ( ) => {
47+ resolve ( this . tokenInfo . access_token )
48+ } )
49+ . catch ( error => reject ( error ) )
50+ }
51+
52+ if ( this . isTokenExpired ( ) ) {
53+ // 1. load a new token
54+ loadToken ( )
55+ } else if ( this . tokenInfo . access_token ) {
56+ // 2. return the cached valid token
4357 resolve ( this . tokenInfo . access_token )
58+ } else if ( this . tokenLoadingPromise ) {
59+ // 3. a token loading operation is already running
60+ this . tokenLoadingPromise
61+ . then ( ( ) => {
62+ // 3a) it was successful, so return the fresh token
63+ resolve ( this . tokenInfo . access_token )
64+ } )
65+ . catch ( ( ) => {
66+ // 3b) give it one more try - obviously, we hoped for a Promise triggered by another invocation to
67+ // return the token for us, but it didn't work out. So we need to trigger another attempt.
68+ loadToken ( )
69+ } )
70+ } else {
71+ // 4. just trigger the token loading
72+ loadToken ( )
4473 }
4574 } )
4675 }
@@ -53,6 +82,24 @@ module.exports = class TokenManager {
5382 return `Bearer ${ token } `
5483 } )
5584 }
85+ /**
86+ * Triggers the remote IAM API token call, saves the response and resolves the loading promise
87+ * with the access_token
88+ *
89+ * @returns {Promise }
90+ */
91+ loadToken ( ) {
92+ // reset buffered tokenInfo, as we're about to load a new token
93+ this . tokenInfo = { }
94+
95+ // let other callers know that we're currently loading a new token
96+ this . tokenLoadingPromise = this . requestToken ( ) . then ( tokenResponse => {
97+ this . saveTokenInfo ( tokenResponse )
98+ return this . tokenInfo . access_token
99+ } )
100+
101+ return this . tokenLoadingPromise
102+ }
56103 /**
57104 * Request an IAM token using an API key and IAM URL.
58105 *
@@ -107,6 +154,11 @@ module.exports = class TokenManager {
107154 * @returns {boolean }
108155 */
109156 isTokenExpired ( ) {
157+ // the token cannot be considered expired, if we don't have one (yet)
158+ if ( ! this . tokenInfo || ! this . tokenInfo . access_token ) {
159+ return false
160+ }
161+
110162 if ( ! this . tokenInfo . expires_in || ! this . tokenInfo . expiration ) {
111163 return true
112164 }
0 commit comments