deadhead-space-freight-tycoon/apps/web/src/state/player.ts

56 lines
1.9 KiB
TypeScript

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 session (the server says 401). */
export async function fetchPlayerState(api: ApiClient): Promise<PlayerState | null> {
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<string>());
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]);
}