From d63c66063824666f78712ea9cd910c3c51ee460b Mon Sep 17 00:00:00 2001 From: sua yoo Date: Thu, 26 Jun 2025 18:03:07 -0700 Subject: [PATCH 1/2] only share auth if present --- frontend/src/utils/AuthService.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/utils/AuthService.ts b/frontend/src/utils/AuthService.ts index 4d627edcc5..23a444be42 100644 --- a/frontend/src/utils/AuthService.ts +++ b/frontend/src/utils/AuthService.ts @@ -201,11 +201,15 @@ export default class AuthService { "message", ({ data }: MessageEvent) => { if (data.name === "requesting_auth") { - // A new tab/window opened and is requesting shared auth - AuthService.broadcastChannel.postMessage({ - name: "responding_auth", - auth: AuthService.getCurrentTabAuth(), - } as AuthResponseEventDetail); + const auth = AuthService.getCurrentTabAuth(); + + if (auth) { + // A new tab/window opened and is requesting shared auth + AuthService.broadcastChannel.postMessage({ + name: "responding_auth", + auth: AuthService.getCurrentTabAuth(), + } as AuthResponseEventDetail); + } } }, ); From 77f6be1cec8fcf810b075df3a61fe9ca4e8a41fb Mon Sep 17 00:00:00 2001 From: sua yoo Date: Thu, 26 Jun 2025 18:27:51 -0700 Subject: [PATCH 2/2] check tab count --- frontend/src/utils/AuthService.ts | 13 +++++- frontend/src/utils/TabSyncService.ts | 64 ++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 frontend/src/utils/TabSyncService.ts diff --git a/frontend/src/utils/AuthService.ts b/frontend/src/utils/AuthService.ts index 23a444be42..e3ae82f235 100644 --- a/frontend/src/utils/AuthService.ts +++ b/frontend/src/utils/AuthService.ts @@ -1,6 +1,7 @@ import { APIError } from "./api"; import { urlForName } from "./router"; import appState, { AppStateService } from "./state"; +import TabSyncService from "./TabSyncService"; import type { APIUser } from "@/index"; import type { Auth } from "@/types/auth"; @@ -64,7 +65,10 @@ export default class AuthService { static logOutEvent: keyof AuthEventMap = "btrix-log-out"; static needLoginEvent: keyof AuthEventMap = "btrix-need-login"; - static broadcastChannel = new BroadcastChannel(AuthService.storageKey); + static tabSync = new TabSyncService(AuthService.storageKey); + static get broadcastChannel() { + return AuthService.tabSync.channel; + } static storage = { getItem() { return window.sessionStorage.getItem(AuthService.storageKey); @@ -245,12 +249,17 @@ export default class AuthService { }; AuthService.broadcastChannel.addEventListener("message", cb); }); + // Ensure that `getSharedSessionAuth` is resolved within a reasonable // timeframe, even if another window/tab doesn't respond: const timeoutPromise = new Promise((resolve) => { + if (!this.tabSync.tabCount || this.tabSync.tabCount === 1) { + resolve(null); + return; + } window.setTimeout(() => { resolve(null); - }, 10); + }, 500); }); return Promise.race([broadcastPromise, timeoutPromise]).then( diff --git a/frontend/src/utils/TabSyncService.ts b/frontend/src/utils/TabSyncService.ts new file mode 100644 index 0000000000..cda9db8c9a --- /dev/null +++ b/frontend/src/utils/TabSyncService.ts @@ -0,0 +1,64 @@ +import { nanoid } from "nanoid"; + +type TabSyncConfig = { tabIds: string[] }; + +/** + * Service for syncing data across tabs using `BroadcastChannel` + */ +export default class TabSyncService { + static storageKey = "btrix.tabSync"; + + public tabId = nanoid(); + public channel: BroadcastChannel; + public get tabCount() { + return this.getStoredSyncConfig()?.tabIds.length; + } + + constructor(channelName: string) { + // Open channel + this.channel = new BroadcastChannel(channelName); + + // Update number of open tabs + const syncConfig = this.getStoredSyncConfig() || { tabIds: [] }; + + syncConfig.tabIds.push(this.tabId); + + window.localStorage.setItem( + TabSyncService.storageKey, + JSON.stringify({ + ...syncConfig, + // Somewhat arbitrary, but only store latest 20 tabs to keep list managable + tabIds: syncConfig.tabIds.slice(-20), + }), + ); + + // Remove tab ID on page unload + window.addEventListener("unload", () => { + const syncConfig = this.getStoredSyncConfig(); + + if (syncConfig) { + const tabIds = syncConfig.tabIds.filter((id) => id === this.tabId); + + window.localStorage.setItem( + TabSyncService.storageKey, + JSON.stringify({ + ...syncConfig, + tabIds, + }), + ); + } + }); + } + + private getStoredSyncConfig(): TabSyncConfig | null { + const storedSyncConfig = window.localStorage.getItem( + TabSyncService.storageKey, + ); + + if (storedSyncConfig) { + return JSON.parse(storedSyncConfig) as TabSyncConfig; + } + + return null; + } +}