From 1c4c8bc19c056641f3c28fe517cd2f29dfdfafcd Mon Sep 17 00:00:00 2001 From: cottongin Date: Fri, 20 Mar 2026 11:25:01 -0400 Subject: [PATCH] feat: add startMonitor, stopMonitor, cleanupAllShards module exports --- backend/utils/ecast-shard-client.js | 149 ++++++++++++++++++++++++++- tests/api/ecast-shard-client.test.js | 16 +++ 2 files changed, 164 insertions(+), 1 deletion(-) diff --git a/backend/utils/ecast-shard-client.js b/backend/utils/ecast-shard-client.js index ef51244..9f8ab60 100644 --- a/backend/utils/ecast-shard-client.js +++ b/backend/utils/ecast-shard-client.js @@ -1,5 +1,7 @@ const WebSocket = require('ws'); +const db = require('../database'); +const { getWebSocketManager } = require('./websocket-manager'); const { getRoomInfo } = require('./jackbox-api'); class EcastShardClient { @@ -420,4 +422,149 @@ class EcastShardClient { } } -module.exports = { EcastShardClient }; +const activeShards = new Map(); + +function broadcastAndPersist(sessionId, gameId) { + return (eventType, eventData) => { + const wsManager = getWebSocketManager(); + if (wsManager) { + wsManager.broadcastEvent(eventType, eventData, parseInt(sessionId, 10)); + } + + if (['room.connected', 'lobby.player-joined', 'game.started', 'game.ended'].includes(eventType)) { + const status = eventType === 'game.ended' ? 'completed' : 'monitoring'; + try { + db.prepare( + 'UPDATE session_games SET player_count = ?, player_count_check_status = ? WHERE session_id = ? AND id = ?' + ).run(eventData.playerCount ?? null, status, sessionId, gameId); + } catch (e) { + console.error('[Shard Monitor] DB update failed:', e.message); + } + } + + if (eventType === 'room.disconnected') { + const reason = eventData.reason; + const status = + reason === 'room_closed' ? 'completed' : reason === 'manually_stopped' ? 'stopped' : 'failed'; + try { + const game = db + .prepare('SELECT player_count_check_status FROM session_games WHERE session_id = ? AND id = ?') + .get(sessionId, gameId); + if (game && game.player_count_check_status !== 'completed') { + db.prepare('UPDATE session_games SET player_count_check_status = ? WHERE session_id = ? AND id = ?').run( + status, + sessionId, + gameId + ); + } + } catch (e) { + console.error('[Shard Monitor] DB update failed:', e.message); + } + } + }; +} + +async function startMonitor(sessionId, gameId, roomCode, maxPlayers = 8) { + const monitorKey = `${sessionId}-${gameId}`; + + if (activeShards.has(monitorKey)) { + console.log(`[Shard Monitor] Already monitoring ${monitorKey}`); + return; + } + + console.log(`[Shard Monitor] Starting monitor for room ${roomCode} (${monitorKey})`); + + const roomInfo = await getRoomInfo(roomCode); + if (!roomInfo.exists) { + console.log(`[Shard Monitor] Room ${roomCode} not found`); + const onEvent = broadcastAndPersist(sessionId, gameId); + onEvent('room.disconnected', { + sessionId, + gameId, + roomCode, + reason: 'room_not_found', + finalPlayerCount: null, + }); + return; + } + + const onEvent = broadcastAndPersist(sessionId, gameId); + + try { + db.prepare('UPDATE session_games SET player_count_check_status = ? WHERE session_id = ? AND id = ?').run( + 'monitoring', + sessionId, + gameId + ); + } catch (e) { + console.error('[Shard Monitor] DB update failed:', e.message); + } + + const client = new EcastShardClient({ + sessionId, + gameId, + roomCode, + maxPlayers: roomInfo.maxPlayers || maxPlayers, + onEvent, + }); + + activeShards.set(monitorKey, client); + + try { + await client.connect(roomInfo); + } catch (e) { + console.error(`[Shard Monitor] Failed to connect to room ${roomCode}:`, e.message); + activeShards.delete(monitorKey); + onEvent('room.disconnected', { + sessionId, + gameId, + roomCode, + reason: 'connection_failed', + finalPlayerCount: null, + }); + } +} + +async function stopMonitor(sessionId, gameId) { + const monitorKey = `${sessionId}-${gameId}`; + const client = activeShards.get(monitorKey); + + if (client) { + client.manuallyStopped = true; + client.disconnect(); + activeShards.delete(monitorKey); + + const game = db + .prepare('SELECT player_count_check_status FROM session_games WHERE session_id = ? AND id = ?') + .get(sessionId, gameId); + + if (game && game.player_count_check_status !== 'completed' && game.player_count_check_status !== 'failed') { + db.prepare('UPDATE session_games SET player_count_check_status = ? WHERE session_id = ? AND id = ?').run( + 'stopped', + sessionId, + gameId + ); + } + + client.onEvent('room.disconnected', { + sessionId, + gameId, + roomCode: client.roomCode, + reason: 'manually_stopped', + finalPlayerCount: client.playerCount, + }); + + console.log(`[Shard Monitor] Stopped monitor for ${monitorKey}`); + } +} + +async function cleanupAllShards() { + for (const [, client] of activeShards) { + client.manuallyStopped = true; + client.disconnect(); + } + activeShards.clear(); + console.log('[Shard Monitor] Cleaned up all active shards'); +} + +module.exports = { EcastShardClient, startMonitor, stopMonitor, cleanupAllShards }; diff --git a/tests/api/ecast-shard-client.test.js b/tests/api/ecast-shard-client.test.js index 5d15d54..51c9313 100644 --- a/tests/api/ecast-shard-client.test.js +++ b/tests/api/ecast-shard-client.test.js @@ -376,6 +376,22 @@ describe('EcastShardClient', () => { }); }); + describe('module exports', () => { + const { startMonitor, stopMonitor, cleanupAllShards } = require('../../backend/utils/ecast-shard-client'); + + test('startMonitor is exported', () => { + expect(typeof startMonitor).toBe('function'); + }); + + test('stopMonitor is exported', () => { + expect(typeof stopMonitor).toBe('function'); + }); + + test('cleanupAllShards is exported', () => { + expect(typeof cleanupAllShards).toBe('function'); + }); + }); + describe('handleError with code 2027', () => { test('marks game as finished and emits events on room-closed error', () => { const events = [];