143 lines
4.8 KiB
TypeScript
143 lines
4.8 KiB
TypeScript
// 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();
|
|
});
|
|
});
|