Skip to content

Commit

Permalink
feat: acquire lock around visibility change callback (#764)
Browse files Browse the repository at this point in the history
Similarly to how the refresh token ticker needs to acquire the lock
before checking whether the session needs to be refreshed, we need to do
the same for the visibility change callback. This is difficult to spot
because when one uses tabs instead of windows, there's often just one
tab visible at once. But when using multiple windows, a window is not
hidden when not focused, so events like waking from sleep will cause all
windows to refresh the token at once. Adding the lock will serialize
access, meaning that all windows will check the session, but only one
will refresh.
  • Loading branch information
hf authored Aug 23, 2023
1 parent bdfd212 commit a86f07d
Showing 1 changed file with 29 additions and 20 deletions.
49 changes: 29 additions & 20 deletions src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1964,30 +1964,39 @@ export default class GoTrueClient {
/**
* Callback registered with `window.addEventListener('visibilitychange')`.
*/
private async _onVisibilityChanged(isInitial: boolean) {
this._debug(`#_onVisibilityChanged(${isInitial})`, 'visibilityState', document.visibilityState)
private async _onVisibilityChanged(calledFromInitialize: boolean) {
const methodName = `#_onVisibilityChanged(${calledFromInitialize})`
this._debug(methodName, 'visibilityState', document.visibilityState)

if (document.visibilityState === 'visible') {
// to avoid recursively depending on #_initialize(), run the visibility
// changed callback in the next event loop tick
setTimeout(async () => {
if (!isInitial) {
// initial visibility change setup is handled in another flow under #initialize()
await this.initializePromise
await this._recoverAndRefresh()
if (this.autoRefreshToken) {
// in browser environments the refresh token ticker runs only on focused tabs
// which prevents race conditions
this._startAutoRefresh()
}

if (!calledFromInitialize) {
// called when the visibility has changed, i.e. the browser
// transitioned from hidden -> visible so we need to see if the session
// should be recovered immediately... but to do that we need to acquire
// the lock first asynchronously
await this.initializePromise

await this._acquireLock(-1, async () => {
if (document.visibilityState !== 'visible') {
this._debug(
methodName,
'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
)

this._debug(
'#_onVisibilityChanged()',
'finished waiting for initialize, _recoverAndRefresh'
)
}
// visibility has changed while waiting for the lock, abort
return
}

if (this.autoRefreshToken) {
// in browser environments the refresh token ticker runs only on focused tabs
// which prevents race conditions
this._startAutoRefresh()
}
}, 0)
// recover the session
await this._recoverAndRefresh()
})
}
} else if (document.visibilityState === 'hidden') {
if (this.autoRefreshToken) {
this._stopAutoRefresh()
Expand Down

0 comments on commit a86f07d

Please sign in to comment.