import { describe, expect, it } from 'vitest'; import { broadcast, subscribedPlayerIds, subscribe } from './sse'; // ISS-129 (v0.57.0): the fan-out registry behind /api/events — no DB or server needed. Subscribers // leak across tests in this file (module-level state), which is fine: each test asserts on its own // arrays and uses distinct player ids where isolation matters. v1.4.0 (ISS-112): keyed by PLAYER ID // — a frame for one player must never reach another player's streams. describe('lib/sse', () => { it('delivers broadcasts to subscribers of the SAME player only, and stops after unsubscribe', () => { const a: string[] = []; const b: string[] = []; // same player as a — both get its frames const c: string[] = []; // different player — never sees their frames const unsubA = subscribe('p-a', (t) => a.push(t)); const unsubB = subscribe('p-a', (t) => b.push(t)); const unsubC = subscribe('p-c', (t) => c.push(t)); broadcast(new Map([['p-a', 'x']])); expect(a).toEqual(['x']); expect(b).toEqual(['x']); // same group expect(c).toEqual([]); // other player's frame never crosses over unsubA(); broadcast(new Map([['p-a', 'y'], ['p-c', 'z']])); expect(a).toEqual(['x']); // unsubscribed — no more events expect(b).toEqual(['x', 'y']); expect(c).toEqual(['z']); unsubB(); unsubC(); }); it('subscribedPlayerIds lists exactly the players with live streams, dropping empty groups', () => { const got: string[] = []; const unsub1 = subscribe('p-ids-1', (t) => got.push(t)); const unsub2 = subscribe('p-ids-1', (t) => got.push(t)); // second member of the same group const unsub3 = subscribe('p-ids-2', (t) => got.push(t)); expect(subscribedPlayerIds().sort()).toEqual(['p-ids-1', 'p-ids-2']); unsub1(); // p-ids-1 still has one member — id stays listed expect(subscribedPlayerIds().sort()).toEqual(['p-ids-1', 'p-ids-2']); unsub3(); // p-ids-2's last stream goes → its projections stop expect(subscribedPlayerIds()).toEqual(['p-ids-1']); unsub2(); }); it('a throwing subscriber does not break the others in its group', () => { const got: string[] = []; subscribe('p-boom', () => { throw new Error('boom'); }); const unsub = subscribe('p-boom', (t) => got.push(t)); broadcast(new Map([['p-boom', 'z']])); expect(got).toEqual(['z']); unsub(); }); it('unsubscribe is idempotent and broadcasting to an unknown player is a no-op', () => { const got: string[] = []; const unsub = subscribe('p-idem', (t) => got.push(t)); unsub(); unsub(); // second call must not throw broadcast(new Map([['nobody-subscribed-here', 'w']])); // no group → dropped, no throw expect(got).toEqual([]); }); });