import React, { useState, useCallback, useEffect, useRef } from "react"; import { BrowserRouter as Router, Route, Routes, useParams } from "react-router-dom"; import Paper from '@material-ui/core/Paper'; import Button from '@material-ui/core/Button'; import { GlobalContext } from "./GlobalContext.js"; //import { PingPong } from "./PingPong.js"; import { PlayerList } from "./PlayerList.js"; import { Chat } from "./Chat.js"; import { MediaAgent } from "./MediaControl.js"; import { Board } from "./Board.js"; import { Actions } from "./Actions.js"; import { base, gamesPath } from './Common.js'; import { GameOrder } from "./GameOrder.js"; import { Activities } from "./Activities.js"; import { SelectPlayer } from "./SelectPlayer.js"; import { PlayersStatus } from "./PlayersStatus.js"; import { ViewCard } from "./ViewCard.js"; import { ChooseCard } from "./ChooseCard.js"; import { Hand } from "./Hand.js"; import { Trade } from "./Trade.js"; import { Winner } from "./Winner.js"; import history from "./history.js"; import "./App.css"; import equal from "fast-deep-equal"; const Table = () => { const params = useParams(); const [ gameId, setGameId ] = useState(params.gameId ? params.gameId : undefined); const [ ws, setWs ] = useState(); /* 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); const [ peers, setPeers ] = useState({}); const [loaded, setLoaded] = useState(false); const [state, setState] = useState(undefined); const [color, setColor] = useState(undefined); const [priv, setPriv] = useState(undefined); const [buildActive, setBuildActive] = useState(false); const [cardActive, setCardActive] = useState(undefined); const [winnerDismissed, setWinnerDismissed] = useState(undefined); const fields = [ 'id', 'state', 'color', 'name', 'private' ]; useEffect(() => { console.log(`app - media-agent - peers`, peers); }, [peers]); const onWsOpen = (event) => { console.log(`ws: open`); setError(""); /* We do not set the socket as connected until the 'open' message * comes through */ setConnection(ws); /* Request a full game-update * We only need gameId and name for App.js, however in the event * of a network disconnect, we need to refresh the entire game * state on reload so all bound components reflect the latest * state */ event.target.send(JSON.stringify({ type: 'game-update' })); event.target.send(JSON.stringify({ type: 'get', fields })); }; const onWsMessage = (event) => { const data = JSON.parse(event.data); 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(() => { console.log(`todo: stack warnings in a window and have them disappear one at a time.`); console.log(`app - clearing warning`); setWarning(""); }, 3000); break; case 'game-update': if (!loaded) { setLoaded(true); } console.log(`ws: message - ${data.type}`, data.update); if ('private' in data.update && !equal(priv, data.update.private)) { const priv = data.update.private; if (priv.name !== name) { console.log(`App - setting name (via private): ${priv.name}`); setName(priv.name); } if (priv.color !== color) { console.log(`App - setting color (via private): ${priv.color}`); setColor(priv.color); } setPriv(priv); } if ('name' in data.update) { if (data.update.name) { console.log(`App - setting name: ${data.update.name}`); setName(data.update.name); } else { setWarning(""); setError(""); setPriv(undefined); } } if ('id' in data.update && data.update.id !== gameId) { console.log(`App - setting gameId ${data.update.id}`); setGameId(data.update.id); } if ('state' in data.update && data.update.state !== state) { console.log(`App - setting game state: ${data.update.state}`); setState(data.update.state); } if ('color' in data.update && data.update.color !== color) { console.log(`App - setting color: ${color}`); setColor(data.update.color); } break; default: break; } }; const sendUpdate = (update) => { ws.send(JSON.stringify(update)); }; const cbResetConnection = useCallback(() => { let timer = 0; function reset() { timer = 0; setRetryConnection(true); }; return _ => { if (timer) { clearTimeout(timer); } timer = setTimeout(reset, 5000); }; }, [setRetryConnection]); const resetConnection = cbResetConnection(); const onWsError = (event) => { console.error(`ws: error`, event); const error = `Connection to Ketr Ketran game server failed! ` + `Connection attempt will be retried every 5 seconds.`; setError(error); setWs(undefined); /* clear the socket */ setConnection(undefined); /* clear the connection */ resetConnection(); }; const onWsClose = (event) => { const error = `Connection to Ketr Ketran game was lost. ` + `Attempting to reconnect...`; console.warn(`ws: close`); setError(error); setWs(undefined); /* clear the socket */ setConnection(undefined); /* clear the connection */ resetConnection(); }; /* callback refs are used to provide correct state reference * in the callback handlers, while also preventing rebinding * of event handlers on every render */ const refWsOpen = useRef(onWsOpen); useEffect(() => { refWsOpen.current = onWsOpen; }); const refWsMessage = useRef(onWsMessage); useEffect(() => { refWsMessage.current = onWsMessage; }); const refWsClose = useRef(onWsClose); useEffect(() => { refWsClose.current = onWsClose; }); const refWsError = useRef(onWsError); useEffect(() => { refWsError.current = onWsError; }); /* This effect is responsible for triggering a new game load if a * game id is not provided in the URL. If the game is provided * in the URL, the backend will create a new game if necessary * during the WebSocket connection sequence. * * This should be the only HTTP request made from the game. */ useEffect(() => { if (gameId) { console.log(`Game in use ${gameId}`) return; } console.log(`Requesting new game.`); window.fetch(`${base}/api/v1/games/`, { method: 'POST', cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, }).then((res) => { if (res.status >= 400) { const error = `Unable to connect to Ketr Ketran game server! ` + `Try refreshing your browser in a few seconds.`; console.error(error); setError(error); throw new Error(error); } return res.json(); }).then((update) => { if (update.id !== gameId) { console.log(`Game available: ${update.id}`); history.push(`${gamesPath}/${update.id}`); setGameId(update.id); } }).catch((error) => { console.error(error); }); }, [ gameId, setGameId ]); /* Once a game id is known, create the sole WebSocket connection * to the backend. This WebSocket is then shared with any component * that performs game state updates. Those components should * bind to the 'message:game-update' WebSocket event and parse * their update information from those messages */ useEffect(() => { if (!gameId) { return; } const unbind = () => { console.log(`table - unbind`); } console.log(`table - bind`); if (!ws && !connection && retryConnection) { let loc = window.location, 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}`; console.log(`Attempting WebSocket connection to ${new_uri}`); setWs(new WebSocket(new_uri)); setConnection(undefined); setRetryConnection(false); return unbind; } if (!ws) { return unbind; } const cbOpen = e => refWsOpen.current(e); const cbMessage = e => refWsMessage.current(e); const cbClose = e => refWsClose.current(e); const cbError = e => 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); } }, [ setWs, connection, setConnection, retryConnection, setRetryConnection, gameId, ws, refWsOpen, refWsMessage, refWsClose, refWsError ]); return { /* */ } { error && { error } { setError("")}}>dismiss } { priv && priv.turnNotice && { priv.turnNotice } { sendUpdate({type: 'turn-notice'}) }}>dismiss } { warning && { warning } { setWarning("")}}>dismiss } { state === 'normal' && } { color && state === 'game-order' && } { !winnerDismissed && } { name !== "" && } { name !== "" && } { /* name !== "" && */ } { loaded && } ; }; const App = () => { const [playerId, setPlayerId] = useState(undefined); const [error, setError] = useState(undefined); useEffect(() => { if (playerId) { return; } window.fetch(`${base}/api/v1/games/`, { method: 'GET', cache: 'no-cache', credentials: 'same-origin', /* include cookies */ headers: { 'Content-Type': 'application/json' }, }).then((res) => { if (res.status >= 400) { const error = `Unable to connect to Ketr Ketran game server! ` + `Try refreshing your browser in a few seconds.`; console.error(error); setError(error); } console.log(res.headers); return res.json(); }).then((data) => { setPlayerId(data.player); }).catch((error) => { }); }, [playerId, setPlayerId]); if (!playerId) { return <>{ error }>; } return ( } path={`${base}/:gameId`}/> } path={`${base}`}/> ); } export default App;