import type { PlayerState } from "@deadhead/schema"; import { useQuery } from "@tanstack/react-query"; import { useEffect, useRef } from "react"; import { ApiError, type ApiClient } from "../api/client"; import { useApi } from "../api/context"; import { useClock } from "../clock/clock"; export const PLAYER_STATE_KEY = ["player-state"] as const; /** Player state, or null when there is no company on this device yet ("unauthorized"). */ export async function fetchPlayerState(api: ApiClient): Promise { try { return await api.call("getPlayerState", {}); } catch (error) { if (error instanceof ApiError && error.code === "unauthorized") return null; throw error; } } /** * Server player state (Step 8.5), shared by every screen. Refetched when the app * regains focus; `App` also runs `useRefetchOnArrival` once for arrivals. */ export function usePlayerState() { const api = useApi(); const query = useQuery({ queryKey: PLAYER_STATE_KEY, queryFn: () => fetchPlayerState(api), // Screens share this query; fresh for 10 s so mounting a screen does not refetch. // Returning to the app, arrivals and actions always refetch. staleTime: 10_000, refetchOnWindowFocus: "always", }); return query; } /** * Refetches once the server clock passes the earliest in-flight arrival, so the * server resolves it and the UI shows the result. Each dispatch triggers at most * one refetch. */ export function useRefetchOnArrival(state: PlayerState | null, refetch: () => unknown) { const now = useClock((s) => s.now); const triggered = useRef(new Set()); useEffect(() => { if (!state) return; const arrived = state.activeDispatches.filter( (d) => d.arrivalAt <= now && !triggered.current.has(d.id), ); if (arrived.length === 0) return; for (const d of arrived) triggered.current.add(d.id); void refetch(); }, [state, now, refetch]); }