From 02642840dacd26a8f0cb6881d2820282ae9a1c8d Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 11 Apr 2026 17:11:05 -0400 Subject: [PATCH] =?UTF-8?q?feat(soak):=20RoomCoordinator=20with=20host?= =?UTF-8?q?=E2=86=92joiners=20handoff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lazy Deferred per roomId with a timeout on await. Lets concurrent joiner sessions block until their host announces the room code without polling or page scraping. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/soak/core/room-coordinator.ts | 42 +++++++++++++++++++++++ tests/soak/tests/room-coordinator.test.ts | 29 ++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/soak/core/room-coordinator.ts create mode 100644 tests/soak/tests/room-coordinator.test.ts 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); + }); +});