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
This commit is contained in:
2026-08-26 22:41:05 -07:00
parent f5b49d49ee
commit 96d5535293
2 changed files with 15 additions and 5 deletions
+3
View File
@@ -53,12 +53,14 @@ export function Countdown({ date, label, color }: CountdownProps) {
return ( return (
<Group gap="xs"> <Group gap="xs">
{label && <Text size='sm' fw={500}>{label}:</Text>} {label && <Text size='sm' fw={500}>{label}:</Text>}
{/* Server and client render different seconds; the first client tick corrects it. */}
<Text <Text
size='sm' size='sm'
fw={600} fw={600}
c={isFinalStretch ? 'red' : color} c={isFinalStretch ? 'red' : color}
ff="monospace" ff="monospace"
className={classes.time} className={classes.time}
suppressHydrationWarning
> >
{prefix} {prefix}
<span <span
@@ -66,6 +68,7 @@ export function Countdown({ date, label, color }: CountdownProps) {
className={`${classes.seconds} ${ className={`${classes.seconds} ${
isFinalStretch ? classes.finalTick : classes.tick isFinalStretch ? classes.finalTick : classes.tick
}`} }`}
suppressHydrationWarning
> >
{pad(timeLeft.seconds)} {pad(timeLeft.seconds)}
</span> </span>
+7
View File
@@ -20,6 +20,8 @@ const INVALIDATE_JITTER_MS = 1500;
const invalidateTimers = new Map<string, ReturnType<typeof setTimeout>>(); const invalidateTimers = new Map<string, ReturnType<typeof setTimeout>>();
const WATCHDOG_MS = 45_000; const WATCHDOG_MS = 45_000;
const REFRESH_ATTEMPT_COOLDOWN_MS = 60_000;
let lastRefreshAttemptAt = 0;
function debouncedInvalidate( function debouncedInvalidate(
queryClient: ReturnType<typeof useQueryClient>, queryClient: ReturnType<typeof useQueryClient>,
@@ -163,13 +165,18 @@ export function useServerEvents() {
// an expired session rather than a network blip. Try to refresh; if the // 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 // session is genuinely gone, stop retrying so we don't hammer the
// endpoint forever — a real re-login (or a wake event) will reconnect. // 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; let sessionValid = true;
if (Date.now() - lastRefreshAttemptAt > REFRESH_ATTEMPT_COOLDOWN_MS) {
lastRefreshAttemptAt = Date.now();
try { try {
const { attemptRefreshingSession } = await import('supertokens-web-js/recipe/session'); const { attemptRefreshingSession } = await import('supertokens-web-js/recipe/session');
sessionValid = await attemptRefreshingSession(); sessionValid = await attemptRefreshingSession();
} catch { } catch {
// Refresh call itself failed (likely offline) — treat as transient. // Refresh call itself failed (likely offline) — treat as transient.
} }
}
if (disposed) return; if (disposed) return;