feat: add periodic game.status broadcast and live status REST endpoint
Add 20-second game.status WebSocket heartbeat from active shard monitors containing full game state, and GET /status-live REST endpoint for on-demand polling. Fix missing token destructuring in SessionInfo causing crash. Relax frontend polling from 3s to 60s since WebSocket events now cover real-time updates. Bump version to 0.6.0. Made-with: Cursor
This commit is contained in:
@@ -376,8 +376,121 @@ describe('EcastShardClient', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSnapshot', () => {
|
||||
test('returns correct shape with current state', () => {
|
||||
const client = new EcastShardClient({
|
||||
sessionId: 1, gameId: 5, roomCode: 'LSBN', maxPlayers: 8, onEvent: () => {}
|
||||
});
|
||||
client.appTag = 'drawful2international';
|
||||
client.playerCount = 3;
|
||||
client.playerNames = ['Alice', 'Bob', 'Charlie'];
|
||||
client.lobbyState = 'CanStart';
|
||||
client.gameState = 'Lobby';
|
||||
client.gameStarted = false;
|
||||
client.gameFinished = false;
|
||||
|
||||
const snapshot = client.getSnapshot();
|
||||
|
||||
expect(snapshot).toEqual({
|
||||
sessionId: 1,
|
||||
gameId: 5,
|
||||
roomCode: 'LSBN',
|
||||
appTag: 'drawful2international',
|
||||
maxPlayers: 8,
|
||||
playerCount: 3,
|
||||
players: ['Alice', 'Bob', 'Charlie'],
|
||||
lobbyState: 'CanStart',
|
||||
gameState: 'Lobby',
|
||||
gameStarted: false,
|
||||
gameFinished: false,
|
||||
monitoring: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns a defensive copy of playerNames', () => {
|
||||
const client = new EcastShardClient({
|
||||
sessionId: 1, gameId: 5, roomCode: 'TEST', maxPlayers: 8, onEvent: () => {}
|
||||
});
|
||||
client.playerNames = ['Alice'];
|
||||
|
||||
const snapshot = client.getSnapshot();
|
||||
snapshot.players.push('Mutated');
|
||||
|
||||
expect(client.playerNames).toEqual(['Alice']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startStatusBroadcast / stopStatusBroadcast', () => {
|
||||
beforeEach(() => jest.useFakeTimers());
|
||||
afterEach(() => jest.useRealTimers());
|
||||
|
||||
test('broadcasts game.status every 20 seconds', () => {
|
||||
const events = [];
|
||||
const client = new EcastShardClient({
|
||||
sessionId: 1, gameId: 5, roomCode: 'TEST', maxPlayers: 8,
|
||||
onEvent: (type, data) => events.push({ type, data }),
|
||||
});
|
||||
client.playerCount = 2;
|
||||
client.playerNames = ['A', 'B'];
|
||||
client.gameState = 'Lobby';
|
||||
|
||||
client.startStatusBroadcast();
|
||||
|
||||
jest.advanceTimersByTime(20000);
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].type).toBe('game.status');
|
||||
expect(events[0].data.monitoring).toBe(true);
|
||||
|
||||
jest.advanceTimersByTime(20000);
|
||||
expect(events).toHaveLength(2);
|
||||
|
||||
client.stopStatusBroadcast();
|
||||
|
||||
jest.advanceTimersByTime(40000);
|
||||
expect(events).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('disconnect stops the status broadcast', () => {
|
||||
const events = [];
|
||||
const client = new EcastShardClient({
|
||||
sessionId: 1, gameId: 5, roomCode: 'TEST', maxPlayers: 8,
|
||||
onEvent: (type, data) => events.push({ type, data }),
|
||||
});
|
||||
|
||||
client.startStatusBroadcast();
|
||||
|
||||
jest.advanceTimersByTime(20000);
|
||||
expect(events).toHaveLength(1);
|
||||
|
||||
client.disconnect();
|
||||
|
||||
jest.advanceTimersByTime(40000);
|
||||
expect(events).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('handleWelcome starts status broadcast', () => {
|
||||
const events = [];
|
||||
const client = new EcastShardClient({
|
||||
sessionId: 1, gameId: 5, roomCode: 'TEST', maxPlayers: 8,
|
||||
onEvent: (type, data) => events.push({ type, data }),
|
||||
});
|
||||
|
||||
client.handleWelcome({
|
||||
id: 7,
|
||||
secret: 'abc',
|
||||
reconnect: false,
|
||||
entities: {},
|
||||
here: {},
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(20000);
|
||||
const statusEvents = events.filter(e => e.type === 'game.status');
|
||||
expect(statusEvents).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('module exports', () => {
|
||||
const { startMonitor, stopMonitor, cleanupAllShards } = require('../../backend/utils/ecast-shard-client');
|
||||
const { startMonitor, stopMonitor, cleanupAllShards, getMonitorSnapshot } = require('../../backend/utils/ecast-shard-client');
|
||||
|
||||
test('startMonitor is exported', () => {
|
||||
expect(typeof startMonitor).toBe('function');
|
||||
@@ -390,6 +503,15 @@ describe('EcastShardClient', () => {
|
||||
test('cleanupAllShards is exported', () => {
|
||||
expect(typeof cleanupAllShards).toBe('function');
|
||||
});
|
||||
|
||||
test('getMonitorSnapshot is exported', () => {
|
||||
expect(typeof getMonitorSnapshot).toBe('function');
|
||||
});
|
||||
|
||||
test('getMonitorSnapshot returns null when no shard active', () => {
|
||||
const snapshot = getMonitorSnapshot(999, 999);
|
||||
expect(snapshot).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleError with code 2027', () => {
|
||||
|
||||
Reference in New Issue
Block a user