// 2-client multiplayer integration test. // // Uses the deterministic `MemoryTransport` shipped in this package as the real // `@suaveplan/network` Transport implementation. Real WebRTC E2E (via // `@suaveplan/network-transport-webrtc`) would require Playwright + a TURN // server, both out of scope for the unit-test layer; the canonical wiring at // the application boundary swaps `MemoryTransport` for the WebRTC transport // without touching the rest of the stack since both implement the same // structural `Transport` contract. // // This test exercises: // 1. Two peers connect over the memory hub. // 2. The server peer authors a snapshot frame, encodes it, broadcasts it. // 3. The client peer decodes the frame, feeds it into a sync manager, // runs interpolation, and reads the smoothed transform. // 4. The client builds and sends a delta back; the server applies it. import { createEnvelope } from "@suaveplan/network"; import { describe, expect, it } from "@suaveplan/testing/runner"; import { applyDeltaFrame, buildDeltaFrame, createMemoryHub, decodeDeltaFrame, decodeSnapshotFrame, type EntitySnapshot, EntitySyncManager, encodeDeltaFrame, encodeSnapshotFrame, MemoryTransport, type SnapshotFrame, } from "../src/index.js"; const SNAPSHOT_TYPE = "r3f_net.snapshot"; const DELTA_TYPE = "r3f_net.delta"; describe("2-client integration over MemoryTransport", () => { it("round-trips a snapshot from server to client and a delta back", { timeout: 5000, }, async () => { const hub = createMemoryHub(); const server = new MemoryTransport("server", hub); const client = new MemoryTransport("c1", hub); const clientSync = new EntitySyncManager({ sendRate: 60, interpolationDelay: 0, }); clientSync.registerEntity("e1", "server", false); let receivedSnapshot: SnapshotFrame | null = null; client.onMessage((_from, env) => { if (env.type === SNAPSHOT_TYPE) { receivedSnapshot = decodeSnapshotFrame(env.payload as string); } }); const receivedDeltas: number[] = []; server.onMessage((_from, env) => { if (env.type === DELTA_TYPE) { const d = decodeDeltaFrame(env.payload as string); receivedDeltas.push(d.seq); } }); await server.connect("c1", "memory"); await client.connect("server", "memory"); // Step 1: server → client snapshot. const baseState: EntitySnapshot = { e1: { position: [1, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1], }, }; const baseFrame: SnapshotFrame = { seq: 1, timestamp: 1000, state: baseState, }; await server.send( "c1", createEnvelope({ type: SNAPSHOT_TYPE, senderId: "server", targetId: "c1", payload: encodeSnapshotFrame(baseFrame), }) ); expect(receivedSnapshot).not.toBeNull(); const restored = receivedSnapshot as unknown as SnapshotFrame; expect(restored.seq).toBe(1); expect(restored.state.e1?.position[0]).toBe(1); // Feed the snapshot through the client sync layer. clientSync.receiveUpdate({ entityId: "e1", transform: restored.state.e1!, timestamp: restored.timestamp, seq: restored.seq, }); clientSync.receiveUpdate({ entityId: "e1", transform: { ...restored.state.e1!, position: [2, 0, 0] }, timestamp: restored.timestamp + 100, seq: 2, }); clientSync.updateRemoteEntities(restored.timestamp + 100); const e = clientSync.getEntity("e1"); expect(e?.transform.position[0]).toBeCloseTo(2); // Step 2: client → server delta. const nextFrame: SnapshotFrame = { seq: 2, timestamp: 1100, state: { e1: { ...baseState.e1!, position: [3, 0, 0] }, }, }; const delta = buildDeltaFrame(baseFrame, nextFrame); await client.send( "server", createEnvelope({ type: DELTA_TYPE, senderId: "c1", targetId: "server", payload: encodeDeltaFrame(delta), }) ); expect(receivedDeltas).toEqual([2]); // Server applies the delta to confirm the round-trip is valid. const applied = applyDeltaFrame(baseFrame, delta); expect(applied.state.e1?.position[0]).toBe(3); await server.close(); await client.close(); }); });