Skip to content
This repository was archived by the owner on Jan 17, 2025. It is now read-only.

Commit 21996ff

Browse files
authored
12 enhance token info caching behavior (#13)
* Enhanced IAM Token caching behavior for 'concurrent' invocations of getAuthHeader() * Bumped version to 1.0.5 * Improved code documentation
1 parent eeb8a7f commit 21996ff

File tree

3 files changed

+81
-12
lines changed

3 files changed

+81
-12
lines changed

__tests__/tokens.spec.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,20 @@ test('should fetch auth header', () => {
1919
needle.mockResolvedValue(Promise.resolve(validToken))
2020
return tm.getAuthHeader().then(h => expect(h).toEqual(`Bearer ${validToken.body.access_token}`))
2121
})
22+
test('should call requestToken() only once with many parallel getAuthHeader() calls', () => {
23+
needle.mockResolvedValue(Promise.resolve(validToken))
24+
const spy = jest.spyOn(tm, 'requestToken')
25+
const spy2 = jest.spyOn(tm, 'getToken')
26+
27+
const proms = []
28+
for (let i = 0; i < 10; i++) {
29+
proms.push(tm.getAuthHeader().then(h => expect(h).toEqual(`Bearer ${validToken.body.access_token}`)))
30+
}
31+
return Promise.all(proms)
32+
.then(() => {
33+
expect(spy).toHaveBeenCalledTimes(1)
34+
expect(spy2).toHaveBeenCalledTimes(10)
35+
spy.mockRestore()
36+
spy2.mockRestore()
37+
})
38+
})

index.js

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ibm-functions/iam-token-manager",
3-
"version": "1.0.4",
3+
"version": "1.0.5",
44
"description": "Helper library to handle IBM Cloud IAM Tokens",
55
"main": "index.js",
66
"files": [

0 commit comments

Comments
 (0)