@signe/room 1.4.2 → 2.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/dist/index.d.ts +258 -22
- package/dist/index.js +1447 -60
- package/dist/index.js.map +1 -1
- package/examples/game/app/client.tsx +2 -2
- package/examples/game/app/components/Admin.tsx +1089 -0
- package/examples/game/app/components/Room.tsx +158 -0
- package/examples/game/party/server.ts +3 -2
- package/examples/game/party/shard.ts +5 -0
- package/examples/game/partykit.json +5 -1
- package/package.json +2 -2
- package/readme.md +226 -2
- package/src/decorators.ts +34 -2
- package/src/index.ts +4 -1
- package/src/interfaces.ts +13 -0
- package/src/jwt.ts +217 -0
- package/src/mock.ts +39 -3
- package/src/server.ts +595 -79
- package/src/shard.ts +244 -0
- package/src/testing.ts +47 -6
- package/src/utils.ts +7 -0
- package/src/world.guard.ts +28 -0
- package/src/world.ts +448 -0
- package/examples/game/app/components/Counter.tsx +0 -82
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { connectionWorld } from '../../../../../sync/src/client';
|
|
3
|
+
import { RoomSchema } from "../../shared/room.schema";
|
|
4
|
+
import { effect } from "@signe/reactive";
|
|
5
|
+
|
|
6
|
+
export default function Room() {
|
|
7
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
8
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
9
|
+
const [error, setError] = useState<string | null>(null);
|
|
10
|
+
const [roomId, setRoomId] = useState("quiz");
|
|
11
|
+
const [count, setCount] = useState(0);
|
|
12
|
+
const socketRef = useRef<any>(null);
|
|
13
|
+
const roomRef = useRef<any>(null);
|
|
14
|
+
|
|
15
|
+
const connectToRoom = async () => {
|
|
16
|
+
setIsConnecting(true);
|
|
17
|
+
setError(null);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Initialize room schema
|
|
21
|
+
roomRef.current = new RoomSchema();
|
|
22
|
+
|
|
23
|
+
// Connect to the room through the World service with auto-creation enabled
|
|
24
|
+
socketRef.current = await connectionWorld({
|
|
25
|
+
worldUrl: 'http://localhost:1999',
|
|
26
|
+
roomId: roomId,
|
|
27
|
+
autoCreate: true // Enable auto-creation of room and shards
|
|
28
|
+
}, roomRef.current);
|
|
29
|
+
|
|
30
|
+
// Listen for disconnection events
|
|
31
|
+
socketRef.current.on('disconnect', () => {
|
|
32
|
+
setIsConnected(false);
|
|
33
|
+
setError('Disconnected from server');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
effect(() => {
|
|
37
|
+
if (roomRef.current) {
|
|
38
|
+
setCount(roomRef.current.count());
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
setIsConnected(true);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error('Connection error:', err);
|
|
45
|
+
setError(`Failed to connect to the room: ${err instanceof Error ? err.message : String(err)}`);
|
|
46
|
+
} finally {
|
|
47
|
+
setIsConnecting(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const disconnectFromRoom = () => {
|
|
52
|
+
if (socketRef.current) {
|
|
53
|
+
socketRef.current.close();
|
|
54
|
+
socketRef.current = null;
|
|
55
|
+
}
|
|
56
|
+
roomRef.current = null;
|
|
57
|
+
setIsConnected(false);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Clean up on component unmount
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
return () => {
|
|
63
|
+
if (socketRef.current) {
|
|
64
|
+
socketRef.current.close();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
// Styles
|
|
70
|
+
const buttonStyles = {
|
|
71
|
+
backgroundColor: isConnected ? "#f43f5e" : "#2563eb",
|
|
72
|
+
borderRadius: "9999px",
|
|
73
|
+
border: "none",
|
|
74
|
+
color: "white",
|
|
75
|
+
fontSize: "0.95rem",
|
|
76
|
+
cursor: "pointer",
|
|
77
|
+
padding: "1rem 3rem",
|
|
78
|
+
margin: "1rem 0rem",
|
|
79
|
+
disabled: isConnecting
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const inputStyles = {
|
|
83
|
+
padding: "0.75rem 1rem",
|
|
84
|
+
borderRadius: "0.5rem",
|
|
85
|
+
border: "1px solid #ccc",
|
|
86
|
+
fontSize: "0.95rem",
|
|
87
|
+
width: "100%",
|
|
88
|
+
maxWidth: "300px",
|
|
89
|
+
margin: "0.5rem 0"
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const containerStyles = {
|
|
93
|
+
display: "flex",
|
|
94
|
+
flexDirection: "column" as "column",
|
|
95
|
+
alignItems: "center",
|
|
96
|
+
justifyContent: "center",
|
|
97
|
+
padding: "2rem",
|
|
98
|
+
gap: "1rem"
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div style={containerStyles}>
|
|
103
|
+
<h1>Room Connection</h1>
|
|
104
|
+
|
|
105
|
+
{error && (
|
|
106
|
+
<div style={{ color: "red", margin: "1rem 0" }}>
|
|
107
|
+
{error}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{!isConnected ? (
|
|
112
|
+
<>
|
|
113
|
+
<div style={{ marginBottom: "1rem", width: "100%", maxWidth: "300px" }}>
|
|
114
|
+
<label htmlFor="roomId" style={{ display: "block", marginBottom: "0.5rem" }}>
|
|
115
|
+
Room ID:
|
|
116
|
+
</label>
|
|
117
|
+
<input
|
|
118
|
+
id="roomId"
|
|
119
|
+
type="text"
|
|
120
|
+
value={roomId}
|
|
121
|
+
onChange={(e) => setRoomId(e.target.value)}
|
|
122
|
+
style={inputStyles}
|
|
123
|
+
placeholder="Enter room ID"
|
|
124
|
+
disabled={isConnecting}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<button
|
|
129
|
+
style={buttonStyles}
|
|
130
|
+
onClick={connectToRoom}
|
|
131
|
+
disabled={isConnecting || !roomId.trim()}
|
|
132
|
+
>
|
|
133
|
+
{isConnecting ? "Connecting..." : "Connect to Room"}
|
|
134
|
+
</button>
|
|
135
|
+
</>
|
|
136
|
+
) : (
|
|
137
|
+
<>
|
|
138
|
+
<div style={{ marginBottom: "1rem" }}>
|
|
139
|
+
<span style={{ fontWeight: "bold" }}>Connected to room: </span>
|
|
140
|
+
<span>{roomId}</span>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div style={{ marginBottom: "2rem" }}>
|
|
144
|
+
<span>Count: {count}</span>
|
|
145
|
+
<button className="btn btn-primary" onClick={() => socketRef.current.emit('increment')}>Increment</button>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<button
|
|
149
|
+
style={buttonStyles}
|
|
150
|
+
onClick={disconnectFromRoom}
|
|
151
|
+
>
|
|
152
|
+
Leave Room
|
|
153
|
+
</button>
|
|
154
|
+
</>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Server } from '../../../src';
|
|
1
|
+
import { Server, WorldRoom } from '../../../src';
|
|
2
2
|
import type * as Party from "../../../src/types/party";
|
|
3
3
|
import { GameRoom } from "./game.room";
|
|
4
4
|
|
|
5
5
|
export default class MainServer extends Server {
|
|
6
6
|
rooms = [
|
|
7
|
-
GameRoom
|
|
7
|
+
GameRoom ,
|
|
8
|
+
WorldRoom
|
|
8
9
|
]
|
|
9
10
|
}
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
"$schema": "https://www.partykit.io/schema.json",
|
|
3
3
|
"name": "signe",
|
|
4
4
|
"main": "party/server.ts",
|
|
5
|
-
"compatibilityDate": "
|
|
5
|
+
"compatibilityDate": "2025-02-04",
|
|
6
|
+
"parties": {
|
|
7
|
+
"shard": "party/shard.ts",
|
|
8
|
+
"world": "party/server.ts"
|
|
9
|
+
},
|
|
6
10
|
"serve": {
|
|
7
11
|
"path": "public",
|
|
8
12
|
"build": "app/client.tsx"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signe/room",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"keywords": [],
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dset": "^3.1.3",
|
|
18
18
|
"partysocket": "^1.0.1",
|
|
19
19
|
"zod": "^3.23.8",
|
|
20
|
-
"@signe/sync": "
|
|
20
|
+
"@signe/sync": "2.0.0"
|
|
21
21
|
},
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
package/readme.md
CHANGED
|
@@ -13,6 +13,7 @@ npm install @signe/room @signe/reactive @signe/sync
|
|
|
13
13
|
- 🔄 Automatic state synchronization across clients
|
|
14
14
|
- 👥 Built-in user management with customizable player classes
|
|
15
15
|
- 🎮 Action-based message handling with type safety
|
|
16
|
+
- 🌐 HTTP request routing with path parameters
|
|
16
17
|
- 🔐 Flexible authentication and authorization system
|
|
17
18
|
- 🛡️ Guard system for room and action-level security
|
|
18
19
|
- 🎯 Full TypeScript support
|
|
@@ -58,7 +59,7 @@ export default class GameServer extends Server {
|
|
|
58
59
|
}
|
|
59
60
|
```
|
|
60
61
|
|
|
61
|
-
##
|
|
62
|
+
## Action
|
|
62
63
|
|
|
63
64
|
An action is a function that is called when a client sends a message to the server.
|
|
64
65
|
|
|
@@ -68,6 +69,69 @@ Function have to be decorated with the `@Action` decorator and have 3 parameter
|
|
|
68
69
|
- The second parameter is the value of the action
|
|
69
70
|
- The third parameter is the Party.Connection instance
|
|
70
71
|
|
|
72
|
+
## HTTP Request Handling
|
|
73
|
+
|
|
74
|
+
The `@Request` decorator allows you to handle HTTP requests with specific routes and methods:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { z } from "zod";
|
|
78
|
+
import { Room, Request, RequestGuard } from "@signe/room";
|
|
79
|
+
|
|
80
|
+
@Room({
|
|
81
|
+
path: "api"
|
|
82
|
+
})
|
|
83
|
+
class ApiRoom {
|
|
84
|
+
@sync() gameState = signal("waiting");
|
|
85
|
+
@users(Player) players = signal({});
|
|
86
|
+
@sync() scores = signal([]);
|
|
87
|
+
|
|
88
|
+
// Handle GET requests
|
|
89
|
+
@Request({ path: "/status" })
|
|
90
|
+
getStatus(req: Party.Request) {
|
|
91
|
+
return {
|
|
92
|
+
status: "online",
|
|
93
|
+
players: Object.keys(this.players()).length,
|
|
94
|
+
gameState: this.gameState(),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle requests with path parameters
|
|
99
|
+
@Request({ path: "/players/:id" })
|
|
100
|
+
getPlayer(req: Party.Request, body: any, params: { id: string }) {
|
|
101
|
+
const player = this.players()[params.id];
|
|
102
|
+
if (!player) {
|
|
103
|
+
return new Response(JSON.stringify({ error: "Player not found" }), { status: 404 });
|
|
104
|
+
}
|
|
105
|
+
return player;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Handle POST requests with body validation
|
|
109
|
+
@Request(
|
|
110
|
+
{ path: "/scores", method: "POST" },
|
|
111
|
+
z.object({
|
|
112
|
+
playerId: z.string(),
|
|
113
|
+
score: z.number().min(0)
|
|
114
|
+
})
|
|
115
|
+
)
|
|
116
|
+
@RequestGuard([isAuthenticated])
|
|
117
|
+
submitScore(req: Party.Request, body: { playerId: string; score: number }) {
|
|
118
|
+
this.scores.update(scores => [...scores, body]);
|
|
119
|
+
return { success: true };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Request handler methods receive these parameters:
|
|
125
|
+
1. `req`: The original Party.Request object
|
|
126
|
+
2. `body`: The validated request body (if validation schema was provided)
|
|
127
|
+
3. `params`: An object containing any path parameters
|
|
128
|
+
4. `room`: The Party.Room instance
|
|
129
|
+
|
|
130
|
+
You can return:
|
|
131
|
+
- A Response object for complete control
|
|
132
|
+
- An object that will be serialized as JSON
|
|
133
|
+
- A string that will be returned as text/plain
|
|
134
|
+
|
|
71
135
|
## Advanced Features
|
|
72
136
|
|
|
73
137
|
### Room Configuration
|
|
@@ -111,6 +175,12 @@ class AdminRoom {
|
|
|
111
175
|
async deleteUser(admin: Player, userId: string) {
|
|
112
176
|
// Only authenticated admins can execute this
|
|
113
177
|
}
|
|
178
|
+
|
|
179
|
+
@Request({ path: "/admin/users", method: "DELETE" })
|
|
180
|
+
@RequestGuard([isAdmin]) // Applied only to this request handler
|
|
181
|
+
async deleteUserViaHttp(req: Party.Request) {
|
|
182
|
+
// Only authenticated admins can access this endpoint
|
|
183
|
+
}
|
|
114
184
|
}
|
|
115
185
|
```
|
|
116
186
|
|
|
@@ -176,13 +246,167 @@ class GameRoom {
|
|
|
176
246
|
}
|
|
177
247
|
```
|
|
178
248
|
|
|
249
|
+
### Connecting to World Service
|
|
250
|
+
|
|
251
|
+
The World Service provides optimal room and shard assignment for distributed applications. It handles load balancing and allows clients to connect to the most appropriate server.
|
|
252
|
+
|
|
253
|
+
#### Environment Variables
|
|
254
|
+
|
|
255
|
+
To use the Signe room system, you need to configure two essential environment variables:
|
|
256
|
+
|
|
257
|
+
```env
|
|
258
|
+
# Required for JWT authentication
|
|
259
|
+
AUTH_JWT_SECRET=a-string-secret-at-least-256-bits-long
|
|
260
|
+
|
|
261
|
+
# Required for secure communication between shards
|
|
262
|
+
SHARD_SECRET=your_shard_secret
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
These secrets should be strong, unique values and kept secure.
|
|
266
|
+
|
|
267
|
+
#### Server Configuration
|
|
268
|
+
|
|
269
|
+
To use the World service, you need to:
|
|
270
|
+
|
|
271
|
+
1. Add `WorldRoom` to your server:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import { Server, WorldRoom } from '@signe/room';
|
|
275
|
+
|
|
276
|
+
export default class MainServer extends Server {
|
|
277
|
+
rooms = [
|
|
278
|
+
GameRoom,
|
|
279
|
+
WorldRoom // Add WorldRoom to enable World service
|
|
280
|
+
]
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
2. Configure your `partykit.json` file:
|
|
285
|
+
|
|
286
|
+
```json
|
|
287
|
+
{
|
|
288
|
+
"$schema": "https://www.partykit.io/schema.json",
|
|
289
|
+
"name": "yourapp",
|
|
290
|
+
"main": "party/server.ts",
|
|
291
|
+
"compatibilityDate": "2025-02-04",
|
|
292
|
+
"parties": {
|
|
293
|
+
"shard": "party/shard.ts", // Shard implementation
|
|
294
|
+
"world": "party/server.ts" // World service implementation
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
#### Client Connection
|
|
300
|
+
|
|
301
|
+
On the client side, use the `connectionWorld` function to connect to your room through the World service:
|
|
302
|
+
|
|
303
|
+
```js
|
|
304
|
+
import { connectionWorld } from '@signe/sync/client';
|
|
305
|
+
|
|
306
|
+
// Initialize your room instance
|
|
307
|
+
const room = new YourRoomSchema();
|
|
308
|
+
|
|
309
|
+
// Connect through the World service
|
|
310
|
+
const connection = await connectionWorld({
|
|
311
|
+
worldUrl: 'https://your-app-url.com', // Your application URL
|
|
312
|
+
roomId: 'unique-room-id', // Room identifier
|
|
313
|
+
worldId: 'your-world-id', // Optional, defaults to 'world-default'
|
|
314
|
+
autoCreate: true, // Auto-create room if it doesn't exist
|
|
315
|
+
retryCount: 3, // Number of connection attempts
|
|
316
|
+
retryDelay: 1000, // Delay between retries in ms
|
|
317
|
+
socketOptions: { // Optional PartySocket configuration
|
|
318
|
+
protocols: ['your-protocol']
|
|
319
|
+
}
|
|
320
|
+
}, room);
|
|
321
|
+
|
|
322
|
+
// Listen for events
|
|
323
|
+
connection.on('customEvent', (data) => {
|
|
324
|
+
console.log('Received custom event:', data);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Send events to the room
|
|
328
|
+
connection.emit('increment', { value: 1 });
|
|
329
|
+
|
|
330
|
+
// Close the connection when done
|
|
331
|
+
connection.close();
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
For connecting to a standard room (not through World service), use the `connectionRoom` function:
|
|
335
|
+
|
|
336
|
+
```js
|
|
337
|
+
import { connectionRoom } from '@signe/sync/client';
|
|
338
|
+
|
|
339
|
+
// Initialize your room instance
|
|
340
|
+
const room = new YourRoomSchema();
|
|
341
|
+
|
|
342
|
+
// Connect directly to a room
|
|
343
|
+
const connection = await connectionRoom({
|
|
344
|
+
host: window.location.origin,
|
|
345
|
+
room: 'your-room-name',
|
|
346
|
+
party: 'your-party-name', // Optional, defaults to main party
|
|
347
|
+
query: {} // Optional query parameters
|
|
348
|
+
}, room);
|
|
349
|
+
|
|
350
|
+
// For connecting to a World room with authentication
|
|
351
|
+
const worldConnection = await connectionRoom({
|
|
352
|
+
host: window.location.origin,
|
|
353
|
+
room: 'world-default',
|
|
354
|
+
party: 'world',
|
|
355
|
+
query: {
|
|
356
|
+
// Use pre-generated JWT token for authentication
|
|
357
|
+
'world-auth-token': 'your-jwt-token'
|
|
358
|
+
}
|
|
359
|
+
}, worldRoom);
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
The `connectionWorld` function:
|
|
363
|
+
1. Queries the World service to find the optimal shard for the requested room
|
|
364
|
+
2. Establishes a WebSocket connection to the assigned shard
|
|
365
|
+
3. Returns a connection object with methods for sending and receiving messages
|
|
366
|
+
|
|
367
|
+
This approach offers several benefits:
|
|
368
|
+
- Automatic load balancing across multiple servers
|
|
369
|
+
- Simplified connection management
|
|
370
|
+
- Built-in retry logic for reliability
|
|
371
|
+
- Room creation on demand
|
|
372
|
+
|
|
373
|
+
### Packet Interception
|
|
374
|
+
|
|
375
|
+
You can implement the `interceptorPacket` method in your room to inspect and modify packets before they're sent to users:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
class GameRoom {
|
|
379
|
+
// Intercept packets before they're sent to users
|
|
380
|
+
async interceptorPacket(user: Player, packet: any, conn: Party.Connection) {
|
|
381
|
+
// Modify the packet based on user-specific logic
|
|
382
|
+
if (user.role === 'spectator') {
|
|
383
|
+
delete modifiedPacket.secretData;
|
|
384
|
+
return modifiedPacket;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Return null to prevent the packet from being sent to this user
|
|
388
|
+
if (user.isBlocked) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Return the packet as is or with modifications
|
|
393
|
+
return packet;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
The `interceptorPacket` method allows you to:
|
|
399
|
+
- Modify packets on a per-user basis before they're sent
|
|
400
|
+
- Return a modified packet to change what the user receives
|
|
401
|
+
- Return `null` to prevent the packet from being sent to that user
|
|
402
|
+
- Implement user-specific filtering or censoring of data
|
|
403
|
+
|
|
179
404
|
### Lifecycle Hooks
|
|
180
405
|
|
|
181
406
|
Rooms provide several lifecycle hooks:
|
|
182
407
|
|
|
183
408
|
```ts
|
|
184
409
|
class GameRoom {
|
|
185
|
-
async onCreate()
|
|
186
410
|
async onJoin(player: Player, conn: Connection, ctx: ConnectionContext) {}
|
|
187
411
|
async onLeave(player: Player, conn: Connection) {}
|
|
188
412
|
}
|
package/src/decorators.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type * as Party from "./types/party"
|
|
2
2
|
import type { z } from "zod"
|
|
3
|
-
type GuardFn = (sender: Party.Connection, value: any) => boolean | Promise<boolean>;
|
|
4
|
-
type RoomGuardFn = (conn: Party.Connection, ctx: Party.ConnectionContext) => boolean | Promise<boolean>;
|
|
3
|
+
type GuardFn = (sender: Party.Connection, value: any | Party.Request, room: Party.Room) => boolean | Promise<boolean | Response>;
|
|
4
|
+
type RoomGuardFn = (conn: Party.Connection, ctx: Party.ConnectionContext, room: Party.Room) => boolean | Promise<boolean | Response>;
|
|
5
5
|
|
|
6
6
|
export function Action(name: string, bodyValidation?: z.ZodSchema) {
|
|
7
7
|
return function (target: any, propertyKey: string) {
|
|
@@ -15,6 +15,38 @@ export function Action(name: string, bodyValidation?: z.ZodSchema) {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Request decorator for handling HTTP requests with path and method routing
|
|
20
|
+
* @param options Configuration for the HTTP request handler
|
|
21
|
+
* @param bodyValidation Optional Zod schema for request body validation
|
|
22
|
+
*/
|
|
23
|
+
export interface RequestOptions {
|
|
24
|
+
path: string;
|
|
25
|
+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function Request(options: RequestOptions, bodyValidation?: z.ZodSchema) {
|
|
29
|
+
return function (target: any, propertyKey: string) {
|
|
30
|
+
if (!target.constructor._requestMetadata) {
|
|
31
|
+
target.constructor._requestMetadata = new Map();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Format the path to ensure it starts with a slash
|
|
35
|
+
const path = options.path.startsWith('/') ? options.path : `/${options.path}`;
|
|
36
|
+
const method = options.method || 'GET';
|
|
37
|
+
|
|
38
|
+
// Create a unique key for this route using method and path
|
|
39
|
+
const routeKey = `${method}:${path}`;
|
|
40
|
+
|
|
41
|
+
target.constructor._requestMetadata.set(routeKey, {
|
|
42
|
+
key: propertyKey,
|
|
43
|
+
path,
|
|
44
|
+
method,
|
|
45
|
+
bodyValidation,
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
18
50
|
export interface RoomOptions {
|
|
19
51
|
path: string;
|
|
20
52
|
maxUsers?: number;
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export * from './decorators';
|
|
2
2
|
export { ClientIo, MockConnection, ServerIo } from './mock';
|
|
3
3
|
export { Server } from './server';
|
|
4
|
-
export * from './testing';
|
|
4
|
+
export * from './testing';
|
|
5
|
+
export * from './shard';
|
|
6
|
+
export * from './world';
|
|
7
|
+
export * from './interfaces';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as Party from "./types/party";
|
|
2
|
+
|
|
3
|
+
export interface RoomInterceptorPacket {
|
|
4
|
+
interceptorPacket(user: any, obj: any, conn: Party.Connection): Promise<any> | null | any;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RoomOnJoin {
|
|
8
|
+
onJoin(user: any, conn: Party.Connection, ctx: Party.ConnectionContext): Promise<any> | null | any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RoomOnLeave {
|
|
12
|
+
onLeave(user: any, conn: Party.Connection, ctx: Party.ConnectionContext): Promise<any> | null | any;
|
|
13
|
+
}
|