const db = require('../database'); const { getWebSocketManager } = require('./websocket-manager'); const { checkRoomStatus } = require('./jackbox-api'); const POLL_INTERVAL_MS = 10000; // Active room monitors, keyed by "{sessionId}-{gameId}" const activeMonitors = new Map(); /** * Broadcast game.started event when room becomes locked */ function broadcastGameStarted(sessionId, gameId, roomCode, maxPlayers) { try { const wsManager = getWebSocketManager(); if (wsManager) { wsManager.broadcastEvent('game.started', { sessionId, gameId, roomCode, maxPlayers }, parseInt(sessionId)); } console.log(`[Room Monitor] Broadcasted game.started for room ${roomCode} (max: ${maxPlayers})`); } catch (error) { console.error('[Room Monitor] Failed to broadcast game.started:', error.message); } } /** * Start monitoring a Jackbox room for game start (locked state). * * Polls the Jackbox REST API every 10 seconds. When the room becomes * locked, broadcasts a game.started WebSocket event and then hands off * to the player-count-checker to join as audience. */ async function startRoomMonitor(sessionId, gameId, roomCode, maxPlayers = 8) { const monitorKey = `${sessionId}-${gameId}`; if (activeMonitors.has(monitorKey)) { console.log(`[Room Monitor] Already monitoring ${monitorKey}`); return; } console.log(`[Room Monitor] Starting monitor for room ${roomCode} (${monitorKey})`); const onGameStarted = (realMaxPlayers) => { broadcastGameStarted(sessionId, gameId, roomCode, realMaxPlayers); // Lazy require breaks the circular dependency with player-count-checker const { startPlayerCountCheck } = require('./player-count-checker'); console.log(`[Room Monitor] Room ${roomCode} locked — handing off to player count checker`); startPlayerCountCheck(sessionId, gameId, roomCode, realMaxPlayers); }; const pollRoom = async () => { const game = db.prepare(` SELECT status FROM session_games WHERE session_id = ? AND id = ? `).get(sessionId, gameId); if (!game || game.status === 'skipped' || game.status === 'played') { console.log(`[Room Monitor] Stopping — game status changed for ${monitorKey}`); stopRoomMonitor(sessionId, gameId); return; } const roomStatus = await checkRoomStatus(roomCode); if (!roomStatus.exists) { console.log(`[Room Monitor] Room ${roomCode} does not exist — stopping`); stopRoomMonitor(sessionId, gameId); return; } if (roomStatus.locked) { stopRoomMonitor(sessionId, gameId); onGameStarted(roomStatus.maxPlayers); return; } if (roomStatus.full) { console.log(`[Room Monitor] Room ${roomCode} is full but not locked yet — waiting`); } else { console.log(`[Room Monitor] Room ${roomCode} lobby still open — waiting`); } }; // Poll immediately, then every POLL_INTERVAL_MS activeMonitors.set(monitorKey, { sessionId, gameId, roomCode, interval: null }); await pollRoom(); // If the monitor was already stopped (room locked or gone on first check), bail if (!activeMonitors.has(monitorKey)) return; const interval = setInterval(() => pollRoom(), POLL_INTERVAL_MS); const monitor = activeMonitors.get(monitorKey); if (monitor) monitor.interval = interval; } /** * Stop monitoring a room */ function stopRoomMonitor(sessionId, gameId) { const monitorKey = `${sessionId}-${gameId}`; const monitor = activeMonitors.get(monitorKey); if (monitor) { if (monitor.interval) clearInterval(monitor.interval); activeMonitors.delete(monitorKey); console.log(`[Room Monitor] Stopped monitor for ${monitorKey}`); } } /** * Clean up all active monitors (for graceful shutdown) */ function cleanupAllMonitors() { for (const [, monitor] of activeMonitors.entries()) { if (monitor.interval) clearInterval(monitor.interval); } activeMonitors.clear(); console.log('[Room Monitor] Cleaned up all active monitors'); } module.exports = { startRoomMonitor, stopRoomMonitor, cleanupAllMonitors };