feat(frontend): useGameSocket hook with auto-reconnect

This commit is contained in:
meelstorm
2026-07-17 17:54:34 +00:00
committed by EugeneTes
parent c6075d88ca
commit dc35a3b1b4

View 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 };
}