mirror of
https://github.com/AlecM33/Werewolf.git
synced 2025-12-26 07:47:50 +01:00
redis effort part 5
This commit is contained in:
@@ -2,8 +2,8 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const debugMode = Array.from(process.argv.map((arg) => arg.trim().toLowerCase())).includes('debug');
|
||||
const logger = require('../modules/Logger')(debugMode);
|
||||
const socketManager = (require('../modules/singletons/SocketManager.js')).instance;
|
||||
const activeGameRunner = (require('../modules/singletons/ActiveGameRunner.js')).instance;
|
||||
const eventManager = (require('../modules/singletons/EventManager.js')).instance;
|
||||
const timerManager = (require('../modules/singletons/TimerManager.js')).instance;
|
||||
const globals = require('../config/globals.js');
|
||||
const cors = require('cors');
|
||||
|
||||
@@ -16,13 +16,13 @@ router.post('/sockets/broadcast', (req, res, next) => {
|
||||
// TODO: implement client-side display of this message.
|
||||
router.post('/sockets/broadcast', function (req, res) {
|
||||
logger.info('admin user broadcasting message: ' + req.body?.message);
|
||||
socketManager.broadcast(req.body?.message);
|
||||
eventManager.broadcast(req.body?.message);
|
||||
res.status(201).send('Broadcasted message to all connected sockets: ' + req.body?.message);
|
||||
});
|
||||
|
||||
router.get('/games/state', async (req, res) => {
|
||||
const gamesArray = [];
|
||||
await activeGameRunner.client.keys('*').then(async (r) => {
|
||||
await timerManager.client.keys('*').then(async (r) => {
|
||||
Object.values(r).forEach((v) => gamesArray.push(JSON.parse(v)));
|
||||
});
|
||||
res.status(200).send(gamesArray);
|
||||
|
||||
@@ -77,7 +77,7 @@ router.patch('/:code/players', async function (req, res) {
|
||||
) {
|
||||
res.status(400).send();
|
||||
} else {
|
||||
const game = await gameManager.activeGameRunner.getActiveGame(req.body.accessCode);
|
||||
const game = await gameManager.getActiveGame(req.body.accessCode);
|
||||
if (game) {
|
||||
const inUseCookie = gameManager.environment === globals.ENVIRONMENT.PRODUCTION ? req.body.localCookie : req.body.sessionCookie;
|
||||
gameManager.joinGame(game, req.body.playerName, inUseCookie, req.body.joinAsSpectator).then((data) => {
|
||||
@@ -102,7 +102,7 @@ router.patch('/:code/restart', async function (req, res) {
|
||||
) {
|
||||
res.status(400).send();
|
||||
} else {
|
||||
const game = await gameManager.activeGameRunner.getActiveGame(req.body.accessCode);
|
||||
const game = await gameManager.getActiveGame(req.body.accessCode);
|
||||
if (game) {
|
||||
gameManager.restartGame(game, gameManager.namespace).then((data) => {
|
||||
res.status(200).send();
|
||||
|
||||
@@ -42,8 +42,7 @@ const globals = {
|
||||
RESUME_TIMER: 'resumeTimer',
|
||||
END_TIMER: 'endTimer',
|
||||
GET_TIME_REMAINING: 'getTimeRemaining',
|
||||
SOURCE_TIME_REMAINING: 'sourceTimeRemaining',
|
||||
SHARE_TIME_REMAINING: 'shareTimeRemaining',
|
||||
SOURCE_TIMER_EVENT: 'sourceTimerEvent',
|
||||
KILL_PLAYER: 'killPlayer',
|
||||
REVEAL_PLAYER: 'revealPlayer',
|
||||
TRANSFER_MODERATOR: 'transferModerator',
|
||||
@@ -55,7 +54,8 @@ const globals = {
|
||||
ADD_SPECTATOR: 'addSpectator',
|
||||
SYNC_GAME_STATE: 'syncGameState',
|
||||
UPDATE_SOCKET: 'updateSocket',
|
||||
ASSIGN_DEDICATED_MOD: 'assignDedicatedMod'
|
||||
ASSIGN_DEDICATED_MOD: 'assignDedicatedMod',
|
||||
TIMER_EVENT: 'timerEvent'
|
||||
},
|
||||
SYNCABLE_EVENTS: function () {
|
||||
return [
|
||||
@@ -81,7 +81,8 @@ const globals = {
|
||||
return [
|
||||
this.EVENT_IDS.RESUME_TIMER,
|
||||
this.EVENT_IDS.PAUSE_TIMER,
|
||||
this.EVENT_IDS.END_TIMER
|
||||
this.EVENT_IDS.END_TIMER,
|
||||
this.EVENT_IDS.GET_TIME_REMAINING
|
||||
];
|
||||
},
|
||||
MESSAGES: {
|
||||
|
||||
@@ -53,15 +53,6 @@ const Events = [
|
||||
}
|
||||
}
|
||||
},
|
||||
// {
|
||||
// id: EVENT_IDS.UPDATE_SOCKET,
|
||||
// stateChange: (game, socketArgs, vars) => {
|
||||
// const matchingPerson = vars.gameManager.findPersonByField(game, 'id', socketArgs.personId);
|
||||
// if (matchingPerson) {
|
||||
// matchingPerson.socketId = socketArgs.socketId;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
{
|
||||
id: EVENT_IDS.SYNC_GAME_STATE,
|
||||
stateChange: async (game, socketArgs, vars) => {},
|
||||
@@ -79,7 +70,7 @@ const Events = [
|
||||
game.status = globals.STATUS.IN_PROGRESS;
|
||||
if (game.hasTimer) {
|
||||
game.timerParams.paused = true;
|
||||
await vars.activeGameRunner.runGame(game, vars.gameManager.namespace, vars.socketManager, vars.gameManager);
|
||||
await vars.timerManager.runTimer(game, vars.gameManager.namespace, vars.eventManager, vars.gameManager);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -133,10 +124,10 @@ const Events = [
|
||||
id: EVENT_IDS.END_GAME,
|
||||
stateChange: async (game, socketArgs, vars) => {
|
||||
game.status = globals.STATUS.ENDED;
|
||||
// if (this.activeGameRunner.timerThreads[game.accessCode]) {
|
||||
// this.logger.trace('KILLING TIMER PROCESS FOR ENDED GAME ' + game.accessCode);
|
||||
// this.activeGameRunner.timerThreads[game.accessCode].kill();
|
||||
// }
|
||||
if (vars.timerManager.timerThreads[game.accessCode]) {
|
||||
vars.logger.trace('KILLING TIMER PROCESS FOR ENDED GAME ' + game.accessCode);
|
||||
vars.timerManager.timerThreads[game.accessCode].kill();
|
||||
}
|
||||
for (const person of game.people) {
|
||||
person.revealed = true;
|
||||
}
|
||||
@@ -208,7 +199,15 @@ const Events = [
|
||||
},
|
||||
{
|
||||
id: EVENT_IDS.RESTART_GAME,
|
||||
stateChange: async (game, socketArgs, vars) => {},
|
||||
stateChange: async (game, socketArgs, vars) => {
|
||||
if (vars.instanceId !== vars.senderInstanceId
|
||||
&& vars.timerManager.timerThreads[game.accessCode]
|
||||
&& !vars.timerManager.timerThreads[game.accessCode].killed
|
||||
) {
|
||||
vars.timerManager.timerThreads[game.accessCode].kill();
|
||||
delete vars.timerManager.timerThreads[game.accessCode];
|
||||
}
|
||||
},
|
||||
communicate: async (game, socketArgs, vars) => {
|
||||
if (vars.ackFn) {
|
||||
vars.ackFn();
|
||||
@@ -217,51 +216,51 @@ const Events = [
|
||||
}
|
||||
},
|
||||
{
|
||||
id: EVENT_IDS.GET_TIME_REMAINING,
|
||||
id: EVENT_IDS.TIMER_EVENT,
|
||||
stateChange: async (game, socketArgs, vars) => {},
|
||||
communicate: async (game, socketArgs, vars) => {
|
||||
const thread = vars.activeGameRunner.timerThreads[game.accessCode];
|
||||
const thread = vars.timerManager.timerThreads[game.accessCode];
|
||||
if (thread && (!thread.killed && thread.exitCode === null)) {
|
||||
thread.send({
|
||||
command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
|
||||
command: vars.timerEventSubtype,
|
||||
accessCode: game.accessCode,
|
||||
socketId: vars.socketId,
|
||||
logLevel: vars.logger.logLevel
|
||||
});
|
||||
} else if (thread) {
|
||||
console.log(game.timerParams);
|
||||
if (game.timerParams && game.timerParams.timeRemaining === 0) {
|
||||
vars.gameManager.namespace.to(vars.socketId)
|
||||
.emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused);
|
||||
await vars.socketManager.publisher.publish(
|
||||
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
|
||||
game.accessCode + ';' + globals.EVENT_IDS.SHARE_TIME_REMAINING + ';' +
|
||||
JSON.stringify({
|
||||
socketId: vars.socketId,
|
||||
timeRemaining: game.timerParams.timeRemaining,
|
||||
paused: game.timerParams.paused
|
||||
}) +
|
||||
';' + vars.instanceId
|
||||
);
|
||||
if (vars.timerEventSubtype === EVENT_IDS.GET_TIME_REMAINING && game.timerParams && game.timerParams.timeRemaining === 0) {
|
||||
// vars.gameManager.namespace.to(vars.socketId)
|
||||
// .emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused);
|
||||
// await vars.eventManager.publisher.publish(
|
||||
// globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
|
||||
// game.accessCode + ';' + globals.EVENT_IDS.SHARE_TIME_REMAINING + ';' +
|
||||
// JSON.stringify({
|
||||
// socketId: vars.socketId,
|
||||
// timeRemaining: game.timerParams.timeRemaining,
|
||||
// paused: game.timerParams.paused
|
||||
// }) +
|
||||
// ';' + vars.instanceId
|
||||
// );
|
||||
}
|
||||
} else { // we need to consult another container for the timer data
|
||||
await vars.socketManager.publisher?.publish(
|
||||
await vars.eventManager.publisher?.publish(
|
||||
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
|
||||
game.accessCode + ';' + globals.EVENT_IDS.SOURCE_TIME_REMAINING + ';' + JSON.stringify({ socketId: vars.socketId }) + ';' + vars.instanceId
|
||||
game.accessCode + ';' + globals.EVENT_IDS.SOURCE_TIMER_EVENT + ';' +
|
||||
JSON.stringify({ socketId: vars.socketId, timerEventSubtype: vars.timerEventSubtype }) + ';' + vars.instanceId
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
/* unlike the GET_TIME_REMAINING event, this event is a request from another instance for timer data. In response
|
||||
/* This event is a request from another instance to consult its timer data. In response
|
||||
* to this event, this instance will check if it is home to a particular timer thread. */
|
||||
id: EVENT_IDS.SOURCE_TIME_REMAINING,
|
||||
id: EVENT_IDS.SOURCE_TIMER_EVENT,
|
||||
stateChange: async (game, socketArgs, vars) => {},
|
||||
communicate: async (game, socketArgs, vars) => {
|
||||
const thread = vars.activeGameRunner.timerThreads[game.accessCode];
|
||||
const thread = vars.timerManager.timerThreads[game.accessCode];
|
||||
if (thread && (!thread.killed && thread.exitCode === null)) {
|
||||
thread.send({
|
||||
command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
|
||||
command: socketArgs.timerEventSubtype,
|
||||
accessCode: game.accessCode,
|
||||
socketId: socketArgs.socketId,
|
||||
logLevel: vars.logger.logLevel
|
||||
@@ -271,7 +270,7 @@ const Events = [
|
||||
vars.gameManager.namespace.to(vars.socketId)
|
||||
.emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused);
|
||||
}
|
||||
await vars.socketManager.publisher.publish(
|
||||
await vars.eventManager.publisher.publish(
|
||||
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
|
||||
game.accessCode + ';' + globals.EVENT_IDS.SHARE_TIME_REMAINING + ';' +
|
||||
JSON.stringify({
|
||||
@@ -284,22 +283,12 @@ const Events = [
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
/* This is an event fired when an instance receives timer data from another instance. In this case, we should check if the socket id
|
||||
* given in the message is connected to this namespace. If it is, emit the time remaining to them. */
|
||||
id: EVENT_IDS.SHARE_TIME_REMAINING,
|
||||
stateChange: async (game, socketArgs, vars) => {},
|
||||
communicate: async (game, socketArgs, vars) => {
|
||||
const socket = vars.gameManager.namespace.sockets.get(socketArgs.socketId);
|
||||
if (socket) {
|
||||
vars.gameManager.namespace.to(socket.id)
|
||||
.emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, socketArgs.timeRemaining, socketArgs.paused);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: EVENT_IDS.END_TIMER,
|
||||
stateChange: async (game, socketArgs, vars) => {
|
||||
if (vars.timerManager.timerThreads[game.accessCode]) {
|
||||
delete vars.timerManager.timerThreads[game.accessCode];
|
||||
}
|
||||
game.timerParams.paused = false;
|
||||
game.timerParams.timeRemaining = 0;
|
||||
},
|
||||
@@ -314,7 +303,7 @@ const Events = [
|
||||
game.timerParams.timeRemaining = socketArgs.timeRemaining;
|
||||
},
|
||||
communicate: async (game, socketArgs, vars) => {
|
||||
vars.gameManager.namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, game.timerParams.timeRemaining);
|
||||
vars.gameManager.namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, socketArgs.timeRemaining);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -324,7 +313,19 @@ const Events = [
|
||||
game.timerParams.timeRemaining = socketArgs.timeRemaining;
|
||||
},
|
||||
communicate: async (game, socketArgs, vars) => {
|
||||
vars.gameManager.namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, game.timerParams.timeRemaining);
|
||||
vars.gameManager.namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, socketArgs.timeRemaining);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: EVENT_IDS.GET_TIME_REMAINING,
|
||||
stateChange: async (game, socketArgs, vars) => {
|
||||
game.timerParams.timeRemaining = socketArgs.timeRemaining;
|
||||
},
|
||||
communicate: async (game, socketArgs, vars) => {
|
||||
const socket = vars.gameManager.namespace.sockets.get(socketArgs.socketId);
|
||||
if (socket) {
|
||||
vars.gameManager.namespace.to(socket.id).emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, socketArgs.timeRemaining, game.timerParams.paused);
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -3,7 +3,7 @@ const ServerTimer = require('./ServerTimer.js');
|
||||
|
||||
let timer;
|
||||
|
||||
// This is a subprocess spawned by logic in the ActiveGameRunner module.
|
||||
// This is a subprocess spawned by logic in the TimerManager module.
|
||||
process.on('message', (msg) => {
|
||||
const logger = require('./Logger')(msg.logLevel);
|
||||
switch (msg.command) {
|
||||
|
||||
@@ -4,9 +4,9 @@ const https = require('https');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
const SocketManager = require('./singletons/SocketManager.js');
|
||||
const EventManager = require('./singletons/EventManager.js');
|
||||
const GameManager = require('./singletons/GameManager.js');
|
||||
const ActiveGameRunner = require('./singletons/ActiveGameRunner.js');
|
||||
const TimerManager = require('./singletons/TimerManager.js');
|
||||
const { ENVIRONMENT } = require('../config/globals.js');
|
||||
const rateLimit = require('express-rate-limit').default;
|
||||
|
||||
@@ -14,8 +14,8 @@ const ServerBootstrapper = {
|
||||
|
||||
singletons: (logger, instanceId) => {
|
||||
return {
|
||||
activeGameRunner: new ActiveGameRunner(logger, instanceId),
|
||||
socketManager: new SocketManager(logger, instanceId),
|
||||
timerManager: new TimerManager(logger, instanceId),
|
||||
eventManager: new EventManager(logger, instanceId),
|
||||
gameManager: process.env.NODE_ENV.trim() === 'development'
|
||||
? new GameManager(logger, ENVIRONMENT.LOCAL, instanceId)
|
||||
: new GameManager(logger, ENVIRONMENT.PRODUCTION, instanceId)
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
const { fork } = require('child_process');
|
||||
const path = require('path');
|
||||
const globals = require('../../config/globals');
|
||||
const redis = require('redis');
|
||||
|
||||
class ActiveGameRunner {
|
||||
constructor (logger, instanceId) {
|
||||
if (ActiveGameRunner.instance) {
|
||||
throw new Error('The server tried to instantiate more than one ActiveGameRunner');
|
||||
}
|
||||
logger.info('CREATING SINGLETON ACTIVE GAME RUNNER');
|
||||
this.timerThreads = {};
|
||||
this.logger = logger;
|
||||
this.client = redis.createClient();
|
||||
this.subscriber = null;
|
||||
this.instanceId = instanceId;
|
||||
ActiveGameRunner.instance = this;
|
||||
}
|
||||
|
||||
getActiveGame = async (accessCode) => {
|
||||
const r = await this.client.get(accessCode);
|
||||
return r === null ? r : JSON.parse(r);
|
||||
}
|
||||
|
||||
createGameSyncSubscriber = async (gameManager, socketManager) => {
|
||||
this.subscriber = this.client.duplicate();
|
||||
await this.subscriber.connect();
|
||||
await this.subscriber.subscribe(globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, async (message) => {
|
||||
this.logger.info('MESSAGE: ' + message);
|
||||
const messageComponents = message.split(';');
|
||||
if (messageComponents[messageComponents.length - 1] === this.instanceId) {
|
||||
this.logger.trace('Disregarding self-authored message');
|
||||
return;
|
||||
}
|
||||
const game = await this.getActiveGame(messageComponents[0]);
|
||||
let args;
|
||||
if (messageComponents[2]) {
|
||||
args = JSON.parse(messageComponents[2]);
|
||||
}
|
||||
if (game) {
|
||||
await socketManager.handleEventById(
|
||||
messageComponents[1],
|
||||
game,
|
||||
null,
|
||||
game?.accessCode || messageComponents[0],
|
||||
args || null,
|
||||
null,
|
||||
true
|
||||
);
|
||||
}
|
||||
});
|
||||
this.logger.info('ACTIVE GAME RUNNER - CREATED GAME SYNC SUBSCRIBER');
|
||||
}
|
||||
|
||||
/* We're only going to fork a child process for games with a timer. They will report back to the parent process whenever
|
||||
the timer is up.
|
||||
*/
|
||||
runGame = async (game, namespace, socketManager, gameManager) => {
|
||||
this.logger.debug('running game ' + game.accessCode);
|
||||
const gameProcess = fork(path.join(__dirname, '../GameProcess.js'));
|
||||
this.timerThreads[game.accessCode] = gameProcess;
|
||||
console.log(this.timerThreads);
|
||||
this.logger.debug('game ' + game.accessCode + ' now associated with subProcess ' + gameProcess.pid);
|
||||
gameProcess.on('message', async (msg) => {
|
||||
game = await this.getActiveGame(game.accessCode);
|
||||
switch (msg.command) {
|
||||
case globals.GAME_PROCESS_COMMANDS.END_TIMER:
|
||||
await socketManager.handleEventById(globals.EVENT_IDS.END_TIMER, game, msg.socketId, game.accessCode, msg, null, false);
|
||||
this.logger.trace('PARENT: END TIMER');
|
||||
break;
|
||||
case globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER:
|
||||
await socketManager.handleEventById(globals.EVENT_IDS.PAUSE_TIMER, game, msg.socketId, game.accessCode, msg, null, false);
|
||||
break;
|
||||
case globals.GAME_PROCESS_COMMANDS.RESUME_TIMER:
|
||||
await socketManager.handleEventById(globals.EVENT_IDS.RESUME_TIMER, game, msg.socketId, game.accessCode, msg, null, false);
|
||||
break;
|
||||
case globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING:
|
||||
this.logger.trace(msg);
|
||||
game.timerParams.timeRemaining = msg.timeRemaining;
|
||||
this.logger.trace('PARENT: GET TIME REMAINING');
|
||||
msg.paused = game.timerParams.paused;
|
||||
await socketManager.publisher.publish(
|
||||
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
|
||||
game.accessCode + ';' + globals.EVENT_IDS.SHARE_TIME_REMAINING + ';' + JSON.stringify(msg) + ';' + this.instanceId
|
||||
);
|
||||
const socket = namespace.sockets.get(msg.socketId);
|
||||
if (socket) {
|
||||
namespace.to(socket.id).emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (globals.SYNCABLE_EVENTS().includes(msg.command)) {
|
||||
await gameManager.refreshGame(game);
|
||||
await socketManager.publisher.publish(
|
||||
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
|
||||
game.accessCode + ';' + msg.command + ';' + JSON.stringify(msg) + ';' + this.instanceId
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
gameProcess.on('exit', (code, signal) => {
|
||||
this.logger.debug('Game timer thread ' + gameProcess.pid + ' exiting with code ' + code + ' - game ' + game.accessCode);
|
||||
});
|
||||
gameProcess.send({
|
||||
command: globals.GAME_PROCESS_COMMANDS.START_TIMER,
|
||||
accessCode: game.accessCode,
|
||||
logLevel: this.logger.logLevel,
|
||||
hours: game.timerParams.hours,
|
||||
minutes: game.timerParams.minutes
|
||||
});
|
||||
game.startTime = new Date().toJSON();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = ActiveGameRunner;
|
||||
@@ -1,22 +1,23 @@
|
||||
const globals = require('../../config/globals');
|
||||
const EVENT_IDS = globals.EVENT_IDS;
|
||||
const { RateLimiterMemory } = require('rate-limiter-flexible');
|
||||
const redis = require('redis');
|
||||
const Events = require('../Events');
|
||||
|
||||
class SocketManager {
|
||||
class EventManager {
|
||||
constructor (logger, instanceId) {
|
||||
if (SocketManager.instance) {
|
||||
throw new Error('The server attempted to instantiate more than one SocketManager.');
|
||||
if (EventManager.instance) {
|
||||
throw new Error('The server attempted to instantiate more than one EventManager.');
|
||||
}
|
||||
logger.info('CREATING SINGLETON SOCKET MANAGER');
|
||||
logger.info('CREATING SINGLETON EVENT MANAGER');
|
||||
this.logger = logger;
|
||||
this.client = redis.createClient();
|
||||
this.io = null;
|
||||
this.publisher = null;
|
||||
this.activeGameRunner = null;
|
||||
this.subscriber = null;
|
||||
this.timerManager = null;
|
||||
this.gameManager = null;
|
||||
this.instanceId = instanceId;
|
||||
SocketManager.instance = this;
|
||||
EventManager.instance = this;
|
||||
}
|
||||
|
||||
broadcast = (message) => {
|
||||
@@ -26,7 +27,38 @@ class SocketManager {
|
||||
createRedisPublisher = async () => {
|
||||
this.publisher = redis.createClient();
|
||||
await this.publisher.connect();
|
||||
this.logger.info('SOCKET MANAGER - CREATED GAME SYNC PUBLISHER');
|
||||
this.logger.info('EVENT MANAGER - CREATED PUBLISHER');
|
||||
}
|
||||
|
||||
createGameSyncSubscriber = async (gameManager, eventManager) => {
|
||||
this.subscriber = this.client.duplicate();
|
||||
await this.subscriber.connect();
|
||||
await this.subscriber.subscribe(globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, async (message) => {
|
||||
this.logger.info('MESSAGE: ' + message);
|
||||
const messageComponents = message.split(';');
|
||||
if (messageComponents[messageComponents.length - 1] === this.instanceId) {
|
||||
this.logger.trace('Disregarding self-authored message');
|
||||
return;
|
||||
}
|
||||
const game = await gameManager.getActiveGame(messageComponents[0]);
|
||||
let args;
|
||||
if (messageComponents[2]) {
|
||||
args = JSON.parse(messageComponents[2]);
|
||||
}
|
||||
if (game) {
|
||||
await eventManager.handleEventById(
|
||||
messageComponents[1],
|
||||
messageComponents[messageComponents.length - 1],
|
||||
game,
|
||||
null,
|
||||
game?.accessCode || messageComponents[0],
|
||||
args || null,
|
||||
null,
|
||||
true
|
||||
);
|
||||
}
|
||||
});
|
||||
this.logger.info('EVENT MANAGER - CREATED SUBSCRIBER');
|
||||
}
|
||||
|
||||
createSocketServer = (main, app, port, logger) => {
|
||||
@@ -49,24 +81,24 @@ class SocketManager {
|
||||
|
||||
createGameSocketNamespace = (server, logger, gameManager) => {
|
||||
const namespace = server.of('/in-game');
|
||||
const registerHandlers = this.registerHandlers;
|
||||
const registerSocketHandler = this.registerSocketHandler;
|
||||
registerRateLimiter(namespace, logger);
|
||||
namespace.on('connection', function (socket) {
|
||||
socket.on('disconnecting', (reason) => {
|
||||
logger.trace('client socket disconnecting because: ' + reason);
|
||||
});
|
||||
|
||||
registerHandlers(namespace, socket, gameManager);
|
||||
registerSocketHandler(namespace, socket, gameManager);
|
||||
});
|
||||
return server.of('/in-game');
|
||||
};
|
||||
|
||||
registerHandlers = (namespace, socket) => {
|
||||
registerSocketHandler = (namespace, socket, gameManager) => {
|
||||
socket.on(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, async (eventId, accessCode, args = null, ackFn = null) => {
|
||||
const game = await this.activeGameRunner.getActiveGame(accessCode);
|
||||
const game = await gameManager.getActiveGame(accessCode);
|
||||
if (game) {
|
||||
if (globals.TIMER_EVENTS().includes(eventId)) {
|
||||
await this.handleAndSyncTimerEvent(eventId, game, socket, args, ackFn, false);
|
||||
await this.handleEventById(globals.EVENT_IDS.TIMER_EVENT, null, game, socket.id, game.accessCode, args, ackFn, true, eventId);
|
||||
} else {
|
||||
await this.handleAndSyncSocketEvent(eventId, game, socket, args, ackFn, false);
|
||||
}
|
||||
@@ -77,7 +109,7 @@ class SocketManager {
|
||||
};
|
||||
|
||||
handleAndSyncSocketEvent = async (eventId, game, socket, socketArgs, ackFn) => {
|
||||
await this.handleEventById(eventId, game, socket?.id, game.accessCode, socketArgs, ackFn, false);
|
||||
await this.handleEventById(eventId, null, game, socket?.id, game.accessCode, socketArgs, ackFn, false);
|
||||
/* This server should publish events initiated by a connected socket to Redis for consumption by other instances. */
|
||||
if (globals.SYNCABLE_EVENTS().includes(eventId)) {
|
||||
await this.gameManager.refreshGame(game);
|
||||
@@ -88,33 +120,22 @@ class SocketManager {
|
||||
}
|
||||
}
|
||||
|
||||
handleAndSyncTimerEvent = async (eventId, game, socketId, accessCode, socketArgs, ackFn, syncOnly) => {
|
||||
switch (eventId) {
|
||||
case EVENT_IDS.PAUSE_TIMER:
|
||||
await this.gameManager.pauseTimer(game, this.logger);
|
||||
break;
|
||||
case EVENT_IDS.RESUME_TIMER:
|
||||
await this.gameManager.resumeTimer(game, this.logger);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleEventById = async (eventId, game, socketId, accessCode, socketArgs, ackFn, syncOnly) => {
|
||||
handleEventById = async (eventId, senderInstanceId, game, socketId, accessCode, socketArgs, ackFn, syncOnly, timerEventSubtype = null) => {
|
||||
this.logger.trace('ARGS TO HANDLER: ' + JSON.stringify(socketArgs));
|
||||
const event = Events.find((event) => event.id === eventId);
|
||||
const additionalVars = {
|
||||
gameManager: this.gameManager,
|
||||
activeGameRunner: this.activeGameRunner,
|
||||
socketManager: this,
|
||||
timerManager: this.timerManager,
|
||||
eventManager: this,
|
||||
socketId: socketId,
|
||||
ackFn: ackFn,
|
||||
logger: this.logger,
|
||||
instanceId: this.instanceId
|
||||
instanceId: this.instanceId,
|
||||
senderInstanceId: senderInstanceId,
|
||||
timerEventSubtype: timerEventSubtype
|
||||
};
|
||||
if (event) {
|
||||
if (!syncOnly) {
|
||||
if (!syncOnly || eventId === globals.EVENT_IDS.RESTART_GAME) {
|
||||
await event.stateChange(game, socketArgs, additionalVars);
|
||||
}
|
||||
await event.communicate(game, socketArgs, additionalVars);
|
||||
@@ -140,4 +161,4 @@ function registerRateLimiter (server, logger) {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = SocketManager;
|
||||
module.exports = EventManager;
|
||||
@@ -13,20 +13,25 @@ class GameManager {
|
||||
logger.info('CREATING SINGLETON GAME MANAGER');
|
||||
this.logger = logger;
|
||||
this.environment = environment;
|
||||
this.activeGameRunner = null;
|
||||
this.socketManager = null;
|
||||
this.timerManager = null;
|
||||
this.eventManager = null;
|
||||
this.namespace = null;
|
||||
this.instanceId = instanceId;
|
||||
GameManager.instance = this;
|
||||
}
|
||||
|
||||
getActiveGame = async (accessCode) => {
|
||||
const r = await this.eventManager.client.get(accessCode);
|
||||
return r === null ? r : JSON.parse(r);
|
||||
}
|
||||
|
||||
setGameSocketNamespace = (namespace) => {
|
||||
this.namespace = namespace;
|
||||
};
|
||||
|
||||
refreshGame = async (game) => {
|
||||
this.logger.debug('PUSHING REFRESH OF ' + game.accessCode);
|
||||
await this.activeGameRunner.client.set(game.accessCode, JSON.stringify(game));
|
||||
await this.eventManager.client.set(game.accessCode, JSON.stringify(game));
|
||||
}
|
||||
|
||||
createGame = async (gameParams) => {
|
||||
@@ -61,7 +66,7 @@ class GameManager {
|
||||
new Date().toJSON(),
|
||||
req.timerParams
|
||||
);
|
||||
await this.activeGameRunner.client.set(newAccessCode, JSON.stringify(newGame), {
|
||||
await this.eventManager.client.set(newAccessCode, JSON.stringify(newGame), {
|
||||
EX: globals.STALE_GAME_SECONDS
|
||||
});
|
||||
return Promise.resolve({ accessCode: newAccessCode, cookie: moderator.cookie, environment: this.environment });
|
||||
@@ -73,7 +78,7 @@ class GameManager {
|
||||
};
|
||||
|
||||
pauseTimer = async (game, logger) => {
|
||||
const thread = this.activeGameRunner.timerThreads[game.accessCode];
|
||||
const thread = this.timerManager.timerThreads[game.accessCode];
|
||||
if (thread && !thread.killed) {
|
||||
this.logger.debug('Timer thread found for game ' + game.accessCode);
|
||||
thread.send({
|
||||
@@ -85,7 +90,7 @@ class GameManager {
|
||||
};
|
||||
|
||||
resumeTimer = async (game, logger) => {
|
||||
const thread = this.activeGameRunner.timerThreads[game.accessCode];
|
||||
const thread = this.timerManager.timerThreads[game.accessCode];
|
||||
if (thread && !thread.killed) {
|
||||
this.logger.debug('Timer thread found for game ' + game.accessCode);
|
||||
thread.send({
|
||||
@@ -98,7 +103,7 @@ class GameManager {
|
||||
|
||||
getTimeRemaining = async (game, socketId) => {
|
||||
if (socketId) {
|
||||
const thread = this.activeGameRunner.timerThreads[game.accessCode];
|
||||
const thread = this.timerManager.timerThreads[game.accessCode];
|
||||
if (thread && (!thread.killed && thread.exitCode === null)) {
|
||||
thread.send({
|
||||
command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
|
||||
@@ -115,7 +120,7 @@ class GameManager {
|
||||
};
|
||||
|
||||
checkAvailability = async (code) => {
|
||||
const game = await this.activeGameRunner.getActiveGame(code.toUpperCase().trim());
|
||||
const game = await this.getActiveGame(code.toUpperCase().trim());
|
||||
if (game) {
|
||||
return Promise.resolve({ accessCode: code, playerCount: getGameSize(game.deck), timerParams: game.timerParams });
|
||||
} else {
|
||||
@@ -127,7 +132,7 @@ class GameManager {
|
||||
const charCount = charPool.length;
|
||||
let codeDigits, accessCode;
|
||||
let attempts = 0;
|
||||
while (!accessCode || ((await this.activeGameRunner.client.keys('*')).includes(accessCode)
|
||||
while (!accessCode || ((await this.eventManager.client.keys('*')).includes(accessCode)
|
||||
&& attempts < globals.ACCESS_CODE_GENERATION_ATTEMPTS)) {
|
||||
codeDigits = [];
|
||||
let iterations = globals.ACCESS_CODE_LENGTH;
|
||||
@@ -138,7 +143,7 @@ class GameManager {
|
||||
accessCode = codeDigits.join('');
|
||||
attempts ++;
|
||||
}
|
||||
return (await this.activeGameRunner.client.keys('*')).includes(accessCode)
|
||||
return (await this.eventManager.client.keys('*')).includes(accessCode)
|
||||
? null
|
||||
: accessCode;
|
||||
};
|
||||
@@ -156,7 +161,7 @@ class GameManager {
|
||||
) {
|
||||
return Promise.reject({ status: 400, reason: 'There are too many people already spectating.' });
|
||||
} else if (joinAsSpectator) {
|
||||
return await addSpectator(game, name, this.logger, this.namespace, this.socketManager.publisher, this.instanceId, this.refreshGame);
|
||||
return await addSpectator(game, name, this.logger, this.namespace, this.eventManager.publisher, this.instanceId, this.refreshGame);
|
||||
}
|
||||
const unassignedPerson = this.findPersonByField(game, 'id', game.currentModeratorId).assigned === false
|
||||
? this.findPersonByField(game, 'id', game.currentModeratorId)
|
||||
@@ -172,7 +177,7 @@ class GameManager {
|
||||
GameStateCurator.mapPerson(unassignedPerson),
|
||||
game.isFull
|
||||
);
|
||||
await this.activeGameRunner.publisher?.publish(
|
||||
await this.eventManager.publisher?.publish(
|
||||
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
|
||||
game.accessCode + ';' + globals.EVENT_IDS.PLAYER_JOINED + ';' + JSON.stringify(unassignedPerson) + ';' + this.instanceId
|
||||
);
|
||||
@@ -181,20 +186,20 @@ class GameManager {
|
||||
if (game.people.filter(person => person.userType === globals.USER_TYPES.SPECTATOR).length === globals.MAX_SPECTATORS) {
|
||||
return Promise.reject({ status: 400, reason: 'This game has reached the maximum number of players and spectators.' });
|
||||
}
|
||||
return await addSpectator(game, name, this.logger, this.namespace, this.socketManager.publisher, this.instanceId, this.refreshGame);
|
||||
return await addSpectator(game, name, this.logger, this.namespace, this.eventManager.publisher, this.instanceId, this.refreshGame);
|
||||
}
|
||||
};
|
||||
|
||||
restartGame = async (game, namespace) => {
|
||||
// kill any outstanding timer threads
|
||||
const subProcess = this.activeGameRunner.timerThreads[game.accessCode];
|
||||
const subProcess = this.timerManager.timerThreads[game.accessCode];
|
||||
if (subProcess) {
|
||||
if (!subProcess.killed) {
|
||||
this.logger.info('Killing timer process ' + subProcess.pid + ' for: ' + game.accessCode);
|
||||
this.activeGameRunner.timerThreads[game.accessCode].kill();
|
||||
this.timerManager.timerThreads[game.accessCode].kill();
|
||||
}
|
||||
this.logger.debug('Deleting reference to subprocess ' + subProcess.pid);
|
||||
delete this.activeGameRunner.timerThreads[game.accessCode];
|
||||
delete this.timerManager.timerThreads[game.accessCode];
|
||||
}
|
||||
|
||||
// re-shuffle the deck
|
||||
@@ -230,11 +235,11 @@ class GameManager {
|
||||
game.status = globals.STATUS.IN_PROGRESS;
|
||||
if (game.hasTimer) {
|
||||
game.timerParams.paused = true;
|
||||
await this.activeGameRunner.runGame(game, namespace, this.socketManager, this);
|
||||
await this.timerManager.runTimer(game, namespace, this.eventManager, this);
|
||||
}
|
||||
|
||||
await this.refreshGame(game);
|
||||
await this.socketManager.publisher?.publish(
|
||||
await this.eventManager.publisher?.publish(
|
||||
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
|
||||
game.accessCode + ';' + globals.EVENT_IDS.RESTART_GAME + ';' + JSON.stringify({}) + ';' + this.instanceId
|
||||
);
|
||||
|
||||
47
server/modules/singletons/TimerManager.js
Normal file
47
server/modules/singletons/TimerManager.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const { fork } = require('child_process');
|
||||
const path = require('path');
|
||||
const globals = require('../../config/globals');
|
||||
|
||||
class TimerManager {
|
||||
constructor (logger, instanceId) {
|
||||
if (TimerManager.instance) {
|
||||
throw new Error('The server tried to instantiate more than one TimerManager');
|
||||
}
|
||||
logger.info('CREATING SINGLETON TIMER MANAGER');
|
||||
this.timerThreads = {};
|
||||
this.logger = logger;
|
||||
this.subscriber = null;
|
||||
this.instanceId = instanceId;
|
||||
TimerManager.instance = this;
|
||||
}
|
||||
|
||||
runTimer = async (game, namespace, eventManager, gameManager) => {
|
||||
this.logger.debug('running timer for game ' + game.accessCode);
|
||||
const gameProcess = fork(path.join(__dirname, '../GameProcess.js'));
|
||||
this.timerThreads[game.accessCode] = gameProcess;
|
||||
this.logger.debug('game ' + game.accessCode + ' now associated with subProcess ' + gameProcess.pid);
|
||||
gameProcess.on('message', async (msg) => {
|
||||
game = await gameManager.getActiveGame(game.accessCode);
|
||||
await eventManager.handleEventById(msg.command, null, game, msg.socketId, game.accessCode, msg, null, false);
|
||||
await gameManager.refreshGame(game);
|
||||
await eventManager.publisher.publish(
|
||||
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,
|
||||
game.accessCode + ';' + msg.command + ';' + JSON.stringify(msg) + ';' + this.instanceId
|
||||
);
|
||||
});
|
||||
|
||||
gameProcess.on('exit', (code, signal) => {
|
||||
this.logger.debug('Game timer thread ' + gameProcess.pid + ' exiting with code ' + code + ' - game ' + game.accessCode);
|
||||
});
|
||||
gameProcess.send({
|
||||
command: globals.GAME_PROCESS_COMMANDS.START_TIMER,
|
||||
accessCode: game.accessCode,
|
||||
logLevel: this.logger.logLevel,
|
||||
hours: game.timerParams.hours,
|
||||
minutes: game.timerParams.minutes
|
||||
});
|
||||
game.startTime = new Date().toJSON();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TimerManager;
|
||||
Reference in New Issue
Block a user