| |
| |
| |
| |
| |
| import { useEffect, useRef, useCallback } from "react"; |
|
|
| export type WSEventType = |
| | "game_start" |
| | "move" |
| | "coaching_request" |
| | "coaching_result" |
| | "game_end" |
| | "training_step" |
| | "economy_update" |
| | "pong"; |
|
|
| export interface WSMessage { |
| type: WSEventType; |
| data: Record<string, unknown>; |
| } |
|
|
| interface UseBackendWSOptions { |
| url: string; |
| onMessage: (msg: WSMessage) => void; |
| onOpen?: () => void; |
| onClose?: () => void; |
| enabled?: boolean; |
| } |
|
|
| export function useBackendWS({ |
| url, |
| onMessage, |
| onOpen, |
| onClose, |
| enabled = true, |
| }: UseBackendWSOptions) { |
| const wsRef = useRef<WebSocket | null>(null); |
| const reconnectTimer = useRef<ReturnType<typeof setTimeout> | null>(null); |
| const mountedRef = useRef(true); |
|
|
| const connect = useCallback(() => { |
| if (!enabled || !mountedRef.current) return; |
| if (wsRef.current?.readyState === WebSocket.OPEN) return; |
|
|
| try { |
| const ws = new WebSocket(url); |
| wsRef.current = ws; |
|
|
| ws.onopen = () => { |
| if (onOpen) onOpen(); |
| |
| const ping = setInterval(() => { |
| if (ws.readyState === WebSocket.OPEN) { |
| ws.send(JSON.stringify({ action: "ping" })); |
| } else { |
| clearInterval(ping); |
| } |
| }, 30_000); |
| }; |
|
|
| ws.onmessage = (event) => { |
| try { |
| const msg = JSON.parse(event.data) as WSMessage; |
| onMessage(msg); |
| } catch { |
| |
| } |
| }; |
|
|
| ws.onclose = () => { |
| if (onClose) onClose(); |
| |
| if (mountedRef.current && enabled) { |
| reconnectTimer.current = setTimeout(connect, 3_000); |
| } |
| }; |
|
|
| ws.onerror = () => { |
| ws.close(); |
| }; |
| } catch { |
| |
| } |
| }, [url, onMessage, onOpen, onClose, enabled]); |
|
|
| const send = useCallback((action: string, payload: Record<string, unknown> = {}) => { |
| if (wsRef.current?.readyState === WebSocket.OPEN) { |
| wsRef.current.send(JSON.stringify({ action, ...payload })); |
| } |
| }, []); |
|
|
| const disconnect = useCallback(() => { |
| if (reconnectTimer.current) clearTimeout(reconnectTimer.current); |
| wsRef.current?.close(); |
| }, []); |
|
|
| useEffect(() => { |
| mountedRef.current = true; |
| if (enabled) connect(); |
| return () => { |
| mountedRef.current = false; |
| disconnect(); |
| }; |
| }, [connect, disconnect, enabled]); |
|
|
| return { send, disconnect, connect }; |
| } |
|
|