diff --git a/src/components/countdown.tsx b/src/components/countdown.tsx index c2dae28..de3c5d4 100644 --- a/src/components/countdown.tsx +++ b/src/components/countdown.tsx @@ -53,12 +53,14 @@ export function Countdown({ date, label, color }: CountdownProps) { return ( {label && {label}:} + {/* Server and client render different seconds; the first client tick corrects it. */} {prefix} {pad(timeLeft.seconds)} diff --git a/src/hooks/use-server-events.ts b/src/hooks/use-server-events.ts index 75cb8b9..f71272d 100644 --- a/src/hooks/use-server-events.ts +++ b/src/hooks/use-server-events.ts @@ -20,6 +20,8 @@ const INVALIDATE_JITTER_MS = 1500; const invalidateTimers = new Map>(); const WATCHDOG_MS = 45_000; +const REFRESH_ATTEMPT_COOLDOWN_MS = 60_000; +let lastRefreshAttemptAt = 0; function debouncedInvalidate( queryClient: ReturnType, @@ -163,12 +165,17 @@ export function useServerEvents() { // an expired session rather than a network blip. Try to refresh; if the // session is genuinely gone, stop retrying so we don't hammer the // endpoint forever — a real re-login (or a wake event) will reconnect. + // Cooldown: a flaky stream (proxy idle timeouts) must not turn every + // reconnect into a refresh call. let sessionValid = true; - try { - const { attemptRefreshingSession } = await import('supertokens-web-js/recipe/session'); - sessionValid = await attemptRefreshingSession(); - } catch { - // Refresh call itself failed (likely offline) — treat as transient. + if (Date.now() - lastRefreshAttemptAt > REFRESH_ATTEMPT_COOLDOWN_MS) { + lastRefreshAttemptAt = Date.now(); + try { + const { attemptRefreshingSession } = await import('supertokens-web-js/recipe/session'); + sessionValid = await attemptRefreshingSession(); + } catch { + // Refresh call itself failed (likely offline) — treat as transient. + } } if (disposed) return;