From 96d5535293c8a0b4570a4e4abb5f831e999c1179 Mon Sep 17 00:00:00 2001 From: yohlo Date: Wed, 26 Aug 2026 22:41:05 -0700 Subject: [PATCH] fix: countdown hydration mismatch; rate-limit sse-error session refreshes - suppressHydrationWarning on countdown time text (server second differs from client second; first tick corrects) - sse reconnects attempt a session refresh at most once per minute so a flaky stream cannot hammer the refresh endpoint --- src/components/countdown.tsx | 3 +++ src/hooks/use-server-events.ts | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) 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;