@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.
@@ -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
- worldUrl: 'http://localhost:1999',
26
- roomId: roomId,
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
 
@@ -6,7 +6,6 @@ import { RoomSchema } from "../shared/room.schema";
6
6
  sessionExpiryTime: 5000
7
7
  })
8
8
  export class GameRoom extends RoomSchema {
9
-
10
9
  @Action('increment')
11
10
  increment(player) {
12
11
  this.count.update((count) => count + 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signe/room",
3
- "version": "2.0.1",
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.1"
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
- worldUrl: 'https://your-app-url.com', // Your application URL
320
- roomId: 'unique-room-id', // Room identifier
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, // Delay between retries in ms
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 = generateShortUUID()
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',
@@ -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
- newHeaders.set('Access-Control-Allow-Origin', options.origin || '*');
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
- const meta = subRoom.constructor["_propertyMetadata"];
348
- return meta?.get("users")
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
- private async getSession(privateId: string): Promise<{ publicId: string, state?: any, created?: number, connected?: boolean } | null> {
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
- private async deleteSession(privateId: string) {
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, connectionInfo } = message;
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
- 'x-forwarded-for': connectionInfo.ip,
633
- 'user-agent': connectionInfo.userAgent,
634
- // Add other headers as needed
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
- // Broadcast user disconnection
821
- this.broadcast({
822
- type: "user_disconnected",
823
- value: { publicId }
824
- }, subRoom);
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
- // Notify the main server about the new connection with connection metadata
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
- connectionInfo: {
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();