fix: preserve custom session fields on focus refresh (#8354)

This commit is contained in:
Joél Solano
2026-03-10 15:03:39 -07:00
committed by Alex Yang
parent 3ecd22d876
commit 2bd994babc
2 changed files with 123 additions and 13 deletions
@@ -622,6 +622,109 @@ describe("session-refresh", () => {
vi.useRealTimers();
});
/**
* https://github.com/better-auth/better-auth/issues/8325
*/
it("should preserve custom session fields on poll refresh", async () => {
vi.useFakeTimers();
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 customSessionData = {
user: { id: "1", email: "test@test.com" },
session: { id: "session-1" },
activationInfo: { isActivated: true, activatedAt: "2024-01-01" },
role: "admin",
};
const mockFetch = vi.fn(async () => ({
data: customSessionData,
error: null,
}));
const manager = createSessionRefreshManager({
sessionAtom,
sessionSignal,
$fetch: mockFetch as any,
options: {
sessionOptions: {
refetchInterval: 5,
},
},
});
manager.init();
await vi.advanceTimersByTimeAsync(5000);
const updatedSession = sessionAtom.get();
expect(updatedSession.data).toEqual(customSessionData);
expect((updatedSession.data as any)?.activationInfo).toEqual({
isActivated: true,
activatedAt: "2024-01-01",
});
expect((updatedSession.data as any)?.role).toBe("admin");
manager.cleanup();
vi.useRealTimers();
});
it("should preserve custom session fields on visibilitychange refresh", async () => {
vi.useFakeTimers();
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 customSessionData = {
user: { id: "1", email: "test@test.com" },
session: { id: "session-1" },
customField: "custom value",
};
const mockFetch = vi.fn(async () => ({
data: customSessionData,
error: null,
}));
const manager = createSessionRefreshManager({
sessionAtom,
sessionSignal,
$fetch: mockFetch as any,
options: {
sessionOptions: {
refetchOnWindowFocus: true,
},
},
});
manager.init();
manager.triggerRefetch({ event: "visibilitychange" });
await vi.runAllTimersAsync();
const updatedSession = sessionAtom.get();
expect(updatedSession.data).toEqual(customSessionData);
expect((updatedSession.data as any)?.customField).toBe("custom value");
manager.cleanup();
vi.useRealTimers();
});
it("should broadcast session update when broadcastSessionUpdate is called with signout", () => {
const channel = getGlobalBroadcastChannel();
const postSpy = vi.spyOn(channel, "post");
@@ -15,10 +15,12 @@ const now = () => Math.floor(Date.now() / 1000);
const FOCUS_REFETCH_RATE_LIMIT_SECONDS = 5;
export interface SessionRefreshOptions {
sessionAtom: AuthQueryAtom<{
user: User;
session: Session;
}>;
sessionAtom: AuthQueryAtom<
{
user: User;
session: Session;
} & Record<string, any>
>;
sessionSignal: WritableAtom<boolean>;
$fetch: BetterFetch;
options?: BetterAuthClientOptions | undefined;
@@ -34,11 +36,19 @@ interface SessionRefreshState {
unsubscribeOnline?: (() => void) | undefined;
}
interface SessionResponse {
session: Session | null;
user: User | null;
needsRefresh?: boolean;
}
export type SessionResponse = (
| {
session: null;
user: null;
needsRefresh?: boolean;
}
| {
session: Session;
user: User;
needsRefresh?: boolean;
}
) &
Record<string, any>;
export function createSessionRefreshManager(opts: SessionRefreshOptions) {
const { sessionAtom, sessionSignal, $fetch, options = {} } = opts;
@@ -93,10 +103,7 @@ export function createSessionRefreshManager(opts: SessionRefreshOptions) {
} catch {}
}
const sessionData =
data?.session && data?.user
? { session: data.session, user: data.user }
: null;
const sessionData = data?.session && data?.user ? data : null;
sessionAtom.set({
...currentSession,