/** * The browser's half of the multiplayer transport, driven with a fake `EventSource` and `fetch`. * `createRemoteSession` is pure otherwise — no DOM — so it runs here as it does in the page. */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { createRemoteSession } from '../src/web/session.ts'; type Fake = { onmessage: ((ev: { data: string }) => void) | null; emit(data: unknown): void; close(): void }; function fakeTransport(): { source: () => Fake; bodies: () => { seq: number }[]; fail: (on: boolean) => void } { let last: Fake | null = null; const bodies: { seq: number }[] = []; let failing = false; const g = globalThis as unknown as Record; g['EventSource'] = class { onmessage: ((ev: { data: string }) => void) | null = null; onerror: (() => void) | null = null; constructor() { last = this; } emit(data: unknown): void { this.onmessage?.({ data: JSON.stringify(data) }); } close(): void {} }; g['fetch'] = async (_url: string, init?: { body?: string }) => { if (init?.body) bodies.push(JSON.parse(init.body) as { seq: number }); await new Promise((r) => setTimeout(r, 5)); if (failing) throw new Error('network down'); return { ok: true, status: 200, json: async () => ({ ok: true }) }; }; return { source: () => last!, bodies: () => bodies, fail: (on) => (failing = on) }; } const connectPush = (lastSeq: number): unknown => ({ frame: null, menu: null, lines: [], lastSeq }); describe('the remote session (v0.8.4)', () => { it('continues the intent count from where the server says, not from 1', async () => { const t = fakeTransport(); const s = createRemoteSession('tok', 0); t.source().emit(connectPush(7)); await s.submit({ type: 'draw.end' }); assert.deepEqual(t.bodies().map((b) => b.seq), [8], 'the first intent after a reload re-used a number the server had already applied'); // A later reconnect never moves the count backwards. t.source().emit(connectPush(3)); await s.submit({ type: 'draw.end' }); assert.deepEqual(t.bodies().map((b) => b.seq), [8, 9]); }); it('drops a second submit while the first is still in flight', async () => { const t = fakeTransport(); const s = createRemoteSession('tok', 0); t.source().emit(connectPush(0)); const [a, b] = await Promise.all([s.submit({ type: 'draw.end' }), s.submit({ type: 'draw.end' })]); assert.equal(a, true); assert.equal(b, false, 'a double-click posted twice'); assert.equal(t.bodies().length, 1, 'two intents went over the wire for one click'); // And the next one, after the round trip, goes through as normal. assert.equal(await s.submit({ type: 'draw.end' }), true); assert.equal(t.bodies().length, 2); }); it('answers false, not an unhandled rejection, when the network fails', async () => { const t = fakeTransport(); const s = createRemoteSession('tok', 0); t.source().emit(connectPush(0)); t.fail(true); assert.equal(await s.submit({ type: 'draw.end' }), false); t.fail(false); assert.equal(await s.submit({ type: 'draw.end' }), true, 'the session did not recover after a failed submit'); }); it('drops a push that is not JSON instead of throwing out of the handler', () => { const t = fakeTransport(); createRemoteSession('tok', 0); assert.doesNotThrow(() => t.source().onmessage?.({ data: '{not json' })); }); });