diff --git a/tests/soak/core/room-coordinator.ts b/tests/soak/core/room-coordinator.ts new file mode 100644 index 0000000..0493752 --- /dev/null +++ b/tests/soak/core/room-coordinator.ts @@ -0,0 +1,42 @@ +/** + * RoomCoordinator — tiny host→joiners handoff primitive. + * + * Lazy Deferred per roomId. `announce` resolves the promise; `await` + * blocks until the promise resolves or a per-call timeout fires. + * Rooms are keyed by string; each key has at most one Deferred. + */ + +import { deferred, Deferred } from './deferred'; +import type { RoomCoordinatorApi } from './types'; + +export class RoomCoordinator implements RoomCoordinatorApi { + private rooms = new Map>(); + + announce(roomId: string, code: string): void { + this.getOrCreate(roomId).resolve(code); + } + + async await(roomId: string, timeoutMs: number = 30_000): Promise { + const d = this.getOrCreate(roomId); + let timer: NodeJS.Timeout | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => { + reject(new Error(`RoomCoordinator: room "${roomId}" timed out after ${timeoutMs}ms`)); + }, timeoutMs); + }); + try { + return await Promise.race([d.promise, timeout]); + } finally { + if (timer) clearTimeout(timer); + } + } + + private getOrCreate(roomId: string): Deferred { + let d = this.rooms.get(roomId); + if (!d) { + d = deferred(); + this.rooms.set(roomId, d); + } + return d; + } +} diff --git a/tests/soak/tests/room-coordinator.test.ts b/tests/soak/tests/room-coordinator.test.ts new file mode 100644 index 0000000..f3fd6aa --- /dev/null +++ b/tests/soak/tests/room-coordinator.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest'; +import { RoomCoordinator } from '../core/room-coordinator'; + +describe('RoomCoordinator', () => { + it('resolves await with the announced code (announce then await)', async () => { + const rc = new RoomCoordinator(); + rc.announce('room-1', 'ABCD'); + await expect(rc.await('room-1')).resolves.toBe('ABCD'); + }); + + it('resolves await with the announced code (await then announce)', async () => { + const rc = new RoomCoordinator(); + const p = rc.await('room-2'); + rc.announce('room-2', 'WXYZ'); + await expect(p).resolves.toBe('WXYZ'); + }); + + it('rejects await after timeout if not announced', async () => { + const rc = new RoomCoordinator(); + await expect(rc.await('room-3', 50)).rejects.toThrow(/timed out/i); + }); + + it('isolates rooms — announcing room-A does not unblock room-B', async () => { + const rc = new RoomCoordinator(); + const pB = rc.await('room-B', 100); + rc.announce('room-A', 'A-CODE'); + await expect(pB).rejects.toThrow(/timed out/i); + }); +});