1
0

Rewriting socket code

This commit is contained in:
James Ketr 2025-09-27 14:25:30 -07:00
parent 3c2c92eb79
commit d4f34cd43f
3 changed files with 132 additions and 209 deletions

View File

@ -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 { 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 Paper from "@mui/material/Paper";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
@ -44,14 +45,9 @@ const loadAudio = (src: string) => {
}; };
const Table: React.FC = () => { const Table: React.FC = () => {
console.log("Table component rendered");
const params = useParams(); const params = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const [gameId, setGameId] = useState<string | undefined>(params.gameId ? (params.gameId as string) : undefined); const [gameId, setGameId] = useState<string | undefined>(params.gameId ? (params.gameId as string) : undefined);
const [ws, setWs] = useState<WebSocket | undefined>(undefined); /* tracks full websocket lifetime */
const [connection, setConnection] = useState<WebSocket | undefined>(undefined); /* set after ws is in OPEN */
const [retryConnection, setRetryConnection] =
useState<boolean>(true); /* set when connection should be re-established */
const [name, setName] = useState<string>(""); const [name, setName] = useState<string>("");
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
const [warning, setWarning] = useState<string | undefined>(undefined); const [warning, setWarning] = useState<string | undefined>(undefined);
@ -71,7 +67,6 @@ const Table: React.FC = () => {
const [houseRulesActive, setHouseRulesActive] = useState<boolean>(false); const [houseRulesActive, setHouseRulesActive] = useState<boolean>(false);
const [winnerDismissed, setWinnerDismissed] = useState<boolean>(false); const [winnerDismissed, setWinnerDismissed] = useState<boolean>(false);
const [global, setGlobal] = useState<Record<string, unknown>>({}); const [global, setGlobal] = useState<Record<string, unknown>>({});
const [count, setCount] = useState<number>(0);
const [audio, setAudio] = useState<boolean>( const [audio, setAudio] = useState<boolean>(
localStorage.getItem("audio") ? JSON.parse(localStorage.getItem("audio") as string) : false 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 fields = ["id", "state", "color", "name", "private", "dice", "turn"];
const onWsOpen = (event: Event) => { const loc = window.location;
console.log(`ws: open`); const protocol = loc.protocol === "https:" ? "wss" : "ws";
console.log("WebSocket opened, sending game-update and get"); const socketUrl = gameId ? `${protocol}://${loc.host}${base}/api/v1/games/ws/${gameId}` : null;
setError("");
setConnection(ws); const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(socketUrl, {
const sock = event.target as WebSocket; shouldReconnect: (closeEvent) => true,
sock.send(JSON.stringify({ type: "game-update" })); reconnectInterval: 5000,
sock.send(JSON.stringify({ type: "get", fields })); onOpen: () => {
}; console.log(`ws: open`);
setError("");
const onWsMessage = (event: MessageEvent) => { sendJsonMessage({ type: "game-update" });
const data = JSON.parse(event.data as string); sendJsonMessage({ type: "get", fields });
switch (data.type) { },
case "error": onError: (err) => {
console.error(`App - error`, data.error); console.log("WebSocket error", err);
setError(data.error); const error = `Connection to Ketr Ketran game server failed! Connection attempt will be retried every 5 seconds.`;
break; setError(error);
case "warning": setGlobal(Object.assign({}, global, { ws: undefined }));
console.warn(`App - warning`, data.warning); },
setWarning(data.warning); onClose: () => {
setTimeout(() => { console.log("WebSocket closed");
setWarning(""); const error = `Connection to Ketr Ketran game was lost. Attempting to reconnect...`;
}, 3000); setError(error);
break; setGlobal(Object.assign({}, global, { ws: undefined }));
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 sendUpdate = (update: unknown) => { const sendUpdate = (update: unknown) => {
if (ws) ws.send(JSON.stringify(update)); sendJsonMessage(update);
}; };
const cbResetConnection = useCallback(() => { useEffect(() => {
let timer: number | null = null; if (lastJsonMessage) {
function reset() { const data = lastJsonMessage as any;
timer = null; switch (data.type) {
setRetryConnection(true); case "error":
} console.error(`App - error`, data.error);
return () => { setError(data.error);
if (timer) { break;
clearTimeout(timer); 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); }
}; }, [lastJsonMessage]);
}, [setRetryConnection]);
const resetConnection = cbResetConnection(); const globalValue = useMemo(() => ({
ws: readyState === ReadyState.OPEN ? {} : undefined,
if (global.ws !== connection || global.name !== name || global.gameId !== gameId) { name,
setGlobal({ gameId,
ws: connection, }), [readyState, name, gameId]);
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]);
useEffect(() => { useEffect(() => {
setGlobal(globalValue);
}, [globalValue, setGlobal]);
useEffect(() => {
console.log("Table useEffect for POST running, gameId =", gameId);
if (gameId) { if (gameId) {
return; return;
} }
@ -242,6 +206,7 @@ const Table: React.FC = () => {
}, },
}) })
.then((res) => { .then((res) => {
console.log("POST fetch response status:", res.status);
if (res.status >= 400) { if (res.status >= 400) {
const error = const error =
`Unable to connect to Ketr Ketran game server! ` + `Try refreshing your browser in a few seconds.`; `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(); return res.json();
}) })
.then((update) => { .then((update) => {
console.log("POST fetch response data:", update);
if (update.id !== gameId) { if (update.id !== gameId) {
navigate(`/${update.id}`); navigate(`/${update.id}`);
setGameId(update.id); setGameId(update.id);
} }
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error("POST fetch error:", error);
}); });
}, [gameId, setGameId]); }, [gameId, setGameId]);
useEffect(() => { // WebSocket logic moved to useWebSocket
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,
]);
useEffect(() => { useEffect(() => {
if (state === "volcano") { if (state === "volcano") {
@ -576,6 +482,7 @@ const App: React.FC = () => {
}, },
}) })
.then((res) => { .then((res) => {
console.log("GET fetch response status:", res.status);
if (res.status >= 400) { if (res.status >= 400) {
const error = const error =
`Unable to connect to Ketr Ketran game server! ` + `Try refreshing your browser in a few seconds.`; `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(); return res.json();
}) })
.then((data) => { .then((data) => {
console.log("GET fetch response data:", data);
setPlayerId(data.player); setPlayerId(data.player);
}) })
.catch(() => {}); .catch((error) => {
console.error("GET fetch error:", error);
});
}, [playerId, setPlayerId]); }, [playerId, setPlayerId]);
if (!playerId) { if (!playerId) {
@ -596,8 +506,8 @@ const App: React.FC = () => {
return ( return (
<Router basename={base}> <Router basename={base}>
<Routes> <Routes>
<Route element={<Table />} path="/:gameId" />
<Route element={<Table />} path="/" /> <Route element={<Table />} path="/" />
<Route element={<Table />} path="/:gameId" />
</Routes> </Routes>
</Router> </Router>
); );

View File

@ -9,7 +9,6 @@ import Videocam from "@mui/icons-material/Videocam";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import useWebSocket, { ReadyState } from "react-use-websocket"; import useWebSocket, { ReadyState } from "react-use-websocket";
import { Session } from "./GlobalContext";
import WebRTCStatus from "./WebRTCStatus"; import WebRTCStatus from "./WebRTCStatus";
import Moveable from "react-moveable"; import Moveable from "react-moveable";
import { flushSync } from "react-dom"; 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. // 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 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<string, any>;
name: string;
id: string;
};
/* ---------- Synthetic Tracks Helpers ---------- */ /* ---------- Synthetic Tracks Helpers ---------- */
// Helper to hash a string to a color // Helper to hash a string to a color
@ -1701,7 +1709,7 @@ const MediaControl: React.FC<MediaControlProps> = ({
snapThreshold={5} snapThreshold={5}
origin={false} origin={false}
edge edge
onDragStart={(e) => { onDragStart={(e: any) => {
const controls = containerRef.current?.querySelector(".Controls"); const controls = containerRef.current?.querySelector(".Controls");
const target = e.inputEvent?.target as HTMLElement; const target = e.inputEvent?.target as HTMLElement;
if (controls && target && (target.closest("button") || target.closest(".MuiIconButton-root"))) { if (controls && target && (target.closest("button") || target.closest(".MuiIconButton-root"))) {

View File

@ -28,6 +28,11 @@ console.log("Hosting server from: " + basePath);
let userDB: any, gameDB: any; let userDB: any, gameDB: any;
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
app.use(bodyParser.json()); app.use(bodyParser.json());
/* App is behind an nginx proxy which we trust, so use the remote address /* App is behind an nginx proxy which we trust, so use the remote address