fix(client): broadcast session updates to other tabs on sign-out and user update (#8177)

Co-authored-by: Abhinav-kodes <183825080+Abhinav-kodes@users.noreply.github.com>
This commit is contained in:
Abhinav Singh
2026-02-27 09:16:27 +05:30
committed by GitHub
parent fd964c23e9
commit c45edd6b38
5 changed files with 89 additions and 1 deletions

View File

@@ -61,7 +61,10 @@ export const getClientConfig = (
...pluginsFetchPlugins,
],
});
const { $sessionSignal, session } = getSessionAtom($fetch, options);
const { $sessionSignal, session, broadcastSessionUpdate } = getSessionAtom(
$fetch,
options,
);
const plugins = options?.plugins || [];
let pluginsActions = {} as Record<string, any>;
const pluginsAtoms = {
@@ -92,6 +95,13 @@ export const getClientConfig = (
return matchesCommonPaths;
},
callback(path) {
if (path === "/sign-out") {
broadcastSessionUpdate("signout");
} else if (path === "/update-user" || path === "/update-session") {
broadcastSessionUpdate("updateUser");
}
},
},
];

View File

@@ -118,6 +118,8 @@ export function createDynamicPathProxy<T extends Record<string, any>>(
//@ts-expect-error
signal.set(!val);
}, 10);
// we also call the callback if it exists
match.callback?.(routePath);
}
},
});

View File

@@ -23,6 +23,10 @@ export function getSessionAtom(
method: "GET",
});
let broadcastSessionUpdate: (
trigger: "signout" | "getSession" | "updateUser",
) => void = () => {};
onMount(session, () => {
const refreshManager = createSessionRefreshManager({
sessionAtom: session,
@@ -32,6 +36,7 @@ export function getSessionAtom(
});
refreshManager.init();
broadcastSessionUpdate = refreshManager.broadcastSessionUpdate;
return () => {
refreshManager.cleanup();
@@ -41,5 +46,8 @@ export function getSessionAtom(
return {
session,
$sessionSignal: $signal,
broadcastSessionUpdate: (
trigger: "signout" | "getSession" | "updateUser",
) => broadcastSessionUpdate(trigger),
};
}

View File

@@ -2,6 +2,7 @@
import { atom } from "nanostores";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getGlobalBroadcastChannel } from "./broadcast-channel";
import { getGlobalOnlineManager } from "./online-manager";
import type { SessionAtom } from "./session-atom";
import { createSessionRefreshManager } from "./session-refresh";
@@ -15,6 +16,8 @@ describe("session-refresh", () => {
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
delete (globalThis as any)[Symbol.for("better-auth:broadcast-channel")];
});
it("should trigger network fetch and update session when refetchInterval fires", async () => {
@@ -618,4 +621,68 @@ describe("session-refresh", () => {
manager.cleanup();
vi.useRealTimers();
});
it("should broadcast session update when broadcastSessionUpdate is called with signout", () => {
const channel = getGlobalBroadcastChannel();
const postSpy = vi.spyOn(channel, "post");
const sessionAtom: SessionAtom = atom({
data: {
user: { id: "1", email: "test@test.com" },
session: { id: "session-1" },
},
error: null,
isPending: false,
});
const sessionSignal = atom(false);
const mockFetch = vi.fn(async () => ({ data: null, error: null }));
const manager = createSessionRefreshManager({
sessionAtom,
sessionSignal,
$fetch: mockFetch as any,
options: {},
});
manager.init();
manager.broadcastSessionUpdate("signout");
expect(postSpy).toHaveBeenCalledWith(
expect.objectContaining({ data: { trigger: "signout" } }),
);
manager.cleanup();
});
it("should broadcast session update when broadcastSessionUpdate is called with updateUser", () => {
const channel = getGlobalBroadcastChannel();
const postSpy = vi.spyOn(channel, "post");
const sessionAtom: SessionAtom = atom({
data: {
user: { id: "1", email: "test@test.com" },
session: { id: "session-1" },
},
error: null,
isPending: false,
});
const sessionSignal = atom(false);
const mockFetch = vi.fn(async () => ({ data: null, error: null }));
const manager = createSessionRefreshManager({
sessionAtom,
sessionSignal,
$fetch: mockFetch as any,
options: {},
});
manager.init();
manager.broadcastSessionUpdate("updateUser");
expect(postSpy).toHaveBeenCalledWith(
expect.objectContaining({ data: { trigger: "updateUser" } }),
);
manager.cleanup();
});
});

View File

@@ -17,6 +17,7 @@ export interface ClientStore {
export type ClientAtomListener = {
matcher: (path: string) => boolean;
signal: "$sessionSignal" | Omit<string, "$sessionSignal">;
callback?: (path: string) => void;
};
/**