@signe/room 2.10.0 → 3.0.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/CHANGELOG.md +7 -0
- package/dist/chunk-EUXUH3YW.js +15 -0
- package/dist/chunk-EUXUH3YW.js.map +1 -0
- package/dist/cloudflare/index.d.ts +71 -0
- package/dist/cloudflare/index.js +320 -0
- package/dist/cloudflare/index.js.map +1 -0
- package/dist/index.d.ts +66 -187
- package/dist/index.js +727 -106
- package/dist/index.js.map +1 -1
- package/dist/node/index.d.ts +164 -0
- package/dist/node/index.js +786 -0
- package/dist/node/index.js.map +1 -0
- package/dist/party-dNs-hqkq.d.ts +175 -0
- package/examples/cloudflare/README.md +62 -0
- package/examples/cloudflare/node_modules/.bin/tsc +17 -0
- package/examples/cloudflare/node_modules/.bin/tsserver +17 -0
- package/examples/cloudflare/node_modules/.bin/wrangler +17 -0
- package/examples/cloudflare/node_modules/.bin/wrangler2 +17 -0
- package/examples/cloudflare/package.json +24 -0
- package/examples/cloudflare/public/index.html +443 -0
- package/examples/cloudflare/src/index.ts +28 -0
- package/examples/cloudflare/src/room.ts +44 -0
- package/examples/cloudflare/tsconfig.json +10 -0
- package/examples/cloudflare/wrangler.jsonc +25 -0
- package/examples/node/README.md +57 -0
- package/examples/node/node_modules/.bin/tsc +17 -0
- package/examples/node/node_modules/.bin/tsserver +17 -0
- package/examples/node/node_modules/.bin/tsx +17 -0
- package/examples/node/package.json +23 -0
- package/examples/node/public/index.html +443 -0
- package/examples/node/room.ts +44 -0
- package/examples/node/server.sqlite.ts +52 -0
- package/examples/node/server.ts +51 -0
- package/examples/node/tsconfig.json +10 -0
- package/examples/node-game/README.md +66 -0
- package/examples/node-game/package.json +23 -0
- package/examples/node-game/public/index.html +705 -0
- package/examples/node-game/room.ts +145 -0
- package/examples/node-game/server.sqlite.ts +54 -0
- package/examples/node-game/server.ts +53 -0
- package/examples/node-game/tsconfig.json +10 -0
- package/examples/node-shard/README.md +32 -0
- package/examples/node-shard/dev.ts +39 -0
- package/examples/node-shard/package.json +24 -0
- package/examples/node-shard/public/index.html +777 -0
- package/examples/node-shard/room-server.ts +68 -0
- package/examples/node-shard/room.ts +105 -0
- package/examples/node-shard/shared.ts +6 -0
- package/examples/node-shard/tsconfig.json +14 -0
- package/examples/node-shard/world-server.ts +169 -0
- package/package.json +14 -5
- package/readme.md +371 -4
- package/src/cloudflare/index.ts +474 -0
- package/src/jwt.ts +1 -5
- package/src/mock.ts +29 -7
- package/src/node/index.ts +1112 -0
- package/src/server.ts +600 -51
- package/src/session.guard.ts +6 -2
- package/src/shard.ts +91 -23
- package/src/storage.ts +29 -5
- package/src/testing.ts +4 -3
- package/src/types/party.ts +4 -1
- package/src/world.guard.ts +23 -4
- package/src/world.ts +121 -21
- package/examples/game/.vscode/launch.json +0 -11
- package/examples/game/.vscode/settings.json +0 -11
- package/examples/game/README.md +0 -40
- package/examples/game/app/client.tsx +0 -15
- package/examples/game/app/components/Admin.tsx +0 -1089
- package/examples/game/app/components/Room.tsx +0 -162
- package/examples/game/app/styles.css +0 -31
- package/examples/game/package-lock.json +0 -225
- package/examples/game/package.json +0 -20
- package/examples/game/party/game.room.ts +0 -32
- package/examples/game/party/server.ts +0 -10
- package/examples/game/party/shard.ts +0 -5
- package/examples/game/partykit.json +0 -14
- package/examples/game/public/favicon.ico +0 -0
- package/examples/game/public/index.html +0 -27
- package/examples/game/public/normalize.css +0 -351
- package/examples/game/shared/room.schema.ts +0 -14
- 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,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
|
+
}
|