1
0

Improved WebSocket handshake

Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
This commit is contained in:
James Ketrenos 2022-03-02 13:55:30 -08:00
parent f023ecd41d
commit e3ccc122dd
3 changed files with 164 additions and 106 deletions

View File

@ -1,8 +1,6 @@
import React, { useState, useCallback, useEffect } from "react";
import React, { useState } from "react";
import "./Activities.css";
import Paper from '@material-ui/core/Paper';
import Resource from './Resource.js';
import { getPlayerName } from './Common.js';
import PlayerColor from './PlayerColor.js';
import Dice from './Dice.js';
@ -65,33 +63,41 @@ const Activities = ({ table }) => {
normalPlay = (game.state === 'initial-placement' || game.state === 'normal'),
mustDiscard = game.player ? parseInt(game.player.mustDiscard ? game.player.mustDiscard : 0) : 0,
mustPlaceRobber = (game.turn && !game.turn.placedRobber && game.turn.robberInAction),
isInitialPlacement = (game.state == 'initial-placement'),
placeRoad = isInitialPlacement && game.turn && game.turn.actions.indexOf('place-road') !== -1;
isInitialPlacement = (game.state === 'initial-placement'),
placeRoad = isInitialPlacement && game.turn && game.turn.actions && game.turn.actions.indexOf('place-road') !== -1,
mustStealResource = game.turn && game.turn.actions && game.turn.actions.indexOf('steal-resource') !== -1;
const list = game.activities
.filter(activity => game.timestamp - activity.date < 11000)
.map(activity => {
return <Activity key={activity.date} activity={activity}/>;
});
let who;
if (isTurn) {
who = 'You';
} else {
who = <><PlayerColor color={table.game.turn.color}/> {table.game.turn.name}</>
}
return (
<div className="Activities">
{ list }
{ !isTurn && normalPlay && game.player && mustDiscard === 0 && mustPlaceRobber &&
<div className="Requirement"><PlayerColor color={table.game.turn.color}/> {table.game.turn.name} must move the Robber.</div>
{ normalPlay && mustDiscard === 0 && mustPlaceRobber &&
<div className="Requirement">{who} must move the Robber.</div>
}
{ isTurn && normalPlay && game.player && mustDiscard === 0 && mustPlaceRobber &&
<div className="Requirement">You must move the Robber.</div>
{ isInitialPlacement &&
<div className="Requirement">{who} must place a {placeRoad ? 'road' : 'settlement'}.</div>
}
{ normalPlay && game.player && mustDiscard !== 0 &&
{ mustStealResource &&
<div className="Requirement">{who} must select a player to steal from.</div>
}
{ normalPlay && mustDiscard !== 0 &&
<div className="Requirement">You must discard <b>{mustDiscard}</b> cards.</div>
}
{ isTurn && isInitialPlacement &&
<div className="Requirement">You must place a {placeRoad ? 'road' : 'settlement'}.</div>
}
{ !isTurn && normalPlay &&
<div>It is <PlayerColor color={table.game.turn.color}/> {table.game.turn.name}'s turn.</div>

View File

@ -738,7 +738,7 @@ class Table extends React.Component {
if (isDead) {
console.log(`Short circuiting keep-alive`);
} else {
console.log(`Resetting keep-alive`);
console.log(`${this.game.name} Resetting keep-alive: ${(Date.now() - this.game.startTime) / 1000}`);
}
if (this.keepAlive) {
@ -749,7 +749,7 @@ class Table extends React.Component {
}
this.keepAlive = setTimeout(() => {
console.error(`No server ping after 10 seconds (or connection closed by server)!`);
console.log(`${this.game.name} No ping after 10 seconds: ${(Date.now() - this.game.startTime) / 1000}`);
this.setState({ noNetwork: true });
if (this.ws) {
this.ws.close();
@ -782,12 +782,13 @@ class Table extends React.Component {
this.ws = new WebSocket(new_uri);
this.ws.onopen = (event) => {
console.log(`WebSocket open:`, event);
this.ws.addEventListener('open', (event) => {
console.log(`${this.game.name} WebSocket open: Sending game-update request: ${(Date.now() - this.game.startTime) / 1000}`);
this.ws.send(JSON.stringify({ type: 'game-update' }));
this.resetKeepAlive();
};
});
this.ws.onmessage = (event) => {
this.ws.addEventListener('message', (event) => {
this.resetKeepAlive();
let data;
@ -813,19 +814,19 @@ class Table extends React.Component {
console.log(`Unknown event type: ${data.type}`);
break;
}
}
});
this.ws.onerror = (event) => {
this.ws.addEventListener('error', (event) => {
this.setState({ error: event.message });
console.error(`WebSocket error:`, event);
console.error(`${this.game.name} WebSocket error: ${(Date.now() - this.game.startTime) / 1000}`);
this.resetKeepAlive(true);
};
});
this.ws.onclose = (event) => {
console.error(`WebSocket close:`, event);
this.ws.addEventListener('close', (event) => {
console.log(`${this.game.name} WebSocket close: ${(Date.now() - this.game.startTime) / 1000}`);
this.setState({ error: event.message });
this.resetKeepAlive(true);
};
});
}
componentDidMount() {

View File

@ -445,8 +445,7 @@ const getSession = (game, session) => {
const id = session.player_id;
/* If this session is not yet in the game,
* add it and set the player's name */
/* If this session is not yet in the game, add it and set the player's name */
if (!(id in game.sessions)) {
game.sessions[id] = {
name: undefined,
@ -455,6 +454,22 @@ const getSession = (game, session) => {
};
}
/* Expire old unused sessions */
for (let id in game.sessions) {
const tmp = game.sessions[id];
if (tmp.color || tmp.name || tmp.player) {
continue;
}
if (tmp.player_id === session.player_id) {
continue;
}
/* 10 minutes */
if (tmp.lastActive && tmp.lastActive < Date.now() - 10 * 60 * 1000) {
console.log(`Expiring old session ${id}`);
delete game.sessions[id];
}
}
return game.sessions[id];
};
@ -1762,7 +1777,6 @@ router.put("/:id/:action/:value?", async (req, res) => {
if (colors.length) {
game.turn.actions = [ 'steal-resource' ],
game.turn.limits = { players: colors };
addChatMessage(game, session, `${session.name} must select player to steal resource from.`);
} else {
game.turn.actions = [];
game.turn.robberInAction = false;
@ -1844,6 +1858,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
debugChat(game, 'Before development purchase');
addActivity(game, session, `${session.name} purchased a development card.`);
addChatMessage(game, session, `${session.name} spent 1 stone, 1 wheat, and 1 sheep to purchase a development card.`)
player.stone--;
player.wheat--;
player.sheep--;
@ -2105,6 +2120,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
player.settlements--;
if (!game.turn.free) {
addChatMessage(game, session, `${session.name} spent 1 brick, 1 wood, 1 sheep, and 1 wheat to purchase a settlement.`)
player.brick--;
player.wood--;
player.wheat--;
@ -2263,6 +2279,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
player.cities--;
player.settlements++;
if (!game.turn.free) {
addChatMessage(game, session, `${session.name} spent 2 wheat, and 1 stone to upgrade to a city.`)
player.wheat -= 2;
player.stone -= 3;
}
@ -2351,6 +2368,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
player.roads--;
if (!game.turn.free) {
addChatMessage(game, session, `${session.name} spent 1 brick and 1 wood to purchase a road.`)
player.brick--;
player.wood--;
}
@ -2515,14 +2533,50 @@ const ping = (session) => {
if (session.keepAlive) {
clearTimeout(session.keepAlive);
}
session.keepAlive = setTimeout(() => { ping(session); }, 2500);
session.keepAlive = setTimeout(() => { ping(session); }, 2500);
}
router.ws("/ws/:id", async (ws, req) => {
const { id } = req.params;
console.log(`WebSocket connect from game ${id}`);
/* Setup WebSocket event handlers prior to performing any async calls or
* we may miss the first messages from clients */
ws.on('error', (event) => {
console.error(`WebSocket error: `, event.message);
});
ws.on('open', (event) => {
console.log(`WebSocket open: `, event.message);
});
ws.on('message', async (message) => {
/* Ensure the session is loaded prior to the first 'message'
* being processed */
const game = await loadGame(id);
if (!game) {
console.error(`Unable to load/create new game for WS request.`);
return;
}
const session = getSession(game, req.session);
try {
const data = JSON.parse(message);
switch (data.type) {
case 'pong':
console.log(`Latency for ${session.name ? session.name : 'Unammed'} is ${Date.now() - data.timestamp}`);
break;
case 'game-update':
console.log(`Player ${session.name ? session.name : 'Unnamed'} requested a game update.`);
sendGame(req, undefined, game, undefined, ws);
break;
}
} catch (error) {
console.error(error);
}
});
/* This will result in the node tick moving forward; if we haven't already
* setup the event handlers, a 'message' could come through prior to this
* completing */
const game = await loadGame(id);
if (!game) {
console.error(`Unable to load/create new game for WS request.`);
@ -2530,7 +2584,9 @@ router.ws("/ws/:id", async (ws, req) => {
}
const session = getSession(game, req.session);
console.log(`WebSocket connect from game ${id}:${session.name ? session.name : "Unnamed"}`);
if (session) {
console.log(`WebSocket connected for ${session.name ? session.name : "Unnamed"}`);
session.ws = ws;
@ -2541,27 +2597,6 @@ router.ws("/ws/:id", async (ws, req) => {
} else {
console.log(`No session found for WebSocket with id ${id}`);
}
ws.on('error', (event) => {
console.error(`WebSocket error: `, event.message);
});
ws.on('open', (event) => {
console.log(`WebSocket open: `, event.message);
});
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
switch (data.type) {
case 'pong':
console.log(`Latency for ${session.name ? session.name : 'Unammed'} is ${Date.now() - data.timestamp}`);
break;
}
} catch (error) {
console.error(error);
}
});
});
router.get("/:id", async (req, res/*, next*/) => {
@ -2619,7 +2654,50 @@ const getActiveCount = (game) => {
return active;
}
const sendGame = async (req, res, game, error) => {
const sendGameToSession = (session, reducedSessions, game, reducedGame, error, res) => {
const player = session.player ? session.player : undefined;
if (player) {
player.haveResources = player.wheat > 0 ||
player.brick > 0 ||
player.sheep > 0 ||
player.stone > 0 ||
player.wood > 0;
}
/* Strip out data that should not be shared with players */
delete reducedGame.developmentCards;
const playerGame = Object.assign({}, reducedGame, {
timestamp: Date.now(),
status: error ? error : "success",
name: session.name,
color: session.color,
order: (session.color in game.players) ? game.players[session.color].order : 0,
player: player,
sessions: reducedSessions,
layout: layout
});
if (!res) {
if (!error) {
if (!session.ws) {
console.error(`No WebSocket connection to ${session.name}`);
} else {
console.log(`Sending update to ${session.name}`);
session.ws.send(JSON.stringify({
type: 'game-update',
update: playerGame
}));
}
}
} else {
console.log(`Returning update to ${session.name ? session.name : 'Unnamed'}`);
res.status(200).send(playerGame);
}
}
const sendGame = async (req, res, game, error, wsUpdate) => {
const active = getActiveCount(game);
/* Enforce game limit of >= 2 players */
@ -2726,60 +2804,33 @@ const sendGame = async (req, res, game, error) => {
reducedSessions.push(reduced);
}
/* Save per turn while debugging... */
await writeFile(`games/${game.id}.${game.turns}`, JSON.stringify(reducedGame, null, 2))
.catch((error) => {
console.error(`Unable to write to games/${game.id}`);
console.error(error);
});
await writeFile(`games/${game.id}`, JSON.stringify(reducedGame, null, 2))
.catch((error) => {
console.error(`Unable to write to games/${game.id}`);
console.error(error);
});
for (let id in game.sessions) {
const target = game.sessions[id],
useWS = target !== session,
player = target.player ? target.player : undefined;
if (player) {
player.haveResources = player.wheat > 0 ||
player.brick > 0 ||
player.sheep > 0 ||
player.stone > 0 ||
player.wood > 0;
}
/* Strip out data that should not be shared with players */
delete reducedGame.developmentCards;
const playerGame = Object.assign({}, reducedGame, {
timestamp: Date.now(),
status: error ? error : "success",
name: target.name,
color: target.color,
order: (target.color in game.players) ? game.players[target.color].order : 0,
player: player,
sessions: reducedSessions,
layout: layout
if (!wsUpdate) {
/* Save per turn while debugging... */
await writeFile(`games/${game.id}.${game.turns}`, JSON.stringify(reducedGame, null, 2))
.catch((error) => {
console.error(`Unable to write to games/${game.id}`);
console.error(error);
});
await writeFile(`games/${game.id}`, JSON.stringify(reducedGame, null, 2))
.catch((error) => {
console.error(`Unable to write to games/${game.id}`);
console.error(error);
});
}
if (useWS) {
if (!error) {
if (!target.ws) {
console.error(`No WebSocket connection to ${target.name}`);
} else {
console.log(`Sending update to ${target.name}`);
target.ws.send(JSON.stringify({
type: 'game-update',
update: playerGame
}));
if (wsUpdate) {
/* This is a one-shot request from a client to send the game-update over WebSocket */
sendGameToSession(session, reducedSessions, game, reducedGame);
} else {
for (let id in game.sessions) {
const target = game.sessions[id], useWS = target !== session;
if (useWS) {
if (!error) {
sendGameToSession(target, reducedSessions, game, reducedGame);
}
} else {
sendGameToSession(target, reducedSessions, game, reducedGame, error, res);
}
} else {
console.log(`Returning update to ${target.name ? target.name : 'Unnamed'}`);
res.status(200).send(playerGame);
}
}
}