Skip to content

Commit

Permalink
feat: add multi-tab state change notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
hf committed Jan 23, 2023
1 parent 013afae commit 94d1e84
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 39 deletions.
107 changes: 85 additions & 22 deletions src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export default class GoTrueClient {
protected storage: SupportedStorage
protected stateChangeEmitters: Map<string, Subscription> = new Map()
protected autoRefreshTicker: ReturnType<typeof setInterval> | null = null
protected visibilityChangedCallback: (() => Promise<any>) | null = null
protected refreshingDeferred: Deferred<CallRefreshTokenResult> | null = null
/**
* Keeps track of the async client initialization.
Expand All @@ -129,6 +130,11 @@ export default class GoTrueClient {
}
protected fetch: Fetch

/**
* Used to broadcast state change events to other tabs listening.
*/
protected broadcastChannel: BroadcastChannel | null = null

/**
* Create a new client for use in the browser.
*/
Expand Down Expand Up @@ -160,6 +166,13 @@ export default class GoTrueClient {
getAuthenticatorAssuranceLevel: this._getAuthenticatorAssuranceLevel.bind(this),
}

if (isBrowser() && globalThis.BroadcastChannel && this.persistSession && this.storageKey) {
this.broadcastChannel = new globalThis.BroadcastChannel(this.storageKey)
this.broadcastChannel.addEventListener('message', (event) => {
this._notifyAllSubscribers(event.data.event, event.data.session, false) // broadcast = false so we don't get an endless loop of messages
})
}

this.initialize()
}

Expand Down Expand Up @@ -1053,7 +1066,15 @@ export default class GoTrueClient {
}
}

private _notifyAllSubscribers(event: AuthChangeEvent, session: Session | null) {
private _notifyAllSubscribers(
event: AuthChangeEvent,
session: Session | null,
broadcast: boolean = true
) {
if (this.broadcastChannel && broadcast) {
this.broadcastChannel.postMessage({ event, session })
}

this.stateChangeEmitters.forEach((x) => x.callback(event, session))
}

Expand Down Expand Up @@ -1083,6 +1104,53 @@ export default class GoTrueClient {
}
}

/**
* Removes any registered visibilitychange callback.
*
* {@see #startAutoRefresh}
* {@see #stopAutoRefresh}
*/
private _removeVisibilityChangedCallback() {
const callback = this.visibilityChangedCallback
this.visibilityChangedCallback = null

try {
if (callback && isBrowser() && window?.removeEventListener) {
window.removeEventListener('visibilitychange', callback)
}
} catch (e) {
console.error('removing visibilitychange callback failed', e)
}
}

/**
* This is the private implementation of {@link #startAutoRefresh}. Use this
* within the library.
*/
private async _startAutoRefresh() {
await this._stopAutoRefresh()
this.autoRefreshTicker = setInterval(
() => this._autoRefreshTokenTick(),
AUTO_REFRESH_TICK_DURATION
)

// run the tick immediately
await this._autoRefreshTokenTick()
}

/**
* This is the private implementation of {@link #stopAutoRefresh}. Use this
* within the library.
*/
private async _stopAutoRefresh() {
const ticker = this.autoRefreshTicker
this.autoRefreshTicker = null

if (ticker) {
clearInterval(ticker)
}
}

/**
* Starts an auto-refresh process in the background. The session is checked
* every few seconds. Close to the time of expiration a process is started to
Expand All @@ -1094,7 +1162,9 @@ export default class GoTrueClient {
*
* On browsers the refresh process works only when the tab/window is in the
* foreground to conserve resources as well as prevent race conditions and
* flooding auth with requests.
* flooding auth with requests. If you call this method any managed
* visibility change callback will be removed and you must manage visibility
* changes on your own.
*
* On non-browser platforms the refresh process works *continuously* in the
* background, which may not be desireable. You should hook into your
Expand All @@ -1104,27 +1174,21 @@ export default class GoTrueClient {
* {@see #stopAutoRefresh}
*/
async startAutoRefresh() {
await this.stopAutoRefresh()
this.autoRefreshTicker = setInterval(
() => this._autoRefreshTokenTick(),
AUTO_REFRESH_TICK_DURATION
)

// run the tick immediately
await this._autoRefreshTokenTick()
this._removeVisibilityChangedCallback()
await this._startAutoRefresh()
}

/**
* Stops an active auto refresh process running in the background (if any).
*
* If you call this method any managed visibility change callback will be
* removed and you must manage visibility changes on your own.
*
* See {@link #startAutoRefresh} for more details.
*/
async stopAutoRefresh() {
const ticker = this.autoRefreshTicker
this.autoRefreshTicker = null

if (ticker) {
clearInterval(ticker)
}
this._removeVisibilityChangedCallback()
await this._stopAutoRefresh()
}

/**
Expand Down Expand Up @@ -1172,10 +1236,9 @@ export default class GoTrueClient {
}

try {
window?.addEventListener(
'visibilitychange',
async () => await this._onVisibilityChanged(false)
)
this.visibilityChangedCallback = async () => await this._onVisibilityChanged(false)

window?.addEventListener('visibilitychange', this.visibilityChangedCallback)

// now immediately call the visbility changed callback to setup with the
// current visbility state
Expand All @@ -1199,11 +1262,11 @@ export default class GoTrueClient {
if (this.autoRefreshToken) {
// in browser environments the refresh token ticker runs only on focused tabs
// which prevents race conditions
this.startAutoRefresh()
this._startAutoRefresh()
}
} else if (document.visibilityState === 'hidden') {
if (this.autoRefreshToken) {
this.stopAutoRefresh()
this._stopAutoRefresh()
}
}
}
Expand Down
34 changes: 17 additions & 17 deletions src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,31 +79,31 @@ export const removeItemAsync = async (storage: SupportedStorage, key: string): P
}

export function decodeBase64URL(value: string): string {
const key = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
let base64 = '';
let chr1, chr2, chr3;
let enc1, enc2, enc3, enc4;
let i = 0;
value = value.replace('-', '+').replace('_', '/');
const key = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
let base64 = ''
let chr1, chr2, chr3
let enc1, enc2, enc3, enc4
let i = 0
value = value.replace('-', '+').replace('_', '/')

while (i < value.length) {
enc1 = key.indexOf(value.charAt(i++));
enc2 = key.indexOf(value.charAt(i++));
enc3 = key.indexOf(value.charAt(i++));
enc4 = key.indexOf(value.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
base64 = base64 + String.fromCharCode(chr1);
enc1 = key.indexOf(value.charAt(i++))
enc2 = key.indexOf(value.charAt(i++))
enc3 = key.indexOf(value.charAt(i++))
enc4 = key.indexOf(value.charAt(i++))
chr1 = (enc1 << 2) | (enc2 >> 4)
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2)
chr3 = ((enc3 & 3) << 6) | enc4
base64 = base64 + String.fromCharCode(chr1)

if (enc3 != 64 && chr2 != 0) {
base64 = base64 + String.fromCharCode(chr2);
base64 = base64 + String.fromCharCode(chr2)
}
if (enc4 != 64 && chr3 != 0) {
base64 = base64 + String.fromCharCode(chr3);
base64 = base64 + String.fromCharCode(chr3)
}
}
return base64;
return base64
}

/**
Expand Down

0 comments on commit 94d1e84

Please sign in to comment.