feat(frontend): useGameSocket hook with auto-reconnect
This commit is contained in:
72
Frontend/src/hooks/useGameSocket.ts
Normal file
72
Frontend/src/hooks/useGameSocket.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import type { InitFrame, ServerFrame, StateFrame, CursorMessage } from '../protocol';
|
||||||
|
import { isInit, isState } from '../protocol';
|
||||||
|
|
||||||
|
export interface GameSocketState {
|
||||||
|
connected: boolean;
|
||||||
|
init: InitFrame | null;
|
||||||
|
state: StateFrame | null;
|
||||||
|
sendCursor: (x: number, y: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a WebSocket to the given URL, tracks the latest init/state frames.
|
||||||
|
* Auto-reconnects with exponential backoff on close.
|
||||||
|
*/
|
||||||
|
export function useGameSocket(url: string): GameSocketState {
|
||||||
|
const [connected, setConnected] = useState(false);
|
||||||
|
const [init, setInit] = useState<InitFrame | null>(null);
|
||||||
|
const [state, setState] = useState<StateFrame | null>(null);
|
||||||
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let closed = false;
|
||||||
|
let backoffMs = 500;
|
||||||
|
let reconnectTimer: number | null = null;
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
const ws = new WebSocket(url);
|
||||||
|
wsRef.current = ws;
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
setConnected(true);
|
||||||
|
backoffMs = 500; // reset backoff on successful connect
|
||||||
|
};
|
||||||
|
ws.onclose = () => {
|
||||||
|
setConnected(false);
|
||||||
|
wsRef.current = null;
|
||||||
|
if (!closed) {
|
||||||
|
reconnectTimer = window.setTimeout(open, backoffMs);
|
||||||
|
backoffMs = Math.min(backoffMs * 2, 8000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onerror = () => { /* let onclose handle it */ };
|
||||||
|
ws.onmessage = (ev) => {
|
||||||
|
let frame: ServerFrame;
|
||||||
|
try {
|
||||||
|
frame = JSON.parse(ev.data) as ServerFrame;
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isInit(frame)) setInit(frame);
|
||||||
|
else if (isState(frame)) setState(frame);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
open();
|
||||||
|
return () => {
|
||||||
|
closed = true;
|
||||||
|
if (reconnectTimer !== null) window.clearTimeout(reconnectTimer);
|
||||||
|
wsRef.current?.close();
|
||||||
|
};
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
const sendCursor = (x: number, y: number) => {
|
||||||
|
const ws = wsRef.current;
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||||
|
const msg: CursorMessage = { type: 'cursor', x, y };
|
||||||
|
ws.send(JSON.stringify(msg));
|
||||||
|
};
|
||||||
|
|
||||||
|
return { connected, init, state, sendCursor };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user