diff --git a/web/src/flow/tabs/broadcast.ts b/web/src/flow/tabs/broadcast.ts index 5f3fe38f1666..41f0d122ed93 100644 --- a/web/src/flow/tabs/broadcast.ts +++ b/web/src/flow/tabs/broadcast.ts @@ -17,16 +17,16 @@ export interface BroadcastMessage { [key: string]: unknown; } -export class Broadcast extends BroadcastChannel { +export class Broadcast extends BroadcastChannel implements Disposable { static shared = new Broadcast(); - private discoveredTabIds = new Set(); - exitedTabIds: string[] = []; + protected discoveredTabIDs = new Set(); + public exitedTabIDs: string[] = []; - #logger: Logger; + protected logger: Logger; - #onMessage = (ev: MessageEvent) => { - this.#logger.debug("broadcast event", ev.data); + protected messageListener = (ev: MessageEvent) => { + this.logger.debug("broadcast event", ev.data); switch (ev.data.type) { case BroadcastMessageType.discover: if (ev.data.sender === TabID.shared.current) { @@ -38,40 +38,50 @@ export class Broadcast extends BroadcastChannel { }); return; case BroadcastMessageType.discoverReply: - this.discoveredTabIds.add(ev.data.sender as string); + this.discoveredTabIDs.add(ev.data.sender as string); return; case BroadcastMessageType.exit: - this.exitedTabIds.push(ev.data.sender); + this.exitedTabIDs.push(ev.data.sender); return; case BroadcastMessageType.continue: if (ev.data.target === TabID.shared.current) { - this.#logger.debug("Continuing upon event"); + this.logger.debug("Continuing upon event"); window.dispatchEvent(new CustomEvent("ak-multitab-continue")); } return; } }; + protected pageHideListener = () => { + this.akExitTab(); + }; + constructor() { super(BROADCAST_CHANNEL_NAME); - this.addEventListener("message", this.#onMessage); - this.#logger = ConsoleLogger.prefix("mtab/broadcast"); + + this.addEventListener("message", this.messageListener); + window.addEventListener("pagehide", this.pageHideListener); + + this.logger = ConsoleLogger.prefix("mtab/broadcast"); } [Symbol.dispose]() { - this.removeEventListener("message", this.#onMessage); + this.removeEventListener("message", this.messageListener); } async akTabDiscover(): Promise> { - this.discoveredTabIds.clear(); + this.discoveredTabIDs.clear(); + this.postMessage({ type: BroadcastMessageType.discover, sender: TabID.shared.current, }); + await new Promise((r) => { setTimeout(r, 20); }); - return this.discoveredTabIds; + + return this.discoveredTabIDs; } akResumeTab(tabId: string) { diff --git a/web/src/flow/tabs/orchestrator.ts b/web/src/flow/tabs/orchestrator.ts index fad4fae047d6..0e43d199ca1f 100644 --- a/web/src/flow/tabs/orchestrator.ts +++ b/web/src/flow/tabs/orchestrator.ts @@ -8,10 +8,9 @@ import { ConsoleLogger } from "#logger/browser"; const lockKey = "authentik-tab-locked"; const logger = ConsoleLogger.prefix("mtab/orchestrate"); +const TAB_EXIT_TIMEOUT_MS = 3000; + export function multiTabOrchestrateLeave() { - if (!globalAK().brand.flags.flowsContinuousLogin) { - return; - } Broadcast.shared.akExitTab(); TabID.shared.clear(); } @@ -20,35 +19,54 @@ export async function multiTabOrchestrateResume() { if (!globalAK().brand.flags.flowsContinuousLogin) { return; } - const lockTabId = localStorage.getItem(lockKey); + + const lockTabID = localStorage.getItem(lockKey); const tabs = await Broadcast.shared.akTabDiscover(); + logger.debug("Got list of tabs", tabs); - if (lockTabId && tabs.has(lockTabId)) { + if (lockTabID && tabs.has(lockTabID)) { logger.debug("Tabs locked, leaving."); multiTabOrchestrateLeave(); return; } + logger.debug("Locking tabs"); localStorage.setItem(lockKey, TabID.shared.current); for (const tab of tabs) { logger.debug("Telling tab to continue", tab); Broadcast.shared.akResumeTab(tab); + const done = Promise.withResolvers(); - const checker = setInterval(() => { - if (Broadcast.shared.exitedTabIds.includes(tab)) { + + let timeout = -1; + + const checker = requestAnimationFrame(() => { + if (Broadcast.shared.exitedTabIDs.includes(tab)) { logger.debug("tab exited", tab); - setTimeout(() => { + self.clearTimeout(timeout); + + self.setTimeout(() => { logger.debug("continue exited", tab); done.resolve(); }, 1000); - clearInterval(checker); + + cancelAnimationFrame(checker); } - }, 1); + }); + + timeout = self.setTimeout(() => { + logger.warn("Timed out waiting for tab to exit, moving on", tab); + cancelAnimationFrame(checker); + done.resolve(); + }, TAB_EXIT_TIMEOUT_MS); + await done.promise; + logger.debug("Tab done, continuing", tab); } + logger.debug("All tabs done."); localStorage.removeItem(lockKey); }