diff --git a/server/modules/Events.js b/server/modules/Events.js index 6648ff7..0fea4d4 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -2,6 +2,95 @@ const GameStateCurator = require('./GameStateCurator'); const GameCreationRequest = require('../model/GameCreationRequest'); const { EVENT_IDS, STATUS, USER_TYPES, GAME_PROCESS_COMMANDS, REDIS_CHANNELS, PRIMITIVES } = require('../config/globals'); +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: + 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; + } +} + const Events = [ { id: EVENT_IDS.PLAYER_JOINED, @@ -164,7 +253,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); } } }, @@ -220,9 +309,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; @@ -297,12 +387,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,68 +404,35 @@ 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 - ); - } - } - } else { // we need to consult another container for the timer data - 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); } }, { - /* 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_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 { + 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, - socketArgs.timerEventSubtype, + GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, vars.instanceId, JSON.stringify({ socketId: socketArgs.socketId, - timeRemaining: game.timerParams.timeRemaining, - paused: game.timerParams.paused + 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); + } } } }, @@ -405,6 +460,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/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/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 3206dd0..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 @@ -12,6 +11,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) { @@ -21,20 +21,18 @@ 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 = {}; 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]) { + this.timers[accessCode].stopTimer(); + delete this.timers[accessCode]; } let activeGame; if (r !== null) { @@ -108,46 +106,64 @@ 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; + + timer.runTimer(true).then(async () => { + this.logger.debug('Timer finished for ' + game.accessCode); + 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, + '{}' + ) + ); + } + delete this.timers[game.accessCode]; + }); + game.startTime = new Date().toJSON(); }; - 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 - }); + 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; }; - 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); - } - } + 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; }; checkAvailability = async (code) => { @@ -248,15 +264,15 @@ 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]; + const timer = this.timers[game.accessCode]; + if (timer) { + this.logger.info('Stopping timer for: ' + game.accessCode); + timer.stopTimer(); + delete this.timers[game.accessCode]; + } + + if (game.timerParams) { + game.timerParams.ended = false; } for (let i = 0; i < game.people.length; i ++) { diff --git a/server/modules/singletons/TimerManager.js b/server/modules/singletons/TimerManager.js deleted file mode 100644 index e216f78..0000000 --- a/server/modules/singletons/TimerManager.js +++ /dev/null @@ -1,47 +0,0 @@ -const { fork } = require('child_process'); -const path = require('path'); -const { REDIS_CHANNELS, GAME_PROCESS_COMMANDS } = 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( - 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 493db0f..12300c7 100644 --- a/spec/unit/server/modules/Events_Spec.js +++ b/spec/unit/server/modules/Events_Spec.js @@ -1,15 +1,14 @@ // 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'); 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,7 @@ describe('Events', () => { spyOn(eventManager.publisher, 'publish').and.callThrough(); spyOn(eventManager, 'createMessageToPublish').and.stub(); namespace.sockets = new Map(); - timerManager.timerThreads = {}; + gameManager.timers = {}; }); describe(EVENT_IDS.PLAYER_JOINED, () => { @@ -272,12 +270,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 }); + .stateChange(game, { id: 'b', assigned: true }, { gameManager: gameManager }); expect(game.status).toEqual(STATUS.IN_PROGRESS); expect(game.timerParams.paused).toEqual(true); - expect(timerManager.runTimer).toHaveBeenCalled(); + expect(gameManager.runTimer).toHaveBeenCalled(); }); }); describe('communicate', () => { @@ -363,13 +361,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: () => {} } }); + .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(timerManager.timerThreads.ABCD.kill).toHaveBeenCalled(); + expect(stopTimerSpy).toHaveBeenCalled(); + expect(gameManager.timers.ABCD).toBeUndefined(); }); }); 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); + .stateChange(game, { personId: 'b' }, { gameManager: gameManager, instanceId: '111', senderInstanceId: '222' }); + 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); + .stateChange(game, { personId: 'b' }, { gameManager: gameManager, instanceId: '111', senderInstanceId: '111' }); + expect(mockTimer.stopTimer).not.toHaveBeenCalled(); + expect(Object.keys(gameManager.timers).length).toEqual(1); }); }); describe('communicate', () => { @@ -570,29 +570,31 @@ 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, timerManager: timerManager, eventManager: eventManager }); + .communicate(game, {}, { + gameManager: gameManager, + 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 }; + namespace.sockets.set('2', { id: '2' }); 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' } + logger: { logLevel: 'trace' }, + instanceId: 'test' }); - expect(mockThread.send).toHaveBeenCalledWith({ - command: EVENT_IDS.GET_TIME_REMAINING, - accessCode: 'ABCD', - socketId: '2', - logLevel: 'trace' - }); + expect(namespace.to).toHaveBeenCalledWith('2'); }); }); }); diff --git a/spec/unit/server/modules/singletons/GameManager_Spec.js b/spec/unit/server/modules/singletons/GameManager_Spec.js index 72a078a..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, @@ -91,16 +88,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); }); @@ -111,6 +109,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);