From 81deed4c75df915ab2f07953386003325bbbbaa3 Mon Sep 17 00:00:00 2001 From: AmanTahiliani Date: Wed, 29 Jul 2026 03:56:22 -0400 Subject: [PATCH] fix(#98): rearm capped race hub refresh --- frontend/src/lib/schedule.ts | 4 +++- frontend/src/pages/RaceHubPage.tsx | 11 +++++++++-- frontend/src/test/RaceHubPage.test.tsx | 17 +++++++++++++++++ frontend/src/test/schedule.test.ts | 7 +++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/schedule.ts b/frontend/src/lib/schedule.ts index 00cd527..2c29f32 100644 --- a/frontend/src/lib/schedule.ts +++ b/frontend/src/lib/schedule.ts @@ -145,11 +145,13 @@ export function formatSessionScheduleTime(value: string): string { }) } +export const MAX_BROWSER_TIMEOUT = 2_147_483_647 + export function refreshDeadlineDelay(refreshAt: string | undefined, now = Date.now()): number | null { if (!refreshAt) return null const deadline = Date.parse(refreshAt) if (Number.isNaN(deadline)) return null - return Math.min(Math.max(0, deadline - now), 2_147_483_647) + return Math.min(Math.max(0, deadline - now), MAX_BROWSER_TIMEOUT) } export type FocusMeetingKind = 'current' | 'next' | 'recent' | 'fallback' diff --git a/frontend/src/pages/RaceHubPage.tsx b/frontend/src/pages/RaceHubPage.tsx index fb84eeb..46cc189 100644 --- a/frontend/src/pages/RaceHubPage.tsx +++ b/frontend/src/pages/RaceHubPage.tsx @@ -17,6 +17,7 @@ import { SourceBadge } from '../components/SourceBadge' import { countryAccent, countryDecal, formatGpDateRange } from '../lib/gpIdentity' import { formatCoverageHint, sessionTypeAbbrev } from '../lib/coverage' import { + MAX_BROWSER_TIMEOUT, formatCountdown, formatSessionScheduleTime, refreshDeadlineDelay, @@ -33,6 +34,7 @@ export function RaceHubPage({ sessionKey }: Props) { const [activeTab, setActiveTab] = useState('overview') const [switcherOpen, setSwitcherOpen] = useState(false) const [now, setNow] = useState(() => Date.now()) + const [refreshGeneration, setRefreshGeneration] = useState(0) // The server owns bare Race Hub selection so every open tab crosses the // one-hour handoff at the same instant. @@ -57,9 +59,14 @@ export function RaceHubPage({ sessionKey }: Props) { if (sessionKey !== 0) return const delay = refreshDeadlineDelay(context?.race_hub_refresh_at) if (delay == null) return - const timer = window.setTimeout(() => { void refetchContext() }, delay) + const rearmAfterRefetch = delay === MAX_BROWSER_TIMEOUT + const timer = window.setTimeout(() => { + void refetchContext().finally(() => { + if (rearmAfterRefetch) setRefreshGeneration((generation) => generation + 1) + }) + }, delay) return () => window.clearTimeout(timer) - }, [sessionKey, context?.race_hub_refresh_at, refetchContext]) + }, [sessionKey, context?.race_hub_refresh_at, refetchContext, refreshGeneration]) useEffect(() => { if (!preSession) return diff --git a/frontend/src/test/RaceHubPage.test.tsx b/frontend/src/test/RaceHubPage.test.tsx index cbadde5..e1c8b79 100644 --- a/frontend/src/test/RaceHubPage.test.tsx +++ b/frontend/src/test/RaceHubPage.test.tsx @@ -9,6 +9,7 @@ import { createRoute, } from '@tanstack/react-router' import { RaceHubPage } from '../pages/RaceHubPage' +import { MAX_BROWSER_TIMEOUT } from '../lib/schedule' import type { ContextAvailability, DatasetInfo, Meeting, RaceHub, Session, Weekend, WeekendContext } from '../types' vi.mock('../api', () => ({ @@ -375,6 +376,22 @@ describe('RaceHubPage', () => { expect(mockFetchRaceHub).not.toHaveBeenCalledWith(9473) }) + it('re-arms a bare route refresh after a capped browser timer', async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }) + mockFetchWeekendContext.mockResolvedValue({ + ...analysisContext, + race_hub_refresh_at: new Date(Date.now() + MAX_BROWSER_TIMEOUT + 1_000).toISOString(), + }) + + renderRaceHub(0) + await screen.findByTestId('race-hub') + await act(async () => { await vi.advanceTimersByTimeAsync(MAX_BROWSER_TIMEOUT) }) + + await waitFor(() => expect(mockFetchWeekendContext).toHaveBeenCalledTimes(2)) + await act(async () => { await vi.advanceTimersByTimeAsync(1_000) }) + await waitFor(() => expect(mockFetchWeekendContext).toHaveBeenCalledTimes(3)) + }) + it('keeps an explicit session URL stable across the canonical refresh boundary', async () => { vi.useFakeTimers() renderRaceHub(9472) diff --git a/frontend/src/test/schedule.test.ts b/frontend/src/test/schedule.test.ts index 0be065c..5d70f6e 100644 --- a/frontend/src/test/schedule.test.ts +++ b/frontend/src/test/schedule.test.ts @@ -5,6 +5,7 @@ import { focusMeetingKind, focusMeetingLabel, formatCountdown, + MAX_BROWSER_TIMEOUT, nextUpcomingMeeting, pickFocusMeeting, refreshDeadlineDelay, @@ -84,4 +85,10 @@ describe('schedule helpers', () => { expect(refreshDeadlineDelay('2025-05-25T13:00:00Z', Date.parse('2025-05-25T12:59:30Z'))).toBe(30_000) expect(refreshDeadlineDelay(undefined)).toBeNull() }) + + it('caps a refresh deadline beyond the browser timer maximum', () => { + const now = Date.parse('2025-05-25T12:00:00Z') + const deadline = new Date(now + MAX_BROWSER_TIMEOUT + 1_000).toISOString() + expect(refreshDeadlineDelay(deadline, now)).toBe(MAX_BROWSER_TIMEOUT) + }) })