@signe/room 2.1.0 → 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.
@@ -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.1.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.1.0"
20
+ "@signe/sync": "2.2.0"
21
21
  },
22
22
  "publishConfig": {
23
23
  "access": "public"
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',
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));
@@ -627,20 +696,16 @@ export class Server implements Party.Server {
627
696
  * @returns {Promise<void>}
628
697
  */
629
698
  private async handleShardClientConnect(message: any, shardConnection: Party.Connection) {
630
- const { privateId, connectionInfo } = message;
699
+ const { privateId, requestInfo } = message;
631
700
  const shardState = shardConnection.state as any;
632
701
 
633
702
  // Create a virtual connection context for the client
634
703
  const virtualContext: Party.ConnectionContext = {
635
- request: {
636
- headers: new Headers({
637
- 'x-forwarded-for': connectionInfo.ip,
638
- 'user-agent': connectionInfo.userAgent,
639
- // Add other headers as needed
640
- }),
641
- method: 'GET',
642
- url: ''
643
- } 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
644
709
  };
645
710
 
646
711
  // Create a virtual connection for the client
@@ -817,16 +882,22 @@ export class Server implements Party.Server {
817
882
 
818
883
  if (!user) return;
819
884
 
820
- await awaitReturn(subRoom["onLeave"]?.(user, conn));
821
-
822
885
  // Mark session as disconnected instead of deleting it
823
886
  await this.updateSessionConnection(privateId, false);
824
887
 
825
- // Broadcast user disconnection
826
- this.broadcast({
827
- type: "user_disconnected",
828
- value: { publicId }
829
- }, 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
+ }
830
901
  }
831
902
 
832
903
  async onAlarm() {
@@ -1066,7 +1137,7 @@ export class Server implements Party.Server {
1066
1137
  // Create a context that preserves original client information
1067
1138
  const originalClientIp = req.headers.get('x-original-client-ip');
1068
1139
  const enhancedReq = this.createEnhancedRequest(req, originalClientIp);
1069
-
1140
+
1070
1141
  try {
1071
1142
  // First try to match using the registered @Request handlers
1072
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>