feat: add startMonitor, stopMonitor, cleanupAllShards module exports

This commit is contained in:
cottongin
2026-03-20 11:25:01 -04:00
parent de395d3a28
commit 1c4c8bc19c
2 changed files with 164 additions and 1 deletions

View File

@@ -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 };

View File

@@ -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 = [];