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,19 +78,40 @@ 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("");
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) => {
sendJsonMessage(update);
}; };
const onWsMessage = (event: MessageEvent) => { useEffect(() => {
const data = JSON.parse(event.data as string); if (lastJsonMessage) {
const data = lastJsonMessage as any;
switch (data.type) { switch (data.type) {
case "error": case "error":
console.error(`App - error`, data.error); console.error(`App - error`, data.error);
@ -161,73 +177,21 @@ const Table: React.FC = () => {
default: default:
break; break;
} }
};
const sendUpdate = (update: unknown) => {
if (ws) ws.send(JSON.stringify(update));
};
const cbResetConnection = useCallback(() => {
let timer: number | null = null;
function reset() {
timer = null;
setRetryConnection(true);
} }
return () => { }, [lastJsonMessage]);
if (timer) {
clearTimeout(timer);
}
timer = window.setTimeout(reset, 5000);
};
}, [setRetryConnection]);
const resetConnection = cbResetConnection(); const globalValue = useMemo(() => ({
ws: readyState === ReadyState.OPEN ? {} : undefined,
if (global.ws !== connection || global.name !== name || global.gameId !== gameId) {
setGlobal({
ws: connection,
name, name,
gameId, gameId,
}); }), [readyState, 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