@signe/room 2.10.0 → 3.0.1

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.
Files changed (84) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/chunk-EUXUH3YW.js +15 -0
  3. package/dist/chunk-EUXUH3YW.js.map +1 -0
  4. package/dist/cloudflare/index.d.ts +71 -0
  5. package/dist/cloudflare/index.js +320 -0
  6. package/dist/cloudflare/index.js.map +1 -0
  7. package/dist/index.d.ts +87 -188
  8. package/dist/index.js +860 -114
  9. package/dist/index.js.map +1 -1
  10. package/dist/node/index.d.ts +164 -0
  11. package/dist/node/index.js +786 -0
  12. package/dist/node/index.js.map +1 -0
  13. package/dist/party-dNs-hqkq.d.ts +175 -0
  14. package/examples/cloudflare/README.md +62 -0
  15. package/examples/cloudflare/node_modules/.bin/tsc +17 -0
  16. package/examples/cloudflare/node_modules/.bin/tsserver +17 -0
  17. package/examples/cloudflare/node_modules/.bin/wrangler +17 -0
  18. package/examples/cloudflare/node_modules/.bin/wrangler2 +17 -0
  19. package/examples/cloudflare/package.json +24 -0
  20. package/examples/cloudflare/public/index.html +443 -0
  21. package/examples/cloudflare/src/index.ts +28 -0
  22. package/examples/cloudflare/src/room.ts +44 -0
  23. package/examples/cloudflare/tsconfig.json +10 -0
  24. package/examples/cloudflare/wrangler.jsonc +25 -0
  25. package/examples/node/README.md +57 -0
  26. package/examples/node/node_modules/.bin/tsc +17 -0
  27. package/examples/node/node_modules/.bin/tsserver +17 -0
  28. package/examples/node/node_modules/.bin/tsx +17 -0
  29. package/examples/node/package.json +23 -0
  30. package/examples/node/public/index.html +443 -0
  31. package/examples/node/room.ts +44 -0
  32. package/examples/node/server.sqlite.ts +52 -0
  33. package/examples/node/server.ts +51 -0
  34. package/examples/node/tsconfig.json +10 -0
  35. package/examples/node-game/README.md +66 -0
  36. package/examples/node-game/package.json +23 -0
  37. package/examples/node-game/public/index.html +705 -0
  38. package/examples/node-game/room.ts +145 -0
  39. package/examples/node-game/server.sqlite.ts +54 -0
  40. package/examples/node-game/server.ts +53 -0
  41. package/examples/node-game/tsconfig.json +10 -0
  42. package/examples/node-shard/README.md +32 -0
  43. package/examples/node-shard/dev.ts +39 -0
  44. package/examples/node-shard/package.json +24 -0
  45. package/examples/node-shard/public/index.html +777 -0
  46. package/examples/node-shard/room-server.ts +68 -0
  47. package/examples/node-shard/room.ts +105 -0
  48. package/examples/node-shard/shared.ts +6 -0
  49. package/examples/node-shard/tsconfig.json +14 -0
  50. package/examples/node-shard/world-server.ts +169 -0
  51. package/package.json +14 -5
  52. package/readme.md +418 -4
  53. package/src/cloudflare/index.ts +474 -0
  54. package/src/index.ts +2 -2
  55. package/src/jwt.ts +1 -5
  56. package/src/mock.ts +29 -7
  57. package/src/node/index.ts +1112 -0
  58. package/src/server.ts +781 -60
  59. package/src/session.guard.ts +6 -2
  60. package/src/shard.ts +91 -23
  61. package/src/storage.ts +29 -5
  62. package/src/testing.ts +4 -3
  63. package/src/types/party.ts +30 -1
  64. package/src/world.guard.ts +23 -4
  65. package/src/world.ts +121 -21
  66. package/tests/storage-restore.spec.ts +122 -0
  67. package/examples/game/.vscode/launch.json +0 -11
  68. package/examples/game/.vscode/settings.json +0 -11
  69. package/examples/game/README.md +0 -40
  70. package/examples/game/app/client.tsx +0 -15
  71. package/examples/game/app/components/Admin.tsx +0 -1089
  72. package/examples/game/app/components/Room.tsx +0 -162
  73. package/examples/game/app/styles.css +0 -31
  74. package/examples/game/package-lock.json +0 -225
  75. package/examples/game/package.json +0 -20
  76. package/examples/game/party/game.room.ts +0 -32
  77. package/examples/game/party/server.ts +0 -10
  78. package/examples/game/party/shard.ts +0 -5
  79. package/examples/game/partykit.json +0 -14
  80. package/examples/game/public/favicon.ico +0 -0
  81. package/examples/game/public/index.html +0 -27
  82. package/examples/game/public/normalize.css +0 -351
  83. package/examples/game/shared/room.schema.ts +0 -14
  84. package/examples/game/tsconfig.json +0 -109
