@signe/room 2.0.1 → 2.2.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 +51 -6
- package/dist/index.js +242 -159
- package/dist/index.js.map +1 -1
- package/examples/game/app/components/Room.tsx +2 -2
- package/examples/game/party/game.room.ts +0 -1
- package/package.json +2 -2
- package/readme.md +3 -6
- package/src/mock.ts +10 -5
- package/src/request/cors.ts +5 -1
- package/src/server.ts +105 -22
- package/src/shard.ts +17 -7
- package/src/testing.ts +6 -3
- package/src/world.ts +1 -1
|
@@ -22,8 +22,8 @@ export default function Room() {
|
|
|
22
22
|
|
|
23
23
|
// Connect to the room through the World service with auto-creation enabled
|
|
24
24
|
socketRef.current = await connectionWorld({
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
host: 'http://localhost:1999',
|
|
26
|
+
room: roomId,
|
|
27
27
|
autoCreate: true // Enable auto-creation of room and shards
|
|
28
28
|
}, roomRef.current);
|
|
29
29
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signe/room",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.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": "2.0
|
|
20
|
+
"@signe/sync": "2.2.0"
|
|
21
21
|
},
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
package/readme.md
CHANGED
|
@@ -316,15 +316,12 @@ const room = new YourRoomSchema();
|
|
|
316
316
|
|
|
317
317
|
// Connect through the World service
|
|
318
318
|
const connection = await connectionWorld({
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
host: 'https://your-app-url.com', // Your application URL
|
|
320
|
+
room: 'unique-room-id', // Room identifier
|
|
321
321
|
worldId: 'your-world-id', // Optional, defaults to 'world-default'
|
|
322
322
|
autoCreate: true, // Auto-create room if it doesn't exist
|
|
323
323
|
retryCount: 3, // Number of connection attempts
|
|
324
|
-
retryDelay: 1000
|
|
325
|
-
socketOptions: { // Optional PartySocket configuration
|
|
326
|
-
protocols: ['your-protocol']
|
|
327
|
-
}
|
|
324
|
+
retryDelay: 1000 // Delay between retries in ms
|
|
328
325
|
}, room);
|
|
329
326
|
|
|
330
327
|
// Listen for events
|
package/src/mock.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { generateShortUUID } from "../../sync/src/utils";
|
|
2
2
|
import { Server } from "./server";
|
|
3
3
|
import { Storage } from "./storage";
|
|
4
|
+
import { request } from "./testing";
|
|
4
5
|
|
|
5
6
|
export class MockPartyClient {
|
|
6
7
|
private events: Map<string, Function> = new Map();
|
|
7
|
-
id
|
|
8
|
+
id : string
|
|
8
9
|
conn: MockConnection;
|
|
9
10
|
|
|
10
|
-
constructor(public server: Server) {
|
|
11
|
+
constructor(public server: Server, id?: string) {
|
|
12
|
+
this.id = id || generateShortUUID()
|
|
11
13
|
this.conn = new MockConnection(this)
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -34,6 +36,10 @@ class MockLobby {
|
|
|
34
36
|
socket() {
|
|
35
37
|
return new MockPartyClient(this.server)
|
|
36
38
|
}
|
|
39
|
+
|
|
40
|
+
fetch(url: string, options: any) {
|
|
41
|
+
return request(this.server, url, options)
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
class MockContext {
|
|
@@ -51,7 +57,6 @@ class MockContext {
|
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
|
|
55
60
|
class MockPartyRoom {
|
|
56
61
|
clients: Map<string, MockPartyClient> = new Map();
|
|
57
62
|
storage = new Storage();
|
|
@@ -66,8 +71,8 @@ class MockPartyRoom {
|
|
|
66
71
|
this.env = options.env || {}
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
async connection(server: Server) {
|
|
70
|
-
const socket = new MockPartyClient(server);
|
|
74
|
+
async connection(server: Server, id?: string) {
|
|
75
|
+
const socket = new MockPartyClient(server, id);
|
|
71
76
|
const url = new URL('http://localhost')
|
|
72
77
|
const request = new Request(url.toString(), {
|
|
73
78
|
method: 'GET',
|
package/src/request/cors.ts
CHANGED
|
@@ -2,7 +2,8 @@ export function cors(res: Response, options: CorsOptions = {}) {
|
|
|
2
2
|
const newHeaders = new Headers(res.headers);
|
|
3
3
|
|
|
4
4
|
// Set default CORS headers
|
|
5
|
-
|
|
5
|
+
const requestOrigin = options.origin || '*';
|
|
6
|
+
newHeaders.set('Access-Control-Allow-Origin', requestOrigin);
|
|
6
7
|
|
|
7
8
|
if (options.credentials) {
|
|
8
9
|
newHeaders.set('Access-Control-Allow-Credentials', 'true');
|
|
@@ -27,6 +28,9 @@ export function cors(res: Response, options: CorsOptions = {}) {
|
|
|
27
28
|
|
|
28
29
|
if (options.maxAge) {
|
|
29
30
|
newHeaders.set('Access-Control-Max-Age', options.maxAge.toString());
|
|
31
|
+
} else {
|
|
32
|
+
// Default max-age to 86400 seconds (24 hours)
|
|
33
|
+
newHeaders.set('Access-Control-Max-Age', '86400');
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
return new Response(res.body, {
|
package/src/server.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "./utils";
|
|
19
19
|
import { ServerResponse } from "./request/response";
|
|
20
20
|
import { createCorsInterceptor } from "./request/cors";
|
|
21
|
+
import { Signal, WritableSignal } from "@signe/reactive";
|
|
21
22
|
|
|
22
23
|
const Message = z.object({
|
|
23
24
|
action: z.string(),
|
|
@@ -344,11 +345,63 @@ export class Server implements Party.Server {
|
|
|
344
345
|
}
|
|
345
346
|
|
|
346
347
|
private getUsersPropName(subRoom) {
|
|
347
|
-
|
|
348
|
-
|
|
348
|
+
if (!subRoom) return null;
|
|
349
|
+
const metadata = subRoom.constructor._propertyMetadata;
|
|
350
|
+
if (!metadata) return null;
|
|
351
|
+
return metadata.get("users");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Retrieves the connection status property from a user object.
|
|
356
|
+
*
|
|
357
|
+
* @param {any} user - The user object to get the connection property from.
|
|
358
|
+
* @returns {Function|null} - The connection property signal function or null if not found.
|
|
359
|
+
* @private
|
|
360
|
+
*/
|
|
361
|
+
private getUserConnectionProperty(user: any): WritableSignal<boolean> | null {
|
|
362
|
+
if (!user) return null;
|
|
363
|
+
|
|
364
|
+
const metadata = user.constructor._propertyMetadata;
|
|
365
|
+
if (!metadata) return null;
|
|
366
|
+
|
|
367
|
+
const connectedPropName = metadata.get("connected");
|
|
368
|
+
if (!connectedPropName) return null;
|
|
369
|
+
|
|
370
|
+
return user[connectedPropName];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Updates a user's connection status in the signal.
|
|
375
|
+
*
|
|
376
|
+
* @param {any} user - The user object to update.
|
|
377
|
+
* @param {boolean} isConnected - The new connection status.
|
|
378
|
+
* @returns {boolean} - Whether the update was successful.
|
|
379
|
+
* @private
|
|
380
|
+
*/
|
|
381
|
+
private updateUserConnectionStatus(user: any, isConnected: boolean): boolean {
|
|
382
|
+
const connectionSignal = this.getUserConnectionProperty(user);
|
|
383
|
+
|
|
384
|
+
if (connectionSignal) {
|
|
385
|
+
connectionSignal.set(isConnected);
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return false;
|
|
349
390
|
}
|
|
350
391
|
|
|
351
|
-
|
|
392
|
+
/**
|
|
393
|
+
* @method getSession
|
|
394
|
+
* @private
|
|
395
|
+
* @param {string} privateId - The private ID of the session.
|
|
396
|
+
* @returns {Promise<Object|null>} The session object, or null if not found.
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* ```typescript
|
|
400
|
+
* const session = await server.getSession("privateId");
|
|
401
|
+
* console.log(session);
|
|
402
|
+
* ```
|
|
403
|
+
*/
|
|
404
|
+
async getSession(privateId: string): Promise<{ publicId: string, state?: any, created?: number, connected?: boolean } | null> {
|
|
352
405
|
if (!privateId) return null;
|
|
353
406
|
try {
|
|
354
407
|
const session = await this.room.storage.get(`session:${privateId}`);
|
|
@@ -374,7 +427,18 @@ export class Server implements Party.Server {
|
|
|
374
427
|
}
|
|
375
428
|
}
|
|
376
429
|
|
|
377
|
-
|
|
430
|
+
/**
|
|
431
|
+
* @method deleteSession
|
|
432
|
+
* @private
|
|
433
|
+
* @param {string} privateId - The private ID of the session to delete.
|
|
434
|
+
* @returns {Promise<void>}
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* ```typescript
|
|
438
|
+
* await server.deleteSession("privateId");
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
441
|
+
async deleteSession(privateId: string) {
|
|
378
442
|
await this.room.storage.delete(`session:${privateId}`);
|
|
379
443
|
}
|
|
380
444
|
|
|
@@ -421,6 +485,9 @@ export class Server implements Party.Server {
|
|
|
421
485
|
const snapshot = createStatesSnapshot(user);
|
|
422
486
|
this.room.storage.put(`${usersPropName}.${publicId}`, snapshot);
|
|
423
487
|
}
|
|
488
|
+
else {
|
|
489
|
+
user = signal()[existingSession.publicId];
|
|
490
|
+
}
|
|
424
491
|
|
|
425
492
|
// Only store new session if it doesn't exist
|
|
426
493
|
if (!existingSession) {
|
|
@@ -432,6 +499,8 @@ export class Server implements Party.Server {
|
|
|
432
499
|
await this.updateSessionConnection(conn.id, true);
|
|
433
500
|
}
|
|
434
501
|
}
|
|
502
|
+
// Update user connection status if applicable
|
|
503
|
+
this.updateUserConnectionStatus(user, true);
|
|
435
504
|
|
|
436
505
|
// Call the room's onJoin method if it exists
|
|
437
506
|
await awaitReturn(subRoom["onJoin"]?.(user, conn, ctx));
|
|
@@ -527,6 +596,11 @@ export class Server implements Party.Server {
|
|
|
527
596
|
|
|
528
597
|
const subRoom = await this.getSubRoom()
|
|
529
598
|
|
|
599
|
+
if (!subRoom) {
|
|
600
|
+
console.warn("Room not found");
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
530
604
|
// Check room guards
|
|
531
605
|
const roomGuards = subRoom.constructor['_roomGuards'] || [];
|
|
532
606
|
for (const guard of roomGuards) {
|
|
@@ -622,20 +696,16 @@ export class Server implements Party.Server {
|
|
|
622
696
|
* @returns {Promise<void>}
|
|
623
697
|
*/
|
|
624
698
|
private async handleShardClientConnect(message: any, shardConnection: Party.Connection) {
|
|
625
|
-
const { privateId,
|
|
699
|
+
const { privateId, requestInfo } = message;
|
|
626
700
|
const shardState = shardConnection.state as any;
|
|
627
701
|
|
|
628
702
|
// Create a virtual connection context for the client
|
|
629
703
|
const virtualContext: Party.ConnectionContext = {
|
|
630
|
-
request: {
|
|
631
|
-
headers: new Headers(
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}),
|
|
636
|
-
method: 'GET',
|
|
637
|
-
url: ''
|
|
638
|
-
} as unknown as Party.Request
|
|
704
|
+
request: requestInfo ? {
|
|
705
|
+
headers: new Headers(requestInfo.headers),
|
|
706
|
+
method: requestInfo.method,
|
|
707
|
+
url: requestInfo.url
|
|
708
|
+
} as unknown as Party.Request : undefined
|
|
639
709
|
};
|
|
640
710
|
|
|
641
711
|
// Create a virtual connection for the client
|
|
@@ -812,16 +882,22 @@ export class Server implements Party.Server {
|
|
|
812
882
|
|
|
813
883
|
if (!user) return;
|
|
814
884
|
|
|
815
|
-
await awaitReturn(subRoom["onLeave"]?.(user, conn));
|
|
816
|
-
|
|
817
885
|
// Mark session as disconnected instead of deleting it
|
|
818
886
|
await this.updateSessionConnection(privateId, false);
|
|
819
887
|
|
|
820
|
-
//
|
|
821
|
-
this.
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
888
|
+
// Update user connection status in the signal
|
|
889
|
+
const connectionUpdated = this.updateUserConnectionStatus(user, false);
|
|
890
|
+
|
|
891
|
+
await awaitReturn(subRoom["onLeave"]?.(user, conn));
|
|
892
|
+
|
|
893
|
+
// Only broadcast disconnection if we couldn't update the connection signal
|
|
894
|
+
if (!connectionUpdated) {
|
|
895
|
+
// Broadcast user disconnection the old way
|
|
896
|
+
this.broadcast({
|
|
897
|
+
type: "user_disconnected",
|
|
898
|
+
value: { publicId }
|
|
899
|
+
}, subRoom);
|
|
900
|
+
}
|
|
825
901
|
}
|
|
826
902
|
|
|
827
903
|
async onAlarm() {
|
|
@@ -845,10 +921,17 @@ export class Server implements Party.Server {
|
|
|
845
921
|
// Check if the request is coming from a shard
|
|
846
922
|
const isFromShard = req.headers.has('x-forwarded-by-shard');
|
|
847
923
|
const shardId = req.headers.get('x-shard-id');
|
|
924
|
+
|
|
925
|
+
// Create a response with proper CORS configuration
|
|
848
926
|
const res = new ServerResponse([
|
|
849
927
|
createCorsInterceptor()
|
|
850
928
|
]);
|
|
851
929
|
|
|
930
|
+
if (req.method === 'OPTIONS') {
|
|
931
|
+
// For OPTIONS requests, just return a 200 OK with CORS headers
|
|
932
|
+
return res.status(200).send({});
|
|
933
|
+
}
|
|
934
|
+
|
|
852
935
|
if (isFromShard) {
|
|
853
936
|
return this.handleShardRequest(req, res, shardId);
|
|
854
937
|
}
|
|
@@ -1054,7 +1137,7 @@ export class Server implements Party.Server {
|
|
|
1054
1137
|
// Create a context that preserves original client information
|
|
1055
1138
|
const originalClientIp = req.headers.get('x-original-client-ip');
|
|
1056
1139
|
const enhancedReq = this.createEnhancedRequest(req, originalClientIp);
|
|
1057
|
-
|
|
1140
|
+
|
|
1058
1141
|
try {
|
|
1059
1142
|
// First try to match using the registered @Request handlers
|
|
1060
1143
|
const response = await this.tryMatchRequestHandler(enhancedReq, res, subRoom);
|
package/src/shard.ts
CHANGED
|
@@ -93,15 +93,26 @@ export class Shard {
|
|
|
93
93
|
// Store connection mapping
|
|
94
94
|
this.connectionMap.set(conn.id, conn);
|
|
95
95
|
|
|
96
|
-
//
|
|
96
|
+
// Capture all headers and request information
|
|
97
|
+
const headers: Record<string, string> = {};
|
|
98
|
+
if (ctx.request?.headers) {
|
|
99
|
+
ctx.request.headers.forEach((value, key) => {
|
|
100
|
+
headers[key] = value;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Prepare connection context information
|
|
105
|
+
const requestInfo = ctx.request ? {
|
|
106
|
+
headers,
|
|
107
|
+
url: ctx.request.url,
|
|
108
|
+
method: ctx.request.method
|
|
109
|
+
} : null;
|
|
110
|
+
|
|
111
|
+
// Notify the main server about the new connection with complete connection metadata
|
|
97
112
|
this.ws.send(JSON.stringify({
|
|
98
113
|
type: 'shard.clientConnected',
|
|
99
114
|
privateId: conn.id,
|
|
100
|
-
|
|
101
|
-
ip: ctx.request?.headers.get('x-forwarded-for') || 'unknown',
|
|
102
|
-
userAgent: ctx.request?.headers.get('user-agent') || 'unknown',
|
|
103
|
-
// Add any other relevant connection info
|
|
104
|
-
}
|
|
115
|
+
requestInfo
|
|
105
116
|
}));
|
|
106
117
|
|
|
107
118
|
this.updateWorldStats();
|
|
@@ -222,7 +233,6 @@ export class Shard {
|
|
|
222
233
|
headers,
|
|
223
234
|
body
|
|
224
235
|
};
|
|
225
|
-
|
|
226
236
|
// Forward the request to the main server
|
|
227
237
|
const response = await this.mainServerStub.fetch(path, requestInit);
|
|
228
238
|
return response;
|
package/src/testing.ts
CHANGED
|
@@ -58,6 +58,9 @@ export async function testRoom(Room, options: {
|
|
|
58
58
|
// Add subRoom property to Shard for compatibility with Server
|
|
59
59
|
(shardServer as any).subRoom = null;
|
|
60
60
|
server = shardServer;
|
|
61
|
+
for (const lobby of io.context.parties.main.values()) {
|
|
62
|
+
await lobby.server.onStart();
|
|
63
|
+
}
|
|
61
64
|
} else {
|
|
62
65
|
server = await createServer(io as any);
|
|
63
66
|
}
|
|
@@ -67,14 +70,14 @@ export async function testRoom(Room, options: {
|
|
|
67
70
|
return {
|
|
68
71
|
server,
|
|
69
72
|
room: (server as any).subRoom,
|
|
70
|
-
createClient: async () => {
|
|
71
|
-
const client = await io.connection(server as Server)
|
|
73
|
+
createClient: async (id?: string) => {
|
|
74
|
+
const client = await io.connection(server as Server, id)
|
|
72
75
|
return client
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
export async function request(room: Server, path: string, options: {
|
|
80
|
+
export async function request(room: Server | Shard, path: string, options: {
|
|
78
81
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
79
82
|
body?: any,
|
|
80
83
|
headers?: Record<string, string>
|
package/src/world.ts
CHANGED
|
@@ -272,7 +272,7 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
272
272
|
try {
|
|
273
273
|
// Extract request data
|
|
274
274
|
let data: { roomId: string; autoCreate?: boolean };
|
|
275
|
-
|
|
275
|
+
|
|
276
276
|
try {
|
|
277
277
|
// Handle potential empty body or malformed JSON
|
|
278
278
|
const body = await req.text();
|