I am currently implementing a client-side token refreshing script for a website, and reached a minor problem when the access token needed to be refreshed.
For some pages, the client fetched multiple (let’s say two) documents from the server (which both needed to be fetched with authorised requests, that is with an access token), so when the client access token was expired, this simultaneous fetching invoked two token refresh operations, and the client saved the final access token of the refresh operation that concluded last. This is a waste of resources as the first access token was never used or even saved to the client.
In order to solve this, I made the process that refreshed tokens create a temporary localStorage
entry:
async function getAccessToken(auth) {
// Check if the current token is valid, and return it if so
...
// Case when token needs to be refreshed
localStorage.setItem("TOKEN_PENDING", "TOKEN_PENDING");
// Refresh the token
...
const res = await fetch(...);
...
// Clean up after refresh
localStorage.removeItem("TOKEN_PENDING");
// Return the new token
...
}
Obviously, I now need to check if this entry has been created prior to refreshing the token:
const isTokenPending = () => localStorage.getItem("TOKEN_PENDING") != null;
async function getAccessToken(auth) {
// Check if the current token is valid, and return it if so
...
// Case when token needs to be refreshed
localStorage.setItem("TOKEN_PENDING", "TOKEN_PENDING");
// Check if the token is currently being refreshed in a parallel instance of the function
if (isTokenPending()) {
while (isTokenPending()) {
await new Promise((resolve) => setTimeout(resolve, 0));
}
console.info("Token finished refreshing");
// Recursively call the function, which will now produce the new valid token
return getAccessToken(auth);
}
localStorage.setItem("TOKEN_PENDING", "TOKEN_PENDING");
// Refresh the token
...
const res = await fetch(...);
...
// Clean up after refresh
localStorage.removeItem("TOKEN_PENDING");
// Return the new token
}
The problematic part is when the function detects that the temporary entry is present, and waits until the token is refreshed in some other instance of the function. Currently, I’ve tried the approach seen in the while
loop:
while (isTokenPending()) {
await new Promise((resolve) => setTimeout(resolve, 0));
}
However, I’m not sure this is a good enough solution since it looks hacky and isn’t obvious at first glance what’s going on. I used a self-resolving promise which resolves after a timeout of 0 milliseconds, which seems not to do anything. However, the fact that the promise is being awaited allows the program to "yield" the asynchronous processing and simultaneously handle the parallel instance which is refreshing the token, allowing the while
loop to finally end.
I also tried removing the hackish 0-millisecond promise, but then the process consumed all processing time and didn’t allow the parallel instance to refresh the token. This resulted in the website hanging/freezing.
I expected the two invocations of the function to run in parallel, and for the while
loop to wait until the second one completed, which would make the next call to isTokenPending()
return false
.
Additionally, I found this SO post:
How to wait for a JavaScript Promise to resolve before resuming function?
However, the accepted answer states that there is no way to achieve this, and it is dated 9 years ago so if this even applies to my problem I’d like to know if anything has changed since then. Perhaps the use of yield
?
So what I’m really asking is how could I allow this part of the code (with the while
loop) to wait for allow the asynchronous code to finish completing, and then continue when there are no unresolved promises?
Thanks in advance for your proposed solutions.
2
Answers
I can see what you are going for, but I feel like it might be a little bit more complicated than it needs to be.
Assuming that all your requests use the same Token, you could check validity of the token before doing any of the document fetching requests.
This ensure that only one refresh request is done, and that all the document fetching request have a valid token. I was thinking something like
Now, this code will run all the fetchDocument request one after the other, which might not be what you want.
This is where
Promise.all
comes in. This function takes an array of pending promises and resolved only when all of them are resolved. So you could build an array with pending promises, one for each document to fetch, and thenawait
thePromise.all
function.Now, all the document fetch requests will be run in parallel, and the
fetchDocuments
function will only resolve once all of the documents are fetched.If you’re only worried about issuing one token request per tab/window, this can be done very easily by storing the Promise that will return with a resolved access token. If you are trying to issue one token request per browser even among multiple tabs/windows, you could use the
storage
event to identify when some other browsing context (tab or window) makes that change.Firstly: the question you linked, How to wait for a JavaScript Promise to resolve before resuming function?, effectively asks if you can pause a function that isn’t
async
. That answer is "no", but that’s not what you’re trying to do here.If all you care about is the current tab, then it becomes very easy: separate the logic that refreshes the access token from the logic that gets it.
If you want to store this between tabs, then it’s almost the same, but instead you’d want to detect the case where you have
TOKEN_PENDING
set but your browsing context isn’t the one that initiated it (presumably by storing a local boolean). In that case, you could either poll through a repeatedsetTimeout
or subscribe through astorage
event that detects when the other tab has set the token.Note that you may need to adjust the above code depending on how exactly you store your token. Though Javascript is effectively single-threaded within a browsing context, if you are syncing data between multiple browsing contexts via local storage, you might see
listenForTokenUpdate
catch theTOKEN
update before other tab has cleared theTOKEN_PENDING
status, meaning that the right call order would have your other tab listen forstorage
event that would never arrive. Likewise you might need to catch the case where both browsing contexts detect that no other tab is fetching and then start fetching simultaneously, though ultimately the cost of that race condition is pretty low.