@@ -0,0 +1,145 @@
1
+ import { Action, Request, Room, Server } from "@signe/room";
2
+ import { signal } from "@signe/reactive";
3
+ import { connected, sync, users } from "@signe/sync";
4
+ import { z } from "zod";
5
+
6
+ const ARENA_WIDTH = 900;
7
+ const ARENA_HEIGHT = 560;
8
+ const PLAYER_RADIUS = 16;
9
+ const STAR_RADIUS = 13;
10
+ const COLLECT_DISTANCE = PLAYER_RADIUS + STAR_RADIUS + 8;
11
+
12
+ type Point = {
13
+ x: number;
14
+ y: number;
15
+ };
16
+
17
+ class Player {
18
+ @sync() name = signal("Anonymous");
19
+ @connected() connected = signal(false);
20
+ @sync() x = signal(-1);
21
+ @sync() y = signal(-1);
22
+ @sync() color = signal("#2563eb");
23
+ @sync() score = signal(0);
24
+ }
25
+
26
+ @Room({ path: "{roomId}", sessionExpiryTime: 2000, throttleStorage: 2500 })
27
+ class GameRoom {
28
+ @sync() star = signal<Point>(randomPoint());
29
+ @users(Player) players = signal<Record<string, Player>>({});
30
+
31
+ onJoin(player: Player, _conn: unknown, ctx: { request?: Request }) {
32
+ const url = new URL(ctx.request?.url ?? "http://localhost");
33
+ const name = url.searchParams.get("name")?.trim();
34
+
35
+ if (name) {
36
+ player.name.set(name.slice(0, 40));
37
+ }
38
+
39
+ if (player.x() < 0 || player.y() < 0) {
40
+ const point = randomPoint();
41
+ player.x.set(point.x);
42
+ player.y.set(point.y);
43
+ player.color.set(colorFromName(player.name()));
44
+ }
45
+ }
46
+
47
+ @Action("move", z.object({ x: z.number(), y: z.number() }))
48
+ move(player: Player, value: Point) {
49
+ player.x.set(clamp(value.x, PLAYER_RADIUS, ARENA_WIDTH - PLAYER_RADIUS));
50
+ player.y.set(clamp(value.y, PLAYER_RADIUS, ARENA_HEIGHT - PLAYER_RADIUS));
51
+ }
52
+
53
+ @Action("collect", z.object({}))
54
+ collect(player: Player) {
55
+ const distance = getDistance(player.x(), player.y(), this.star().x, this.star().y);
56
+
57
+ if (distance > COLLECT_DISTANCE) {
58
+ return;
59
+ }
60
+
61
+ player.score.update((score) => score + 1);
62
+ this.star.set(randomPoint());
63
+ }
64
+
65
+ @Request({ path: "/state" })
66
+ getState() {
67
+ return this.snapshot();
68
+ }
69
+
70
+ @Request({ path: "/reset", method: "POST" })
71
+ reset() {
72
+ for (const player of Object.values(this.players())) {
73
+ player.score.set(0);
74
+ }
75
+
76
+ this.star.set(randomPoint());
77
+ return this.snapshot();
78
+ }
79
+
80
+ private snapshot() {
81
+ return {
82
+ arena: {
83
+ width: ARENA_WIDTH,
84
+ height: ARENA_HEIGHT,
85
+ },
86
+ star: this.star(),
87
+ players: Object.fromEntries(
88
+ Object.entries(this.players()).map(([id, player]) => [
89
+ id,
90
+ {
91
+ name: player.name(),
92
+ connected: player.connected(),
93
+ x: player.x(),
94
+ y: player.y(),
95
+ color: player.color(),
96
+ score: player.score(),
97
+ },
98
+ ])
99
+ ),
100
+ };
101
+ }
102
+ }
103
+
104
+ export class GameServer extends Server {
105
+ rooms = [GameRoom];
106
+ }
107
+
108
+ function randomPoint(): Point {
109
+ return {
110
+ x: randomInt(PLAYER_RADIUS + 20, ARENA_WIDTH - PLAYER_RADIUS - 20),
111
+ y: randomInt(PLAYER_RADIUS + 20, ARENA_HEIGHT - PLAYER_RADIUS - 20),
112
+ };
113
+ }
114
+
115
+ function randomInt(min: number, max: number) {
116
+ return Math.floor(Math.random() * (max - min + 1)) + min;
117
+ }
118
+
119
+ function clamp(value: number, min: number, max: number) {
120
+ return Math.min(max, Math.max(min, value));
121
+ }
122
+
123
+ function getDistance(ax: number, ay: number, bx: number, by: number) {
124
+ return Math.hypot(ax - bx, ay - by);
125
+ }
126
+
127
+ function colorFromName(name: string) {
128
+ const colors = [
129
+ "#2563eb",
130
+ "#dc2626",
131
+ "#16a34a",
132
+ "#9333ea",
133
+ "#ea580c",
134
+ "#0891b2",
135
+ "#be123c",
136
+ "#4f46e5",
137
+ ];
138
+ let hash = 0;
139
+
140
+ for (let index = 0; index < name.length; index += 1) {
141
+ hash = (hash * 31 + name.charCodeAt(index)) >>> 0;
142
+ }
143
+
144
+ return colors[hash % colors.length];
145
+ }
@@ -0,0 +1,54 @@
1
+ import { createServer } from "node:http";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { WebSocketServer } from "ws";
6
+ import { createNodeRoomTransport, createSqliteNodeRoomStorage } from "@signe/room/node";
7
+ import { GameServer } from "./room";
8
+
9
+ const root = fileURLToPath(new URL(".", import.meta.url));
10
+ const port = Number(process.env.PORT ?? 3000);
11
+
12
+ const transport = createNodeRoomTransport(GameServer, {
13
+ partiesPath: "/parties/main",
14
+ storage: createSqliteNodeRoomStorage({
15
+ databasePath: join(root, "rooms.sqlite"),
16
+ }),
17
+ });
18
+
19
+ const server = createServer(async (req, res) => {
20
+ if (req.url?.startsWith("/parties/main/")) {
21
+ await transport.handleNodeRequest(req, res);
22
+ return;
23
+ }
24
+
25
+ if (req.url === "/" || req.url === "/index.html" || req.url?.startsWith("/rooms/")) {
26
+ const html = await readFile(join(root, "public/index.html"), "utf8");
27
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
28
+ res.end(html);
29
+ return;
30
+ }
31
+
32
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
33
+ res.end("Not Found");
34
+ });
35
+
36
+ const wsServer = new WebSocketServer({ noServer: true });
37
+
38
+ server.on("upgrade", (request, socket, head) => {
39
+ if (request.url?.startsWith("/parties/main/")) {
40
+ transport.handleUpgrade(wsServer, request, socket, head);
41
+ return;
42
+ }
43
+
44
+ socket.destroy();
45
+ });
46
+
47
+ server.listen(port, () => {
48
+ console.log(`Signe Node room game SQLite example: http://localhost:${port}`);
49
+ console.log("SQLite file: packages/room/examples/node-game/rooms.sqlite");
50
+ console.log(`Game URL: http://localhost:${port}/rooms/demo`);
51
+ console.log(`HTTP state: http://localhost:${port}/parties/main/demo/state`);
52
+ console.log(`HTTP reset: POST http://localhost:${port}/parties/main/demo/reset`);
53
+ console.log(`WebSocket: ws://localhost:${port}/parties/main/demo?name=Sam`);
54
+ });
@@ -0,0 +1,53 @@
1
+ import { createServer } from "node:http";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { WebSocketServer } from "ws";
6
+ import { createMemoryNodeRoomStorage, createNodeRoomTransport } from "@signe/room/node";
7
+ import { GameServer } from "./room";
8
+
9
+ const root = fileURLToPath(new URL(".", import.meta.url));
10
+ const port = Number(process.env.PORT ?? 3000);
11
+
12
+ const storage = createMemoryNodeRoomStorage();
13
+
14
+ const transport = createNodeRoomTransport(GameServer, {
15
+ partiesPath: "/parties/main",
16
+ storage,
17
+ });
18
+
19
+ const server = createServer(async (req, res) => {
20
+ if (req.url?.startsWith("/parties/main/")) {
21
+ await transport.handleNodeRequest(req, res);
22
+ return;
23
+ }
24
+
25
+ if (req.url === "/" || req.url === "/index.html" || req.url?.startsWith("/rooms/")) {
26
+ const html = await readFile(join(root, "public/index.html"), "utf8");
27
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
28
+ res.end(html);
29
+ return;
30
+ }
31
+
32
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
33
+ res.end("Not Found");
34
+ });
35
+
36
+ const wsServer = new WebSocketServer({ noServer: true });
37
+
38
+ server.on("upgrade", (request, socket, head) => {
39
+ if (request.url?.startsWith("/parties/main/")) {
40
+ transport.handleUpgrade(wsServer, request, socket, head);
41
+ return;
42
+ }
43
+
44
+ socket.destroy();
45
+ });
46
+
47
+ server.listen(port, () => {
48
+ console.log(`Signe Node room game example: http://localhost:${port}`);
49
+ console.log(`Game URL: http://localhost:${port}/rooms/demo`);
50
+ console.log(`HTTP state: http://localhost:${port}/parties/main/demo/state`);
51
+ console.log(`HTTP reset: POST http://localhost:${port}/parties/main/demo/reset`);
52
+ console.log(`WebSocket: ws://localhost:${port}/parties/main/demo?name=Sam`);
53
+ });
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "types": ["node"]
7
+ },
8
+ "include": ["room.ts", "server.ts", "server.sqlite.ts"],
9
+ "exclude": []
10
+ }
@@ -0,0 +1,32 @@
1
+ # Signe Node Shard Example
2
+
3
+ This example runs two local Node processes with three Party-style namespaces:
4
+
5
+ - World process on `http://localhost:3002`: the world registry, shard balancer, and dashboard.
6
+ - Room process on `http://localhost:3003`: the authoritative `main` room server and `shard` proxies.
7
+
8
+ The browser UI lets you inspect one world, create or scale room shards, change shard status, and then enter the room through the selected world.
9
+
10
+ ## Run
11
+
12
+ ```bash
13
+ pnpm install
14
+ pnpm --dir packages/room/examples/node-shard dev
15
+ ```
16
+
17
+ Open:
18
+
19
+ ```txt
20
+ http://localhost:3002
21
+ ```
22
+
23
+ ## Useful URLs
24
+
25
+ ```txt
26
+ World dashboard: http://localhost:3002
27
+ World connect: POST http://localhost:3002/api/world/world-default/connect
28
+ Main HTTP: http://localhost:3003/parties/main/demo/state
29
+ Shard WS: ws://localhost:3003/parties/shard/{shardId}
30
+ ```
31
+
32
+ This is a local development dashboard. Management requests are proxied by the world process so the browser does not need to know `SHARD_SECRET`.
@@ -0,0 +1,39 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ const commands = [
4
+ ["world", ["tsx", "world-server.ts"]],
5
+ ["room", ["tsx", "room-server.ts"]],
6
+ ] as const;
7
+
8
+ const children = commands.map(([name, args]) => {
9
+ const child = spawn("pnpm", ["exec", ...args], {
10
+ stdio: ["ignore", "pipe", "pipe"],
11
+ env: process.env,
12
+ });
13
+
14
+ child.stdout.on("data", (chunk) => {
15
+ process.stdout.write(`[${name}] ${chunk}`);
16
+ });
17
+ child.stderr.on("data", (chunk) => {
18
+ process.stderr.write(`[${name}] ${chunk}`);
19
+ });
20
+ child.on("exit", (code) => {
21
+ if (code && code !== 0) {
22
+ process.exitCode = code;
23
+ stop();
24
+ }
25
+ });
26
+
27
+ return child;
28
+ });
29
+
30
+ process.on("SIGINT", stop);
31
+ process.on("SIGTERM", stop);
32
+
33
+ function stop() {
34
+ for (const child of children) {
35
+ if (!child.killed) {
36
+ child.kill("SIGTERM");
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@signe/room-node-shard-example",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "pnpm --filter @signe/room build && tsx dev.ts",
8
+ "dev:world": "tsx world-server.ts",
9
+ "dev:room": "tsx room-server.ts",
10
+ "build": "pnpm --filter @signe/room build && tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@signe/reactive": "workspace:*",
14
+ "@signe/room": "workspace:*",
15
+ "@signe/sync": "workspace:*",
16
+ "ws": "^8.17.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.13.9",
20
+ "@types/ws": "^8.5.12",
21
+ "tsx": "^4.19.2",
22
+ "typescript": "^5.4.5"
23
+ }
24
+ }