Initial Commit
This commit is contained in:
33
packages/threejs/r3f/r3f-networking/tests/dep-graph.test.ts
Normal file
33
packages/threejs/r3f/r3f-networking/tests/dep-graph.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
|
||||
describe("r3f-networking dep-graph", () => {
|
||||
it("loads index and exposes every public surface", {
|
||||
timeout: 30000,
|
||||
}, async () => {
|
||||
const mod = await import("../src/index.js");
|
||||
expect(mod.R3F_NET_CODES).toBeDefined();
|
||||
expect(mod.R3FNetError).toBeDefined();
|
||||
expect(mod.StateBuffer).toBeDefined();
|
||||
expect(mod.EntityInterpolator).toBeDefined();
|
||||
expect(mod.ClientPredictor).toBeDefined();
|
||||
expect(mod.buildReconciliationMachine).toBeDefined();
|
||||
expect(mod.diffSnapshots).toBeDefined();
|
||||
expect(mod.buildDeltaFrame).toBeDefined();
|
||||
expect(mod.applyDeltaFrame).toBeDefined();
|
||||
expect(mod.encodeSnapshotFrame).toBeDefined();
|
||||
expect(mod.decodeSnapshotFrame).toBeDefined();
|
||||
expect(mod.EntitySyncManager).toBeDefined();
|
||||
expect(mod.InterestManager).toBeDefined();
|
||||
expect(mod.ServerAuthority).toBeDefined();
|
||||
expect(mod.createMemoryHub).toBeDefined();
|
||||
expect(mod.MemoryTransport).toBeDefined();
|
||||
expect(mod.useNetworkSync).toBeDefined();
|
||||
expect(mod.useNetworkedEntity).toBeDefined();
|
||||
expect(mod.useNetworkPredict).toBeDefined();
|
||||
expect(mod.NetworkProvider).toBeDefined();
|
||||
expect(mod.NetworkContext).toBeDefined();
|
||||
expect(mod.NetworkedEntity).toBeDefined();
|
||||
expect(mod.applyNetworkedFrame).toBeDefined();
|
||||
expect(mod.NetworkStats).toBeDefined();
|
||||
});
|
||||
});
|
||||
11
packages/threejs/r3f/r3f-networking/tests/setup/polyfills.ts
Normal file
11
packages/threejs/r3f/r3f-networking/tests/setup/polyfills.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// React-specific polyfills. The base webcrypto polyfill loads via
|
||||
// `@suaveplan/testing/polyfills`. This sets the React 18+
|
||||
// `IS_REACT_ACT_ENVIRONMENT` flag so DOM assertions outside `act(...)`
|
||||
// don't generate runtime warnings.
|
||||
interface ReactActEnvWindow {
|
||||
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
||||
}
|
||||
const win = globalThis as unknown as ReactActEnvWindow;
|
||||
if (win.IS_REACT_ACT_ENVIRONMENT === undefined) {
|
||||
win.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user