web/flows: prevent leader tab deadlock in continuous login flow (#21583)

* prevent leader tab deadlock in continuous login flow

* web: Continuous login tidy.

---------

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
This commit is contained in:
Ryan Pesek
2026-04-15 10:50:14 -05:00
committed by GitHub
parent 668f37ea41
commit af747c6c25
2 changed files with 52 additions and 24 deletions

View File

@@ -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<string>();
exitedTabIds: string[] = [];
protected discoveredTabIDs = new Set<string>();
public exitedTabIDs: string[] = [];
#logger: Logger;
protected logger: Logger;
#onMessage = (ev: MessageEvent<BroadcastMessage>) => {
this.#logger.debug("broadcast event", ev.data);
protected messageListener = (ev: MessageEvent<BroadcastMessage>) => {
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<Set<string>> {
this.discoveredTabIds.clear();
this.discoveredTabIDs.clear();
this.postMessage({
type: BroadcastMessageType.discover,
sender: TabID.shared.current,
});
await new Promise<void>((r) => {
setTimeout(r, 20);
});
return this.discoveredTabIds;
return this.discoveredTabIDs;
}
akResumeTab(tabId: string) {

View File

@@ -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<void>();
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);
}