import { useCallback, useEffect, useRef, useState } from 'react' import type { ClientMessage, GameView, Session } from './types' interface ServerMessage { type: 'state' | 'error' state?: GameView error?: string } // useGame owns the WebSocket for a session: it keeps the latest server view, // reconnects with backoff, and exposes send() for actions. Server-rejected // actions surface as a transient `error`. export function useGame(session: Session) { const [view, setView] = useState(null) const [error, setError] = useState(null) const [connected, setConnected] = useState(false) const wsRef = useRef(null) const errorTimer = useRef(undefined) useEffect(() => { let ws: WebSocket let closed = false let retryDelay = 500 let retryTimer: number | undefined function connect() { const proto = location.protocol === 'https:' ? 'wss' : 'ws' const params = new URLSearchParams({ game: session.gameId, player: session.playerId, token: session.token, }) ws = new WebSocket(`${proto}://${location.host}/api/ws?${params}`) wsRef.current = ws ws.onopen = () => { retryDelay = 500 setConnected(true) } ws.onmessage = (ev) => { const msg = JSON.parse(ev.data) as ServerMessage if (msg.type === 'state' && msg.state) setView(msg.state) if (msg.type === 'error' && msg.error) showError(msg.error) } ws.onclose = () => { setConnected(false) if (closed) return retryTimer = window.setTimeout(connect, retryDelay) retryDelay = Math.min(retryDelay * 2, 8000) } } function showError(msg: string) { setError(msg) window.clearTimeout(errorTimer.current) errorTimer.current = window.setTimeout(() => setError(null), 3500) } connect() return () => { closed = true window.clearTimeout(retryTimer) window.clearTimeout(errorTimer.current) ws.close() } }, [session.gameId, session.playerId, session.token]) const send = useCallback((msg: ClientMessage) => { wsRef.current?.send(JSON.stringify(msg)) }, []) return { view, error, connected, send } }