@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.js CHANGED
@@ -14,6 +14,23 @@ function Action(name, bodyValidation) {
14
14
  };
15
15
  }
16
16
  __name(Action, "Action");
17
+ function Request2(options, bodyValidation) {
18
+ return function(target, propertyKey) {
19
+ if (!target.constructor._requestMetadata) {
20
+ target.constructor._requestMetadata = /* @__PURE__ */ new Map();
21
+ }
22
+ const path = options.path.startsWith("/") ? options.path : `/${options.path}`;
23
+ const method = options.method || "GET";
24
+ const routeKey = `${method}:${path}`;
25
+ target.constructor._requestMetadata.set(routeKey, {
26
+ key: propertyKey,
27
+ path,
28
+ method,
29
+ bodyValidation
30
+ });
31
+ };
32
+ }
33
+ __name(Request2, "Request");
17
34
  function Room(options) {
18
35
  return function(target) {
19
36
  target.path = options.path;
@@ -108,6 +125,35 @@ var MockPartyClient = class {
108
125
  return this.server.onMessage(JSON.stringify(data), this.conn);
109
126
  }
110
127
  };
128
+ var MockLobby = class MockLobby2 {
129
+ static {
130
+ __name(this, "MockLobby");
131
+ }
132
+ server;
133
+ constructor(server) {
134
+ this.server = server;
135
+ }
136
+ socket() {
137
+ return new MockPartyClient(this.server);
138
+ }
139
+ };
140
+ var MockContext = class MockContext2 {
141
+ static {
142
+ __name(this, "MockContext");
143
+ }
144
+ room;
145
+ parties;
146
+ constructor(room, options = {}) {
147
+ this.room = room;
148
+ this.parties = {
149
+ main: /* @__PURE__ */ new Map()
150
+ };
151
+ const parties = options.parties || {};
152
+ for (let lobbyId in parties) {
153
+ this.parties.main.set(lobbyId, new MockLobby(parties[lobbyId](room)));
154
+ }
155
+ }
156
+ };
111
157
  var MockPartyRoom = class MockPartyRoom2 {
112
158
  static {
113
159
  __name(this, "MockPartyRoom");
@@ -115,18 +161,30 @@ var MockPartyRoom = class MockPartyRoom2 {
115
161
  id;
116
162
  clients;
117
163
  storage;
164
+ context;
118
165
  env;
119
- constructor(id) {
120
- this.id = id;
166
+ constructor(id2, options = {}) {
167
+ this.id = id2;
121
168
  this.clients = /* @__PURE__ */ new Map();
122
169
  this.storage = new Storage();
123
170
  this.env = {};
124
- this.id = id || generateShortUUID();
171
+ this.id = id2 || generateShortUUID();
172
+ this.context = new MockContext(this, {
173
+ parties: options.parties || {}
174
+ });
175
+ this.env = options.env || {};
125
176
  }
126
177
  async connection(server) {
127
178
  const socket = new MockPartyClient(server);
179
+ const url = new URL("http://localhost");
180
+ const request2 = new Request(url.toString(), {
181
+ method: "GET",
182
+ headers: {
183
+ "Content-Type": "application/json"
184
+ }
185
+ });
128
186
  await server.onConnect(socket.conn, {
129
- request: {}
187
+ request: request2
130
188
  });
131
189
  this.clients.set(socket.id, socket);
132
190
  return socket;
@@ -136,11 +194,11 @@ var MockPartyRoom = class MockPartyRoom2 {
136
194
  client._trigger("message", data);
137
195
  });
138
196
  }
139
- getConnection(id) {
140
- return this.clients.get(id);
197
+ getConnection(id2) {
198
+ return this.clients.get(id2);
141
199
  }
142
200
  getConnections() {
143
- return this.clients;
201
+ return Array.from(this.clients.values()).map((client) => client.conn);
144
202
  }
145
203
  clear() {
146
204
  this.clients.clear();
@@ -258,6 +316,15 @@ function buildObject(valuesMap, allMemory) {
258
316
  return memoryObj;
259
317
  }
260
318
  __name(buildObject, "buildObject");
319
+ function response(status, body) {
320
+ return new Response(JSON.stringify(body), {
321
+ status,
322
+ headers: {
323
+ "Content-Type": "application/json"
324
+ }
325
+ });
326
+ }
327
+ __name(response, "response");
261
328
 
262
329
  // src/server.ts
263
330
  var Message = z.object({
@@ -302,6 +369,22 @@ var Server = class {
302
369
  get roomStorage() {
303
370
  return this.room.storage;
304
371
  }
372
+ async send(conn, obj, subRoom) {
373
+ obj = structuredClone(obj);
374
+ if (subRoom.interceptorPacket) {
375
+ const signal2 = this.getUsersProperty(subRoom);
376
+ const { publicId } = conn.state;
377
+ const user = signal2?.()[publicId];
378
+ obj = await awaitReturn(subRoom["interceptorPacket"]?.(user, obj, conn));
379
+ if (obj === null) return;
380
+ }
381
+ conn.send(JSON.stringify(obj));
382
+ }
383
+ broadcast(obj, subRoom) {
384
+ for (let conn of this.room.getConnections()) {
385
+ this.send(conn, obj, subRoom);
386
+ }
387
+ }
305
388
  /**
306
389
  * @method onStart
307
390
  * @async
@@ -415,10 +498,10 @@ var Server = class {
415
498
  return;
416
499
  }
417
500
  const packet = buildObject(values, instance.$memoryAll);
418
- this.room.broadcast(JSON.stringify({
501
+ this.broadcast({
419
502
  type: "sync",
420
503
  value: packet
421
- }));
504
+ }, instance);
422
505
  values.clear();
423
506
  }, "syncCb");
424
507
  const persistCb = /* @__PURE__ */ __name(async (values) => {
@@ -526,23 +609,7 @@ var Server = class {
526
609
  async deleteSession(privateId) {
527
610
  await this.room.storage.delete(`session:${privateId}`);
528
611
  }
529
- /**
530
- * @method onConnect
531
- * @async
532
- * @param {Party.Connection} conn - The connection object for the new user.
533
- * @param {Party.ConnectionContext} ctx - The context of the connection.
534
- * @description Handles a new user connection, creates a user object, and sends initial sync data.
535
- * @returns {Promise<void>}
536
- *
537
- * @example
538
- * ```typescript
539
- * server.onConnect = async (conn, ctx) => {
540
- * await server.onConnect(conn, ctx);
541
- * console.log("New user connected:", conn.id);
542
- * };
543
- * ```
544
- */
545
- async onConnect(conn, ctx) {
612
+ async onConnectClient(conn, ctx) {
546
613
  const subRoom = await this.getSubRoom({
547
614
  getMemoryAll: true
548
615
  });
@@ -565,13 +632,13 @@ var Server = class {
565
632
  const existingSession = await this.getSession(conn.id);
566
633
  const publicId = existingSession?.publicId || generateShortUUID2();
567
634
  let user = null;
568
- const signal = this.getUsersProperty(subRoom);
635
+ const signal2 = this.getUsersProperty(subRoom);
569
636
  const usersPropName = this.getUsersPropName(subRoom);
570
- if (signal) {
571
- const { classType } = signal.options;
637
+ if (signal2) {
638
+ const { classType } = signal2.options;
572
639
  if (!existingSession?.publicId) {
573
640
  user = isClass(classType) ? new classType() : classType(conn, ctx);
574
- signal()[publicId] = user;
641
+ signal2()[publicId] = user;
575
642
  const snapshot = createStatesSnapshot(user);
576
643
  this.room.storage.put(`${usersPropName}.${publicId}`, snapshot);
577
644
  }
@@ -585,33 +652,70 @@ var Server = class {
585
652
  }
586
653
  await awaitReturn(subRoom["onJoin"]?.(user, conn, ctx));
587
654
  conn.setState({
655
+ ...conn.state,
588
656
  publicId
589
657
  });
590
- conn.send(JSON.stringify({
658
+ this.send(conn, {
591
659
  type: "sync",
592
660
  value: {
593
661
  pId: publicId,
594
662
  ...subRoom.$memoryAll
595
663
  }
596
- }));
664
+ }, subRoom);
597
665
  }
598
666
  /**
599
- * @method onMessage
667
+ * @method onConnect
600
668
  * @async
601
- * @param {string} message - The message received from a user.
602
- * @param {Party.Connection} sender - The connection object of the sender.
603
- * @description Processes incoming messages and triggers corresponding actions in the sub-room.
669
+ * @param {Party.Connection} conn - The connection object for the new user.
670
+ * @param {Party.ConnectionContext} ctx - The context of the connection.
671
+ * @description Handles a new user connection, creates a user object, and sends initial sync data.
604
672
  * @returns {Promise<void>}
605
673
  *
606
674
  * @example
607
675
  * ```typescript
608
- * server.onMessage = async (message, sender) => {
609
- * await server.onMessage(message, sender);
610
- * console.log("Message processed from:", sender.id);
676
+ * server.onConnect = async (conn, ctx) => {
677
+ * await server.onConnect(conn, ctx);
678
+ * console.log("New user connected:", conn.id);
611
679
  * };
612
680
  * ```
613
681
  */
682
+ async onConnect(conn, ctx) {
683
+ if (ctx.request?.headers.has("x-shard-id")) {
684
+ this.onConnectShard(conn, ctx);
685
+ } else {
686
+ await this.onConnectClient(conn, ctx);
687
+ }
688
+ }
689
+ /**
690
+ * @method onConnectShard
691
+ * @private
692
+ * @param {Party.Connection} conn - The connection object for the new shard.
693
+ * @param {Party.ConnectionContext} ctx - The context of the shard connection.
694
+ * @description Handles a new shard connection, setting up the necessary state.
695
+ * @returns {void}
696
+ */
697
+ onConnectShard(conn, ctx) {
698
+ const shardId = ctx.request?.headers.get("x-shard-id") || "unknown-shard";
699
+ conn.setState({
700
+ shard: true,
701
+ shardId,
702
+ clients: /* @__PURE__ */ new Map()
703
+ // Track clients connected through this shard
704
+ });
705
+ }
706
+ /**
707
+ * @method onMessage
708
+ * @async
709
+ * @param {string} message - The message received from a user or shard.
710
+ * @param {Party.Connection} sender - The connection object of the sender.
711
+ * @description Processes incoming messages, handling differently based on if sender is shard or client.
712
+ * @returns {Promise<void>}
713
+ */
614
714
  async onMessage(message, sender) {
715
+ if (sender.state && sender.state.shard) {
716
+ await this.handleShardMessage(message, sender);
717
+ return;
718
+ }
615
719
  let json;
616
720
  try {
617
721
  json = JSON.parse(message);
@@ -625,16 +729,16 @@ var Server = class {
625
729
  const subRoom = await this.getSubRoom();
626
730
  const roomGuards = subRoom.constructor["_roomGuards"] || [];
627
731
  for (const guard of roomGuards) {
628
- const isAuthorized = await guard(sender, result.data.value);
732
+ const isAuthorized = await guard(sender, result.data.value, this.room);
629
733
  if (!isAuthorized) {
630
734
  return;
631
735
  }
632
736
  }
633
737
  const actions = subRoom.constructor["_actionMetadata"];
634
738
  if (actions) {
635
- const signal = this.getUsersProperty(subRoom);
739
+ const signal2 = this.getUsersProperty(subRoom);
636
740
  const { publicId } = sender.state;
637
- const user = signal?.()[publicId];
741
+ const user = signal2?.()[publicId];
638
742
  const actionName = actions.get(result.data.action);
639
743
  if (actionName) {
640
744
  const guards = subRoom.constructor["_actionGuards"]?.get(actionName.key) || [];
@@ -655,6 +759,173 @@ var Server = class {
655
759
  }
656
760
  }
657
761
  /**
762
+ * @method handleShardMessage
763
+ * @private
764
+ * @async
765
+ * @param {string} message - The message received from a shard.
766
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
767
+ * @description Processes messages from shards, extracting client information.
768
+ * @returns {Promise<void>}
769
+ */
770
+ async handleShardMessage(message, shardConnection) {
771
+ let parsedMessage;
772
+ try {
773
+ parsedMessage = JSON.parse(message);
774
+ } catch (e) {
775
+ console.error("Error parsing shard message:", e);
776
+ return;
777
+ }
778
+ const shardState = shardConnection.state;
779
+ const clients = shardState.clients;
780
+ switch (parsedMessage.type) {
781
+ case "shard.clientConnected":
782
+ await this.handleShardClientConnect(parsedMessage, shardConnection);
783
+ break;
784
+ case "shard.clientMessage":
785
+ await this.handleShardClientMessage(parsedMessage, shardConnection);
786
+ break;
787
+ case "shard.clientDisconnected":
788
+ await this.handleShardClientDisconnect(parsedMessage, shardConnection);
789
+ break;
790
+ default:
791
+ console.warn(`Unknown shard message type: ${parsedMessage.type}`);
792
+ }
793
+ }
794
+ /**
795
+ * @method handleShardClientConnect
796
+ * @private
797
+ * @async
798
+ * @param {Object} message - The client connection message from a shard.
799
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
800
+ * @description Handles a new client connection via a shard.
801
+ * @returns {Promise<void>}
802
+ */
803
+ async handleShardClientConnect(message, shardConnection) {
804
+ const { privateId, connectionInfo } = message;
805
+ const shardState = shardConnection.state;
806
+ const virtualContext = {
807
+ request: {
808
+ headers: new Headers({
809
+ "x-forwarded-for": connectionInfo.ip,
810
+ "user-agent": connectionInfo.userAgent
811
+ }),
812
+ method: "GET",
813
+ url: ""
814
+ }
815
+ };
816
+ const virtualConnection = {
817
+ id: privateId,
818
+ send: /* @__PURE__ */ __name((data) => {
819
+ shardConnection.send(JSON.stringify({
820
+ targetClientId: privateId,
821
+ data
822
+ }));
823
+ }, "send"),
824
+ state: {},
825
+ setState: /* @__PURE__ */ __name((state) => {
826
+ const clients = shardState.clients;
827
+ const currentState = clients.get(privateId) || {};
828
+ const mergedState = Object.assign({}, currentState, state);
829
+ clients.set(privateId, mergedState);
830
+ virtualConnection.state = clients.get(privateId);
831
+ return virtualConnection.state;
832
+ }, "setState"),
833
+ close: /* @__PURE__ */ __name(() => {
834
+ shardConnection.send(JSON.stringify({
835
+ type: "shard.closeClient",
836
+ privateId
837
+ }));
838
+ if (shardState.clients) {
839
+ shardState.clients.delete(privateId);
840
+ }
841
+ }, "close")
842
+ };
843
+ if (!shardState.clients.has(privateId)) {
844
+ shardState.clients.set(privateId, {});
845
+ }
846
+ await this.onConnectClient(virtualConnection, virtualContext);
847
+ }
848
+ /**
849
+ * @method handleShardClientMessage
850
+ * @private
851
+ * @async
852
+ * @param {Object} message - The client message from a shard.
853
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
854
+ * @description Handles a message from a client via a shard.
855
+ * @returns {Promise<void>}
856
+ */
857
+ async handleShardClientMessage(message, shardConnection) {
858
+ const { privateId, publicId, payload } = message;
859
+ const shardState = shardConnection.state;
860
+ const clients = shardState.clients;
861
+ if (!clients.has(privateId)) {
862
+ console.warn(`Received message from unknown client ${privateId}, creating virtual connection`);
863
+ clients.set(privateId, {
864
+ publicId
865
+ });
866
+ }
867
+ const virtualConnection = {
868
+ id: privateId,
869
+ send: /* @__PURE__ */ __name((data) => {
870
+ shardConnection.send(JSON.stringify({
871
+ targetClientId: privateId,
872
+ data
873
+ }));
874
+ }, "send"),
875
+ state: clients.get(privateId),
876
+ setState: /* @__PURE__ */ __name((state) => {
877
+ const currentState = clients.get(privateId) || {};
878
+ const mergedState = Object.assign({}, currentState, state);
879
+ clients.set(privateId, mergedState);
880
+ virtualConnection.state = clients.get(privateId);
881
+ return virtualConnection.state;
882
+ }, "setState"),
883
+ close: /* @__PURE__ */ __name(() => {
884
+ shardConnection.send(JSON.stringify({
885
+ type: "shard.closeClient",
886
+ privateId
887
+ }));
888
+ if (shardState.clients) {
889
+ shardState.clients.delete(privateId);
890
+ }
891
+ }, "close")
892
+ };
893
+ const payloadString = typeof payload === "string" ? payload : JSON.stringify(payload);
894
+ await this.onMessage(payloadString, virtualConnection);
895
+ }
896
+ /**
897
+ * @method handleShardClientDisconnect
898
+ * @private
899
+ * @async
900
+ * @param {Object} message - The client disconnection message from a shard.
901
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
902
+ * @description Handles a client disconnection via a shard.
903
+ * @returns {Promise<void>}
904
+ */
905
+ async handleShardClientDisconnect(message, shardConnection) {
906
+ const { privateId, publicId } = message;
907
+ const shardState = shardConnection.state;
908
+ const clients = shardState.clients;
909
+ const clientState = clients.get(privateId);
910
+ if (!clientState) {
911
+ console.warn(`Disconnection for unknown client ${privateId}`);
912
+ return;
913
+ }
914
+ const virtualConnection = {
915
+ id: privateId,
916
+ send: /* @__PURE__ */ __name(() => {
917
+ }, "send"),
918
+ state: clientState,
919
+ setState: /* @__PURE__ */ __name(() => {
920
+ return {};
921
+ }, "setState"),
922
+ close: /* @__PURE__ */ __name(() => {
923
+ }, "close")
924
+ };
925
+ await this.onClose(virtualConnection);
926
+ clients.delete(privateId);
927
+ }
928
+ /**
658
929
  * @method onClose
659
930
  * @async
660
931
  * @param {Party.Connection} conn - The connection object of the disconnecting user.
@@ -674,22 +945,22 @@ var Server = class {
674
945
  if (!subRoom) {
675
946
  return;
676
947
  }
677
- const signal = this.getUsersProperty(subRoom);
948
+ const signal2 = this.getUsersProperty(subRoom);
678
949
  if (!conn.state) {
679
950
  return;
680
951
  }
681
952
  const privateId = conn.id;
682
953
  const { publicId } = conn.state;
683
- const user = signal?.()[publicId];
954
+ const user = signal2?.()[publicId];
684
955
  if (!user) return;
685
956
  await awaitReturn(subRoom["onLeave"]?.(user, conn));
686
957
  await this.updateSessionConnection(privateId, false);
687
- this.room.broadcast(JSON.stringify({
958
+ this.broadcast({
688
959
  type: "user_disconnected",
689
960
  value: {
690
961
  publicId
691
962
  }
692
- }));
963
+ }, subRoom);
693
964
  }
694
965
  async onAlarm() {
695
966
  const subRoom = await this.getSubRoom();
@@ -699,7 +970,30 @@ var Server = class {
699
970
  const subRoom = await this.getSubRoom();
700
971
  await awaitReturn(subRoom["onError"]?.(connection, error));
701
972
  }
973
+ /**
974
+ * @method onRequest
975
+ * @async
976
+ * @param {Party.Request} req - The HTTP request to handle
977
+ * @description Handles HTTP requests, either directly from clients or forwarded by shards
978
+ * @returns {Promise<Response>} The response to return to the client
979
+ */
702
980
  async onRequest(req) {
981
+ const isFromShard = req.headers.has("x-forwarded-by-shard");
982
+ const shardId = req.headers.get("x-shard-id");
983
+ if (isFromShard) {
984
+ return this.handleShardRequest(req, shardId);
985
+ }
986
+ return this.handleDirectRequest(req);
987
+ }
988
+ /**
989
+ * @method handleDirectRequest
990
+ * @private
991
+ * @async
992
+ * @param {Party.Request} req - The HTTP request received directly from a client
993
+ * @description Processes requests received directly from clients
994
+ * @returns {Promise<Response>} The response to return to the client
995
+ */
996
+ async handleDirectRequest(req) {
703
997
  const subRoom = await this.getSubRoom();
704
998
  const res = /* @__PURE__ */ __name((body, status) => {
705
999
  return new Response(JSON.stringify(body), {
@@ -711,29 +1005,443 @@ var Server = class {
711
1005
  error: "Not found"
712
1006
  }, 404);
713
1007
  }
714
- const response = await awaitReturn(subRoom["onRequest"]?.(req, this.room));
715
- if (!response) {
1008
+ const response2 = await this.tryMatchRequestHandler(req, subRoom);
1009
+ if (response2) {
1010
+ return response2;
1011
+ }
1012
+ const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(req, this.room));
1013
+ if (!legacyResponse) {
716
1014
  return res({
717
1015
  error: "Not found"
718
1016
  }, 404);
719
1017
  }
720
- if (response instanceof Response) {
721
- return response;
1018
+ if (legacyResponse instanceof Response) {
1019
+ return legacyResponse;
1020
+ }
1021
+ return res(legacyResponse, 200);
1022
+ }
1023
+ /**
1024
+ * @method tryMatchRequestHandler
1025
+ * @private
1026
+ * @async
1027
+ * @param {Party.Request} req - The HTTP request to handle
1028
+ * @param {Object} subRoom - The room instance
1029
+ * @description Attempts to match the request to a registered @Request handler
1030
+ * @returns {Promise<Response | null>} The response or null if no handler matched
1031
+ */
1032
+ async tryMatchRequestHandler(req, subRoom) {
1033
+ const requestHandlers = subRoom.constructor["_requestMetadata"];
1034
+ if (!requestHandlers) {
1035
+ return null;
1036
+ }
1037
+ const url = new URL(req.url);
1038
+ const method = req.method;
1039
+ let pathname = url.pathname;
1040
+ pathname = "/" + pathname.split("/").slice(4).join("/");
1041
+ for (const [routeKey, handler] of requestHandlers.entries()) {
1042
+ const firstColonIndex = routeKey.indexOf(":");
1043
+ const handlerMethod = routeKey.substring(0, firstColonIndex);
1044
+ const handlerPath = routeKey.substring(firstColonIndex + 1);
1045
+ if (handlerMethod !== method) {
1046
+ continue;
1047
+ }
1048
+ if (this.pathMatches(pathname, handlerPath)) {
1049
+ const params = this.extractPathParams(pathname, handlerPath);
1050
+ const guards = subRoom.constructor["_actionGuards"]?.get(handler.key) || [];
1051
+ for (const guard of guards) {
1052
+ const isAuthorized = await guard(null, req, this.room);
1053
+ if (isAuthorized instanceof Response) {
1054
+ return isAuthorized;
1055
+ }
1056
+ if (!isAuthorized) {
1057
+ return new Response(JSON.stringify({
1058
+ error: "Unauthorized"
1059
+ }), {
1060
+ status: 403
1061
+ });
1062
+ }
1063
+ }
1064
+ let bodyData = null;
1065
+ if (handler.bodyValidation && [
1066
+ "POST",
1067
+ "PUT",
1068
+ "PATCH"
1069
+ ].includes(method)) {
1070
+ try {
1071
+ const contentType = req.headers.get("content-type") || "";
1072
+ if (contentType.includes("application/json")) {
1073
+ const body = await req.json();
1074
+ const validation = handler.bodyValidation.safeParse(body);
1075
+ if (!validation.success) {
1076
+ return new Response(JSON.stringify({
1077
+ error: "Invalid request body",
1078
+ details: validation.error
1079
+ }), {
1080
+ status: 400
1081
+ });
1082
+ }
1083
+ bodyData = validation.data;
1084
+ }
1085
+ } catch (error) {
1086
+ return new Response(JSON.stringify({
1087
+ error: "Failed to parse request body"
1088
+ }), {
1089
+ status: 400
1090
+ });
1091
+ }
1092
+ }
1093
+ try {
1094
+ const result = await awaitReturn(subRoom[handler.key](req, bodyData, params, this.room));
1095
+ if (result instanceof Response) {
1096
+ return result;
1097
+ }
1098
+ return new Response(typeof result === "string" ? result : JSON.stringify(result), {
1099
+ status: 200,
1100
+ headers: {
1101
+ "Content-Type": typeof result === "string" ? "text/plain" : "application/json"
1102
+ }
1103
+ });
1104
+ } catch (error) {
1105
+ console.error("Error executing request handler:", error);
1106
+ return new Response(JSON.stringify({
1107
+ error: "Internal server error"
1108
+ }), {
1109
+ status: 500
1110
+ });
1111
+ }
1112
+ }
1113
+ }
1114
+ return null;
1115
+ }
1116
+ /**
1117
+ * @method pathMatches
1118
+ * @private
1119
+ * @param {string} requestPath - The path from the request
1120
+ * @param {string} handlerPath - The path pattern from the handler
1121
+ * @description Checks if a request path matches a handler path pattern
1122
+ * @returns {boolean} True if the paths match
1123
+ */
1124
+ pathMatches(requestPath, handlerPath) {
1125
+ const pathRegexString = handlerPath.replace(/\//g, "\\/").replace(/:([^\/]+)/g, "([^/]+)");
1126
+ const pathRegex = new RegExp(`^${pathRegexString}$`);
1127
+ return pathRegex.test(requestPath);
1128
+ }
1129
+ /**
1130
+ * @method extractPathParams
1131
+ * @private
1132
+ * @param {string} requestPath - The path from the request
1133
+ * @param {string} handlerPath - The path pattern from the handler
1134
+ * @description Extracts path parameters from the request path based on the handler pattern
1135
+ * @returns {Object} An object containing the path parameters
1136
+ */
1137
+ extractPathParams(requestPath, handlerPath) {
1138
+ const params = {};
1139
+ const paramNames = [];
1140
+ handlerPath.split("/").forEach((segment) => {
1141
+ if (segment.startsWith(":")) {
1142
+ paramNames.push(segment.substring(1));
1143
+ }
1144
+ });
1145
+ const pathRegexString = handlerPath.replace(/\//g, "\\/").replace(/:([^\/]+)/g, "([^/]+)");
1146
+ const pathRegex = new RegExp(`^${pathRegexString}$`);
1147
+ const matches = requestPath.match(pathRegex);
1148
+ if (matches && matches.length > 1) {
1149
+ for (let i = 0; i < paramNames.length; i++) {
1150
+ params[paramNames[i]] = matches[i + 1];
1151
+ }
1152
+ }
1153
+ return params;
1154
+ }
1155
+ /**
1156
+ * @method handleShardRequest
1157
+ * @private
1158
+ * @async
1159
+ * @param {Party.Request} req - The HTTP request forwarded by a shard
1160
+ * @param {string | null} shardId - The ID of the shard that forwarded the request
1161
+ * @description Processes requests forwarded by shards, preserving client context
1162
+ * @returns {Promise<Response>} The response to return to the shard (which will forward it to the client)
1163
+ */
1164
+ async handleShardRequest(req, shardId) {
1165
+ const subRoom = await this.getSubRoom();
1166
+ if (!subRoom) {
1167
+ return new Response(JSON.stringify({
1168
+ error: "Not found"
1169
+ }), {
1170
+ status: 404
1171
+ });
1172
+ }
1173
+ const originalClientIp = req.headers.get("x-original-client-ip");
1174
+ const enhancedReq = this.createEnhancedRequest(req, originalClientIp);
1175
+ try {
1176
+ const response2 = await this.tryMatchRequestHandler(enhancedReq, subRoom);
1177
+ if (response2) {
1178
+ return response2;
1179
+ }
1180
+ const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(enhancedReq, this.room));
1181
+ if (!legacyResponse) {
1182
+ return new Response(JSON.stringify({
1183
+ error: "Not found"
1184
+ }), {
1185
+ status: 404
1186
+ });
1187
+ }
1188
+ if (legacyResponse instanceof Response) {
1189
+ return legacyResponse;
1190
+ }
1191
+ return new Response(JSON.stringify(legacyResponse), {
1192
+ status: 200
1193
+ });
1194
+ } catch (error) {
1195
+ console.error(`Error processing request from shard ${shardId}:`, error);
1196
+ return new Response(JSON.stringify({
1197
+ error: "Internal server error"
1198
+ }), {
1199
+ status: 500
1200
+ });
1201
+ }
1202
+ }
1203
+ /**
1204
+ * @method createEnhancedRequest
1205
+ * @private
1206
+ * @param {Party.Request} originalReq - The original request received from the shard
1207
+ * @param {string | null} originalClientIp - The original client IP, if available
1208
+ * @description Creates an enhanced request object that preserves the original client context
1209
+ * @returns {Party.Request} The enhanced request object
1210
+ */
1211
+ createEnhancedRequest(originalReq, originalClientIp) {
1212
+ const clonedReq = originalReq.clone();
1213
+ clonedReq.viaShard = true;
1214
+ if (originalClientIp) {
1215
+ clonedReq.originalClientIp = originalClientIp;
1216
+ }
1217
+ return clonedReq;
1218
+ }
1219
+ };
1220
+
1221
+ // src/shard.ts
1222
+ var Shard = class {
1223
+ static {
1224
+ __name(this, "Shard");
1225
+ }
1226
+ room;
1227
+ ws;
1228
+ connectionMap;
1229
+ mainServerStub;
1230
+ worldUrl;
1231
+ worldId;
1232
+ lastReportedConnections;
1233
+ statsInterval;
1234
+ statsIntervalId;
1235
+ constructor(room) {
1236
+ this.room = room;
1237
+ this.connectionMap = /* @__PURE__ */ new Map();
1238
+ this.worldUrl = null;
1239
+ this.worldId = "default";
1240
+ this.lastReportedConnections = 0;
1241
+ this.statsInterval = 3e4;
1242
+ this.statsIntervalId = null;
1243
+ }
1244
+ async onStart() {
1245
+ const roomId = this.room.id.split(":")[0];
1246
+ const roomStub = this.room.context.parties.main.get(roomId);
1247
+ if (!roomStub) {
1248
+ console.warn("No room room stub found in main party context");
1249
+ return;
1250
+ }
1251
+ this.mainServerStub = roomStub;
1252
+ this.ws = await roomStub.socket({
1253
+ headers: {
1254
+ "x-shard-id": this.room.id
1255
+ }
1256
+ });
1257
+ this.ws.addEventListener("message", (event) => {
1258
+ try {
1259
+ const message = JSON.parse(event.data);
1260
+ if (message.targetClientId) {
1261
+ const clientConn = this.connectionMap.get(message.targetClientId);
1262
+ if (clientConn) {
1263
+ delete message.targetClientId;
1264
+ clientConn.send(message.data);
1265
+ }
1266
+ } else {
1267
+ this.room.broadcast(event.data);
1268
+ }
1269
+ } catch (error) {
1270
+ console.error("Error processing message from main server:", error);
1271
+ }
1272
+ });
1273
+ await this.updateWorldStats();
1274
+ this.startPeriodicStatsUpdates();
1275
+ }
1276
+ startPeriodicStatsUpdates() {
1277
+ if (!this.worldUrl) {
1278
+ return;
1279
+ }
1280
+ if (this.statsIntervalId) {
1281
+ clearInterval(this.statsIntervalId);
1282
+ }
1283
+ this.statsIntervalId = setInterval(() => {
1284
+ this.updateWorldStats().catch((error) => {
1285
+ console.error("Error in periodic stats update:", error);
1286
+ });
1287
+ }, this.statsInterval);
1288
+ }
1289
+ stopPeriodicStatsUpdates() {
1290
+ if (this.statsIntervalId) {
1291
+ clearInterval(this.statsIntervalId);
1292
+ this.statsIntervalId = null;
1293
+ }
1294
+ }
1295
+ onConnect(conn, ctx) {
1296
+ this.connectionMap.set(conn.id, conn);
1297
+ this.ws.send(JSON.stringify({
1298
+ type: "shard.clientConnected",
1299
+ privateId: conn.id,
1300
+ connectionInfo: {
1301
+ ip: ctx.request?.headers.get("x-forwarded-for") || "unknown",
1302
+ userAgent: ctx.request?.headers.get("user-agent") || "unknown"
1303
+ }
1304
+ }));
1305
+ this.updateWorldStats();
1306
+ }
1307
+ onMessage(message, sender) {
1308
+ try {
1309
+ const parsedMessage = typeof message === "string" ? JSON.parse(message) : message;
1310
+ const wrappedMessage = JSON.stringify({
1311
+ type: "shard.clientMessage",
1312
+ privateId: sender.id,
1313
+ publicId: sender.state?.publicId,
1314
+ payload: parsedMessage
1315
+ });
1316
+ this.ws.send(wrappedMessage);
1317
+ } catch (error) {
1318
+ console.error("Error forwarding message to main server:", error);
1319
+ }
1320
+ }
1321
+ onClose(conn) {
1322
+ this.connectionMap.delete(conn.id);
1323
+ this.ws.send(JSON.stringify({
1324
+ type: "shard.clientDisconnected",
1325
+ privateId: conn.id,
1326
+ publicId: conn.state?.publicId
1327
+ }));
1328
+ this.updateWorldStats();
1329
+ }
1330
+ async updateWorldStats() {
1331
+ const currentConnections = this.connectionMap.size;
1332
+ if (currentConnections === this.lastReportedConnections) {
1333
+ return true;
1334
+ }
1335
+ try {
1336
+ const worldRoom = this.room.context.parties.world.get("world-default");
1337
+ const response2 = await worldRoom.fetch("/update-shard", {
1338
+ method: "POST",
1339
+ headers: {
1340
+ "Content-Type": "application/json",
1341
+ "x-access-shard": this.room.env.SHARD_SECRET
1342
+ },
1343
+ body: JSON.stringify({
1344
+ shardId: this.room.id,
1345
+ connections: currentConnections
1346
+ })
1347
+ });
1348
+ if (!response2.ok) {
1349
+ const errorData = await response2.json().catch(() => ({
1350
+ error: "Unknown error"
1351
+ }));
1352
+ console.error(`Failed to update World stats: ${response2.status} - ${errorData.error || "Unknown error"}`);
1353
+ return false;
1354
+ }
1355
+ this.lastReportedConnections = currentConnections;
1356
+ return true;
1357
+ } catch (error) {
1358
+ console.error("Error updating World stats:", error);
1359
+ return false;
722
1360
  }
723
- return res(response, 200);
1361
+ }
1362
+ /**
1363
+ * @method onRequest
1364
+ * @async
1365
+ * @param {Party.Request} req - The HTTP request to handle
1366
+ * @description Forwards HTTP requests to the main server, preserving client context
1367
+ * @returns {Promise<Response>} The response from the main server
1368
+ */
1369
+ async onRequest(req) {
1370
+ if (!this.mainServerStub) {
1371
+ return response(503, {
1372
+ error: "Shard not connected to main server"
1373
+ });
1374
+ }
1375
+ try {
1376
+ const url = new URL(req.url);
1377
+ const path = url.pathname;
1378
+ const method = req.method;
1379
+ let body = null;
1380
+ if (method !== "GET" && method !== "HEAD") {
1381
+ body = await req.text();
1382
+ }
1383
+ const headers = new Headers();
1384
+ req.headers.forEach((value, key) => {
1385
+ headers.set(key, value);
1386
+ });
1387
+ headers.set("x-shard-id", this.room.id);
1388
+ headers.set("x-forwarded-by-shard", "true");
1389
+ const clientIp = req.headers.get("x-forwarded-for") || "unknown";
1390
+ if (clientIp) {
1391
+ headers.set("x-original-client-ip", clientIp);
1392
+ }
1393
+ const requestInit = {
1394
+ method,
1395
+ headers,
1396
+ body
1397
+ };
1398
+ const response2 = await this.mainServerStub.fetch(path, requestInit);
1399
+ return response2;
1400
+ } catch (error) {
1401
+ return response(500, {
1402
+ error: "Error forwarding request"
1403
+ });
1404
+ }
1405
+ }
1406
+ /**
1407
+ * @method onAlarm
1408
+ * @async
1409
+ * @description Executed periodically, used to perform maintenance tasks
1410
+ */
1411
+ async onAlarm() {
1412
+ await this.updateWorldStats();
724
1413
  }
725
1414
  };
726
1415
 
727
1416
  // src/testing.ts
728
- async function testRoom(Room2, options = {}) {
729
- const io = new ServerIo(Room2.path);
730
- Room2.prototype.throttleSync = 0;
731
- Room2.prototype.throttleStorage = 0;
732
- Room2.prototype.options = options;
733
- const server = new Server(io);
734
- server.rooms = [
735
- Room2
736
- ];
1417
+ async function testRoom(Room3, options = {}) {
1418
+ const createServer = /* @__PURE__ */ __name((io2) => {
1419
+ const server2 = new Server(io2);
1420
+ server2.rooms = [
1421
+ Room3
1422
+ ];
1423
+ return server2;
1424
+ }, "createServer");
1425
+ const isShard = options.shard || false;
1426
+ const io = new ServerIo(Room3.path, isShard ? {
1427
+ parties: {
1428
+ game: createServer
1429
+ },
1430
+ env: options.env
1431
+ } : {
1432
+ env: options.env
1433
+ });
1434
+ Room3.prototype.throttleSync = 0;
1435
+ Room3.prototype.throttleStorage = 0;
1436
+ Room3.prototype.options = options;
1437
+ let server;
1438
+ if (options.shard) {
1439
+ const shardServer = new Shard(io);
1440
+ shardServer.subRoom = null;
1441
+ server = shardServer;
1442
+ } else {
1443
+ server = await createServer(io);
1444
+ }
737
1445
  await server.onStart();
738
1446
  return {
739
1447
  server,
@@ -745,15 +1453,694 @@ async function testRoom(Room2, options = {}) {
745
1453
  };
746
1454
  }
747
1455
  __name(testRoom, "testRoom");
1456
+ async function request(room, path, options = {
1457
+ method: "GET"
1458
+ }) {
1459
+ const url = new URL("http://localhost" + path);
1460
+ const request1 = new Request(url.toString(), options);
1461
+ const response2 = await room.onRequest(request1);
1462
+ return response2;
1463
+ }
1464
+ __name(request, "request");
1465
+
1466
+ // src/world.ts
1467
+ import { signal } from "@signe/reactive";
1468
+ import { sync, id, persist } from "@signe/sync";
1469
+ import { z as z2 } from "zod";
1470
+
1471
+ // src/types/party.ts
1472
+ var party_exports = {};
1473
+
1474
+ // src/jwt.ts
1475
+ var JWTAuth = class {
1476
+ static {
1477
+ __name(this, "JWTAuth");
1478
+ }
1479
+ secret;
1480
+ encoder;
1481
+ decoder;
1482
+ /**
1483
+ * Constructor for the JWTAuth class
1484
+ * @param {string} secret - The secret key used for signing and verifying tokens
1485
+ */
1486
+ constructor(secret) {
1487
+ if (!secret || typeof secret !== "string") {
1488
+ throw new Error("Secret is required and must be a string");
1489
+ }
1490
+ this.secret = secret;
1491
+ this.encoder = new TextEncoder();
1492
+ this.decoder = new TextDecoder();
1493
+ }
1494
+ /**
1495
+ * Convert the secret to a CryptoKey for HMAC operations
1496
+ * @returns {Promise<CryptoKey>} - The CryptoKey for HMAC operations
1497
+ */
1498
+ async getSecretKey() {
1499
+ const keyData = this.encoder.encode(this.secret);
1500
+ return await crypto.subtle.importKey(
1501
+ "raw",
1502
+ keyData,
1503
+ {
1504
+ name: "HMAC",
1505
+ hash: {
1506
+ name: "SHA-256"
1507
+ }
1508
+ },
1509
+ false,
1510
+ [
1511
+ "sign",
1512
+ "verify"
1513
+ ]
1514
+ // key usages
1515
+ );
1516
+ }
1517
+ /**
1518
+ * Base64Url encode a buffer
1519
+ * @param {ArrayBuffer} buffer - The buffer to encode
1520
+ * @returns {string} - The base64url encoded string
1521
+ */
1522
+ base64UrlEncode(buffer) {
1523
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
1524
+ return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
1525
+ }
1526
+ /**
1527
+ * Base64Url decode a string
1528
+ * @param {string} base64Url - The base64url encoded string
1529
+ * @returns {ArrayBuffer} - The decoded buffer
1530
+ */
1531
+ base64UrlDecode(base64Url) {
1532
+ const padding = "=".repeat((4 - base64Url.length % 4) % 4);
1533
+ const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/") + padding;
1534
+ const rawData = atob(base64);
1535
+ const buffer = new Uint8Array(rawData.length);
1536
+ for (let i = 0; i < rawData.length; i++) {
1537
+ buffer[i] = rawData.charCodeAt(i);
1538
+ }
1539
+ return buffer.buffer;
1540
+ }
1541
+ /**
1542
+ * Sign a payload and create a JWT token
1543
+ * @param {JWTPayload} payload - The payload to include in the token
1544
+ * @param {JWTOptions} [options={}] - Options for the token
1545
+ * @param {string | number} [options.expiresIn='1h'] - Token expiration time
1546
+ * @returns {Promise<string>} - The JWT token
1547
+ */
1548
+ async sign(payload, options = {}) {
1549
+ if (!payload || typeof payload !== "object") {
1550
+ throw new Error("Payload must be an object");
1551
+ }
1552
+ const expiresIn = options.expiresIn || "1h";
1553
+ let exp;
1554
+ if (typeof expiresIn === "number") {
1555
+ exp = Math.floor(Date.now() / 1e3) + expiresIn;
1556
+ } else if (typeof expiresIn === "string") {
1557
+ const match = expiresIn.match(/^(\d+)([smhd])$/);
1558
+ if (match) {
1559
+ const value = parseInt(match[1]);
1560
+ const unit = match[2];
1561
+ const seconds = {
1562
+ "s": value,
1563
+ "m": value * 60,
1564
+ "h": value * 60 * 60,
1565
+ "d": value * 60 * 60 * 24
1566
+ }[unit];
1567
+ exp = Math.floor(Date.now() / 1e3) + seconds;
1568
+ } else {
1569
+ throw new Error('Invalid expiresIn format. Use a number (seconds) or a string like "1h", "30m", etc.');
1570
+ }
1571
+ }
1572
+ const fullPayload = {
1573
+ ...payload,
1574
+ iat: Math.floor(Date.now() / 1e3),
1575
+ exp
1576
+ };
1577
+ const header = {
1578
+ alg: "HS256",
1579
+ typ: "JWT"
1580
+ };
1581
+ const encodedHeader = this.base64UrlEncode(this.encoder.encode(JSON.stringify(header)));
1582
+ const encodedPayload = this.base64UrlEncode(this.encoder.encode(JSON.stringify(fullPayload)));
1583
+ const signatureBase = `${encodedHeader}.${encodedPayload}`;
1584
+ const key = await this.getSecretKey();
1585
+ const signature = await crypto.subtle.sign({
1586
+ name: "HMAC"
1587
+ }, key, this.encoder.encode(signatureBase));
1588
+ const encodedSignature = this.base64UrlEncode(signature);
1589
+ return `${signatureBase}.${encodedSignature}`;
1590
+ }
1591
+ /**
1592
+ * Verify a JWT token and return the decoded payload
1593
+ * @param {string} token - The JWT token to verify
1594
+ * @returns {Promise<JWTPayload>} - The decoded payload if verification succeeds
1595
+ * @throws {Error} - If verification fails
1596
+ */
1597
+ async verify(token) {
1598
+ if (!token || typeof token !== "string") {
1599
+ throw new Error("Token is required and must be a string");
1600
+ }
1601
+ const parts = token.split(".");
1602
+ if (parts.length !== 3) {
1603
+ throw new Error("Invalid token format");
1604
+ }
1605
+ const [encodedHeader, encodedPayload, encodedSignature] = parts;
1606
+ try {
1607
+ const header = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedHeader)));
1608
+ const payload = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedPayload)));
1609
+ if (header.alg !== "HS256") {
1610
+ throw new Error(`Unsupported algorithm: ${header.alg}`);
1611
+ }
1612
+ const now = Math.floor(Date.now() / 1e3);
1613
+ if (payload.exp && payload.exp < now) {
1614
+ throw new Error("Token has expired");
1615
+ }
1616
+ const key = await this.getSecretKey();
1617
+ const signatureBase = `${encodedHeader}.${encodedPayload}`;
1618
+ const signature = this.base64UrlDecode(encodedSignature);
1619
+ const isValid = await crypto.subtle.verify({
1620
+ name: "HMAC"
1621
+ }, key, signature, this.encoder.encode(signatureBase));
1622
+ if (!isValid) {
1623
+ throw new Error("Invalid signature");
1624
+ }
1625
+ return payload;
1626
+ } catch (error) {
1627
+ if (error instanceof Error) {
1628
+ throw new Error(`Token verification failed: ${error.message}`);
1629
+ }
1630
+ throw new Error("Token verification failed: Unknown error");
1631
+ }
1632
+ }
1633
+ };
1634
+
1635
+ // src/world.guard.ts
1636
+ var guardManageWorld = /* @__PURE__ */ __name(async (_, req, room) => {
1637
+ const tokenShard = req.headers.get("x-access-shard");
1638
+ if (tokenShard) {
1639
+ if (tokenShard !== room.env.SHARD_SECRET) {
1640
+ return false;
1641
+ }
1642
+ return true;
1643
+ }
1644
+ const url = new URL(req.url);
1645
+ const token = req.headers.get("Authorization") ?? url.searchParams.get("world-auth-token");
1646
+ if (!token) {
1647
+ return false;
1648
+ }
1649
+ const jwt = new JWTAuth(room.env.AUTH_JWT_SECRET);
1650
+ try {
1651
+ const payload = await jwt.verify(token);
1652
+ if (!payload) {
1653
+ return false;
1654
+ }
1655
+ } catch (error) {
1656
+ return false;
1657
+ }
1658
+ return true;
1659
+ }, "guardManageWorld");
1660
+
1661
+ // src/world.ts
1662
+ function _ts_decorate(decorators, target, key, desc) {
1663
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1664
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1665
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1666
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1667
+ }
1668
+ __name(_ts_decorate, "_ts_decorate");
1669
+ function _ts_metadata(k, v) {
1670
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1671
+ }
1672
+ __name(_ts_metadata, "_ts_metadata");
1673
+ var RoomConfigSchema = z2.object({
1674
+ name: z2.string(),
1675
+ balancingStrategy: z2.enum([
1676
+ "round-robin",
1677
+ "least-connections",
1678
+ "random"
1679
+ ]),
1680
+ public: z2.boolean(),
1681
+ maxPlayersPerShard: z2.number().int().positive(),
1682
+ minShards: z2.number().int().min(0),
1683
+ maxShards: z2.number().int().positive().optional()
1684
+ });
1685
+ var RegisterShardSchema = z2.object({
1686
+ shardId: z2.string(),
1687
+ roomId: z2.string(),
1688
+ url: z2.string().url(),
1689
+ maxConnections: z2.number().int().positive()
1690
+ });
1691
+ var UpdateShardStatsSchema = z2.object({
1692
+ connections: z2.number().int().min(0),
1693
+ status: z2.enum([
1694
+ "active",
1695
+ "maintenance",
1696
+ "draining"
1697
+ ]).optional()
1698
+ });
1699
+ var ScaleRoomSchema = z2.object({
1700
+ roomId: z2.string(),
1701
+ targetShardCount: z2.number().int().positive(),
1702
+ shardTemplate: z2.object({
1703
+ urlTemplate: z2.string(),
1704
+ maxConnections: z2.number().int().positive()
1705
+ }).optional()
1706
+ });
1707
+ var RoomConfig = class RoomConfig2 {
1708
+ static {
1709
+ __name(this, "RoomConfig");
1710
+ }
1711
+ id;
1712
+ name = signal("");
1713
+ balancingStrategy = signal("round-robin");
1714
+ public = signal(true);
1715
+ maxPlayersPerShard = signal(100);
1716
+ minShards = signal(1);
1717
+ maxShards = signal(void 0);
1718
+ };
1719
+ _ts_decorate([
1720
+ id(),
1721
+ _ts_metadata("design:type", String)
1722
+ ], RoomConfig.prototype, "id", void 0);
1723
+ _ts_decorate([
1724
+ sync()
1725
+ ], RoomConfig.prototype, "name", void 0);
1726
+ _ts_decorate([
1727
+ sync()
1728
+ ], RoomConfig.prototype, "balancingStrategy", void 0);
1729
+ _ts_decorate([
1730
+ sync()
1731
+ ], RoomConfig.prototype, "public", void 0);
1732
+ _ts_decorate([
1733
+ sync()
1734
+ ], RoomConfig.prototype, "maxPlayersPerShard", void 0);
1735
+ _ts_decorate([
1736
+ sync()
1737
+ ], RoomConfig.prototype, "minShards", void 0);
1738
+ _ts_decorate([
1739
+ sync()
1740
+ ], RoomConfig.prototype, "maxShards", void 0);
1741
+ var ShardInfo = class ShardInfo2 {
1742
+ static {
1743
+ __name(this, "ShardInfo");
1744
+ }
1745
+ id;
1746
+ roomId = signal("");
1747
+ url = signal("");
1748
+ currentConnections = signal(0);
1749
+ maxConnections = signal(100);
1750
+ status = signal("active");
1751
+ lastHeartbeat = signal(0);
1752
+ };
1753
+ _ts_decorate([
1754
+ id(),
1755
+ _ts_metadata("design:type", String)
1756
+ ], ShardInfo.prototype, "id", void 0);
1757
+ _ts_decorate([
1758
+ sync()
1759
+ ], ShardInfo.prototype, "roomId", void 0);
1760
+ _ts_decorate([
1761
+ sync()
1762
+ ], ShardInfo.prototype, "url", void 0);
1763
+ _ts_decorate([
1764
+ sync({
1765
+ persist: false
1766
+ })
1767
+ ], ShardInfo.prototype, "currentConnections", void 0);
1768
+ _ts_decorate([
1769
+ sync()
1770
+ ], ShardInfo.prototype, "maxConnections", void 0);
1771
+ _ts_decorate([
1772
+ sync()
1773
+ ], ShardInfo.prototype, "status", void 0);
1774
+ _ts_decorate([
1775
+ sync()
1776
+ ], ShardInfo.prototype, "lastHeartbeat", void 0);
1777
+ var WorldRoom = class {
1778
+ static {
1779
+ __name(this, "WorldRoom");
1780
+ }
1781
+ room;
1782
+ // Synchronized state
1783
+ rooms;
1784
+ shards;
1785
+ // Only persisted state (not synced to clients)
1786
+ rrCounters;
1787
+ // Configuration
1788
+ defaultShardUrlTemplate;
1789
+ defaultMaxConnectionsPerShard;
1790
+ constructor(room) {
1791
+ this.room = room;
1792
+ this.rooms = signal({});
1793
+ this.shards = signal({});
1794
+ this.rrCounters = signal({});
1795
+ this.defaultShardUrlTemplate = signal("{shardId}");
1796
+ this.defaultMaxConnectionsPerShard = signal(100);
1797
+ const { AUTH_JWT_SECRET, SHARD_SECRET } = this.room.env;
1798
+ if (!AUTH_JWT_SECRET) {
1799
+ throw new Error("AUTH_JWT_SECRET env variable is not set");
1800
+ }
1801
+ if (!SHARD_SECRET) {
1802
+ throw new Error("SHARD_SECRET env variable is not set");
1803
+ }
1804
+ setTimeout(() => this.cleanupInactiveShards(), 6e4);
1805
+ }
1806
+ async onJoin(user, conn, ctx) {
1807
+ const canConnect = await guardManageWorld(user, ctx.request, this.room);
1808
+ conn.setState({
1809
+ ...conn.state,
1810
+ isAdmin: canConnect
1811
+ });
1812
+ }
1813
+ interceptorPacket(_, obj, conn) {
1814
+ if (!conn.state["isAdmin"]) {
1815
+ return null;
1816
+ }
1817
+ return obj;
1818
+ }
1819
+ // Helper methods
1820
+ cleanupInactiveShards() {
1821
+ const now = Date.now();
1822
+ const timeout = 5 * 60 * 1e3;
1823
+ const shardsValue = this.shards();
1824
+ let hasChanges = false;
1825
+ Object.values(shardsValue).forEach((shard) => {
1826
+ if (now - shard.lastHeartbeat() > timeout) {
1827
+ delete this.shards()[shard.id];
1828
+ hasChanges = true;
1829
+ }
1830
+ });
1831
+ setTimeout(() => this.cleanupInactiveShards(), 6e4);
1832
+ }
1833
+ // Actions
1834
+ async registerRoom(req) {
1835
+ const roomConfig = await req.json();
1836
+ const roomId = roomConfig.name;
1837
+ if (!this.rooms()[roomId]) {
1838
+ const newRoom = new RoomConfig();
1839
+ newRoom.id = roomId;
1840
+ newRoom.name.set(roomConfig.name);
1841
+ newRoom.balancingStrategy.set(roomConfig.balancingStrategy);
1842
+ newRoom.public.set(roomConfig.public);
1843
+ newRoom.maxPlayersPerShard.set(roomConfig.maxPlayersPerShard);
1844
+ newRoom.minShards.set(roomConfig.minShards);
1845
+ newRoom.maxShards.set(roomConfig.maxShards);
1846
+ this.rooms()[roomId] = newRoom;
1847
+ if (roomConfig.minShards > 0) {
1848
+ for (let i = 0; i < roomConfig.minShards; i++) {
1849
+ await this.createShard(roomId);
1850
+ }
1851
+ }
1852
+ } else {
1853
+ const room = this.rooms()[roomId];
1854
+ room.balancingStrategy.set(roomConfig.balancingStrategy);
1855
+ room.public.set(roomConfig.public);
1856
+ room.maxPlayersPerShard.set(roomConfig.maxPlayersPerShard);
1857
+ room.minShards.set(roomConfig.minShards);
1858
+ room.maxShards.set(roomConfig.maxShards);
1859
+ }
1860
+ }
1861
+ async updateShardStats(req) {
1862
+ const body = await req.json();
1863
+ const { shardId, connections, status } = body;
1864
+ const shard = this.shards()[shardId];
1865
+ if (!shard) {
1866
+ return {
1867
+ error: `Shard ${shardId} not found`
1868
+ };
1869
+ }
1870
+ shard.currentConnections.set(connections);
1871
+ if (status) {
1872
+ shard.status.set(status);
1873
+ }
1874
+ shard.lastHeartbeat.set(Date.now());
1875
+ }
1876
+ async scaleRoom(req) {
1877
+ const data = await req.json();
1878
+ const { targetShardCount, shardTemplate, roomId } = data;
1879
+ const room = this.rooms()[roomId];
1880
+ if (!room) {
1881
+ return {
1882
+ error: `Room ${roomId} does not exist`
1883
+ };
1884
+ }
1885
+ const roomShards = Object.values(this.shards()).filter((shard) => shard.roomId() === roomId);
1886
+ const previousShardCount = roomShards.length;
1887
+ if (room.maxShards() !== void 0 && targetShardCount > room.maxShards()) {
1888
+ return {
1889
+ error: `Cannot scale beyond maximum allowed shards (${room.maxShards()})`,
1890
+ roomId,
1891
+ currentShardCount: previousShardCount
1892
+ };
1893
+ }
1894
+ if (targetShardCount < previousShardCount) {
1895
+ const shardsToRemove = [
1896
+ ...roomShards
1897
+ ].sort((a, b) => {
1898
+ if (a.status() === "draining" && b.status() !== "draining") return -1;
1899
+ if (a.status() !== "draining" && b.status() === "draining") return 1;
1900
+ return a.currentConnections() - b.currentConnections();
1901
+ }).slice(0, previousShardCount - targetShardCount);
1902
+ const shardsToKeep = roomShards.filter((shard) => !shardsToRemove.some((s) => s.id === shard.id));
1903
+ for (const shard of shardsToRemove) {
1904
+ delete this.shards()[shard.id];
1905
+ }
1906
+ return;
1907
+ }
1908
+ if (targetShardCount > previousShardCount) {
1909
+ const newShards = [];
1910
+ for (let i = 0; i < targetShardCount - previousShardCount; i++) {
1911
+ const newShard = await this.createShard(roomId, shardTemplate?.urlTemplate, shardTemplate?.maxConnections);
1912
+ if (newShard) {
1913
+ newShards.push(newShard);
1914
+ }
1915
+ }
1916
+ }
1917
+ }
1918
+ async connect(req) {
1919
+ try {
1920
+ let data;
1921
+ try {
1922
+ const body = await req.text();
1923
+ if (!body || body.trim() === "") {
1924
+ return response(400, {
1925
+ error: "Request body is empty"
1926
+ });
1927
+ }
1928
+ data = JSON.parse(body);
1929
+ } catch (parseError) {
1930
+ return response(400, {
1931
+ error: "Invalid JSON in request body"
1932
+ });
1933
+ }
1934
+ if (!data.roomId) {
1935
+ return response(400, {
1936
+ error: "roomId parameter is required"
1937
+ });
1938
+ }
1939
+ const autoCreate = data.autoCreate !== void 0 ? data.autoCreate : true;
1940
+ const result = await this.findOptimalShard(data.roomId, autoCreate);
1941
+ if ("error" in result) {
1942
+ return response(404, {
1943
+ error: result.error
1944
+ });
1945
+ }
1946
+ return response(200, {
1947
+ success: true,
1948
+ shardId: result.shardId,
1949
+ url: result.url
1950
+ });
1951
+ } catch (error) {
1952
+ console.error("Error connecting to shard:", error);
1953
+ return response(500, {
1954
+ error: "Internal server error",
1955
+ details: error instanceof Error ? error.message : String(error)
1956
+ });
1957
+ }
1958
+ }
1959
+ async findOptimalShard(roomId, autoCreate = true) {
1960
+ let room = this.rooms()[roomId];
1961
+ if (!room) {
1962
+ if (autoCreate) {
1963
+ const mockRequest = {
1964
+ json: /* @__PURE__ */ __name(async () => ({
1965
+ name: roomId,
1966
+ balancingStrategy: "round-robin",
1967
+ public: true,
1968
+ maxPlayersPerShard: this.defaultMaxConnectionsPerShard(),
1969
+ minShards: 1,
1970
+ maxShards: void 0
1971
+ }), "json")
1972
+ };
1973
+ await this.registerRoom(mockRequest);
1974
+ room = this.rooms()[roomId];
1975
+ if (!room) {
1976
+ return {
1977
+ error: `Failed to create room ${roomId}`
1978
+ };
1979
+ }
1980
+ } else {
1981
+ return {
1982
+ error: `Room ${roomId} does not exist`
1983
+ };
1984
+ }
1985
+ }
1986
+ const roomShards = Object.values(this.shards()).filter((shard) => shard.roomId() === roomId);
1987
+ if (roomShards.length === 0) {
1988
+ if (autoCreate) {
1989
+ const newShard = await this.createShard(roomId);
1990
+ if (newShard) {
1991
+ return {
1992
+ shardId: newShard.id,
1993
+ url: newShard.url()
1994
+ };
1995
+ } else {
1996
+ return {
1997
+ error: `Failed to create shard for room ${roomId}`
1998
+ };
1999
+ }
2000
+ } else {
2001
+ return {
2002
+ error: `No shards available for room ${roomId}`
2003
+ };
2004
+ }
2005
+ }
2006
+ const activeShards = roomShards.filter((shard) => shard && shard.status() === "active");
2007
+ if (activeShards.length === 0) {
2008
+ return {
2009
+ error: `No active shards available for room ${roomId}`
2010
+ };
2011
+ }
2012
+ const balancingStrategy = room.balancingStrategy();
2013
+ let selectedShard;
2014
+ switch (balancingStrategy) {
2015
+ case "least-connections":
2016
+ selectedShard = activeShards.reduce((min, shard) => shard.currentConnections() < min.currentConnections() ? shard : min, activeShards[0]);
2017
+ break;
2018
+ case "random":
2019
+ selectedShard = activeShards[Math.floor(Math.random() * activeShards.length)];
2020
+ break;
2021
+ case "round-robin":
2022
+ default:
2023
+ const counter = this.rrCounters()[roomId] || 0;
2024
+ const nextCounter = (counter + 1) % activeShards.length;
2025
+ this.rrCounters()[roomId] = nextCounter;
2026
+ selectedShard = activeShards[counter];
2027
+ break;
2028
+ }
2029
+ return {
2030
+ shardId: selectedShard.id,
2031
+ url: selectedShard.url()
2032
+ };
2033
+ }
2034
+ // Private methods
2035
+ async createShard(roomId, urlTemplate, maxConnections) {
2036
+ const room = this.rooms()[roomId];
2037
+ if (!room) {
2038
+ console.error(`Cannot create shard for non-existent room: ${roomId}`);
2039
+ return null;
2040
+ }
2041
+ const shardId = `${roomId}:${Date.now()}-${Math.floor(Math.random() * 1e4)}`;
2042
+ const template = urlTemplate || this.defaultShardUrlTemplate();
2043
+ const url = template.replace("{shardId}", shardId).replace("{roomId}", roomId);
2044
+ const max = maxConnections || room.maxPlayersPerShard();
2045
+ const newShard = new ShardInfo();
2046
+ newShard.id = shardId;
2047
+ newShard.roomId.set(roomId);
2048
+ newShard.url.set(url);
2049
+ newShard.maxConnections.set(max);
2050
+ newShard.currentConnections.set(0);
2051
+ newShard.status.set("active");
2052
+ newShard.lastHeartbeat.set(Date.now());
2053
+ this.shards()[shardId] = newShard;
2054
+ return newShard;
2055
+ }
2056
+ };
2057
+ _ts_decorate([
2058
+ sync(RoomConfig)
2059
+ ], WorldRoom.prototype, "rooms", void 0);
2060
+ _ts_decorate([
2061
+ sync(ShardInfo)
2062
+ ], WorldRoom.prototype, "shards", void 0);
2063
+ _ts_decorate([
2064
+ persist()
2065
+ ], WorldRoom.prototype, "rrCounters", void 0);
2066
+ _ts_decorate([
2067
+ Request2({
2068
+ path: "register-room",
2069
+ method: "POST"
2070
+ }),
2071
+ Guard([
2072
+ guardManageWorld
2073
+ ]),
2074
+ _ts_metadata("design:type", Function),
2075
+ _ts_metadata("design:paramtypes", [
2076
+ typeof party_exports === "undefined" || typeof void 0 === "undefined" ? Object : void 0
2077
+ ]),
2078
+ _ts_metadata("design:returntype", Promise)
2079
+ ], WorldRoom.prototype, "registerRoom", null);
2080
+ _ts_decorate([
2081
+ Request2({
2082
+ path: "update-shard",
2083
+ method: "POST"
2084
+ }),
2085
+ Guard([
2086
+ guardManageWorld
2087
+ ]),
2088
+ _ts_metadata("design:type", Function),
2089
+ _ts_metadata("design:paramtypes", [
2090
+ typeof party_exports === "undefined" || typeof void 0 === "undefined" ? Object : void 0
2091
+ ]),
2092
+ _ts_metadata("design:returntype", Promise)
2093
+ ], WorldRoom.prototype, "updateShardStats", null);
2094
+ _ts_decorate([
2095
+ Request2({
2096
+ path: "scale-room",
2097
+ method: "POST"
2098
+ }),
2099
+ Guard([
2100
+ guardManageWorld
2101
+ ]),
2102
+ _ts_metadata("design:type", Function),
2103
+ _ts_metadata("design:paramtypes", [
2104
+ typeof party_exports === "undefined" || typeof void 0 === "undefined" ? Object : void 0
2105
+ ]),
2106
+ _ts_metadata("design:returntype", Promise)
2107
+ ], WorldRoom.prototype, "scaleRoom", null);
2108
+ _ts_decorate([
2109
+ Request2({
2110
+ path: "connect",
2111
+ method: "POST"
2112
+ }),
2113
+ _ts_metadata("design:type", Function),
2114
+ _ts_metadata("design:paramtypes", [
2115
+ typeof party_exports === "undefined" || typeof void 0 === "undefined" ? Object : void 0
2116
+ ]),
2117
+ _ts_metadata("design:returntype", Promise)
2118
+ ], WorldRoom.prototype, "connect", null);
2119
+ WorldRoom = _ts_decorate([
2120
+ Room({
2121
+ path: "world-{worldId}",
2122
+ maxUsers: 100,
2123
+ throttleStorage: 2e3,
2124
+ throttleSync: 500
2125
+ }),
2126
+ _ts_metadata("design:type", Function),
2127
+ _ts_metadata("design:paramtypes", [
2128
+ typeof party_exports === "undefined" || typeof void 0 === "undefined" ? Object : void 0
2129
+ ])
2130
+ ], WorldRoom);
748
2131
  export {
749
2132
  Action,
750
2133
  ClientIo,
751
2134
  Guard,
752
2135
  MockConnection,
2136
+ Request2 as Request,
753
2137
  Room,
754
2138
  RoomGuard,
755
2139
  Server,
756
2140
  ServerIo,
2141
+ Shard,
2142
+ WorldRoom,
2143
+ request,
757
2144
  testRoom
758
2145
  };
759
2146
  //# sourceMappingURL=index.js.map