mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 19:56:18 -04:00
Make response freshness React Query-safe, retry the failed Weekend/Preview/Briefing resources with busy gating, preserve distinct embedded Preview notices, and clear fatal Live errors once SSE supplies usable timing. Co-authored-by: Cursor <cursoragent@cursor.com>
141 lines
5.1 KiB
TypeScript
141 lines
5.1 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { fetchChampionshipHub, fetchNews, fetchWeekendContext } from '../api'
|
|
import { weekendContextNotice, type DataAvailability } from '../lib/availability'
|
|
import { championshipImpact, briefingItems } from '../lib/weekendContext'
|
|
import type {
|
|
WeekendChampionshipImpact,
|
|
WeekendBriefingItem,
|
|
WeekendContext,
|
|
} from '../types'
|
|
|
|
export type WeekendLoadState = 'loading' | 'error' | 'ready'
|
|
|
|
export interface UseWeekendContextResult {
|
|
/** Canonical context, present only when loadState === 'ready'. */
|
|
context: WeekendContext | null
|
|
loadState: WeekendLoadState
|
|
error?: Error
|
|
/** Supplementary championship movers (not part of the #72 contract). */
|
|
championship?: WeekendChampionshipImpact
|
|
/** Supplementary briefing items (not part of the #72 contract). */
|
|
briefing: WeekendBriefingItem[]
|
|
/** Non-blocking availability notice from reported context freshness/headers. */
|
|
availabilityNotice: DataAvailability | null
|
|
/** True when championship or news supplements failed while context succeeded. */
|
|
supplementsLimited: boolean
|
|
now: Date
|
|
/** Refetch canonical context and/or failed supplements as appropriate. */
|
|
refetch: () => void
|
|
/** True while a relevant refetch is in flight. */
|
|
isFetching: boolean
|
|
}
|
|
|
|
/**
|
|
* useWeekendContext reads the canonical /api/v1/weekend-context endpoint as the
|
|
* single source of truth for the Weekend home. When the canonical read succeeds
|
|
* it layers on two pieces of supplementary data that the contract intentionally
|
|
* omits — championship movers and the paddock briefing — and nothing else.
|
|
*
|
|
* It deliberately does NOT fan out to season / meetings / per-weekend / OpenF1
|
|
* session / live-state queries: those would defeat the local-first canonical read
|
|
* model and can hit the rate-limited OpenF1 REST surface during active sessions.
|
|
* Supplementary queries are gated on a successful canonical read so a failing or
|
|
* pending endpoint issues no extra requests.
|
|
*/
|
|
export function useWeekendContext(): UseWeekendContextResult {
|
|
const [now, setNow] = useState(() => Date.now())
|
|
useEffect(() => {
|
|
const timer = window.setInterval(() => setNow(Date.now()), 1000)
|
|
return () => window.clearInterval(timer)
|
|
}, [])
|
|
const nowDate = useMemo(() => new Date(now), [now])
|
|
|
|
const contextQuery = useQuery({
|
|
queryKey: ['weekend-context'],
|
|
queryFn: ({ signal }) => fetchWeekendContext(signal),
|
|
staleTime: 30_000,
|
|
})
|
|
|
|
const canonical = contextQuery.data ?? null
|
|
const canonicalReady = canonical != null
|
|
const season = canonical?.season
|
|
|
|
// Supplementary data — only fetched once the canonical context has resolved,
|
|
// so a pending/failed canonical read never triggers a request fan-out.
|
|
const championshipQuery = useQuery({
|
|
queryKey: ['championship-hub', season ?? 'current'],
|
|
queryFn: ({ signal }) => fetchChampionshipHub(season, signal),
|
|
enabled: canonicalReady,
|
|
staleTime: 60_000,
|
|
})
|
|
|
|
const newsQuery = useQuery({
|
|
queryKey: ['news', 6],
|
|
queryFn: ({ signal }) => fetchNews(6, undefined, signal),
|
|
enabled: canonicalReady,
|
|
staleTime: 60_000,
|
|
})
|
|
|
|
const championship = useMemo(
|
|
() => championshipImpact(championshipQuery.data),
|
|
[championshipQuery.data],
|
|
)
|
|
const briefing = useMemo(() => briefingItems(newsQuery.data ?? []), [newsQuery.data])
|
|
|
|
const availabilityNotice = useMemo(
|
|
() => (canonical ? weekendContextNotice(canonical) : null),
|
|
[canonical],
|
|
)
|
|
|
|
// Keep Limited visible while a failed supplement is refetching so Retry can
|
|
// expose disabled/aria-busy. React Query v5 clears isError during that refetch.
|
|
const supplementDegraded = (q: {
|
|
isError: boolean
|
|
isFetching: boolean
|
|
isFetched: boolean
|
|
data: unknown
|
|
}) => q.isError || (q.isFetching && q.isFetched && q.data == null)
|
|
|
|
const supplementsLimited =
|
|
canonicalReady &&
|
|
(supplementDegraded(championshipQuery) || supplementDegraded(newsQuery))
|
|
|
|
let loadState: WeekendLoadState = 'loading'
|
|
if (contextQuery.isError) loadState = 'error'
|
|
else if (canonicalReady) loadState = 'ready'
|
|
|
|
const refetch = () => {
|
|
if (!canonicalReady) {
|
|
if (!contextQuery.isFetching) void contextQuery.refetch()
|
|
return
|
|
}
|
|
// Freshness notice Retry refreshes the canonical context.
|
|
if (availabilityNotice && !contextQuery.isFetching) {
|
|
void contextQuery.refetch()
|
|
}
|
|
// Limited Retry must hit the failed supplements — not merely context.
|
|
// Gate on degraded (error or in-flight post-error refetch), not isError alone.
|
|
if (championshipQuery.isError && !championshipQuery.isFetching) {
|
|
void championshipQuery.refetch()
|
|
}
|
|
if (newsQuery.isError && !newsQuery.isFetching) {
|
|
void newsQuery.refetch()
|
|
}
|
|
}
|
|
|
|
return {
|
|
context: canonical,
|
|
loadState,
|
|
error: contextQuery.error instanceof Error ? contextQuery.error : undefined,
|
|
championship,
|
|
briefing,
|
|
availabilityNotice,
|
|
supplementsLimited,
|
|
now: nowDate,
|
|
refetch,
|
|
isFetching:
|
|
contextQuery.isFetching || championshipQuery.isFetching || newsQuery.isFetching,
|
|
}
|
|
}
|