chore: session refetch cooldown for window focus

Everytime you focus the browser, we make a get-session request.
If you alt+tab between the browser frequently, you would hit your server several times (and thus the DB too) within the span of a couple seconds.
It's unnessesary to get the session each browser refocus within the span of a couple seconds.
This commit is contained in:
ping-maxwell
2025-11-20 07:44:29 +10:00
committed by Alex Yang
parent d06b1c4ff2
commit bb51390a36
2 changed files with 364 additions and 1 deletions

View File

@@ -1,10 +1,17 @@
// @vitest-environment happy-dom
import { atom } from "nanostores";
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getGlobalOnlineManager } from "./online-manager";
import { createSessionRefreshManager } from "./session-refresh";
describe("session-refresh", () => {
beforeEach(() => {
// Reset online manager state before each test
const onlineManager = getGlobalOnlineManager();
onlineManager.setOnline(true);
});
it("should trigger network fetch and update session when refetchInterval fires", async () => {
vi.useFakeTimers();
@@ -59,4 +66,336 @@ describe("session-refresh", () => {
manager.cleanup();
vi.useRealTimers();
});
it("should rate limit refetch on focus if a session request was made recently", async () => {
vi.useFakeTimers();
const sessionAtom = atom({
data: {
user: { id: "1", email: "test@test.com" },
session: { id: "session-1" },
},
error: null,
isPending: false,
isRefetching: false,
});
const sessionSignal = atom(false);
let signalChangeCount = 0;
const unsubscribeSignal = sessionSignal.subscribe(() => {
signalChangeCount++;
});
const mockFetch = vi.fn(async () => ({
data: { user: { id: "1" }, session: { id: "session-1" } },
error: null,
}));
const manager = createSessionRefreshManager({
sessionAtom,
sessionSignal,
$fetch: mockFetch as any,
options: {
sessionOptions: {
refetchOnWindowFocus: true,
},
},
});
manager.init();
// Trigger a poll event to set lastSessionRequest
manager.triggerRefetch({ event: "poll" });
await vi.runAllTimersAsync();
const initialSignalCount = signalChangeCount;
// Immediately trigger a focus event (within rate limit window)
manager.triggerRefetch({ event: "visibilitychange" });
// Signal should not change because rate limit prevents refetch
expect(signalChangeCount).toBe(initialSignalCount);
unsubscribeSignal();
manager.cleanup();
vi.useRealTimers();
});
it("should allow refetch on focus after rate limit window expires", async () => {
vi.useFakeTimers();
const sessionAtom = atom({
data: {
user: { id: "1", email: "test@test.com" },
session: { id: "session-1" },
},
error: null,
isPending: false,
});
const sessionSignal = atom(false);
let signalChangeCount = 0;
const unsubscribeSignal = sessionSignal.subscribe(() => {
signalChangeCount++;
});
const mockFetch = vi.fn(async () => ({
data: { user: { id: "1" }, session: { id: "session-1" } },
error: null,
}));
const manager = createSessionRefreshManager({
sessionAtom,
sessionSignal,
$fetch: mockFetch as any,
options: {
sessionOptions: {
refetchOnWindowFocus: true,
},
},
});
manager.init();
// Trigger a poll event to set lastSessionRequest
manager.triggerRefetch({ event: "poll" });
await vi.runAllTimersAsync();
const initialSignalCount = signalChangeCount;
// Advance time by 6 seconds (more than the 5 second rate limit)
await vi.advanceTimersByTimeAsync(6000);
// Now trigger a focus event (after rate limit window)
manager.triggerRefetch({ event: "visibilitychange" });
// Signal should change because rate limit has expired
expect(signalChangeCount).toBeGreaterThan(initialSignalCount);
unsubscribeSignal();
manager.cleanup();
vi.useRealTimers();
});
it("should allow refetch on focus when session is null even within rate limit", async () => {
vi.useFakeTimers();
const sessionAtom = atom({
data: {
user: { id: "1", email: "test@test.com" },
session: { id: "session-1" },
},
error: null,
isPending: false,
});
const sessionSignal = atom(false);
let signalChangeCount = 0;
const unsubscribeSignal = sessionSignal.subscribe(() => {
signalChangeCount++;
});
const mockFetch = vi.fn(async () => ({
data: { user: { id: "1" }, session: { id: "session-1" } },
error: null,
}));
const manager = createSessionRefreshManager({
sessionAtom,
sessionSignal,
$fetch: mockFetch as any,
options: {
sessionOptions: {
refetchOnWindowFocus: true,
},
},
});
manager.init();
// Trigger a visibilitychange event with session data to set lastSessionRequest
manager.triggerRefetch({ event: "visibilitychange" });
const signalCountAfterFirstFocus = signalChangeCount;
expect(signalCountAfterFirstFocus).toBeGreaterThan(0);
// Now set session to null and trigger another focus event
sessionAtom.set({
data: null as any,
error: null,
isPending: false,
});
// Immediately trigger another focus event (within rate limit window)
manager.triggerRefetch({ event: "visibilitychange" });
// Signal should change because session is null (rate limit bypassed)
expect(signalChangeCount).toBeGreaterThan(signalCountAfterFirstFocus);
unsubscribeSignal();
manager.cleanup();
vi.useRealTimers();
});
it("should allow refetch on focus when session is undefined even within rate limit", async () => {
vi.useFakeTimers();
const sessionAtom = atom({
data: {
user: { id: "1", email: "test@test.com" },
session: { id: "session-1" },
},
error: null,
isPending: false,
});
const sessionSignal = atom(false);
let signalChangeCount = 0;
const unsubscribeSignal = sessionSignal.subscribe(() => {
signalChangeCount++;
});
const mockFetch = vi.fn(async () => ({
data: { user: { id: "1" }, session: { id: "session-1" } },
error: null,
}));
const manager = createSessionRefreshManager({
sessionAtom,
sessionSignal,
$fetch: mockFetch as any,
options: {
sessionOptions: {
refetchOnWindowFocus: true,
},
},
});
manager.init();
// Trigger a visibilitychange event with session data to set lastSessionRequest
manager.triggerRefetch({ event: "visibilitychange" });
const signalCountAfterFirstFocus = signalChangeCount;
expect(signalCountAfterFirstFocus).toBeGreaterThan(0);
// Now set session to undefined and trigger another focus event
sessionAtom.set({
data: undefined as any,
error: null,
isPending: false,
});
// Immediately trigger another focus event (within rate limit window)
manager.triggerRefetch({ event: "visibilitychange" });
// Signal should change because session is undefined (rate limit bypassed)
expect(signalChangeCount).toBeGreaterThan(signalCountAfterFirstFocus);
unsubscribeSignal();
manager.cleanup();
vi.useRealTimers();
});
it("should update lastSessionRequest when poll event triggers fetch", async () => {
vi.useFakeTimers();
const sessionAtom = atom({
data: {
user: { id: "1", email: "test@test.com" },
session: { id: "session-1" },
},
error: null,
isPending: false,
});
const sessionSignal = atom(false);
let signalChangeCount = 0;
const unsubscribeSignal = sessionSignal.subscribe(() => {
signalChangeCount++;
});
const mockFetch = vi.fn(async () => ({
data: { user: { id: "1" }, session: { id: "session-1" } },
error: null,
}));
const manager = createSessionRefreshManager({
sessionAtom,
sessionSignal,
$fetch: mockFetch as any,
});
// Trigger a poll event - this will trigger a signal change
manager.triggerRefetch({ event: "poll" });
await vi.runAllTimersAsync();
const signalCountAfterPoll = signalChangeCount;
expect(signalCountAfterPoll).toBeGreaterThan(0);
// Immediately trigger a focus event - should be rate limited
manager.triggerRefetch({ event: "visibilitychange" });
// Should be rate limited (no additional signal change)
expect(signalChangeCount).toBe(signalCountAfterPoll);
unsubscribeSignal();
manager.cleanup();
vi.useRealTimers();
});
it("should not refetch when offline unless refetchWhenOffline is true", () => {
const onlineManager = getGlobalOnlineManager();
// Ensure we start online, then set offline
onlineManager.setOnline(true);
onlineManager.setOnline(false);
const sessionAtom = atom({
data: {
user: { id: "1", email: "test@test.com" },
session: { id: "session-1" },
},
error: null,
isPending: false,
});
const sessionSignal = atom(false);
// Track signal changes from the start
let signalChangeCount = 0;
const unsubscribeSignal = sessionSignal.subscribe(() => {
signalChangeCount++;
});
const mockFetch = vi.fn(async () => ({
data: { user: { id: "1" }, session: { id: "session-1" } },
error: null,
}));
const manager = createSessionRefreshManager({
sessionAtom,
sessionSignal,
$fetch: mockFetch as any,
options: {
sessionOptions: {
refetchWhenOffline: false,
},
},
});
// Verify we're offline
expect(onlineManager.isOnline).toBe(false);
// Get initial signal count (should be 0, but capture it)
const initialSignalCount = signalChangeCount;
// Trigger refetch - should be blocked by shouldRefetch() returning false
manager.triggerRefetch({ event: "visibilitychange" });
// Should not refetch when offline (shouldRefetch returns false)
// Signal count should remain the same
expect(signalChangeCount).toBe(initialSignalCount);
expect(mockFetch).not.toHaveBeenCalled();
unsubscribeSignal();
manager.cleanup();
onlineManager.setOnline(true);
});
});

View File

@@ -7,6 +7,11 @@ import { getGlobalOnlineManager } from "./online-manager";
const now = () => Math.floor(Date.now() / 1000);
/**
* Rate limit: don't refetch on focus if a session request was made within this many seconds
*/
const FOCUS_REFETCH_RATE_LIMIT_SECONDS = 5;
export interface SessionRefreshOptions {
sessionAtom: WritableAtom<any>;
sessionSignal: WritableAtom<boolean>;
@@ -16,6 +21,7 @@ export interface SessionRefreshOptions {
interface SessionRefreshState {
lastSync: number;
lastSessionRequest: number;
cachedSession: any;
pollInterval?: ReturnType<typeof setInterval> | undefined;
unsubscribeBroadcast?: (() => void) | undefined;
@@ -34,6 +40,7 @@ export function createSessionRefreshManager(opts: SessionRefreshOptions) {
const state: SessionRefreshState = {
lastSync: 0,
lastSessionRequest: 0,
cachedSession: undefined,
};
@@ -59,6 +66,7 @@ export function createSessionRefreshManager(opts: SessionRefreshOptions) {
const currentSession = sessionAtom.get();
if (event?.event === "poll") {
state.lastSessionRequest = now();
$fetch("/get-session")
.then((res) => {
sessionAtom.set({
@@ -73,11 +81,26 @@ export function createSessionRefreshManager(opts: SessionRefreshOptions) {
return;
}
// Rate limit: don't refetch on focus if a session request was made recently
if (event?.event === "visibilitychange") {
const timeSinceLastRequest = now() - state.lastSessionRequest;
if (
timeSinceLastRequest < FOCUS_REFETCH_RATE_LIMIT_SECONDS &&
currentSession?.data !== null &&
currentSession?.data !== undefined
) {
return;
}
}
if (
currentSession?.data === null ||
currentSession?.data === undefined ||
event?.event === "visibilitychange"
) {
if (event?.event === "visibilitychange") {
state.lastSessionRequest = now();
}
state.lastSync = now();
sessionSignal.set(!sessionSignal.get());
}
@@ -155,6 +178,7 @@ export function createSessionRefreshManager(opts: SessionRefreshOptions) {
state.unsubscribeOnline = undefined;
}
state.lastSync = 0;
state.lastSessionRequest = 0;
state.cachedSession = undefined;
};