Decouple room monitoring from player count, fix Jackbox API fetch
Extracts checkRoomStatus into shared jackbox-api.js with proper User-Agent header (bare fetch was silently rejected by Jackbox API) and always-on error logging (previously gated behind DEBUG flag). Splits room-start detection (room-monitor.js) from audience-based player counting (player-count-checker.js) to eliminate circular dependency and allow immediate game.started detection. Room monitor now polls immediately instead of waiting 10 seconds for first check. Made-with: Cursor
This commit is contained in:
135
backend/utils/room-monitor.js
Normal file
135
backend/utils/room-monitor.js
Normal file
@@ -0,0 +1,135 @@
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user