UNPKG

@four-leaf-studios/rl-socket-hook

Version:

A tiny React wrapper around a Rocket League WebSocket plugin (`ws://localhost:49122`). It provides:

112 lines (109 loc) 4.04 kB
import { useReducer, useState, useRef, useCallback, useEffect } from 'react'; // useRocketLeagueSocket.ts const reducer = (state, action) => ({ ...state, [action.type]: action.payload, }); const normalizeEvent = (raw) => { if (typeof raw === "object" && raw !== null) { // { event: "name", data: {...} } if (typeof raw.event === "string" && "data" in raw) { return [[raw.event, raw.data]]; } // { "evt1": {...}, "evt2": {...} } return Object.entries(raw); } return []; }; function useRocketLeagueSocket(url = "ws://localhost:49122", options) { const { maxRetries = 5, heartbeatIntervalMs = 30_000 } = options || {}; // Reducer state holds the map of eventName → payload const [events, dispatch] = useReducer(reducer, {}); const [readyState, setReadyState] = useState(WebSocket.CLOSED); const [error, setError] = useState(null); // Refs for the socket, retry count, and heartbeat timer const socketRef = useRef(null); const retryRef = useRef(0); const heartbeatRef = useRef(undefined); // Expose a send() helper const send = useCallback((event, data) => { const sock = socketRef.current; if (sock?.readyState === WebSocket.OPEN) { sock.send(JSON.stringify({ event, data })); } else { console.warn("Cannot send, socket not open:", event); } }, []); useEffect(() => { let isMounted = true; const connect = () => { const ws = new WebSocket(url); socketRef.current = ws; setReadyState(ws.readyState); ws.onopen = () => { if (!isMounted) return; retryRef.current = 0; setReadyState(ws.readyState); // Start heartbeat to keep alive heartbeatRef.current = window.setInterval(() => { ws.send(JSON.stringify({ event: "ping" })); }, heartbeatIntervalMs); }; ws.onmessage = (e) => { if (!isMounted) return; let raw; try { raw = JSON.parse(e.data); } catch { console.error("Invalid JSON from server:", e.data); return; } // ignore pong replies if (raw.event === "pong") return; // dispatch each normalized event for (const [evt, payload] of normalizeEvent(raw)) { dispatch({ type: evt, payload }); } }; ws.onerror = (ev) => { if (!isMounted) return; console.error("WebSocket error", ev); setError(ev); }; ws.onclose = () => { if (!isMounted) return; setReadyState(WebSocket.CLOSED); if (heartbeatRef.current !== undefined) { clearInterval(heartbeatRef.current); } // attempt reconnect if (retryRef.current < maxRetries) { const backoff = 2 ** retryRef.current * 1000; retryRef.current += 1; setTimeout(connect, backoff); } else { console.warn("Reached max WebSocket retries"); } }; }; connect(); return () => { isMounted = false; if (heartbeatRef.current !== undefined) { clearInterval(heartbeatRef.current); } socketRef.current?.close(); }; }, [url, maxRetries, heartbeatIntervalMs]); return { events, readyState, error, send }; } export { useRocketLeagueSocket as default, useRocketLeagueSocket }; //# sourceMappingURL=useRocketLeagueSocket.js.map