@signe/room 1.4.1 → 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/src/server.ts CHANGED
@@ -61,19 +61,19 @@ export class Server implements Party.Server {
61
61
  * const server = new MyServer(new ServerIo("game"));
62
62
  * ```
63
63
  */
64
- constructor(readonly room: Party.Room) {}
64
+ constructor(readonly room: Party.Room) { }
65
65
 
66
- /**
67
- * @readonly
68
- * @property {boolean} isHibernate - Indicates whether the server is in hibernate mode.
69
- *
70
- * @example
71
- * ```typescript
72
- * if (!server.isHibernate) {
73
- * console.log("Server is active");
74
- * }
75
- * ```
76
- */
66
+ /**
67
+ * @readonly
68
+ * @property {boolean} isHibernate - Indicates whether the server is in hibernate mode.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * if (!server.isHibernate) {
73
+ * console.log("Server is active");
74
+ * }
75
+ * ```
76
+ */
77
77
  get isHibernate(): boolean {
78
78
  return !!this["options"]?.hibernate;
79
79
  }
@@ -82,6 +82,24 @@ export class Server implements Party.Server {
82
82
  return this.room.storage
83
83
  }
84
84
 
85
+ async send(conn: Party.Connection, obj: any, subRoom: any) {
86
+ obj = structuredClone(obj);
87
+ if (subRoom.interceptorPacket) {
88
+ const signal = this.getUsersProperty(subRoom);
89
+ const { publicId } = conn.state as any;
90
+ const user = signal?.()[publicId];
91
+ obj = await awaitReturn(subRoom["interceptorPacket"]?.(user, obj, conn));
92
+ if (obj === null) return;
93
+ }
94
+ conn.send(JSON.stringify(obj));
95
+ }
96
+
97
+ broadcast(obj: any, subRoom: any) {
98
+ for (let conn of this.room.getConnections()) {
99
+ this.send(conn, obj, subRoom);
100
+ }
101
+ }
102
+
85
103
  /**
86
104
  * @method onStart
87
105
  * @async
@@ -122,7 +140,7 @@ export class Server implements Party.Server {
122
140
  // Store valid publicIds from sessions
123
141
  const validPublicIds = new Set<string>();
124
142
  const expiredPublicIds = new Set<string>();
125
- const SESSION_EXPIRY_TIME = options.sessionExpiryTime
143
+ const SESSION_EXPIRY_TIME = options.sessionExpiryTime
126
144
  const now = Date.now();
127
145
 
128
146
  for (const [key, session] of sessions) {
@@ -130,15 +148,15 @@ export class Server implements Party.Server {
130
148
  if (!key.startsWith('session:')) continue;
131
149
 
132
150
  const privateId = key.replace('session:', '');
133
- const typedSession = session as {publicId: string, created: number, connected: boolean};
134
-
151
+ const typedSession = session as { publicId: string, created: number, connected: boolean };
152
+
135
153
  // Check if session should be deleted based on:
136
154
  // 1. Connection is not active
137
155
  // 2. Session is marked as disconnected
138
156
  // 3. Session is older than expiry time
139
- if (!activePrivateIds.has(privateId) &&
140
- !typedSession.connected &&
141
- (now - typedSession.created) > SESSION_EXPIRY_TIME) {
157
+ if (!activePrivateIds.has(privateId) &&
158
+ !typedSession.connected &&
159
+ (now - typedSession.created) > SESSION_EXPIRY_TIME) {
142
160
  // Delete expired session
143
161
  await this.deleteSession(privateId);
144
162
  expiredPublicIds.add(typedSession.publicId);
@@ -159,7 +177,7 @@ export class Server implements Party.Server {
159
177
  }
160
178
  }
161
179
  }
162
-
180
+
163
181
  } catch (error) {
164
182
  console.error('Error in garbage collector:', error);
165
183
  }
@@ -229,11 +247,12 @@ export class Server implements Party.Server {
229
247
  return;
230
248
  }
231
249
  const packet = buildObject(values, instance.$memoryAll);
232
- this.room.broadcast(
233
- JSON.stringify({
250
+ this.broadcast(
251
+ {
234
252
  type: "sync",
235
253
  value: packet,
236
- })
254
+ },
255
+ instance
237
256
  );
238
257
  values.clear();
239
258
  }
@@ -247,7 +266,7 @@ export class Server implements Party.Server {
247
266
  for (let [path, value] of values) {
248
267
  const _instance =
249
268
  path == "." ? instance : getByPath(instance, path);
250
- const itemValue = createStatesSnapshot(_instance);
269
+ const itemValue = createStatesSnapshot(_instance);
251
270
  if (value == DELETE_TOKEN) {
252
271
  await this.room.storage.delete(path);
253
272
  } else {
@@ -327,17 +346,17 @@ export class Server implements Party.Server {
327
346
  return meta?.get("users")
328
347
  }
329
348
 
330
- private async getSession(privateId: string): Promise<{publicId: string, state?: any, created?: number, connected?: boolean} | null> {
349
+ private async getSession(privateId: string): Promise<{ publicId: string, state?: any, created?: number, connected?: boolean } | null> {
331
350
  if (!privateId) return null;
332
351
  try {
333
352
  const session = await this.room.storage.get(`session:${privateId}`);
334
- return session as {publicId: string, state?: any, created: number, connected: boolean} | null;
353
+ return session as { publicId: string, state?: any, created: number, connected: boolean } | null;
335
354
  } catch (e) {
336
355
  return null;
337
356
  }
338
357
  }
339
358
 
340
- private async saveSession(privateId: string, data: {publicId: string, state?: any, created?: number, connected?: boolean}) {
359
+ private async saveSession(privateId: string, data: { publicId: string, state?: any, created?: number, connected?: boolean }) {
341
360
  const sessionData = {
342
361
  ...data,
343
362
  created: data.created || Date.now(),
@@ -357,23 +376,7 @@ export class Server implements Party.Server {
357
376
  await this.room.storage.delete(`session:${privateId}`);
358
377
  }
359
378
 
360
- /**
361
- * @method onConnect
362
- * @async
363
- * @param {Party.Connection} conn - The connection object for the new user.
364
- * @param {Party.ConnectionContext} ctx - The context of the connection.
365
- * @description Handles a new user connection, creates a user object, and sends initial sync data.
366
- * @returns {Promise<void>}
367
- *
368
- * @example
369
- * ```typescript
370
- * server.onConnect = async (conn, ctx) => {
371
- * await server.onConnect(conn, ctx);
372
- * console.log("New user connected:", conn.id);
373
- * };
374
- * ```
375
- */
376
- async onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {
379
+ async onConnectClient(conn: Party.Connection, ctx: Party.ConnectionContext) {
377
380
  const subRoom = await this.getSubRoom({
378
381
  getMemoryAll: true,
379
382
  })
@@ -397,7 +400,7 @@ export class Server implements Party.Server {
397
400
  }
398
401
 
399
402
  // Check for existing session
400
- const existingSession = await this.getSession(conn.id)
403
+ const existingSession = await this.getSession(conn.id)
401
404
 
402
405
  // Generate IDs
403
406
  const publicId = existingSession?.publicId || generateShortUUID();
@@ -408,7 +411,7 @@ export class Server implements Party.Server {
408
411
 
409
412
  if (signal) {
410
413
  const { classType } = signal.options;
411
-
414
+
412
415
  // Restore state if exists
413
416
  if (!existingSession?.publicId) {
414
417
  user = isClass(classType) ? new classType() : classType(conn, ctx);
@@ -416,7 +419,7 @@ export class Server implements Party.Server {
416
419
  const snapshot = createStatesSnapshot(user);
417
420
  this.room.storage.put(`${usersPropName}.${publicId}`, snapshot);
418
421
  }
419
-
422
+
420
423
  // Only store new session if it doesn't exist
421
424
  if (!existingSession) {
422
425
  await this.saveSession(conn.id, {
@@ -430,40 +433,82 @@ export class Server implements Party.Server {
430
433
 
431
434
  // Call the room's onJoin method if it exists
432
435
  await awaitReturn(subRoom["onJoin"]?.(user, conn, ctx));
433
-
436
+
434
437
  // Store both IDs in connection state
435
- conn.setState({ publicId });
438
+ conn.setState({
439
+ ...conn.state,
440
+ publicId
441
+ });
436
442
 
437
443
  // Send initial sync data with both IDs to the new connection
438
- conn.send(
439
- JSON.stringify({
440
- type: "sync",
441
- value: {
442
- pId: publicId,
443
- ...subRoom.$memoryAll,
444
- },
445
- })
446
- );
444
+ this.send(conn, {
445
+ type: "sync",
446
+ value: {
447
+ pId: publicId,
448
+ ...subRoom.$memoryAll,
449
+ },
450
+ }, subRoom)
447
451
  }
448
452
 
449
453
  /**
450
- * @method onMessage
454
+ * @method onConnect
451
455
  * @async
452
- * @param {string} message - The message received from a user.
453
- * @param {Party.Connection} sender - The connection object of the sender.
454
- * @description Processes incoming messages and triggers corresponding actions in the sub-room.
456
+ * @param {Party.Connection} conn - The connection object for the new user.
457
+ * @param {Party.ConnectionContext} ctx - The context of the connection.
458
+ * @description Handles a new user connection, creates a user object, and sends initial sync data.
455
459
  * @returns {Promise<void>}
456
460
  *
457
461
  * @example
458
462
  * ```typescript
459
- * server.onMessage = async (message, sender) => {
460
- * await server.onMessage(message, sender);
461
- * console.log("Message processed from:", sender.id);
463
+ * server.onConnect = async (conn, ctx) => {
464
+ * await server.onConnect(conn, ctx);
465
+ * console.log("New user connected:", conn.id);
462
466
  * };
463
467
  * ```
464
468
  */
469
+ async onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {
470
+ if (ctx.request?.headers.has('x-shard-id')) {
471
+ this.onConnectShard(conn, ctx);
472
+ }
473
+ else {
474
+ await this.onConnectClient(conn, ctx);
475
+ }
476
+ }
465
477
 
478
+ /**
479
+ * @method onConnectShard
480
+ * @private
481
+ * @param {Party.Connection} conn - The connection object for the new shard.
482
+ * @param {Party.ConnectionContext} ctx - The context of the shard connection.
483
+ * @description Handles a new shard connection, setting up the necessary state.
484
+ * @returns {void}
485
+ */
486
+ onConnectShard(conn: Party.Connection, ctx: Party.ConnectionContext) {
487
+ // Set shard metadata in connection state
488
+ const shardId = ctx.request?.headers.get('x-shard-id') || 'unknown-shard';
489
+ conn.setState({
490
+ shard: true,
491
+ shardId,
492
+ clients: new Map() // Track clients connected through this shard
493
+ });
494
+ }
495
+
496
+ /**
497
+ * @method onMessage
498
+ * @async
499
+ * @param {string} message - The message received from a user or shard.
500
+ * @param {Party.Connection} sender - The connection object of the sender.
501
+ * @description Processes incoming messages, handling differently based on if sender is shard or client.
502
+ * @returns {Promise<void>}
503
+ */
466
504
  async onMessage(message: string, sender: Party.Connection) {
505
+ // Check if message is from a shard
506
+ if (sender.state && (sender.state as any).shard) {
507
+ await this.handleShardMessage(message, sender);
508
+ return;
509
+ }
510
+
511
+ // Regular client message handling
467
512
  let json
468
513
  try {
469
514
  json = JSON.parse(message)
@@ -471,16 +516,19 @@ export class Server implements Party.Server {
471
516
  catch (e) {
472
517
  return;
473
518
  }
474
- // Validate incoming messages
519
+
520
+ // Validate incoming messages
475
521
  const result = Message.safeParse(json);
476
522
  if (!result.success) {
477
523
  return;
478
524
  }
525
+
479
526
  const subRoom = await this.getSubRoom()
527
+
480
528
  // Check room guards
481
529
  const roomGuards = subRoom.constructor['_roomGuards'] || [];
482
530
  for (const guard of roomGuards) {
483
- const isAuthorized = await guard(sender, result.data.value);
531
+ const isAuthorized = await guard(sender, result.data.value, this.room);
484
532
  if (!isAuthorized) {
485
533
  return;
486
534
  }
@@ -520,6 +568,214 @@ export class Server implements Party.Server {
520
568
  }
521
569
  }
522
570
 
571
+ /**
572
+ * @method handleShardMessage
573
+ * @private
574
+ * @async
575
+ * @param {string} message - The message received from a shard.
576
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
577
+ * @description Processes messages from shards, extracting client information.
578
+ * @returns {Promise<void>}
579
+ */
580
+ private async handleShardMessage(message: string, shardConnection: Party.Connection) {
581
+ let parsedMessage;
582
+ try {
583
+ parsedMessage = JSON.parse(message);
584
+ } catch (e) {
585
+ console.error("Error parsing shard message:", e);
586
+ return;
587
+ }
588
+
589
+ const shardState = shardConnection.state as any;
590
+ const clients = shardState.clients;
591
+
592
+ switch (parsedMessage.type) {
593
+ case 'shard.clientConnected':
594
+ // Handle new client connection through shard
595
+ await this.handleShardClientConnect(parsedMessage, shardConnection);
596
+ break;
597
+
598
+ case 'shard.clientMessage':
599
+ // Handle message from a client through shard
600
+ await this.handleShardClientMessage(parsedMessage, shardConnection);
601
+ break;
602
+
603
+ case 'shard.clientDisconnected':
604
+ // Handle client disconnection through shard
605
+ await this.handleShardClientDisconnect(parsedMessage, shardConnection);
606
+ break;
607
+
608
+ default:
609
+ console.warn(`Unknown shard message type: ${parsedMessage.type}`);
610
+ }
611
+ }
612
+
613
+ /**
614
+ * @method handleShardClientConnect
615
+ * @private
616
+ * @async
617
+ * @param {Object} message - The client connection message from a shard.
618
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
619
+ * @description Handles a new client connection via a shard.
620
+ * @returns {Promise<void>}
621
+ */
622
+ private async handleShardClientConnect(message: any, shardConnection: Party.Connection) {
623
+ const { privateId, connectionInfo } = message;
624
+ const shardState = shardConnection.state as any;
625
+
626
+ // Create a virtual connection context for the client
627
+ const virtualContext: Party.ConnectionContext = {
628
+ request: {
629
+ headers: new Headers({
630
+ 'x-forwarded-for': connectionInfo.ip,
631
+ 'user-agent': connectionInfo.userAgent,
632
+ // Add other headers as needed
633
+ }),
634
+ method: 'GET',
635
+ url: ''
636
+ } as unknown as Party.Request
637
+ };
638
+
639
+ // Create a virtual connection for the client
640
+ const virtualConnection: Partial<Party.Connection> = {
641
+ id: privateId,
642
+ send: (data: string) => {
643
+ // Forward to the actual client through the shard
644
+ shardConnection.send(JSON.stringify({
645
+ targetClientId: privateId,
646
+ data
647
+ }));
648
+ },
649
+ state: {},
650
+ setState: (state: unknown) => {
651
+ // Store client state in the shard's client map
652
+ const clients = shardState.clients;
653
+ const currentState = clients.get(privateId) || {};
654
+ const mergedState = Object.assign({}, currentState, state as object);
655
+ clients.set(privateId, mergedState);
656
+
657
+ // Update our virtual connection's state reference
658
+ virtualConnection.state = clients.get(privateId);
659
+ return virtualConnection.state as any;
660
+ },
661
+ close: () => {
662
+ // Send close command to the shard
663
+ shardConnection.send(JSON.stringify({
664
+ type: 'shard.closeClient',
665
+ privateId
666
+ }));
667
+
668
+ // Clean up virtual connection
669
+ if (shardState.clients) {
670
+ shardState.clients.delete(privateId);
671
+ }
672
+ }
673
+ };
674
+
675
+ // Initialize the client's state in the shard state
676
+ if (!shardState.clients.has(privateId)) {
677
+ shardState.clients.set(privateId, {});
678
+ }
679
+
680
+ // Now handle this virtual connection as a regular client connection
681
+ await this.onConnectClient(virtualConnection as Party.Connection, virtualContext);
682
+ }
683
+
684
+ /**
685
+ * @method handleShardClientMessage
686
+ * @private
687
+ * @async
688
+ * @param {Object} message - The client message from a shard.
689
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
690
+ * @description Handles a message from a client via a shard.
691
+ * @returns {Promise<void>}
692
+ */
693
+ private async handleShardClientMessage(message: any, shardConnection: Party.Connection) {
694
+ const { privateId, publicId, payload } = message;
695
+ const shardState = shardConnection.state as any;
696
+ const clients = shardState.clients;
697
+
698
+ // Get or create virtual connection for this client
699
+ if (!clients.has(privateId)) {
700
+ console.warn(`Received message from unknown client ${privateId}, creating virtual connection`);
701
+ clients.set(privateId, { publicId });
702
+ }
703
+
704
+ // Create a virtual connection for the client
705
+ const virtualConnection: Partial<Party.Connection> = {
706
+ id: privateId,
707
+ send: (data: string) => {
708
+ // Forward to the actual client through the shard
709
+ shardConnection.send(JSON.stringify({
710
+ targetClientId: privateId,
711
+ data
712
+ }));
713
+ },
714
+ state: clients.get(privateId),
715
+ setState: (state: unknown) => {
716
+ const currentState = clients.get(privateId) || {};
717
+ const mergedState = Object.assign({}, currentState, state as object);
718
+ clients.set(privateId, mergedState);
719
+ virtualConnection.state = clients.get(privateId);
720
+ return virtualConnection.state as any;
721
+ },
722
+ close: () => {
723
+ shardConnection.send(JSON.stringify({
724
+ type: 'shard.closeClient',
725
+ privateId
726
+ }));
727
+
728
+ if (shardState.clients) {
729
+ shardState.clients.delete(privateId);
730
+ }
731
+ }
732
+ };
733
+
734
+ // Process the payload using the regular message handler
735
+ const payloadString = typeof payload === 'string' ? payload : JSON.stringify(payload);
736
+ await this.onMessage(payloadString, virtualConnection as Party.Connection);
737
+ }
738
+
739
+ /**
740
+ * @method handleShardClientDisconnect
741
+ * @private
742
+ * @async
743
+ * @param {Object} message - The client disconnection message from a shard.
744
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
745
+ * @description Handles a client disconnection via a shard.
746
+ * @returns {Promise<void>}
747
+ */
748
+ private async handleShardClientDisconnect(message: any, shardConnection: Party.Connection) {
749
+ const { privateId, publicId } = message;
750
+ const shardState = shardConnection.state as any;
751
+ const clients = shardState.clients;
752
+
753
+ // Get client state
754
+ const clientState = clients.get(privateId);
755
+ if (!clientState) {
756
+ console.warn(`Disconnection for unknown client ${privateId}`);
757
+ return;
758
+ }
759
+
760
+ // Create a virtual connection for the client one last time
761
+ const virtualConnection: Partial<Party.Connection> = {
762
+ id: privateId,
763
+ send: () => { }, // No-op since client is disconnecting
764
+ state: clientState,
765
+ setState: () => {
766
+ // No-op since client is disconnecting
767
+ return {} as any;
768
+ },
769
+ close: () => { }
770
+ };
771
+
772
+ // Handle disconnection with the regular onClose handler
773
+ await this.onClose(virtualConnection as Party.Connection);
774
+
775
+ // Clean up
776
+ clients.delete(privateId);
777
+ }
778
+
523
779
  /**
524
780
  * @method onClose
525
781
  * @async
@@ -560,12 +816,10 @@ export class Server implements Party.Server {
560
816
  await this.updateSessionConnection(privateId, false);
561
817
 
562
818
  // Broadcast user disconnection
563
- this.room.broadcast(
564
- JSON.stringify({
565
- type: "user_disconnected",
566
- value: { publicId }
567
- })
568
- );
819
+ this.broadcast({
820
+ type: "user_disconnected",
821
+ value: { publicId }
822
+ }, subRoom);
569
823
  }
570
824
 
571
825
  async onAlarm() {
@@ -578,26 +832,288 @@ export class Server implements Party.Server {
578
832
  await awaitReturn(subRoom["onError"]?.(connection, error));
579
833
  }
580
834
 
835
+ /**
836
+ * @method onRequest
837
+ * @async
838
+ * @param {Party.Request} req - The HTTP request to handle
839
+ * @description Handles HTTP requests, either directly from clients or forwarded by shards
840
+ * @returns {Promise<Response>} The response to return to the client
841
+ */
581
842
  async onRequest(req: Party.Request) {
582
- const subRoom = await this.getSubRoom()
843
+ // Check if the request is coming from a shard
844
+ const isFromShard = req.headers.has('x-forwarded-by-shard');
845
+ const shardId = req.headers.get('x-shard-id');
846
+
847
+ if (isFromShard) {
848
+ return this.handleShardRequest(req, shardId);
849
+ }
850
+
851
+ // Handle regular client request
852
+ return this.handleDirectRequest(req);
853
+ }
854
+
855
+ /**
856
+ * @method handleDirectRequest
857
+ * @private
858
+ * @async
859
+ * @param {Party.Request} req - The HTTP request received directly from a client
860
+ * @description Processes requests received directly from clients
861
+ * @returns {Promise<Response>} The response to return to the client
862
+ */
863
+ private async handleDirectRequest(req: Party.Request): Promise<Response> {
864
+ const subRoom = await this.getSubRoom();
583
865
  const res = (body: any, status: number) => {
584
866
  return new Response(JSON.stringify(body), { status });
585
- }
867
+ };
868
+
586
869
  if (!subRoom) {
587
870
  return res({
588
871
  error: "Not found"
589
872
  }, 404);
590
873
  }
591
874
 
592
- const response = await awaitReturn(subRoom["onRequest"]?.(req, this.room));
593
- if (!response) {
875
+ // First try to match using the registered @Request handlers
876
+ const response = await this.tryMatchRequestHandler(req, subRoom);
877
+ if (response) {
878
+ return response;
879
+ }
880
+
881
+ // Fall back to the legacy onRequest method if no handler matched
882
+ const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(req, this.room));
883
+ if (!legacyResponse) {
594
884
  return res({
595
885
  error: "Not found"
596
886
  }, 404);
597
887
  }
598
- if (response instanceof Response) {
599
- return response;
888
+ if (legacyResponse instanceof Response) {
889
+ return legacyResponse;
890
+ }
891
+ return res(legacyResponse, 200);
892
+ }
893
+
894
+ /**
895
+ * @method tryMatchRequestHandler
896
+ * @private
897
+ * @async
898
+ * @param {Party.Request} req - The HTTP request to handle
899
+ * @param {Object} subRoom - The room instance
900
+ * @description Attempts to match the request to a registered @Request handler
901
+ * @returns {Promise<Response | null>} The response or null if no handler matched
902
+ */
903
+ private async tryMatchRequestHandler(req: Party.Request, subRoom: any): Promise<Response | null> {
904
+ const requestHandlers = subRoom.constructor["_requestMetadata"];
905
+ if (!requestHandlers) {
906
+ return null;
907
+ }
908
+
909
+ const url = new URL(req.url);
910
+ const method = req.method;
911
+ let pathname = url.pathname;
912
+
913
+ pathname = '/' + pathname.split('/').slice(4).join('/');
914
+
915
+ // Check each registered handler
916
+ for (const [routeKey, handler] of requestHandlers.entries()) {
917
+ const firstColonIndex = routeKey.indexOf(':');
918
+ const handlerMethod = routeKey.substring(0, firstColonIndex);
919
+ const handlerPath = routeKey.substring(firstColonIndex + 1);
920
+
921
+ // Check if method matches
922
+ if (handlerMethod !== method) {
923
+ continue;
924
+ }
925
+
926
+ // Simple path matching (could be enhanced with path params)
927
+ if (this.pathMatches(pathname, handlerPath)) {
928
+ // Extract path params if any
929
+ const params = this.extractPathParams(pathname, handlerPath);
930
+ // Check request guards if they exist
931
+ const guards = subRoom.constructor['_actionGuards']?.get(handler.key) || [];
932
+ for (const guard of guards) {
933
+ const isAuthorized = await guard(null, req, this.room);
934
+ if (isAuthorized instanceof Response) {
935
+ return isAuthorized;
936
+ }
937
+ if (!isAuthorized) {
938
+ return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 403 });
939
+ }
940
+ }
941
+
942
+ // Validate request body if needed
943
+ let bodyData = null;
944
+ if (handler.bodyValidation && ['POST', 'PUT', 'PATCH'].includes(method)) {
945
+ try {
946
+ const contentType = req.headers.get('content-type') || '';
947
+ if (contentType.includes('application/json')) {
948
+ const body = await req.json();
949
+ const validation = handler.bodyValidation.safeParse(body);
950
+ if (!validation.success) {
951
+ return new Response(
952
+ JSON.stringify({ error: "Invalid request body", details: validation.error }),
953
+ { status: 400 }
954
+ );
955
+ }
956
+ bodyData = validation.data;
957
+ }
958
+ } catch (error) {
959
+ return new Response(
960
+ JSON.stringify({ error: "Failed to parse request body" }),
961
+ { status: 400 }
962
+ );
963
+ }
964
+ }
965
+
966
+ // Execute the handler method
967
+ try {
968
+ const result = await awaitReturn(
969
+ subRoom[handler.key](req, bodyData, params, this.room)
970
+ );
971
+
972
+ if (result instanceof Response) {
973
+ return result;
974
+ }
975
+
976
+ return new Response(
977
+ typeof result === 'string' ? result : JSON.stringify(result),
978
+ {
979
+ status: 200,
980
+ headers: { 'Content-Type': typeof result === 'string' ? 'text/plain' : 'application/json' }
981
+ }
982
+ );
983
+ } catch (error) {
984
+ console.error('Error executing request handler:', error);
985
+ return new Response(
986
+ JSON.stringify({ error: "Internal server error" }),
987
+ { status: 500 }
988
+ );
989
+ }
990
+ }
600
991
  }
601
- return res(response, 200);
992
+
993
+ return null;
994
+ }
995
+
996
+ /**
997
+ * @method pathMatches
998
+ * @private
999
+ * @param {string} requestPath - The path from the request
1000
+ * @param {string} handlerPath - The path pattern from the handler
1001
+ * @description Checks if a request path matches a handler path pattern
1002
+ * @returns {boolean} True if the paths match
1003
+ */
1004
+ private pathMatches(requestPath: string, handlerPath: string): boolean {
1005
+ // Convert handler path pattern to regex
1006
+ // Replace :param with named capture groups
1007
+ const pathRegexString = handlerPath
1008
+ .replace(/\//g, '\\/') // Escape slashes
1009
+ .replace(/:([^\/]+)/g, '([^/]+)'); // Convert :params to capture groups
1010
+
1011
+ const pathRegex = new RegExp(`^${pathRegexString}$`);
1012
+ return pathRegex.test(requestPath);
1013
+ }
1014
+
1015
+ /**
1016
+ * @method extractPathParams
1017
+ * @private
1018
+ * @param {string} requestPath - The path from the request
1019
+ * @param {string} handlerPath - The path pattern from the handler
1020
+ * @description Extracts path parameters from the request path based on the handler pattern
1021
+ * @returns {Object} An object containing the path parameters
1022
+ */
1023
+ private extractPathParams(requestPath: string, handlerPath: string): Record<string, string> {
1024
+ const params: Record<string, string> = {};
1025
+
1026
+ // Extract parameter names from handler path
1027
+ const paramNames: string[] = [];
1028
+ handlerPath.split('/').forEach(segment => {
1029
+ if (segment.startsWith(':')) {
1030
+ paramNames.push(segment.substring(1));
1031
+ }
1032
+ });
1033
+
1034
+ // Extract parameter values from request path
1035
+ const pathRegexString = handlerPath
1036
+ .replace(/\//g, '\\/') // Escape slashes
1037
+ .replace(/:([^\/]+)/g, '([^/]+)'); // Convert :params to capture groups
1038
+
1039
+ const pathRegex = new RegExp(`^${pathRegexString}$`);
1040
+ const matches = requestPath.match(pathRegex);
1041
+
1042
+ if (matches && matches.length > 1) {
1043
+ // Skip the first match (the full string)
1044
+ for (let i = 0; i < paramNames.length; i++) {
1045
+ params[paramNames[i]] = matches[i + 1];
1046
+ }
1047
+ }
1048
+
1049
+ return params;
1050
+ }
1051
+
1052
+ /**
1053
+ * @method handleShardRequest
1054
+ * @private
1055
+ * @async
1056
+ * @param {Party.Request} req - The HTTP request forwarded by a shard
1057
+ * @param {string | null} shardId - The ID of the shard that forwarded the request
1058
+ * @description Processes requests forwarded by shards, preserving client context
1059
+ * @returns {Promise<Response>} The response to return to the shard (which will forward it to the client)
1060
+ */
1061
+ private async handleShardRequest(req: Party.Request, shardId: string | null): Promise<Response> {
1062
+ const subRoom = await this.getSubRoom();
1063
+
1064
+ if (!subRoom) {
1065
+ return new Response(JSON.stringify({ error: "Not found" }), { status: 404 });
1066
+ }
1067
+
1068
+ // Create a context that preserves original client information
1069
+ const originalClientIp = req.headers.get('x-original-client-ip');
1070
+ const enhancedReq = this.createEnhancedRequest(req, originalClientIp);
1071
+
1072
+ try {
1073
+ // First try to match using the registered @Request handlers
1074
+ const response = await this.tryMatchRequestHandler(enhancedReq, subRoom);
1075
+ if (response) {
1076
+ return response;
1077
+ }
1078
+
1079
+ // Fall back to the legacy onRequest handler
1080
+ const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(enhancedReq, this.room));
1081
+
1082
+ if (!legacyResponse) {
1083
+ return new Response(JSON.stringify({ error: "Not found" }), { status: 404 });
1084
+ }
1085
+
1086
+ if (legacyResponse instanceof Response) {
1087
+ return legacyResponse;
1088
+ }
1089
+
1090
+ return new Response(JSON.stringify(legacyResponse), { status: 200 });
1091
+ } catch (error) {
1092
+ console.error(`Error processing request from shard ${shardId}:`, error);
1093
+ return new Response(JSON.stringify({ error: "Internal server error" }), { status: 500 });
1094
+ }
1095
+ }
1096
+
1097
+ /**
1098
+ * @method createEnhancedRequest
1099
+ * @private
1100
+ * @param {Party.Request} originalReq - The original request received from the shard
1101
+ * @param {string | null} originalClientIp - The original client IP, if available
1102
+ * @description Creates an enhanced request object that preserves the original client context
1103
+ * @returns {Party.Request} The enhanced request object
1104
+ */
1105
+ private createEnhancedRequest(originalReq: Party.Request, originalClientIp: string | null): Party.Request {
1106
+ // Clone the original request to avoid mutating it
1107
+ const clonedReq = originalReq.clone();
1108
+
1109
+ // Add a custom property to the request to indicate it came via a shard
1110
+ (clonedReq as any).viaShard = true;
1111
+
1112
+ // If we have the original client IP, we can use it for things like rate limiting or geolocation
1113
+ if (originalClientIp) {
1114
+ (clonedReq as any).originalClientIp = originalClientIp;
1115
+ }
1116
+
1117
+ return clonedReq;
602
1118
  }
603
1119
  }