@pylonsync/react 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.12.0",
6
+ "version": "0.13.0",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",
@@ -15,8 +15,8 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@msgpack/msgpack": "^3.1.3",
18
- "@pylonsync/sdk": "0.12.0",
19
- "@pylonsync/sync": "0.12.0"
18
+ "@pylonsync/sdk": "0.13.0",
19
+ "@pylonsync/sync": "0.13.0"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "react": ">=19.0.0"
@@ -0,0 +1,151 @@
1
+ // End-to-end: examples/shard-arena on a real `pylon start`. Its shard logic
2
+ // is Rust compiled to WebAssembly and loaded by the stock binary.
3
+ //
4
+ // Runs only when PYLON_WASM_SHARD_E2E holds the server's host:port
5
+ // (tools/smoke-wasm-shard.sh sets it up).
6
+
7
+ import { afterAll, beforeAll, expect, test } from "bun:test";
8
+
9
+ import { connectShard } from "./useShard";
10
+ import type { ShardInputRejection } from "./shardWire";
11
+
12
+ const host = process.env.PYLON_WASM_SHARD_E2E ?? "";
13
+
14
+ // The test preload installs happy-dom, whose fetch enforces CORS against the
15
+ // page URL. Put the page on the server's origin for these tests.
16
+ const happyDOM = (globalThis as { happyDOM?: { setURL(url: string): void } }).happyDOM;
17
+ let previousUrl = "";
18
+ beforeAll(() => {
19
+ if (!host || !happyDOM) return;
20
+ previousUrl = location.href;
21
+ happyDOM.setURL(`http://${host}/`);
22
+ });
23
+ afterAll(() => {
24
+ if (previousUrl && happyDOM) happyDOM.setURL(previousUrl);
25
+ });
26
+
27
+ interface Player {
28
+ id: string;
29
+ x: number;
30
+ y: number;
31
+ tx: number;
32
+ ty: number;
33
+ hue: number;
34
+ }
35
+
36
+ interface Arena {
37
+ width: number;
38
+ height: number;
39
+ players: Player[];
40
+ }
41
+
42
+ type Input = "join" | { move_to: { x: number; y: number } };
43
+
44
+ interface Join {
45
+ shardId: string;
46
+ subscriberId: string;
47
+ ticket: string;
48
+ }
49
+
50
+ async function guestJoin(): Promise<Join> {
51
+ const guest = await fetch(`http://${host}/api/auth/guest`, { method: "POST" });
52
+ expect(guest.ok).toBe(true);
53
+ const { token } = (await guest.json()) as { token: string };
54
+ const res = await fetch(`http://${host}/api/fn/joinArena`, {
55
+ method: "POST",
56
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
57
+ body: "{}",
58
+ });
59
+ expect(res.status).toBe(200);
60
+ return (await res.json()) as Join;
61
+ }
62
+
63
+ function waitFor<T>(what: string, check: () => T | undefined, ms = 8000): Promise<T> {
64
+ return new Promise((resolve, reject) => {
65
+ const start = Date.now();
66
+ const timer = setInterval(() => {
67
+ const v = check();
68
+ if (v !== undefined) {
69
+ clearInterval(timer);
70
+ resolve(v);
71
+ } else if (Date.now() - start > ms) {
72
+ clearInterval(timer);
73
+ reject(new Error(`timed out waiting for ${what}`));
74
+ }
75
+ }, 20);
76
+ });
77
+ }
78
+
79
+ function open(join: Join, ticket = join.ticket) {
80
+ const client = connectShard<Arena, Input>(join.shardId, {
81
+ subscriberId: join.subscriberId,
82
+ ticket,
83
+ baseUrl: host,
84
+ autoReconnect: false,
85
+ });
86
+ const state: {
87
+ latest?: Arena;
88
+ ack: number;
89
+ rejections: ShardInputRejection[];
90
+ errors: Error[];
91
+ opened: boolean;
92
+ closed: boolean;
93
+ } = { ack: 0, rejections: [], errors: [], opened: false, closed: false };
94
+ client.onSnapshot((snapshot, _tick, ack) => {
95
+ state.latest = snapshot;
96
+ state.ack = ack;
97
+ });
98
+ client.onInputRejected((r) => state.rejections.push(r));
99
+ client.onError((e) => state.errors.push(e));
100
+ client.onOpen(() => (state.opened = true));
101
+ client.onClose(() => (state.closed = true));
102
+ return { client, state };
103
+ }
104
+
105
+ test.skipIf(!host)("two players move in a WebAssembly shard over /shard", async () => {
106
+ const [a, b] = await Promise.all([guestJoin(), guestJoin()]);
107
+ expect(a.shardId).toBe(b.shardId);
108
+ const pa = open(a);
109
+ const pb = open(b);
110
+
111
+ await waitFor("both snapshots", () => (pa.state.latest && pb.state.latest ? true : undefined));
112
+ // MessagePack snapshots decode into objects.
113
+ expect(pa.state.latest!.width).toBe(800);
114
+
115
+ pa.client.send("join");
116
+ pb.client.send("join");
117
+ const target = { x: 700, y: 400 };
118
+ const seq = pa.client.send({ move_to: target });
119
+
120
+ // B sees A arrive at the target: the module's tick moves the dot.
121
+ const seenByB = await waitFor("A at its target, seen by B", () =>
122
+ pb.state.latest?.players.find(
123
+ (p) => p.id === a.subscriberId && p.x === target.x && p.y === target.y,
124
+ ),
125
+ );
126
+ expect(seenByB.tx).toBe(target.x);
127
+ expect(pa.state.ack).toBeGreaterThanOrEqual(seq);
128
+ expect(pb.state.latest!.players.some((p) => p.id === b.subscriberId)).toBe(true);
129
+
130
+ // The module refuses a move outside the arena; the refusal comes back.
131
+ const bad = pa.client.send({ move_to: { x: 5000, y: 0 } });
132
+ const rejection = await waitFor("a rejection", () => pa.state.rejections[0]);
133
+ expect(rejection).toMatchObject({ clientSeq: bad, code: "apply_failed" });
134
+ expect(rejection.message).toContain("outside the arena");
135
+
136
+ expect(pa.state.errors).toEqual([]);
137
+ expect(pb.state.errors).toEqual([]);
138
+ pa.client.close();
139
+ pb.client.close();
140
+ });
141
+
142
+ test.skipIf(!host)("a ticket for another subscriber is refused", async () => {
143
+ const [a, b] = await Promise.all([guestJoin(), guestJoin()]);
144
+ // B's ticket names B; presenting it as A fails the handshake.
145
+ const stolen = open(a, b.ticket);
146
+ await waitFor("the refused connection to close", () =>
147
+ stolen.state.closed || stolen.state.errors.length > 0 ? true : undefined,
148
+ );
149
+ expect(stolen.state.latest).toBeUndefined();
150
+ stolen.client.close();
151
+ });