fix(client): handle throw:true in session refresh (#8610)

This commit is contained in:
Taesu
2026-03-20 14:07:25 -07:00
committed by Alex Yang
parent 1ed42714f8
commit 275ca46fe7
2 changed files with 117 additions and 4 deletions
@@ -725,6 +725,103 @@ describe("session-refresh", () => {
vi.useRealTimers();
});
it("should preserve session when $fetch returns unwrapped data (throw: true)", async () => {
vi.useFakeTimers();
const initialData = {
user: { id: "1", email: "old@test.com" },
session: { id: "session-1" },
};
const refreshedData = {
user: { id: "1", email: "new@test.com" },
session: { id: "session-1" },
};
const sessionAtom: SessionAtom = atom({
data: initialData,
error: null,
isPending: false,
isRefetching: false,
});
const sessionSignal = atom(false);
// When throw: true, $fetch returns data directly instead of { data, error }
const mockFetch = vi.fn(async () => refreshedData);
const manager = createSessionRefreshManager({
sessionAtom,
sessionSignal,
$fetch: mockFetch as any,
options: {
sessionOptions: {
refetchInterval: 5,
},
},
});
manager.init();
await vi.advanceTimersByTimeAsync(5000);
expect(mockFetch).toHaveBeenCalledTimes(1);
const updatedSession = sessionAtom.get();
expect(updatedSession.data).toEqual(refreshedData);
expect(updatedSession.data?.user?.email).toBe("new@test.com");
manager.cleanup();
vi.useRealTimers();
});
it("should preserve session on visibilitychange when $fetch returns unwrapped data (throw: true)", async () => {
vi.useFakeTimers();
const initialData = {
user: { id: "1", email: "old@test.com" },
session: { id: "session-1" },
};
const refreshedData = {
user: { id: "1", email: "new@test.com" },
session: { id: "session-1" },
};
const sessionAtom: SessionAtom = atom({
data: initialData,
error: null,
isPending: false,
isRefetching: false,
});
const sessionSignal = atom(false);
// When throw: true, $fetch returns data directly
const mockFetch = vi.fn(async () => refreshedData);
const manager = createSessionRefreshManager({
sessionAtom,
sessionSignal,
$fetch: mockFetch as any,
options: {
sessionOptions: {
refetchOnWindowFocus: true,
},
},
});
manager.init();
manager.triggerRefetch({ event: "visibilitychange" });
await vi.runAllTimersAsync();
expect(mockFetch).toHaveBeenCalledTimes(1);
const updatedSession = sessionAtom.get();
expect(updatedSession.data).toEqual(refreshedData);
expect(updatedSession.data?.user?.email).toBe("new@test.com");
manager.cleanup();
vi.useRealTimers();
});
it("should broadcast session update when broadcastSessionUpdate is called with signout", () => {
const channel = getGlobalBroadcastChannel();
const postSpy = vi.spyOn(channel, "post");
@@ -9,6 +9,24 @@ import type { AuthQueryAtom } from "./query";
const now = () => Math.floor(Date.now() / 1000);
/**
* Normalize $fetch response: `throw: true` returns data directly, otherwise `{ data, error }`.
*/
function normalizeSessionResponse(res: unknown): {
data: SessionResponse | null;
error: unknown;
} {
if (
typeof res === "object" &&
res !== null &&
"data" in res &&
"error" in res
) {
return res as { data: SessionResponse | null; error: unknown };
}
return { data: res as SessionResponse, error: null };
}
/**
* Rate limit: don't refetch on focus if a session request was made within this many seconds
*/
@@ -90,16 +108,14 @@ export function createSessionRefreshManager(opts: SessionRefreshOptions) {
state.lastSessionRequest = now();
$fetch<SessionResponse>("/get-session")
.then(async (res) => {
let data = res.data;
let error = res.error || null;
let { data, error } = normalizeSessionResponse(res);
if (data?.needsRefresh) {
try {
const refreshRes = await $fetch<SessionResponse>("/get-session", {
method: "POST",
});
data = refreshRes.data;
error = refreshRes.error || null;
({ data, error } = normalizeSessionResponse(refreshRes));
} catch {}
}