| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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 pingTimer = useRef<ReturnType<typeof setInterval> | null>(null); |
| | const mountedRef = useRef(true); |
| |
|
| | |
| | const onMessageRef = useRef(onMessage); |
| | const onOpenRef = useRef(onOpen); |
| | const onCloseRef = useRef(onClose); |
| | const enabledRef = useRef(enabled); |
| |
|
| | |
| | onMessageRef.current = onMessage; |
| | onOpenRef.current = onOpen; |
| | onCloseRef.current = onClose; |
| | enabledRef.current = enabled; |
| |
|
| | const clearPing = useCallback(() => { |
| | if (pingTimer.current) { |
| | clearInterval(pingTimer.current); |
| | pingTimer.current = null; |
| | } |
| | }, []); |
| |
|
| | |
| | const connect = useCallback(() => { |
| | if (!enabledRef.current || !mountedRef.current) return; |
| | |
| | if ( |
| | wsRef.current?.readyState === WebSocket.OPEN || |
| | wsRef.current?.readyState === WebSocket.CONNECTING |
| | ) return; |
| |
|
| | try { |
| | const ws = new WebSocket(url); |
| | wsRef.current = ws; |
| |
|
| | ws.onopen = () => { |
| | if (onOpenRef.current) onOpenRef.current(); |
| | |
| | clearPing(); |
| | pingTimer.current = setInterval(() => { |
| | if (ws.readyState === WebSocket.OPEN) { |
| | ws.send(JSON.stringify({ action: "ping" })); |
| | } else { |
| | clearPing(); |
| | } |
| | }, 30_000); |
| | }; |
| |
|
| | ws.onmessage = (event) => { |
| | try { |
| | const msg = JSON.parse(event.data) as WSMessage; |
| | onMessageRef.current(msg); |
| | } catch { |
| | |
| | } |
| | }; |
| |
|
| | ws.onclose = () => { |
| | clearPing(); |
| | if (onCloseRef.current) onCloseRef.current(); |
| | |
| | if (mountedRef.current && enabledRef.current) { |
| | reconnectTimer.current = setTimeout(connect, 3_000); |
| | } |
| | }; |
| |
|
| | ws.onerror = () => { |
| | ws.close(); |
| | }; |
| | } catch { |
| | |
| | if (mountedRef.current && enabledRef.current) { |
| | reconnectTimer.current = setTimeout(connect, 3_000); |
| | } |
| | } |
| | |
| | |
| | }, [url, clearPing]); |
| |
|
| | 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); |
| | reconnectTimer.current = null; |
| | } |
| | clearPing(); |
| | wsRef.current?.close(); |
| | wsRef.current = null; |
| | }, [clearPing]); |
| |
|
| | |
| | useEffect(() => { |
| | mountedRef.current = true; |
| | if (enabled) { |
| | connect(); |
| | } else { |
| | disconnect(); |
| | } |
| | return () => { |
| | mountedRef.current = false; |
| | disconnect(); |
| | }; |
| | |
| | }, [enabled, url]); |
| |
|
| | return { send, disconnect, connect }; |
| | } |
| |
|