From d4f34cd43f55ec6a5a3747a5908bf9e6d18f4fe9 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Sat, 27 Sep 2025 14:25:30 -0700 Subject: [PATCH] Rewriting socket code --- client/src/App.tsx | 324 +++++++++++++----------------------- client/src/MediaControl.tsx | 12 +- server/src/app.ts | 5 + 3 files changed, 132 insertions(+), 209 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 3e6e269..a0d02f7 100755 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,5 +1,6 @@ -import React, { useState, useCallback, useEffect, useRef } from "react"; +import React, { useState, useCallback, useEffect, useRef, useMemo } from "react"; import { BrowserRouter as Router, Route, Routes, useParams, useNavigate } from "react-router-dom"; +import useWebSocket, { ReadyState } from "react-use-websocket"; import Paper from "@mui/material/Paper"; import Button from "@mui/material/Button"; @@ -44,14 +45,9 @@ const loadAudio = (src: string) => { }; const Table: React.FC = () => { - console.log("Table component rendered"); const params = useParams(); const navigate = useNavigate(); const [gameId, setGameId] = useState(params.gameId ? (params.gameId as string) : undefined); - const [ws, setWs] = useState(undefined); /* tracks full websocket lifetime */ - const [connection, setConnection] = useState(undefined); /* set after ws is in OPEN */ - const [retryConnection, setRetryConnection] = - useState(true); /* set when connection should be re-established */ const [name, setName] = useState(""); const [error, setError] = useState(undefined); const [warning, setWarning] = useState(undefined); @@ -71,7 +67,6 @@ const Table: React.FC = () => { const [houseRulesActive, setHouseRulesActive] = useState(false); const [winnerDismissed, setWinnerDismissed] = useState(false); const [global, setGlobal] = useState>({}); - const [count, setCount] = useState(0); const [audio, setAudio] = useState( localStorage.getItem("audio") ? JSON.parse(localStorage.getItem("audio") as string) : false ); @@ -83,151 +78,120 @@ const Table: React.FC = () => { ); const fields = ["id", "state", "color", "name", "private", "dice", "turn"]; - const onWsOpen = (event: Event) => { - console.log(`ws: open`); - console.log("WebSocket opened, sending game-update and get"); - setError(""); + const loc = window.location; + const protocol = loc.protocol === "https:" ? "wss" : "ws"; + const socketUrl = gameId ? `${protocol}://${loc.host}${base}/api/v1/games/ws/${gameId}` : null; - setConnection(ws); - const sock = event.target as WebSocket; - sock.send(JSON.stringify({ type: "game-update" })); - sock.send(JSON.stringify({ type: "get", fields })); - }; - - const onWsMessage = (event: MessageEvent) => { - const data = JSON.parse(event.data as string); - switch (data.type) { - case "error": - console.error(`App - error`, data.error); - setError(data.error); - break; - case "warning": - console.warn(`App - warning`, data.warning); - setWarning(data.warning); - setTimeout(() => { - setWarning(""); - }, 3000); - break; - case "game-update": - console.log("Received game-update:", data.update); - if (!loaded) { - setLoaded(true); - console.log("App: setLoaded to true"); - } - console.log(`app - message - ${data.type}`, data.update); - - if ("private" in data.update && !equal(priv, data.update.private)) { - const priv = data.update.private; - if (priv.name !== name) { - setName(priv.name); - console.log("App: setName from priv.name =", priv.name); - } - if (priv.color !== color) { - setColor(priv.color); - } - setPriv(priv); - } - - if ("name" in data.update) { - if (data.update.name) { - setName(data.update.name); - console.log("App: setName from data.update.name =", data.update.name); - } else { - console.log("App: data.update.name is empty"); - setWarning(""); - setError(""); - setPriv(undefined); - } - } - if ("id" in data.update && data.update.id !== gameId) { - setGameId(data.update.id); - } - if ("state" in data.update && data.update.state !== state) { - if (data.update.state !== "winner" && winnerDismissed) { - setWinnerDismissed(false); - } - setState(data.update.state); - } - if ("dice" in data.update && !equal(data.update.dice, dice)) { - setDice(data.update.dice); - } - if ("turn" in data.update && !equal(data.update.turn, turn)) { - setTurn(data.update.turn); - } - if ("color" in data.update && data.update.color !== color) { - setColor(data.update.color); - } - break; - default: - break; - } - }; + const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(socketUrl, { + shouldReconnect: (closeEvent) => true, + reconnectInterval: 5000, + onOpen: () => { + console.log(`ws: open`); + setError(""); + sendJsonMessage({ type: "game-update" }); + sendJsonMessage({ type: "get", fields }); + }, + onError: (err) => { + console.log("WebSocket error", err); + const error = `Connection to Ketr Ketran game server failed! Connection attempt will be retried every 5 seconds.`; + setError(error); + setGlobal(Object.assign({}, global, { ws: undefined })); + }, + onClose: () => { + console.log("WebSocket closed"); + const error = `Connection to Ketr Ketran game was lost. Attempting to reconnect...`; + setError(error); + setGlobal(Object.assign({}, global, { ws: undefined })); + }, + }); const sendUpdate = (update: unknown) => { - if (ws) ws.send(JSON.stringify(update)); + sendJsonMessage(update); }; - const cbResetConnection = useCallback(() => { - let timer: number | null = null; - function reset() { - timer = null; - setRetryConnection(true); - } - return () => { - if (timer) { - clearTimeout(timer); + useEffect(() => { + if (lastJsonMessage) { + const data = lastJsonMessage as any; + switch (data.type) { + case "error": + console.error(`App - error`, data.error); + setError(data.error); + break; + case "warning": + console.warn(`App - warning`, data.warning); + setWarning(data.warning); + setTimeout(() => { + setWarning(""); + }, 3000); + break; + case "game-update": + console.log("Received game-update:", data.update); + if (!loaded) { + setLoaded(true); + console.log("App: setLoaded to true"); + } + console.log(`app - message - ${data.type}`, data.update); + + if ("private" in data.update && !equal(priv, data.update.private)) { + const priv = data.update.private; + if (priv.name !== name) { + setName(priv.name); + console.log("App: setName from priv.name =", priv.name); + } + if (priv.color !== color) { + setColor(priv.color); + } + setPriv(priv); + } + + if ("name" in data.update) { + if (data.update.name) { + setName(data.update.name); + console.log("App: setName from data.update.name =", data.update.name); + } else { + console.log("App: data.update.name is empty"); + setWarning(""); + setError(""); + setPriv(undefined); + } + } + if ("id" in data.update && data.update.id !== gameId) { + setGameId(data.update.id); + } + if ("state" in data.update && data.update.state !== state) { + if (data.update.state !== "winner" && winnerDismissed) { + setWinnerDismissed(false); + } + setState(data.update.state); + } + if ("dice" in data.update && !equal(data.update.dice, dice)) { + setDice(data.update.dice); + } + if ("turn" in data.update && !equal(data.update.turn, turn)) { + setTurn(data.update.turn); + } + if ("color" in data.update && data.update.color !== color) { + setColor(data.update.color); + } + break; + default: + break; } - timer = window.setTimeout(reset, 5000); - }; - }, [setRetryConnection]); + } + }, [lastJsonMessage]); - const resetConnection = cbResetConnection(); - - if (global.ws !== connection || global.name !== name || global.gameId !== gameId) { - setGlobal({ - ws: connection, - name, - gameId, - }); - } - - const onWsError = () => { - const error = - `Connection to Ketr Ketran game server failed! ` + `Connection attempt will be retried every 5 seconds.`; - setError(error); - setGlobal(Object.assign({}, global, { ws: undefined })); - setWs(undefined); /* clear the socket */ - setConnection(undefined); /* clear the connection */ - resetConnection(); - }; - - const onWsClose = () => { - const error = `Connection to Ketr Ketran game was lost. ` + `Attempting to reconnect...`; - setError(error); - setGlobal(Object.assign({}, global, { ws: undefined })); - setWs(undefined); /* clear the socket */ - setConnection(undefined); /* clear the connection */ - resetConnection(); - }; - - const refWsOpen = useRef<(e: Event) => void>(() => {}); - useEffect(() => { - refWsOpen.current = onWsOpen; - }, [onWsOpen]); - const refWsMessage = useRef<(e: MessageEvent) => void>(() => {}); - useEffect(() => { - refWsMessage.current = onWsMessage; - }, [onWsMessage]); - const refWsClose = useRef<(e: CloseEvent) => void>(() => {}); - useEffect(() => { - refWsClose.current = onWsClose; - }, [onWsClose]); - const refWsError = useRef<(e: Event) => void>(() => {}); - useEffect(() => { - refWsError.current = onWsError; - }, [onWsError]); + const globalValue = useMemo(() => ({ + ws: readyState === ReadyState.OPEN ? {} : undefined, + name, + gameId, + }), [readyState, name, gameId]); useEffect(() => { + setGlobal(globalValue); + }, [globalValue, setGlobal]); + + useEffect(() => { + console.log("Table useEffect for POST running, gameId =", gameId); if (gameId) { return; } @@ -242,6 +206,7 @@ const Table: React.FC = () => { }, }) .then((res) => { + console.log("POST fetch response status:", res.status); if (res.status >= 400) { const error = `Unable to connect to Ketr Ketran game server! ` + `Try refreshing your browser in a few seconds.`; @@ -251,77 +216,18 @@ const Table: React.FC = () => { return res.json(); }) .then((update) => { + console.log("POST fetch response data:", update); if (update.id !== gameId) { navigate(`/${update.id}`); setGameId(update.id); } }) .catch((error) => { - console.error(error); + console.error("POST fetch error:", error); }); }, [gameId, setGameId]); - useEffect(() => { - if (!gameId) { - return; - } - - const unbind = () => { - console.log(`table - unbind`); - }; - - if (!ws && !connection && retryConnection) { - const loc = window.location; - let new_uri = ""; - if (loc.protocol === "https:") { - new_uri = "wss"; - } else { - new_uri = "ws"; - } - new_uri = `${new_uri}://${loc.host}${base}/api/v1/games/ws/${gameId}?${count}`; - setWs(new WebSocket(new_uri)); - setConnection(undefined); - setRetryConnection(false); - setCount(count + 1); - return unbind; - } - - if (!ws) { - return unbind; - } - - const cbOpen = (e: Event) => refWsOpen.current(e); - const cbMessage = (e: MessageEvent) => refWsMessage.current(e); - const cbClose = (e: CloseEvent) => refWsClose.current(e); - const cbError = (e: Event) => refWsError.current(e); - - ws.addEventListener("open", cbOpen); - ws.addEventListener("close", cbClose); - ws.addEventListener("error", cbError); - ws.addEventListener("message", cbMessage); - - return () => { - unbind(); - ws.removeEventListener("open", cbOpen); - ws.removeEventListener("close", cbClose); - ws.removeEventListener("error", cbError); - ws.removeEventListener("message", cbMessage); - }; - }, [ - ws, - setWs, - connection, - setConnection, - retryConnection, - setRetryConnection, - gameId, - refWsOpen, - refWsMessage, - refWsClose, - refWsError, - count, - setCount, - ]); + // WebSocket logic moved to useWebSocket useEffect(() => { if (state === "volcano") { @@ -576,6 +482,7 @@ const App: React.FC = () => { }, }) .then((res) => { + console.log("GET fetch response status:", res.status); if (res.status >= 400) { const error = `Unable to connect to Ketr Ketran game server! ` + `Try refreshing your browser in a few seconds.`; @@ -584,9 +491,12 @@ const App: React.FC = () => { return res.json(); }) .then((data) => { + console.log("GET fetch response data:", data); setPlayerId(data.player); }) - .catch(() => {}); + .catch((error) => { + console.error("GET fetch error:", error); + }); }, [playerId, setPlayerId]); if (!playerId) { @@ -596,8 +506,8 @@ const App: React.FC = () => { return ( - } path="/:gameId" /> } path="/" /> + } path="/:gameId" /> ); diff --git a/client/src/MediaControl.tsx b/client/src/MediaControl.tsx index 8d077c0..3183a11 100644 --- a/client/src/MediaControl.tsx +++ b/client/src/MediaControl.tsx @@ -9,7 +9,6 @@ import Videocam from "@mui/icons-material/Videocam"; import Box from "@mui/material/Box"; import IconButton from "@mui/material/IconButton"; import useWebSocket, { ReadyState } from "react-use-websocket"; -import { Session } from "./GlobalContext"; import WebRTCStatus from "./WebRTCStatus"; import Moveable from "react-moveable"; import { flushSync } from "react-dom"; @@ -18,6 +17,15 @@ const debug = true; // When true, do not send host candidates to the signaling server. Keeps TURN relays preferred. const FILTER_HOST_CANDIDATES = false; // Temporarily disabled to test direct connections +type Session = { + session_id: string; + peer_name: string; + has_media?: boolean; // Whether this user provides audio/video streams + attributes?: Record; + name: string; + id: string; +}; + /* ---------- Synthetic Tracks Helpers ---------- */ // Helper to hash a string to a color @@ -1701,7 +1709,7 @@ const MediaControl: React.FC = ({ snapThreshold={5} origin={false} edge - onDragStart={(e) => { + onDragStart={(e: any) => { const controls = containerRef.current?.querySelector(".Controls"); const target = e.inputEvent?.target as HTMLElement; if (controls && target && (target.closest("button") || target.closest(".MuiIconButton-root"))) { diff --git a/server/src/app.ts b/server/src/app.ts index d14674b..91e19f5 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -28,6 +28,11 @@ console.log("Hosting server from: " + basePath); let userDB: any, gameDB: any; +app.use((req, res, next) => { + console.log(`${req.method} ${req.url}`); + next(); +}); + app.use(bodyParser.json()); /* App is behind an nginx proxy which we trust, so use the remote address