Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions apps/desktop-tauri/src/hooks/useProviders.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ function provider(id: string, usedPercent = 20): ProviderUsageSnapshot {
};
}

function providerResettingAt(id: string, resetsAt: string): ProviderUsageSnapshot {
const snapshot = provider(id);
return { ...snapshot, primary: { ...snapshot.primary, resetsAt } };
}

const DAY_MS = 24 * 60 * 60 * 1_000;

function emitProviderEvent(event: string, payload: unknown) {
for (const listener of eventMocks.listeners.get(event) ?? []) {
listener({ payload });
Expand Down Expand Up @@ -403,6 +410,75 @@ describe("useProviders", () => {
}
});

it("refreshes once the soonest reset has passed", async () => {
vi.useFakeTimers({ now: new Date("2026-10-01T00:00:00Z") });
try {
tauriMocks.getCachedProviders.mockResolvedValue([
providerResettingAt("codex", "2026-10-01T00:10:00Z"),
]);
const { result } = renderHook(() => useProviders({ refreshOnMount: false }));
await act(async () => {});
expect(result.current.providers).toHaveLength(1);

// The timer waits one second past the reset.
await act(async () => {
vi.advanceTimersByTime(10 * 60_000);
});
expect(tauriMocks.refreshProviders).not.toHaveBeenCalled();

await act(async () => {
vi.advanceTimersByTime(1_000);
});
expect(tauriMocks.refreshProviders).toHaveBeenCalledTimes(1);
} finally {
vi.useRealTimers();
}
});

it("waits for a reset beyond the 32-bit timer limit instead of refreshing at once", async () => {
vi.useFakeTimers({ now: new Date("2026-10-01T00:00:00Z") });
try {
// 30 days is past the 2^31 - 1 ms (about 24.8 days) that setTimeout can
// hold. A longer delay runs at once, which forced a refresh on every
// provider update.
tauriMocks.getCachedProviders.mockResolvedValue([
providerResettingAt("cursor", "2026-10-31T00:00:00Z"),
]);
const { result } = renderHook(() => useProviders({ refreshOnMount: false }));
await act(async () => {});
expect(result.current.providers).toHaveLength(1);

await act(async () => {
vi.advanceTimersByTime(60_000);
});
expect(tauriMocks.refreshProviders).not.toHaveBeenCalled();

// A fresh snapshot with the same reset re-arms the timer; it must still wait.
act(() => {
emitProviderEvent("provider-updated", providerResettingAt("cursor", "2026-10-31T00:00:00Z"));
});
await act(async () => {
vi.advanceTimersByTime(80);
});
await act(async () => {
vi.advanceTimersByTime(25 * DAY_MS);
});
expect(tauriMocks.refreshProviders).not.toHaveBeenCalled();

await act(async () => {
vi.advanceTimersByTime(5 * DAY_MS - 60_080);
});
expect(tauriMocks.refreshProviders).not.toHaveBeenCalled();

await act(async () => {
vi.advanceTimersByTime(1_000);
});
expect(tauriMocks.refreshProviders).toHaveBeenCalledTimes(1);
} finally {
vi.useRealTimers();
}
});

it("clears each provider from refresh state as it completes", async () => {
const { result } = renderHook(() =>
useProviders({ refreshOnMount: false }),
Expand Down
26 changes: 21 additions & 5 deletions apps/desktop-tauri/src/hooks/useProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import {
refreshProvidersIfStale,
} from "../lib/tauri";

/**
* Longest delay `setTimeout` can hold: browsers, WebView2 included, store it
* as a signed 32-bit integer. A longer delay wraps around and fires early:
* a reset 24.9 to 49.7 days away would refresh at once.
*/
const MAX_TIMER_DELAY_MS = 2_147_483_647;

export interface UseProvidersOptions {
/**
* Delay the automatic stale-aware refresh on mount. Tray/menu surfaces use
Expand Down Expand Up @@ -276,11 +283,20 @@ export function useProviders(options: UseProvidersOptions = {}): UseProvidersRes

if (nextReset === undefined) return;

const delay = Math.max(5_000, nextReset - now + 1_000);
resetRefreshTimerRef.current = window.setTimeout(() => {
resetRefreshTimerRef.current = undefined;
refresh();
}, delay);
const resetAt = nextReset;
const armResetRefresh = () => {
const remaining = resetAt - Date.now() + 1_000;
if (remaining > MAX_TIMER_DELAY_MS) {
// Too far away for one timer: wait the longest delay, then re-check.
resetRefreshTimerRef.current = window.setTimeout(armResetRefresh, MAX_TIMER_DELAY_MS);
return;
}
resetRefreshTimerRef.current = window.setTimeout(() => {
resetRefreshTimerRef.current = undefined;
refresh();
}, Math.max(5_000, remaining));
};
armResetRefresh();

return () => {
if (resetRefreshTimerRef.current !== undefined) {
Expand Down