From 1432cd886d466e125d89c0c6ef74b9b8c0d580c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 03:32:03 +0000 Subject: [PATCH 01/11] Initial plan From ae7cf5cbeba9594738c9e939635735134569c253 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 03:37:43 +0000 Subject: [PATCH 02/11] Refactor timer system to run on main thread instead of child processes Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> --- server/modules/Events.js | 184 +++++++++++++----- server/modules/singletons/GameManager.js | 128 ++++++++---- spec/unit/server/modules/Events_Spec.js | 50 ++--- .../modules/singletons/GameManager_Spec.js | 7 +- 4 files changed, 251 insertions(+), 118 deletions(-) diff --git a/server/modules/Events.js b/server/modules/Events.js index 6648ff7..6ecf6b4 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -164,7 +164,7 @@ const Events = [ vars.gameManager.deal(game); if (game.hasTimer) { game.timerParams.paused = true; - await vars.timerManager.runTimer(game, vars.gameManager.namespace, vars.eventManager, vars.gameManager); + await vars.gameManager.runTimer(game); } } }, @@ -297,12 +297,10 @@ const Events = [ id: EVENT_IDS.RESTART_GAME, stateChange: async (game, socketArgs, vars) => { if (vars.instanceId !== vars.senderInstanceId - && vars.timerManager.timerThreads[game.accessCode] + && vars.gameManager.timers[game.accessCode] ) { - if (!vars.timerManager.timerThreads[game.accessCode].killed) { - vars.timerManager.timerThreads[game.accessCode].kill(); - } - delete vars.timerManager.timerThreads[game.accessCode]; + vars.gameManager.timers[game.accessCode].stopTimer(); + delete vars.gameManager.timers[game.accessCode]; } }, communicate: async (game, socketArgs, vars) => { @@ -316,24 +314,65 @@ const Events = [ id: EVENT_IDS.TIMER_EVENT, stateChange: async (game, socketArgs, vars) => {}, communicate: async (game, socketArgs, vars) => { - const thread = vars.timerManager.timerThreads[game.accessCode]; - if (thread) { - if (!thread.killed && thread.exitCode === null) { - thread.send({ - command: vars.timerEventSubtype, - accessCode: game.accessCode, - socketId: vars.requestingSocketId, - logLevel: vars.logger.logLevel - }); - } else { - const socket = vars.gameManager.namespace.sockets.get(vars.requestingSocketId); - if (socket) { - vars.gameManager.namespace.to(socket.id).emit( - GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, - game.timerParams.timeRemaining, - game.timerParams.paused - ); - } + const timer = vars.gameManager.timers[game.accessCode]; + if (timer) { + // Timer is running on this instance, handle the request directly + switch (vars.timerEventSubtype) { + case GAME_PROCESS_COMMANDS.PAUSE_TIMER: + const pauseTimeRemaining = await vars.gameManager.pauseTimer(game); + if (pauseTimeRemaining !== null) { + // Trigger PAUSE_TIMER event to update state and notify clients + await vars.eventManager.handleEventById( + EVENT_IDS.PAUSE_TIMER, + null, + game, + null, + game.accessCode, + { timeRemaining: pauseTimeRemaining }, + null, + false + ); + await vars.gameManager.refreshGame(game); + await vars.eventManager.publisher.publish( + REDIS_CHANNELS.ACTIVE_GAME_STREAM, + vars.eventManager.createMessageToPublish( + game.accessCode, + EVENT_IDS.PAUSE_TIMER, + vars.instanceId, + JSON.stringify({ timeRemaining: pauseTimeRemaining }) + ) + ); + } + break; + case GAME_PROCESS_COMMANDS.RESUME_TIMER: + const resumeTimeRemaining = await vars.gameManager.resumeTimer(game); + if (resumeTimeRemaining !== null) { + // Trigger RESUME_TIMER event to update state and notify clients + await vars.eventManager.handleEventById( + EVENT_IDS.RESUME_TIMER, + null, + game, + null, + game.accessCode, + { timeRemaining: resumeTimeRemaining }, + null, + false + ); + await vars.gameManager.refreshGame(game); + await vars.eventManager.publisher.publish( + REDIS_CHANNELS.ACTIVE_GAME_STREAM, + vars.eventManager.createMessageToPublish( + game.accessCode, + EVENT_IDS.RESUME_TIMER, + vars.instanceId, + JSON.stringify({ timeRemaining: resumeTimeRemaining }) + ) + ); + } + break; + case GAME_PROCESS_COMMANDS.GET_TIME_REMAINING: + await vars.gameManager.getTimeRemaining(game, vars.requestingSocketId); + break; } } else { // we need to consult another container for the timer data await vars.eventManager.publisher?.publish( @@ -350,34 +389,83 @@ const Events = [ }, { /* 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. */ + * to this event, this instance will check if it is home to a particular timer. */ id: EVENT_IDS.SOURCE_TIMER_EVENT, stateChange: async (game, socketArgs, vars) => {}, communicate: async (game, socketArgs, vars) => { - const thread = vars.timerManager.timerThreads[game.accessCode]; - if (thread) { - if (!thread.killed && thread.exitCode === null) { - thread.send({ - command: socketArgs.timerEventSubtype, - accessCode: game.accessCode, - socketId: socketArgs.socketId, - logLevel: vars.logger.logLevel - }); - } else { - await vars.eventManager.publisher.publish( - REDIS_CHANNELS.ACTIVE_GAME_STREAM, - vars.eventManager.createMessageToPublish( - game.accessCode, - socketArgs.timerEventSubtype, - vars.instanceId, - JSON.stringify({ - socketId: socketArgs.socketId, - timeRemaining: game.timerParams.timeRemaining, - paused: game.timerParams.paused - }) - ) - ); + const timer = vars.gameManager.timers[game.accessCode]; + if (timer) { + // Timer is running on this instance, handle the request + switch (socketArgs.timerEventSubtype) { + case GAME_PROCESS_COMMANDS.PAUSE_TIMER: + const pauseTimeRemaining = await vars.gameManager.pauseTimer(game); + if (pauseTimeRemaining !== null) { + await vars.eventManager.handleEventById( + EVENT_IDS.PAUSE_TIMER, + null, + game, + null, + game.accessCode, + { timeRemaining: pauseTimeRemaining }, + null, + false + ); + await vars.gameManager.refreshGame(game); + await vars.eventManager.publisher.publish( + REDIS_CHANNELS.ACTIVE_GAME_STREAM, + vars.eventManager.createMessageToPublish( + game.accessCode, + EVENT_IDS.PAUSE_TIMER, + vars.instanceId, + JSON.stringify({ timeRemaining: pauseTimeRemaining }) + ) + ); + } + break; + case GAME_PROCESS_COMMANDS.RESUME_TIMER: + const resumeTimeRemaining = await vars.gameManager.resumeTimer(game); + if (resumeTimeRemaining !== null) { + await vars.eventManager.handleEventById( + EVENT_IDS.RESUME_TIMER, + null, + game, + null, + game.accessCode, + { timeRemaining: resumeTimeRemaining }, + null, + false + ); + await vars.gameManager.refreshGame(game); + await vars.eventManager.publisher.publish( + REDIS_CHANNELS.ACTIVE_GAME_STREAM, + vars.eventManager.createMessageToPublish( + game.accessCode, + EVENT_IDS.RESUME_TIMER, + vars.instanceId, + JSON.stringify({ timeRemaining: resumeTimeRemaining }) + ) + ); + } + break; + case GAME_PROCESS_COMMANDS.GET_TIME_REMAINING: + await vars.gameManager.getTimeRemaining(game, socketArgs.socketId); + break; } + } else { + // Timer not running here, publish stored timer state + await vars.eventManager.publisher.publish( + REDIS_CHANNELS.ACTIVE_GAME_STREAM, + vars.eventManager.createMessageToPublish( + game.accessCode, + socketArgs.timerEventSubtype, + vars.instanceId, + JSON.stringify({ + socketId: socketArgs.socketId, + timeRemaining: game.timerParams.timeRemaining, + paused: game.timerParams.paused + }) + ) + ); } } }, diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index 3206dd0..8dc7de4 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -12,6 +12,7 @@ const Person = require('../../model/Person'); const GameStateCurator = require('../GameStateCurator'); const UsernameGenerator = require('../UsernameGenerator'); const GameCreationRequest = require('../../model/GameCreationRequest'); +const ServerTimer = require('../ServerTimer'); class GameManager { constructor (logger, environment, instanceId) { @@ -25,16 +26,16 @@ class GameManager { this.eventManager = null; this.namespace = null; this.instanceId = instanceId; + this.timers = {}; // Map of accessCode -> ServerTimer instance GameManager.instance = this; } getActiveGame = async (accessCode) => { const r = await this.eventManager.publisher.get(accessCode); - if (r === null && this.timerManager.timerThreads[accessCode]) { - if (!this.timerManager.timerThreads[accessCode].killed) { - this.timerManager.timerThreads[accessCode].kill(); - } - delete this.timerManager.timerThreads[accessCode]; + if (r === null && this.timers[accessCode]) { + // Clean up orphaned timer + this.timers[accessCode].stopTimer(); + delete this.timers[accessCode]; } let activeGame; if (r !== null) { @@ -108,43 +109,87 @@ class GameManager { }); }; - pauseTimer = async (game, logger) => { - const thread = this.timerManager.timerThreads[game.accessCode]; - if (thread && !thread.killed) { - this.logger.debug('Timer thread found for game ' + game.accessCode); - thread.send({ - command: GAME_PROCESS_COMMANDS.PAUSE_TIMER, - accessCode: game.accessCode, - logLevel: this.logger.logLevel - }); + runTimer = async (game) => { + this.logger.debug('running timer for game ' + game.accessCode); + const timer = new ServerTimer( + game.timerParams.hours, + game.timerParams.minutes, + PRIMITIVES.CLOCK_TICK_INTERVAL_MILLIS, + this.logger + ); + this.timers[game.accessCode] = timer; + + // Start timer in paused state initially (pausedInitially = true) + timer.runTimer(true).then(async () => { + this.logger.debug('Timer finished for ' + game.accessCode); + // Trigger END_TIMER event + game = await this.getActiveGame(game.accessCode); + if (game) { + await this.eventManager.handleEventById( + EVENT_IDS.END_TIMER, + null, + game, + null, + game.accessCode, + {}, + null, + false + ); + await this.refreshGame(game); + await this.eventManager.publisher.publish( + REDIS_CHANNELS.ACTIVE_GAME_STREAM, + this.eventManager.createMessageToPublish( + game.accessCode, + EVENT_IDS.END_TIMER, + this.instanceId, + '{}' + ) + ); + } + // Clean up timer instance + delete this.timers[game.accessCode]; + }); + game.startTime = new Date().toJSON(); + }; + + pauseTimer = async (game) => { + const timer = this.timers[game.accessCode]; + if (timer) { + this.logger.debug('Timer found for game ' + game.accessCode); + timer.stopTimer(); + return timer.currentTimeInMillis; } + return null; }; - resumeTimer = async (game, logger) => { - const thread = this.timerManager.timerThreads[game.accessCode]; - if (thread && !thread.killed) { - this.logger.debug('Timer thread found for game ' + game.accessCode); - thread.send({ - command: GAME_PROCESS_COMMANDS.RESUME_TIMER, - accessCode: game.accessCode, - logLevel: this.logger.logLevel - }); + resumeTimer = async (game) => { + const timer = this.timers[game.accessCode]; + if (timer) { + this.logger.debug('Timer found for game ' + game.accessCode); + timer.resumeTimer(); + return timer.currentTimeInMillis; } + return null; }; getTimeRemaining = async (game, socketId) => { if (socketId) { - const thread = this.timerManager.timerThreads[game.accessCode]; - if (thread && (!thread.killed && thread.exitCode === null)) { - thread.send({ - command: GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, - accessCode: game.accessCode, - socketId: socketId, - logLevel: this.logger.logLevel - }); - } else if (thread) { - if (game.timerParams && game.timerParams.timeRemaining === 0) { - this.namespace.to(socketId).emit(GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused); + const timer = this.timers[game.accessCode]; + if (timer) { + // Timer is running on this instance, emit directly + this.namespace.to(socketId).emit( + GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + timer.currentTimeInMillis, + game.timerParams.paused + ); + } else { + // Timer not running on this instance, return stored value + if (game.timerParams) { + this.namespace.to(socketId).emit( + GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + game.timerParams.timeRemaining, + game.timerParams.paused + ); } } } @@ -248,15 +293,12 @@ class GameManager { } restartGame = async (game, namespace) => { - // kill any outstanding timer threads - const subProcess = this.timerManager.timerThreads[game.accessCode]; - if (subProcess) { - if (!subProcess.killed) { - this.logger.info('Killing timer process ' + subProcess.pid + ' for: ' + game.accessCode); - this.timerManager.timerThreads[game.accessCode].kill(); - } - this.logger.debug('Deleting reference to subprocess ' + subProcess.pid); - delete this.timerManager.timerThreads[game.accessCode]; + // stop any outstanding timers + const timer = this.timers[game.accessCode]; + if (timer) { + this.logger.info('Stopping timer for: ' + game.accessCode); + timer.stopTimer(); + delete this.timers[game.accessCode]; } for (let i = 0; i < game.people.length; i ++) { diff --git a/spec/unit/server/modules/Events_Spec.js b/spec/unit/server/modules/Events_Spec.js index 493db0f..37c6f45 100644 --- a/spec/unit/server/modules/Events_Spec.js +++ b/spec/unit/server/modules/Events_Spec.js @@ -1,6 +1,6 @@ // TODO: clean up these deep relative paths? jsconfig.json is not working... const Game = require('../../../../server/model/Game'); -const { ENVIRONMENTS, EVENT_IDS, USER_TYPES, STATUS } = require('../../../../server/config/globals.js'); +const { ENVIRONMENTS, EVENT_IDS, USER_TYPES, STATUS, GAME_PROCESS_COMMANDS } = require('../../../../server/config/globals.js'); const GameManager = require('../../../../server/modules/singletons/GameManager.js'); const TimerManager = require('../../../../server/modules/singletons/TimerManager.js'); const EventManager = require('../../../../server/modules/singletons/EventManager.js'); @@ -272,12 +272,12 @@ describe('Events', () => { game.isStartable = true; game.hasTimer = true; game.timerParams = {}; - spyOn(timerManager, 'runTimer').and.callFake((a, b) => {}); + spyOn(gameManager, 'runTimer').and.callFake(() => {}); await Events.find((e) => e.id === EVENT_IDS.START_GAME) .stateChange(game, { id: 'b', assigned: true }, { gameManager: gameManager, timerManager: timerManager }); expect(game.status).toEqual(STATUS.IN_PROGRESS); expect(game.timerParams.paused).toEqual(true); - expect(timerManager.runTimer).toHaveBeenCalled(); + expect(gameManager.runTimer).toHaveBeenCalled(); }); }); describe('communicate', () => { @@ -530,22 +530,22 @@ describe('Events', () => { describe(EVENT_IDS.RESTART_GAME, () => { describe('stateChange', () => { it('should kill any alive timer thread if the instance is home to it', async () => { - const mockThread = { kill: () => {}, killed: false }; - timerManager.timerThreads = { ABCD: mockThread }; - spyOn(timerManager.timerThreads.ABCD, 'kill').and.callThrough(); + const mockTimer = { stopTimer: () => {} }; + gameManager.timers = { ABCD: mockTimer }; + spyOn(gameManager.timers.ABCD, 'stopTimer').and.callThrough(); await Events.find((e) => e.id === EVENT_IDS.RESTART_GAME) .stateChange(game, { personId: 'b' }, { gameManager: gameManager, timerManager: timerManager, instanceId: '111', senderInstanceId: '222' }); - expect(mockThread.kill).toHaveBeenCalled(); - expect(Object.keys(timerManager.timerThreads).length).toEqual(0); + expect(mockTimer.stopTimer).toHaveBeenCalled(); + expect(Object.keys(gameManager.timers).length).toEqual(0); }); it('should not kill the timer thread if the instance sent the event', async () => { - const mockThread = { kill: () => {}, killed: false }; - timerManager.timerThreads = { ABCD: mockThread }; - spyOn(timerManager.timerThreads.ABCD, 'kill').and.callThrough(); + const mockTimer = { stopTimer: () => {} }; + gameManager.timers = { ABCD: mockTimer }; + spyOn(gameManager.timers.ABCD, 'stopTimer').and.callThrough(); await Events.find((e) => e.id === EVENT_IDS.RESTART_GAME) .stateChange(game, { personId: 'b' }, { gameManager: gameManager, timerManager: timerManager, instanceId: '111', senderInstanceId: '111' }); - expect(mockThread.kill).not.toHaveBeenCalled(); - expect(Object.keys(timerManager.timerThreads).length).toEqual(1); + expect(mockTimer.stopTimer).not.toHaveBeenCalled(); + expect(Object.keys(gameManager.timers).length).toEqual(1); }); }); describe('communicate', () => { @@ -571,28 +571,30 @@ describe('Events', () => { describe('communicate', () => { it('should publish an event to source timer data if the timer thread is not found', async () => { await Events.find((e) => e.id === EVENT_IDS.TIMER_EVENT) - .communicate(game, {}, { gameManager: gameManager, timerManager: timerManager, eventManager: eventManager }); + .communicate(game, {}, { + gameManager: gameManager, + timerManager: timerManager, + eventManager: eventManager, + instanceId: 'test', + timerEventSubtype: GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + requestingSocketId: '2' + }); expect(eventManager.publisher.publish).toHaveBeenCalled(); }); it('should send a message to the thread if it is found', async () => { - const mockThread = { exitCode: null, kill: () => {}, send: (...args) => {}, killed: false }; - timerManager.timerThreads = { ABCD: mockThread }; - spyOn(timerManager.timerThreads.ABCD, 'send').and.callThrough(); + const mockTimer = { currentTimeInMillis: 5000 }; + gameManager.timers = { ABCD: mockTimer }; + spyOn(gameManager, 'getTimeRemaining').and.returnValue(Promise.resolve()); await Events.find((e) => e.id === EVENT_IDS.TIMER_EVENT) .communicate(game, {}, { gameManager: gameManager, timerManager: timerManager, eventManager: eventManager, - timerEventSubtype: EVENT_IDS.GET_TIME_REMAINING, + timerEventSubtype: GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, requestingSocketId: '2', logger: { logLevel: 'trace' } }); - expect(mockThread.send).toHaveBeenCalledWith({ - command: EVENT_IDS.GET_TIME_REMAINING, - accessCode: 'ABCD', - socketId: '2', - logLevel: 'trace' - }); + expect(gameManager.getTimeRemaining).toHaveBeenCalledWith(game, '2'); }); }); }); diff --git a/spec/unit/server/modules/singletons/GameManager_Spec.js b/spec/unit/server/modules/singletons/GameManager_Spec.js index 72a078a..60a6e2b 100644 --- a/spec/unit/server/modules/singletons/GameManager_Spec.js +++ b/spec/unit/server/modules/singletons/GameManager_Spec.js @@ -91,16 +91,17 @@ describe('GameManager', () => { it('should reset all relevant game parameters, including when the game has a timer', async () => { game.timerParams = { hours: 2, minutes: 2, paused: false }; game.hasTimer = true; - timerManager.timerThreads = { ABCD: { kill: () => {} } }; + const mockTimer = { stopTimer: () => {} }; + gameManager.timers = { ABCD: mockTimer }; game.status = STATUS.ENDED; - const threadKillSpy = spyOn(timerManager.timerThreads.ABCD, 'kill'); + const stopTimerSpy = spyOn(gameManager.timers.ABCD, 'stopTimer'); const emitSpy = spyOn(namespace.in(), 'emit'); await gameManager.restartGame(game, namespace); expect(game.status).toEqual(STATUS.LOBBY); - expect(threadKillSpy).toHaveBeenCalled(); + expect(stopTimerSpy).toHaveBeenCalled(); expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.RESTART_GAME); }); From 6cead03acd81acecc58642f2505e2d6ccee5155d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 03:40:27 +0000 Subject: [PATCH 03/11] Remove obsolete GameProcess.js and simplify TimerManager to stub Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> --- server/modules/Events.js | 7 +-- server/modules/GameProcess.js | 53 ------------------- server/modules/singletons/TimerManager.js | 38 ++----------- spec/unit/server/modules/Events_Spec.js | 10 ++-- .../modules/singletons/GameManager_Spec.js | 3 ++ 5 files changed, 17 insertions(+), 94 deletions(-) delete mode 100644 server/modules/GameProcess.js diff --git a/server/modules/Events.js b/server/modules/Events.js index 6ecf6b4..b52c5b2 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -220,9 +220,10 @@ const Events = [ id: EVENT_IDS.END_GAME, stateChange: async (game, socketArgs, vars) => { game.status = STATUS.ENDED; - if (game.hasTimer && vars.timerManager.timerThreads[game.accessCode]) { - vars.logger.trace('KILLING TIMER PROCESS FOR ENDED GAME ' + game.accessCode); - vars.timerManager.timerThreads[game.accessCode].kill(); + if (game.hasTimer && vars.gameManager.timers[game.accessCode]) { + vars.logger.trace('STOPPING TIMER FOR ENDED GAME ' + game.accessCode); + vars.gameManager.timers[game.accessCode].stopTimer(); + delete vars.gameManager.timers[game.accessCode]; } for (const person of game.people) { person.revealed = true; diff --git a/server/modules/GameProcess.js b/server/modules/GameProcess.js deleted file mode 100644 index 9d0368a..0000000 --- a/server/modules/GameProcess.js +++ /dev/null @@ -1,53 +0,0 @@ -const { GAME_PROCESS_COMMANDS, PRIMITIVES } = require('../config/globals'); -const ServerTimer = require('./ServerTimer.js'); - -let timer; - -// This is a subprocess spawned by logic in the TimerManager module. -process.on('message', (msg) => { - const logger = require('./Logger')(msg.logLevel); - switch (msg.command) { - case GAME_PROCESS_COMMANDS.START_TIMER: - logger.trace('CHILD PROCESS ' + msg.accessCode + ': START TIMER'); - timer = new ServerTimer( - msg.hours, - msg.minutes, - PRIMITIVES.CLOCK_TICK_INTERVAL_MILLIS, - logger - ); - timer.runTimer().then(() => { - logger.debug('Timer finished for ' + msg.accessCode); - process.send({ command: GAME_PROCESS_COMMANDS.END_TIMER }); - process.exit(0); - }); - - break; - case GAME_PROCESS_COMMANDS.PAUSE_TIMER: - timer.stopTimer(); - logger.trace('CHILD PROCESS ' + msg.accessCode + ': PAUSE TIMER'); - process.send({ command: GAME_PROCESS_COMMANDS.PAUSE_TIMER, timeRemaining: timer.currentTimeInMillis }); - - break; - - case GAME_PROCESS_COMMANDS.RESUME_TIMER: - timer.resumeTimer().then(() => { - logger.debug('Timer finished for ' + msg.accessCode); - process.send({ command: GAME_PROCESS_COMMANDS.END_TIMER }); - process.exit(0); - }); - logger.trace('CHILD PROCESS ' + msg.accessCode + ': RESUME TIMER'); - process.send({ command: GAME_PROCESS_COMMANDS.RESUME_TIMER, timeRemaining: timer.currentTimeInMillis }); - - break; - - case GAME_PROCESS_COMMANDS.GET_TIME_REMAINING: - logger.trace('CHILD PROCESS ' + msg.accessCode + ': GET TIME REMAINING'); - process.send({ - command: GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, - timeRemaining: timer.currentTimeInMillis, - socketId: msg.socketId - }); - - break; - } -}); diff --git a/server/modules/singletons/TimerManager.js b/server/modules/singletons/TimerManager.js index e216f78..9418b55 100644 --- a/server/modules/singletons/TimerManager.js +++ b/server/modules/singletons/TimerManager.js @@ -1,47 +1,15 @@ -const { fork } = require('child_process'); -const path = require('path'); -const { REDIS_CHANNELS, GAME_PROCESS_COMMANDS } = require('../../config/globals'); - +// TimerManager is now deprecated - timer logic has been moved to GameManager +// This class is kept as a stub to maintain compatibility with existing dependency injection 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 = {}; + logger.info('CREATING SINGLETON TIMER MANAGER (deprecated - timers now managed by GameManager)'); 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( - REDIS_CHANNELS.ACTIVE_GAME_STREAM, - eventManager.createMessageToPublish(game.accessCode, msg.command, this.instanceId, JSON.stringify(msg)) - ); - }); - - gameProcess.on('exit', (code, signal) => { - this.logger.debug('Game timer thread ' + gameProcess.pid + ' exiting with code ' + code + ' - game ' + game.accessCode); - }); - gameProcess.send({ - command: 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; diff --git a/spec/unit/server/modules/Events_Spec.js b/spec/unit/server/modules/Events_Spec.js index 37c6f45..0d7eb70 100644 --- a/spec/unit/server/modules/Events_Spec.js +++ b/spec/unit/server/modules/Events_Spec.js @@ -56,6 +56,7 @@ describe('Events', () => { spyOn(eventManager, 'createMessageToPublish').and.stub(); namespace.sockets = new Map(); timerManager.timerThreads = {}; + gameManager.timers = {}; }); describe(EVENT_IDS.PLAYER_JOINED, () => { @@ -363,13 +364,15 @@ describe('Events', () => { }); it('should end the game and kill the associated timer thread', async () => { game.hasTimer = true; - timerManager.timerThreads = { ABCD: { kill: () => {} } }; - spyOn(timerManager.timerThreads.ABCD, 'kill').and.callThrough(); + const mockTimer = { stopTimer: () => {} }; + gameManager.timers = { ABCD: mockTimer }; + const stopTimerSpy = spyOn(mockTimer, 'stopTimer').and.callThrough(); await Events.find((e) => e.id === EVENT_IDS.END_GAME) .stateChange(game, { id: 'b', assigned: true }, { gameManager: gameManager, timerManager: timerManager, logger: { trace: () => {} } }); expect(game.status).toEqual(STATUS.ENDED); expect(game.people.find(p => p.id === 'b').revealed).toBeTrue(); - expect(timerManager.timerThreads.ABCD.kill).toHaveBeenCalled(); + expect(stopTimerSpy).toHaveBeenCalled(); + expect(gameManager.timers.ABCD).toBeUndefined(); }); }); describe('communicate', () => { @@ -570,6 +573,7 @@ describe('Events', () => { describe(EVENT_IDS.TIMER_EVENT, () => { describe('communicate', () => { it('should publish an event to source timer data if the timer thread is not found', async () => { + game.timerParams = { hours: 1, minutes: 0, paused: true, timeRemaining: 3600000 }; await Events.find((e) => e.id === EVENT_IDS.TIMER_EVENT) .communicate(game, {}, { gameManager: gameManager, diff --git a/spec/unit/server/modules/singletons/GameManager_Spec.js b/spec/unit/server/modules/singletons/GameManager_Spec.js index 60a6e2b..7e2d814 100644 --- a/spec/unit/server/modules/singletons/GameManager_Spec.js +++ b/spec/unit/server/modules/singletons/GameManager_Spec.js @@ -112,6 +112,9 @@ describe('GameManager', () => { game.moderator = game.people[0]; game.people.find(p => p.id === 'b').userType = USER_TYPES.MODERATOR; game.hasDedicatedModerator = false; + // Add a mock timer + const mockTimer = { stopTimer: () => {} }; + gameManager.timers = { ABCD: mockTimer }; await gameManager.restartGame(game, namespace); From cab4ae5658fb68079801ebc38c2c83fde4fa196e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 03:43:25 +0000 Subject: [PATCH 04/11] Address code review feedback: add comments and reduce duplication Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> --- server/modules/Events.js | 173 ++++++++--------------- server/modules/singletons/GameManager.js | 2 + 2 files changed, 63 insertions(+), 112 deletions(-) diff --git a/server/modules/Events.js b/server/modules/Events.js index b52c5b2..eb83106 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -2,6 +2,65 @@ const GameStateCurator = require('./GameStateCurator'); const GameCreationRequest = require('../model/GameCreationRequest'); const { EVENT_IDS, STATUS, USER_TYPES, GAME_PROCESS_COMMANDS, REDIS_CHANNELS, PRIMITIVES } = require('../config/globals'); +// Helper function to handle timer commands +async function handleTimerCommand (timerEventSubtype, game, socketId, vars) { + switch (timerEventSubtype) { + case GAME_PROCESS_COMMANDS.PAUSE_TIMER: + const pauseTimeRemaining = await vars.gameManager.pauseTimer(game); + if (pauseTimeRemaining !== null) { + await vars.eventManager.handleEventById( + EVENT_IDS.PAUSE_TIMER, + null, + game, + null, + game.accessCode, + { timeRemaining: pauseTimeRemaining }, + null, + false + ); + await vars.gameManager.refreshGame(game); + await vars.eventManager.publisher.publish( + REDIS_CHANNELS.ACTIVE_GAME_STREAM, + vars.eventManager.createMessageToPublish( + game.accessCode, + EVENT_IDS.PAUSE_TIMER, + vars.instanceId, + JSON.stringify({ timeRemaining: pauseTimeRemaining }) + ) + ); + } + break; + case GAME_PROCESS_COMMANDS.RESUME_TIMER: + const resumeTimeRemaining = await vars.gameManager.resumeTimer(game); + if (resumeTimeRemaining !== null) { + await vars.eventManager.handleEventById( + EVENT_IDS.RESUME_TIMER, + null, + game, + null, + game.accessCode, + { timeRemaining: resumeTimeRemaining }, + null, + false + ); + await vars.gameManager.refreshGame(game); + await vars.eventManager.publisher.publish( + REDIS_CHANNELS.ACTIVE_GAME_STREAM, + vars.eventManager.createMessageToPublish( + game.accessCode, + EVENT_IDS.RESUME_TIMER, + vars.instanceId, + JSON.stringify({ timeRemaining: resumeTimeRemaining }) + ) + ); + } + break; + case GAME_PROCESS_COMMANDS.GET_TIME_REMAINING: + await vars.gameManager.getTimeRemaining(game, socketId); + break; + } +} + const Events = [ { id: EVENT_IDS.PLAYER_JOINED, @@ -318,63 +377,7 @@ const Events = [ const timer = vars.gameManager.timers[game.accessCode]; if (timer) { // Timer is running on this instance, handle the request directly - switch (vars.timerEventSubtype) { - case GAME_PROCESS_COMMANDS.PAUSE_TIMER: - const pauseTimeRemaining = await vars.gameManager.pauseTimer(game); - if (pauseTimeRemaining !== null) { - // Trigger PAUSE_TIMER event to update state and notify clients - await vars.eventManager.handleEventById( - EVENT_IDS.PAUSE_TIMER, - null, - game, - null, - game.accessCode, - { timeRemaining: pauseTimeRemaining }, - null, - false - ); - await vars.gameManager.refreshGame(game); - await vars.eventManager.publisher.publish( - REDIS_CHANNELS.ACTIVE_GAME_STREAM, - vars.eventManager.createMessageToPublish( - game.accessCode, - EVENT_IDS.PAUSE_TIMER, - vars.instanceId, - JSON.stringify({ timeRemaining: pauseTimeRemaining }) - ) - ); - } - break; - case GAME_PROCESS_COMMANDS.RESUME_TIMER: - const resumeTimeRemaining = await vars.gameManager.resumeTimer(game); - if (resumeTimeRemaining !== null) { - // Trigger RESUME_TIMER event to update state and notify clients - await vars.eventManager.handleEventById( - EVENT_IDS.RESUME_TIMER, - null, - game, - null, - game.accessCode, - { timeRemaining: resumeTimeRemaining }, - null, - false - ); - await vars.gameManager.refreshGame(game); - await vars.eventManager.publisher.publish( - REDIS_CHANNELS.ACTIVE_GAME_STREAM, - vars.eventManager.createMessageToPublish( - game.accessCode, - EVENT_IDS.RESUME_TIMER, - vars.instanceId, - JSON.stringify({ timeRemaining: resumeTimeRemaining }) - ) - ); - } - break; - case GAME_PROCESS_COMMANDS.GET_TIME_REMAINING: - await vars.gameManager.getTimeRemaining(game, vars.requestingSocketId); - break; - } + await handleTimerCommand(vars.timerEventSubtype, game, vars.requestingSocketId, vars); } else { // we need to consult another container for the timer data await vars.eventManager.publisher?.publish( REDIS_CHANNELS.ACTIVE_GAME_STREAM, @@ -397,61 +400,7 @@ const Events = [ const timer = vars.gameManager.timers[game.accessCode]; if (timer) { // Timer is running on this instance, handle the request - switch (socketArgs.timerEventSubtype) { - case GAME_PROCESS_COMMANDS.PAUSE_TIMER: - const pauseTimeRemaining = await vars.gameManager.pauseTimer(game); - if (pauseTimeRemaining !== null) { - await vars.eventManager.handleEventById( - EVENT_IDS.PAUSE_TIMER, - null, - game, - null, - game.accessCode, - { timeRemaining: pauseTimeRemaining }, - null, - false - ); - await vars.gameManager.refreshGame(game); - await vars.eventManager.publisher.publish( - REDIS_CHANNELS.ACTIVE_GAME_STREAM, - vars.eventManager.createMessageToPublish( - game.accessCode, - EVENT_IDS.PAUSE_TIMER, - vars.instanceId, - JSON.stringify({ timeRemaining: pauseTimeRemaining }) - ) - ); - } - break; - case GAME_PROCESS_COMMANDS.RESUME_TIMER: - const resumeTimeRemaining = await vars.gameManager.resumeTimer(game); - if (resumeTimeRemaining !== null) { - await vars.eventManager.handleEventById( - EVENT_IDS.RESUME_TIMER, - null, - game, - null, - game.accessCode, - { timeRemaining: resumeTimeRemaining }, - null, - false - ); - await vars.gameManager.refreshGame(game); - await vars.eventManager.publisher.publish( - REDIS_CHANNELS.ACTIVE_GAME_STREAM, - vars.eventManager.createMessageToPublish( - game.accessCode, - EVENT_IDS.RESUME_TIMER, - vars.instanceId, - JSON.stringify({ timeRemaining: resumeTimeRemaining }) - ) - ); - } - break; - case GAME_PROCESS_COMMANDS.GET_TIME_REMAINING: - await vars.gameManager.getTimeRemaining(game, socketArgs.socketId); - break; - } + await handleTimerCommand(socketArgs.timerEventSubtype, game, socketArgs.socketId, vars); } else { // Timer not running here, publish stored timer state await vars.eventManager.publisher.publish( diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index 8dc7de4..321c359 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -120,6 +120,7 @@ class GameManager { this.timers[game.accessCode] = timer; // Start timer in paused state initially (pausedInitially = true) + // Timer must be explicitly resumed by moderator timer.runTimer(true).then(async () => { this.logger.debug('Timer finished for ' + game.accessCode); // Trigger END_TIMER event @@ -156,6 +157,7 @@ class GameManager { const timer = this.timers[game.accessCode]; if (timer) { this.logger.debug('Timer found for game ' + game.accessCode); + // stopTimer() pauses the timer by clearing the setTimeout timer.stopTimer(); return timer.currentTimeInMillis; } From a2f88446d97f2c7da92974de7ddba21d181e7ea7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 03:56:10 +0000 Subject: [PATCH 05/11] Remove comments, delete TimerManager, and simplify SOURCE_TIMER_EVENT Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> --- server/modules/Events.js | 20 +------------------ server/modules/ServerBootstrapper.js | 5 ----- server/modules/singletons/EventManager.js | 2 -- server/modules/singletons/GameManager.js | 12 +---------- server/modules/singletons/TimerManager.js | 15 -------------- spec/unit/server/modules/Events_Spec.js | 15 +++++--------- .../modules/singletons/GameManager_Spec.js | 7 ++----- 7 files changed, 9 insertions(+), 67 deletions(-) delete mode 100644 server/modules/singletons/TimerManager.js diff --git a/server/modules/Events.js b/server/modules/Events.js index eb83106..93d123c 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -2,7 +2,6 @@ const GameStateCurator = require('./GameStateCurator'); const GameCreationRequest = require('../model/GameCreationRequest'); const { EVENT_IDS, STATUS, USER_TYPES, GAME_PROCESS_COMMANDS, REDIS_CHANNELS, PRIMITIVES } = require('../config/globals'); -// Helper function to handle timer commands async function handleTimerCommand (timerEventSubtype, game, socketId, vars) { switch (timerEventSubtype) { case GAME_PROCESS_COMMANDS.PAUSE_TIMER: @@ -376,9 +375,8 @@ const Events = [ communicate: async (game, socketArgs, vars) => { const timer = vars.gameManager.timers[game.accessCode]; if (timer) { - // Timer is running on this instance, handle the request directly await handleTimerCommand(vars.timerEventSubtype, game, vars.requestingSocketId, vars); - } else { // we need to consult another container for the timer data + } else { await vars.eventManager.publisher?.publish( REDIS_CHANNELS.ACTIVE_GAME_STREAM, vars.eventManager.createMessageToPublish( @@ -399,23 +397,7 @@ const Events = [ communicate: async (game, socketArgs, vars) => { const timer = vars.gameManager.timers[game.accessCode]; if (timer) { - // Timer is running on this instance, handle the request await handleTimerCommand(socketArgs.timerEventSubtype, game, socketArgs.socketId, vars); - } else { - // Timer not running here, publish stored timer state - await vars.eventManager.publisher.publish( - REDIS_CHANNELS.ACTIVE_GAME_STREAM, - vars.eventManager.createMessageToPublish( - game.accessCode, - socketArgs.timerEventSubtype, - vars.instanceId, - JSON.stringify({ - socketId: socketArgs.socketId, - timeRemaining: game.timerParams.timeRemaining, - paused: game.timerParams.paused - }) - ) - ); } } }, diff --git a/server/modules/ServerBootstrapper.js b/server/modules/ServerBootstrapper.js index f098af7..a5359b1 100644 --- a/server/modules/ServerBootstrapper.js +++ b/server/modules/ServerBootstrapper.js @@ -6,14 +6,12 @@ const fs = require('fs'); const crypto = require('crypto'); const EventManager = require('./singletons/EventManager.js'); const GameManager = require('./singletons/GameManager.js'); -const TimerManager = require('./singletons/TimerManager.js'); const rateLimit = require('express-rate-limit').default; const ServerBootstrapper = { singletons: (logger, instanceId) => { return { - timerManager: new TimerManager(logger, instanceId), eventManager: new EventManager(logger, instanceId), gameManager: process.env.NODE_ENV.trim() === 'development' ? new GameManager(logger, ENVIRONMENTS.LOCAL, instanceId) @@ -22,12 +20,9 @@ const ServerBootstrapper = { }, injectDependencies: (singletons) => { - const timerManager = require('./singletons/TimerManager').instance; const gameManager = require('./singletons/GameManager').instance; const eventManager = require('./singletons/EventManager').instance; - singletons.gameManager.timerManager = timerManager; singletons.gameManager.eventManager = eventManager; - singletons.eventManager.timerManager = timerManager; singletons.eventManager.gameManager = gameManager; }, diff --git a/server/modules/singletons/EventManager.js b/server/modules/singletons/EventManager.js index 933d69e..bc720d8 100644 --- a/server/modules/singletons/EventManager.js +++ b/server/modules/singletons/EventManager.js @@ -13,7 +13,6 @@ class EventManager { this.io = null; this.publisher = null; this.subscriber = null; - this.timerManager = null; this.gameManager = null; this.instanceId = instanceId; EventManager.instance = this; @@ -185,7 +184,6 @@ class EventManager { const event = Events.find((event) => event.id === eventId); const additionalVars = { gameManager: this.gameManager, - timerManager: this.timerManager, eventManager: this, requestingSocketId: requestingSocketId, ackFn: ackFn, diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index 321c359..ff2de6b 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -22,18 +22,16 @@ class GameManager { logger.info('CREATING SINGLETON GAME MANAGER'); this.logger = logger; this.environment = environment; - this.timerManager = null; this.eventManager = null; this.namespace = null; this.instanceId = instanceId; - this.timers = {}; // Map of accessCode -> ServerTimer instance + this.timers = {}; GameManager.instance = this; } getActiveGame = async (accessCode) => { const r = await this.eventManager.publisher.get(accessCode); if (r === null && this.timers[accessCode]) { - // Clean up orphaned timer this.timers[accessCode].stopTimer(); delete this.timers[accessCode]; } @@ -119,11 +117,8 @@ class GameManager { ); this.timers[game.accessCode] = timer; - // Start timer in paused state initially (pausedInitially = true) - // Timer must be explicitly resumed by moderator timer.runTimer(true).then(async () => { this.logger.debug('Timer finished for ' + game.accessCode); - // Trigger END_TIMER event game = await this.getActiveGame(game.accessCode); if (game) { await this.eventManager.handleEventById( @@ -147,7 +142,6 @@ class GameManager { ) ); } - // Clean up timer instance delete this.timers[game.accessCode]; }); game.startTime = new Date().toJSON(); @@ -157,7 +151,6 @@ class GameManager { const timer = this.timers[game.accessCode]; if (timer) { this.logger.debug('Timer found for game ' + game.accessCode); - // stopTimer() pauses the timer by clearing the setTimeout timer.stopTimer(); return timer.currentTimeInMillis; } @@ -178,14 +171,12 @@ class GameManager { if (socketId) { const timer = this.timers[game.accessCode]; if (timer) { - // Timer is running on this instance, emit directly this.namespace.to(socketId).emit( GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, timer.currentTimeInMillis, game.timerParams.paused ); } else { - // Timer not running on this instance, return stored value if (game.timerParams) { this.namespace.to(socketId).emit( GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, @@ -295,7 +286,6 @@ class GameManager { } restartGame = async (game, namespace) => { - // stop any outstanding timers const timer = this.timers[game.accessCode]; if (timer) { this.logger.info('Stopping timer for: ' + game.accessCode); diff --git a/server/modules/singletons/TimerManager.js b/server/modules/singletons/TimerManager.js deleted file mode 100644 index 9418b55..0000000 --- a/server/modules/singletons/TimerManager.js +++ /dev/null @@ -1,15 +0,0 @@ -// TimerManager is now deprecated - timer logic has been moved to GameManager -// This class is kept as a stub to maintain compatibility with existing dependency injection -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 (deprecated - timers now managed by GameManager)'); - this.logger = logger; - this.instanceId = instanceId; - TimerManager.instance = this; - } -} - -module.exports = TimerManager; diff --git a/spec/unit/server/modules/Events_Spec.js b/spec/unit/server/modules/Events_Spec.js index 0d7eb70..eeb0ccf 100644 --- a/spec/unit/server/modules/Events_Spec.js +++ b/spec/unit/server/modules/Events_Spec.js @@ -2,14 +2,13 @@ const Game = require('../../../../server/model/Game'); const { ENVIRONMENTS, EVENT_IDS, USER_TYPES, STATUS, GAME_PROCESS_COMMANDS } = require('../../../../server/config/globals.js'); const GameManager = require('../../../../server/modules/singletons/GameManager.js'); -const TimerManager = require('../../../../server/modules/singletons/TimerManager.js'); const EventManager = require('../../../../server/modules/singletons/EventManager.js'); const Events = require('../../../../server/modules/Events.js'); const GameStateCurator = require('../../../../server/modules/GameStateCurator.js'); const logger = require('../../../../server/modules/Logger.js')(false); describe('Events', () => { - let gameManager, namespace, socket, game, timerManager, eventManager; + let gameManager, namespace, socket, game, eventManager; beforeAll(() => { spyOn(logger, 'debug'); @@ -19,7 +18,6 @@ describe('Events', () => { namespace = { in: () => { return inObj; }, to: () => { return toObj; }, sockets: new Map() }; socket = { id: '123', emit: () => {}, to: () => { return { emit: () => {} }; } }; gameManager = GameManager.instance ? GameManager.instance : new GameManager(logger, ENVIRONMENTS.PRODUCTION, 'test'); - timerManager = TimerManager.instance ? TimerManager.instance : new TimerManager(logger, 'test'); eventManager = EventManager.instance ? EventManager.instance : new EventManager(logger, 'test'); gameManager.setGameSocketNamespace(namespace); eventManager.publisher = { publish: (...args) => {} }; @@ -55,7 +53,6 @@ describe('Events', () => { spyOn(eventManager.publisher, 'publish').and.callThrough(); spyOn(eventManager, 'createMessageToPublish').and.stub(); namespace.sockets = new Map(); - timerManager.timerThreads = {}; gameManager.timers = {}; }); @@ -275,7 +272,7 @@ describe('Events', () => { game.timerParams = {}; spyOn(gameManager, 'runTimer').and.callFake(() => {}); await Events.find((e) => e.id === EVENT_IDS.START_GAME) - .stateChange(game, { id: 'b', assigned: true }, { gameManager: gameManager, timerManager: timerManager }); + .stateChange(game, { id: 'b', assigned: true }, { gameManager: gameManager }); expect(game.status).toEqual(STATUS.IN_PROGRESS); expect(game.timerParams.paused).toEqual(true); expect(gameManager.runTimer).toHaveBeenCalled(); @@ -368,7 +365,7 @@ describe('Events', () => { gameManager.timers = { ABCD: mockTimer }; const stopTimerSpy = spyOn(mockTimer, 'stopTimer').and.callThrough(); await Events.find((e) => e.id === EVENT_IDS.END_GAME) - .stateChange(game, { id: 'b', assigned: true }, { gameManager: gameManager, timerManager: timerManager, logger: { trace: () => {} } }); + .stateChange(game, { id: 'b', assigned: true }, { gameManager: gameManager, logger: { trace: () => {} } }); expect(game.status).toEqual(STATUS.ENDED); expect(game.people.find(p => p.id === 'b').revealed).toBeTrue(); expect(stopTimerSpy).toHaveBeenCalled(); @@ -537,7 +534,7 @@ describe('Events', () => { gameManager.timers = { ABCD: mockTimer }; spyOn(gameManager.timers.ABCD, 'stopTimer').and.callThrough(); await Events.find((e) => e.id === EVENT_IDS.RESTART_GAME) - .stateChange(game, { personId: 'b' }, { gameManager: gameManager, timerManager: timerManager, instanceId: '111', senderInstanceId: '222' }); + .stateChange(game, { personId: 'b' }, { gameManager: gameManager, instanceId: '111', senderInstanceId: '222' }); expect(mockTimer.stopTimer).toHaveBeenCalled(); expect(Object.keys(gameManager.timers).length).toEqual(0); }); @@ -546,7 +543,7 @@ describe('Events', () => { gameManager.timers = { ABCD: mockTimer }; spyOn(gameManager.timers.ABCD, 'stopTimer').and.callThrough(); await Events.find((e) => e.id === EVENT_IDS.RESTART_GAME) - .stateChange(game, { personId: 'b' }, { gameManager: gameManager, timerManager: timerManager, instanceId: '111', senderInstanceId: '111' }); + .stateChange(game, { personId: 'b' }, { gameManager: gameManager, instanceId: '111', senderInstanceId: '111' }); expect(mockTimer.stopTimer).not.toHaveBeenCalled(); expect(Object.keys(gameManager.timers).length).toEqual(1); }); @@ -577,7 +574,6 @@ describe('Events', () => { await Events.find((e) => e.id === EVENT_IDS.TIMER_EVENT) .communicate(game, {}, { gameManager: gameManager, - timerManager: timerManager, eventManager: eventManager, instanceId: 'test', timerEventSubtype: GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, @@ -592,7 +588,6 @@ describe('Events', () => { await Events.find((e) => e.id === EVENT_IDS.TIMER_EVENT) .communicate(game, {}, { gameManager: gameManager, - timerManager: timerManager, eventManager: eventManager, timerEventSubtype: GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, requestingSocketId: '2', diff --git a/spec/unit/server/modules/singletons/GameManager_Spec.js b/spec/unit/server/modules/singletons/GameManager_Spec.js index 7e2d814..aafe8a8 100644 --- a/spec/unit/server/modules/singletons/GameManager_Spec.js +++ b/spec/unit/server/modules/singletons/GameManager_Spec.js @@ -4,12 +4,11 @@ const globals = require('../../../../../server/config/globals'); const USER_TYPES = globals.USER_TYPES; const STATUS = globals.STATUS; const GameManager = require('../../../../../server/modules/singletons/GameManager.js'); -const TimerManager = require('../../../../../server/modules/singletons/TimerManager.js'); const EventManager = require('../../../../../server/modules/singletons/EventManager.js'); const logger = require('../../../../../server/modules/Logger.js')(false); describe('GameManager', () => { - let gameManager, timerManager, eventManager, namespace, socket, game; + let gameManager, eventManager, namespace, socket, game; beforeAll(() => { spyOn(logger, 'debug'); @@ -19,11 +18,9 @@ describe('GameManager', () => { namespace = { in: () => { return inObj; }, to: () => { return inObj; } }; socket = { id: '123', emit: () => {}, to: () => { return { emit: () => {} }; } }; gameManager = GameManager.instance ? GameManager.instance : new GameManager(logger, globals.ENVIRONMENTS.PRODUCTION, 'test'); - timerManager = TimerManager.instance ? TimerManager.instance : new TimerManager(logger, 'test'); eventManager = EventManager.instance ? EventManager.instance : new EventManager(logger, 'test'); eventManager.publisher = { publish: async (...a) => {} }; gameManager.eventManager = eventManager; - gameManager.timerManager = timerManager; gameManager.setGameSocketNamespace(namespace); spyOn(gameManager, 'refreshGame').and.callFake(async () => {}); spyOn(eventManager.publisher, 'publish').and.callFake(async () => {}); @@ -32,7 +29,7 @@ describe('GameManager', () => { beforeEach(() => { spyOn(namespace, 'to').and.callThrough(); spyOn(socket, 'to').and.callThrough(); - timerManager.timerThreads = {}; + gameManager.timers = {}; game = new Game( 'ABCD', STATUS.LOBBY, From 4e0f3d5a33f9d92ff0437d1ced916d68df9e1215 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 04:46:39 +0000 Subject: [PATCH 06/11] Fix cross-instance timer communication and expired timer handling - GET_TIME_REMAINING now publishes response through Redis for cross-instance communication - SOURCE_TIMER_EVENT handles GET_TIME_REMAINING even when timer is not running locally - Returns 0 time remaining when timer has expired or timerParams is null - Updated test to expect Redis publish instead of direct method call Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> --- server/modules/Events.js | 22 ++++++++++++++++++++-- server/modules/singletons/GameManager.js | 16 ++++++++-------- spec/unit/server/modules/Events_Spec.js | 6 +++--- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/server/modules/Events.js b/server/modules/Events.js index 93d123c..d0cee81 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -55,7 +55,25 @@ async function handleTimerCommand (timerEventSubtype, game, socketId, vars) { } break; case GAME_PROCESS_COMMANDS.GET_TIME_REMAINING: - await vars.gameManager.getTimeRemaining(game, socketId); + const timer = vars.gameManager.timers[game.accessCode]; + const timeRemaining = timer + ? timer.currentTimeInMillis + : (game.timerParams ? game.timerParams.timeRemaining : 0); + const paused = game.timerParams ? game.timerParams.paused : false; + + await vars.eventManager.publisher.publish( + REDIS_CHANNELS.ACTIVE_GAME_STREAM, + vars.eventManager.createMessageToPublish( + game.accessCode, + GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + vars.instanceId, + JSON.stringify({ + socketId: socketId, + timeRemaining: timeRemaining, + paused: paused + }) + ) + ); break; } } @@ -396,7 +414,7 @@ const Events = [ stateChange: async (game, socketArgs, vars) => {}, communicate: async (game, socketArgs, vars) => { const timer = vars.gameManager.timers[game.accessCode]; - if (timer) { + if (timer || socketArgs.timerEventSubtype === GAME_PROCESS_COMMANDS.GET_TIME_REMAINING) { await handleTimerCommand(socketArgs.timerEventSubtype, game, socketArgs.socketId, vars); } } diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index ff2de6b..0939506 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -174,16 +174,16 @@ class GameManager { this.namespace.to(socketId).emit( GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, timer.currentTimeInMillis, - game.timerParams.paused + game.timerParams ? game.timerParams.paused : false ); } else { - if (game.timerParams) { - this.namespace.to(socketId).emit( - GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, - game.timerParams.timeRemaining, - game.timerParams.paused - ); - } + const timeRemaining = game.timerParams ? game.timerParams.timeRemaining : 0; + const paused = game.timerParams ? game.timerParams.paused : false; + this.namespace.to(socketId).emit( + GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + timeRemaining, + paused + ); } } }; diff --git a/spec/unit/server/modules/Events_Spec.js b/spec/unit/server/modules/Events_Spec.js index eeb0ccf..b0e086f 100644 --- a/spec/unit/server/modules/Events_Spec.js +++ b/spec/unit/server/modules/Events_Spec.js @@ -584,16 +584,16 @@ describe('Events', () => { it('should send a message to the thread if it is found', async () => { const mockTimer = { currentTimeInMillis: 5000 }; gameManager.timers = { ABCD: mockTimer }; - spyOn(gameManager, 'getTimeRemaining').and.returnValue(Promise.resolve()); await Events.find((e) => e.id === EVENT_IDS.TIMER_EVENT) .communicate(game, {}, { gameManager: gameManager, eventManager: eventManager, timerEventSubtype: GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, requestingSocketId: '2', - logger: { logLevel: 'trace' } + logger: { logLevel: 'trace' }, + instanceId: 'test' }); - expect(gameManager.getTimeRemaining).toHaveBeenCalledWith(game, '2'); + expect(eventManager.publisher.publish).toHaveBeenCalled(); }); }); }); From d0eeca8d4282bbe758037e0e16d9fed8bc197b70 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 15:57:42 +0000 Subject: [PATCH 07/11] Simplify GET_TIME_REMAINING flow and add ended flag - Add ended: true flag to timerParams when timer completes - Simplify TIMER_EVENT to check ended flag first, then local timer, then publish SOURCE_TIMER_EVENT - SOURCE_TIMER_EVENT only publishes GET_TIME_REMAINING if it has the timer - Remove GET_TIME_REMAINING from handleTimerCommand helper - Direct socket emission for local timers instead of Redis publish - Updated test to verify socket emission Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> --- server/modules/Events.js | 108 +++++++++++++++--------- spec/unit/server/modules/Events_Spec.js | 3 +- 2 files changed, 72 insertions(+), 39 deletions(-) diff --git a/server/modules/Events.js b/server/modules/Events.js index d0cee81..69cdc80 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -54,27 +54,6 @@ async function handleTimerCommand (timerEventSubtype, game, socketId, vars) { ); } break; - case GAME_PROCESS_COMMANDS.GET_TIME_REMAINING: - const timer = vars.gameManager.timers[game.accessCode]; - const timeRemaining = timer - ? timer.currentTimeInMillis - : (game.timerParams ? game.timerParams.timeRemaining : 0); - const paused = game.timerParams ? game.timerParams.paused : false; - - await vars.eventManager.publisher.publish( - REDIS_CHANNELS.ACTIVE_GAME_STREAM, - vars.eventManager.createMessageToPublish( - game.accessCode, - GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, - vars.instanceId, - JSON.stringify({ - socketId: socketId, - timeRemaining: timeRemaining, - paused: paused - }) - ) - ); - break; } } @@ -391,31 +370,83 @@ const Events = [ id: EVENT_IDS.TIMER_EVENT, stateChange: async (game, socketArgs, vars) => {}, communicate: async (game, socketArgs, vars) => { - const timer = vars.gameManager.timers[game.accessCode]; - if (timer) { - await handleTimerCommand(vars.timerEventSubtype, game, vars.requestingSocketId, vars); + if (vars.timerEventSubtype === GAME_PROCESS_COMMANDS.GET_TIME_REMAINING) { + if (game.timerParams && game.timerParams.ended) { + const socket = vars.gameManager.namespace.sockets.get(vars.requestingSocketId); + if (socket) { + vars.gameManager.namespace.to(socket.id).emit( + GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + 0, + false + ); + } + } else { + const timer = vars.gameManager.timers[game.accessCode]; + if (timer) { + const socket = vars.gameManager.namespace.sockets.get(vars.requestingSocketId); + if (socket) { + vars.gameManager.namespace.to(socket.id).emit( + GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + timer.currentTimeInMillis, + game.timerParams ? game.timerParams.paused : false + ); + } + } else { + await vars.eventManager.publisher?.publish( + REDIS_CHANNELS.ACTIVE_GAME_STREAM, + vars.eventManager.createMessageToPublish( + game.accessCode, + EVENT_IDS.SOURCE_TIMER_EVENT, + vars.instanceId, + JSON.stringify({ socketId: vars.requestingSocketId, timerEventSubtype: vars.timerEventSubtype }) + ) + ); + } + } } else { - await vars.eventManager.publisher?.publish( - REDIS_CHANNELS.ACTIVE_GAME_STREAM, - vars.eventManager.createMessageToPublish( - game.accessCode, - EVENT_IDS.SOURCE_TIMER_EVENT, - vars.instanceId, - JSON.stringify({ socketId: vars.requestingSocketId, timerEventSubtype: vars.timerEventSubtype }) - ) - ); + const timer = vars.gameManager.timers[game.accessCode]; + if (timer) { + await handleTimerCommand(vars.timerEventSubtype, game, vars.requestingSocketId, vars); + } else { + await vars.eventManager.publisher?.publish( + REDIS_CHANNELS.ACTIVE_GAME_STREAM, + vars.eventManager.createMessageToPublish( + game.accessCode, + EVENT_IDS.SOURCE_TIMER_EVENT, + vars.instanceId, + JSON.stringify({ socketId: vars.requestingSocketId, timerEventSubtype: vars.timerEventSubtype }) + ) + ); + } } } }, { - /* 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. */ id: EVENT_IDS.SOURCE_TIMER_EVENT, stateChange: async (game, socketArgs, vars) => {}, communicate: async (game, socketArgs, vars) => { - const timer = vars.gameManager.timers[game.accessCode]; - if (timer || socketArgs.timerEventSubtype === GAME_PROCESS_COMMANDS.GET_TIME_REMAINING) { - await handleTimerCommand(socketArgs.timerEventSubtype, game, socketArgs.socketId, vars); + if (socketArgs.timerEventSubtype === GAME_PROCESS_COMMANDS.GET_TIME_REMAINING) { + const timer = vars.gameManager.timers[game.accessCode]; + if (timer) { + await vars.eventManager.publisher.publish( + REDIS_CHANNELS.ACTIVE_GAME_STREAM, + vars.eventManager.createMessageToPublish( + game.accessCode, + GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + vars.instanceId, + JSON.stringify({ + socketId: socketArgs.socketId, + timeRemaining: timer.currentTimeInMillis, + paused: game.timerParams ? game.timerParams.paused : false + }) + ) + ); + } + } else { + const timer = vars.gameManager.timers[game.accessCode]; + if (timer) { + await handleTimerCommand(socketArgs.timerEventSubtype, game, socketArgs.socketId, vars); + } } } }, @@ -443,6 +474,7 @@ const Events = [ stateChange: async (game, socketArgs, vars) => { game.timerParams.paused = false; game.timerParams.timeRemaining = 0; + game.timerParams.ended = true; }, communicate: async (game, socketArgs, vars) => { vars.gameManager.namespace.in(game.accessCode).emit(GAME_PROCESS_COMMANDS.END_TIMER); diff --git a/spec/unit/server/modules/Events_Spec.js b/spec/unit/server/modules/Events_Spec.js index b0e086f..12300c7 100644 --- a/spec/unit/server/modules/Events_Spec.js +++ b/spec/unit/server/modules/Events_Spec.js @@ -584,6 +584,7 @@ describe('Events', () => { it('should send a message to the thread if it is found', async () => { const mockTimer = { currentTimeInMillis: 5000 }; gameManager.timers = { ABCD: mockTimer }; + namespace.sockets.set('2', { id: '2' }); await Events.find((e) => e.id === EVENT_IDS.TIMER_EVENT) .communicate(game, {}, { gameManager: gameManager, @@ -593,7 +594,7 @@ describe('Events', () => { logger: { logLevel: 'trace' }, instanceId: 'test' }); - expect(eventManager.publisher.publish).toHaveBeenCalled(); + expect(namespace.to).toHaveBeenCalledWith('2'); }); }); }); From 65e4c588b124f82c02f9c7d2c58a3349cc482be7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:06:44 +0000 Subject: [PATCH 08/11] Refactor GET_TIME_REMAINING into handleTimerCommand switch case - Moved GET_TIME_REMAINING logic from TIMER_EVENT handler into handleTimerCommand function - Simplified TIMER_EVENT to just call handleTimerCommand for all timer operations - Maintains same flow: check ended flag, check local timer, publish SOURCE_TIMER_EVENT if needed - All tests passing Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> --- server/modules/Events.js | 84 +++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 49 deletions(-) diff --git a/server/modules/Events.js b/server/modules/Events.js index 69cdc80..0fea4d4 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -54,6 +54,40 @@ async function handleTimerCommand (timerEventSubtype, game, socketId, vars) { ); } break; + case GAME_PROCESS_COMMANDS.GET_TIME_REMAINING: + if (game.timerParams && game.timerParams.ended) { + const socket = vars.gameManager.namespace.sockets.get(socketId); + if (socket) { + vars.gameManager.namespace.to(socket.id).emit( + GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + 0, + false + ); + } + } else { + const timer = vars.gameManager.timers[game.accessCode]; + if (timer) { + const socket = vars.gameManager.namespace.sockets.get(socketId); + if (socket) { + vars.gameManager.namespace.to(socket.id).emit( + GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, + timer.currentTimeInMillis, + game.timerParams ? game.timerParams.paused : false + ); + } + } else { + await vars.eventManager.publisher?.publish( + REDIS_CHANNELS.ACTIVE_GAME_STREAM, + vars.eventManager.createMessageToPublish( + game.accessCode, + EVENT_IDS.SOURCE_TIMER_EVENT, + vars.instanceId, + JSON.stringify({ socketId: socketId, timerEventSubtype: timerEventSubtype }) + ) + ); + } + } + break; } } @@ -370,55 +404,7 @@ const Events = [ id: EVENT_IDS.TIMER_EVENT, stateChange: async (game, socketArgs, vars) => {}, communicate: async (game, socketArgs, vars) => { - if (vars.timerEventSubtype === GAME_PROCESS_COMMANDS.GET_TIME_REMAINING) { - if (game.timerParams && game.timerParams.ended) { - const socket = vars.gameManager.namespace.sockets.get(vars.requestingSocketId); - if (socket) { - vars.gameManager.namespace.to(socket.id).emit( - GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, - 0, - false - ); - } - } else { - const timer = vars.gameManager.timers[game.accessCode]; - if (timer) { - const socket = vars.gameManager.namespace.sockets.get(vars.requestingSocketId); - if (socket) { - vars.gameManager.namespace.to(socket.id).emit( - GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, - timer.currentTimeInMillis, - game.timerParams ? game.timerParams.paused : false - ); - } - } else { - await vars.eventManager.publisher?.publish( - REDIS_CHANNELS.ACTIVE_GAME_STREAM, - vars.eventManager.createMessageToPublish( - game.accessCode, - EVENT_IDS.SOURCE_TIMER_EVENT, - vars.instanceId, - JSON.stringify({ socketId: vars.requestingSocketId, timerEventSubtype: vars.timerEventSubtype }) - ) - ); - } - } - } else { - const timer = vars.gameManager.timers[game.accessCode]; - if (timer) { - await handleTimerCommand(vars.timerEventSubtype, game, vars.requestingSocketId, vars); - } else { - await vars.eventManager.publisher?.publish( - REDIS_CHANNELS.ACTIVE_GAME_STREAM, - vars.eventManager.createMessageToPublish( - game.accessCode, - EVENT_IDS.SOURCE_TIMER_EVENT, - vars.instanceId, - JSON.stringify({ socketId: vars.requestingSocketId, timerEventSubtype: vars.timerEventSubtype }) - ) - ); - } - } + await handleTimerCommand(vars.timerEventSubtype, game, vars.requestingSocketId, vars); } }, { From c466779085839294da565eedcb94ba823d241b9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:12:48 +0000 Subject: [PATCH 09/11] Remove unused getTimeRemaining function from GameManager - Function is no longer called after refactoring GET_TIME_REMAINING into handleTimerCommand - All timer time remaining logic now handled in Events.js handleTimerCommand switch case - Tests still passing Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> --- server/modules/singletons/GameManager.js | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index 0939506..3207063 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -167,27 +167,6 @@ class GameManager { return null; }; - getTimeRemaining = async (game, socketId) => { - if (socketId) { - const timer = this.timers[game.accessCode]; - if (timer) { - this.namespace.to(socketId).emit( - GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, - timer.currentTimeInMillis, - game.timerParams ? game.timerParams.paused : false - ); - } else { - const timeRemaining = game.timerParams ? game.timerParams.timeRemaining : 0; - const paused = game.timerParams ? game.timerParams.paused : false; - this.namespace.to(socketId).emit( - GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, - timeRemaining, - paused - ); - } - } - }; - checkAvailability = async (code) => { const game = await this.getActiveGame(code.toUpperCase().trim()); if (game) { From c91e9292f0a4b73c1d22a7ba836bb3d994bc357c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:30:06 +0000 Subject: [PATCH 10/11] Reset timerParams.ended flag when restarting game - Added reset of ended flag to false in restartGame function - Ensures restarted games don't have timer marked as already ended - Placed right after timer cleanup for logical grouping Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> --- server/modules/singletons/GameManager.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index 3207063..e8e1808 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -272,6 +272,10 @@ class GameManager { delete this.timers[game.accessCode]; } + if (game.timerParams) { + game.timerParams.ended = false; + } + for (let i = 0; i < game.people.length; i ++) { if (game.people[i].userType === USER_TYPES.KILLED_PLAYER) { game.people[i].userType = USER_TYPES.PLAYER; From 7ea71b7fe7aadcd6b9b7b6623ee57df79ef1d895 Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Sat, 24 Jan 2026 11:56:33 -0500 Subject: [PATCH 11/11] remuse unused constant --- server/modules/singletons/GameManager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index e8e1808..904e633 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -2,7 +2,6 @@ const { STATUS, PRIMITIVES, ERROR_MESSAGES, - GAME_PROCESS_COMMANDS, USER_TYPES, EVENT_IDS, REDIS_CHANNELS