mirror of
https://github.com/AlecM33/Werewolf.git
synced 2026-02-10 04:03:33 +01:00
Refactor timer system from child processes to main thread (#216)
* Initial plan * Refactor timer system to run on main thread instead of child processes Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> * Remove obsolete GameProcess.js and simplify TimerManager to stub Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> * Address code review feedback: add comments and reduce duplication Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> * Remove comments, delete TimerManager, and simplify SOURCE_TIMER_EVENT Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> * 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> * 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> * 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> * 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> * 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> * remuse unused constant --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> Co-authored-by: AlecM33 <leohfx@gmail.com>
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user