@signe/room 1.4.2 → 2.0.1

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,266 @@ 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");
328
+
329
+ // src/request/response.ts
330
+ var ServerResponse = class {
331
+ static {
332
+ __name(this, "ServerResponse");
333
+ }
334
+ interceptors;
335
+ statusCode = 200;
336
+ responseBody = {};
337
+ responseHeaders = {
338
+ "Content-Type": "application/json"
339
+ };
340
+ /**
341
+ * Creates a new ServerResponse instance
342
+ * @param interceptors Array of interceptor functions that can modify the response
343
+ */
344
+ constructor(interceptors = []) {
345
+ this.interceptors = interceptors;
346
+ }
347
+ /**
348
+ * Sets the status code for the response
349
+ * @param code HTTP status code
350
+ * @returns this instance for chaining
351
+ */
352
+ status(code) {
353
+ this.statusCode = code;
354
+ return this;
355
+ }
356
+ /**
357
+ * Sets the response body and returns this instance (chainable method)
358
+ *
359
+ * @param body Response body
360
+ * @returns this instance for chaining
361
+ */
362
+ body(body) {
363
+ this.responseBody = body;
364
+ return this;
365
+ }
366
+ /**
367
+ * Adds a header to the response
368
+ * @param name Header name
369
+ * @param value Header value
370
+ * @returns this instance for chaining
371
+ */
372
+ header(name, value) {
373
+ this.responseHeaders[name] = value;
374
+ return this;
375
+ }
376
+ /**
377
+ * Adds multiple headers to the response
378
+ * @param headers Object containing headers
379
+ * @returns this instance for chaining
380
+ */
381
+ setHeaders(headers) {
382
+ this.responseHeaders = {
383
+ ...this.responseHeaders,
384
+ ...headers
385
+ };
386
+ return this;
387
+ }
388
+ /**
389
+ * Add an interceptor to the chain
390
+ * @param interceptor Function that takes a Response and returns a modified Response
391
+ * @returns this instance for chaining
392
+ */
393
+ use(interceptor) {
394
+ this.interceptors.push(interceptor);
395
+ return this;
396
+ }
397
+ /**
398
+ * Builds and returns the Response object after applying all interceptors
399
+ * @returns Promise<Response> The final Response object
400
+ * @private Internal method used by terminal methods
401
+ */
402
+ async buildResponse() {
403
+ let response2 = new Response(JSON.stringify(this.responseBody), {
404
+ status: this.statusCode,
405
+ headers: this.responseHeaders
406
+ });
407
+ for (const interceptor of this.interceptors) {
408
+ try {
409
+ const interceptedResponse = interceptor(response2);
410
+ if (interceptedResponse instanceof Promise) {
411
+ response2 = await interceptedResponse;
412
+ } else {
413
+ response2 = interceptedResponse;
414
+ }
415
+ } catch (error) {
416
+ console.error("Error in interceptor:", error);
417
+ }
418
+ }
419
+ return response2;
420
+ }
421
+ /**
422
+ * Sets the response body to the JSON-stringified version of the provided value
423
+ * and sends the response (terminal method)
424
+ *
425
+ * @param body Response body to be JSON stringified
426
+ * @returns Promise<Response> The final Response object
427
+ */
428
+ async json(body) {
429
+ this.responseBody = body;
430
+ this.responseHeaders["Content-Type"] = "application/json";
431
+ return this.buildResponse();
432
+ }
433
+ /**
434
+ * Sends the response with the current configuration (terminal method)
435
+ *
436
+ * @param body Optional body to set before sending
437
+ * @returns Promise<Response> The final Response object
438
+ */
439
+ async send(body) {
440
+ if (body !== void 0) {
441
+ this.responseBody = body;
442
+ }
443
+ return this.buildResponse();
444
+ }
445
+ /**
446
+ * Sends a plain text response (terminal method)
447
+ *
448
+ * @param text Text to send
449
+ * @returns Promise<Response> The final Response object
450
+ */
451
+ async text(text) {
452
+ this.responseBody = text;
453
+ this.responseHeaders["Content-Type"] = "text/plain";
454
+ let response2 = new Response(text, {
455
+ status: this.statusCode,
456
+ headers: this.responseHeaders
457
+ });
458
+ for (const interceptor of this.interceptors) {
459
+ try {
460
+ const interceptedResponse = interceptor(response2);
461
+ if (interceptedResponse instanceof Promise) {
462
+ response2 = await interceptedResponse;
463
+ } else {
464
+ response2 = interceptedResponse;
465
+ }
466
+ } catch (error) {
467
+ console.error("Error in interceptor:", error);
468
+ }
469
+ }
470
+ return response2;
471
+ }
472
+ /**
473
+ * Redirects to the specified URL (terminal method)
474
+ *
475
+ * @param url URL to redirect to
476
+ * @param statusCode HTTP status code (default: 302)
477
+ * @returns Promise<Response> The final Response object
478
+ */
479
+ async redirect(url, statusCode = 302) {
480
+ this.statusCode = statusCode;
481
+ this.responseHeaders["Location"] = url;
482
+ return this.buildResponse();
483
+ }
484
+ /**
485
+ * Creates a success response with status 200
486
+ * @param body Response body
487
+ * @returns Promise<Response> The final Response object
488
+ */
489
+ async success(body = {}) {
490
+ return this.status(200).json(body);
491
+ }
492
+ /**
493
+ * Creates an error response with status 400
494
+ * @param message Error message
495
+ * @param details Additional error details
496
+ * @returns Promise<Response> The final Response object
497
+ */
498
+ async badRequest(message, details = {}) {
499
+ return this.status(400).json({
500
+ error: message,
501
+ ...details
502
+ });
503
+ }
504
+ /**
505
+ * Creates an error response with status 403
506
+ * @param message Error message
507
+ * @returns Promise<Response> The final Response object
508
+ */
509
+ async notPermitted(message = "Not permitted") {
510
+ return this.status(403).json({
511
+ error: message
512
+ });
513
+ }
514
+ /**
515
+ * Creates an error response with status 401
516
+ * @param message Error message
517
+ * @returns Promise<Response> The final Response object
518
+ */
519
+ async unauthorized(message = "Unauthorized") {
520
+ return this.status(401).json({
521
+ error: message
522
+ });
523
+ }
524
+ /**
525
+ * Creates an error response with status 404
526
+ * @param message Error message
527
+ * @returns Promise<Response> The final Response object
528
+ */
529
+ async notFound(message = "Not found") {
530
+ return this.status(404).json({
531
+ error: message
532
+ });
533
+ }
534
+ /**
535
+ * Creates an error response with status 500
536
+ * @param message Error message
537
+ * @returns Promise<Response> The final Response object
538
+ */
539
+ async serverError(message = "Internal Server Error") {
540
+ return this.status(500).json({
541
+ error: message
542
+ });
543
+ }
544
+ };
545
+
546
+ // src/request/cors.ts
547
+ function cors(res, options = {}) {
548
+ const newHeaders = new Headers(res.headers);
549
+ newHeaders.set("Access-Control-Allow-Origin", options.origin || "*");
550
+ if (options.credentials) {
551
+ newHeaders.set("Access-Control-Allow-Credentials", "true");
552
+ }
553
+ if (options.exposedHeaders && options.exposedHeaders.length) {
554
+ newHeaders.set("Access-Control-Expose-Headers", options.exposedHeaders.join(", "));
555
+ }
556
+ if (options.methods && options.methods.length) {
557
+ newHeaders.set("Access-Control-Allow-Methods", options.methods.join(", "));
558
+ } else {
559
+ newHeaders.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
560
+ }
561
+ if (options.allowedHeaders && options.allowedHeaders.length) {
562
+ newHeaders.set("Access-Control-Allow-Headers", options.allowedHeaders.join(", "));
563
+ } else {
564
+ newHeaders.set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
565
+ }
566
+ if (options.maxAge) {
567
+ newHeaders.set("Access-Control-Max-Age", options.maxAge.toString());
568
+ }
569
+ return new Response(res.body, {
570
+ status: res.status,
571
+ headers: newHeaders
572
+ });
573
+ }
574
+ __name(cors, "cors");
575
+ function createCorsInterceptor(options = {}) {
576
+ return (res) => cors(res, options);
577
+ }
578
+ __name(createCorsInterceptor, "createCorsInterceptor");
261
579
 
262
580
  // src/server.ts
263
581
  var Message = z.object({
@@ -302,6 +620,22 @@ var Server = class {
302
620
  get roomStorage() {
303
621
  return this.room.storage;
304
622
  }
623
+ async send(conn, obj, subRoom) {
624
+ obj = structuredClone(obj);
625
+ if (subRoom.interceptorPacket) {
626
+ const signal2 = this.getUsersProperty(subRoom);
627
+ const { publicId } = conn.state;
628
+ const user = signal2?.()[publicId];
629
+ obj = await awaitReturn(subRoom["interceptorPacket"]?.(user, obj, conn));
630
+ if (obj === null) return;
631
+ }
632
+ conn.send(JSON.stringify(obj));
633
+ }
634
+ broadcast(obj, subRoom) {
635
+ for (let conn of this.room.getConnections()) {
636
+ this.send(conn, obj, subRoom);
637
+ }
638
+ }
305
639
  /**
306
640
  * @method onStart
307
641
  * @async
@@ -415,10 +749,10 @@ var Server = class {
415
749
  return;
416
750
  }
417
751
  const packet = buildObject(values, instance.$memoryAll);
418
- this.room.broadcast(JSON.stringify({
752
+ this.broadcast({
419
753
  type: "sync",
420
754
  value: packet
421
- }));
755
+ }, instance);
422
756
  values.clear();
423
757
  }, "syncCb");
424
758
  const persistCb = /* @__PURE__ */ __name(async (values) => {
@@ -526,23 +860,7 @@ var Server = class {
526
860
  async deleteSession(privateId) {
527
861
  await this.room.storage.delete(`session:${privateId}`);
528
862
  }
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) {
863
+ async onConnectClient(conn, ctx) {
546
864
  const subRoom = await this.getSubRoom({
547
865
  getMemoryAll: true
548
866
  });
@@ -565,13 +883,13 @@ var Server = class {
565
883
  const existingSession = await this.getSession(conn.id);
566
884
  const publicId = existingSession?.publicId || generateShortUUID2();
567
885
  let user = null;
568
- const signal = this.getUsersProperty(subRoom);
886
+ const signal2 = this.getUsersProperty(subRoom);
569
887
  const usersPropName = this.getUsersPropName(subRoom);
570
- if (signal) {
571
- const { classType } = signal.options;
888
+ if (signal2) {
889
+ const { classType } = signal2.options;
572
890
  if (!existingSession?.publicId) {
573
891
  user = isClass(classType) ? new classType() : classType(conn, ctx);
574
- signal()[publicId] = user;
892
+ signal2()[publicId] = user;
575
893
  const snapshot = createStatesSnapshot(user);
576
894
  this.room.storage.put(`${usersPropName}.${publicId}`, snapshot);
577
895
  }
@@ -585,33 +903,70 @@ var Server = class {
585
903
  }
586
904
  await awaitReturn(subRoom["onJoin"]?.(user, conn, ctx));
587
905
  conn.setState({
906
+ ...conn.state,
588
907
  publicId
589
908
  });
590
- conn.send(JSON.stringify({
909
+ this.send(conn, {
591
910
  type: "sync",
592
911
  value: {
593
912
  pId: publicId,
594
913
  ...subRoom.$memoryAll
595
914
  }
596
- }));
915
+ }, subRoom);
597
916
  }
598
917
  /**
599
- * @method onMessage
918
+ * @method onConnect
600
919
  * @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.
920
+ * @param {Party.Connection} conn - The connection object for the new user.
921
+ * @param {Party.ConnectionContext} ctx - The context of the connection.
922
+ * @description Handles a new user connection, creates a user object, and sends initial sync data.
604
923
  * @returns {Promise<void>}
605
924
  *
606
925
  * @example
607
926
  * ```typescript
608
- * server.onMessage = async (message, sender) => {
609
- * await server.onMessage(message, sender);
610
- * console.log("Message processed from:", sender.id);
927
+ * server.onConnect = async (conn, ctx) => {
928
+ * await server.onConnect(conn, ctx);
929
+ * console.log("New user connected:", conn.id);
611
930
  * };
612
931
  * ```
613
932
  */
933
+ async onConnect(conn, ctx) {
934
+ if (ctx.request?.headers.has("x-shard-id")) {
935
+ this.onConnectShard(conn, ctx);
936
+ } else {
937
+ await this.onConnectClient(conn, ctx);
938
+ }
939
+ }
940
+ /**
941
+ * @method onConnectShard
942
+ * @private
943
+ * @param {Party.Connection} conn - The connection object for the new shard.
944
+ * @param {Party.ConnectionContext} ctx - The context of the shard connection.
945
+ * @description Handles a new shard connection, setting up the necessary state.
946
+ * @returns {void}
947
+ */
948
+ onConnectShard(conn, ctx) {
949
+ const shardId = ctx.request?.headers.get("x-shard-id") || "unknown-shard";
950
+ conn.setState({
951
+ shard: true,
952
+ shardId,
953
+ clients: /* @__PURE__ */ new Map()
954
+ // Track clients connected through this shard
955
+ });
956
+ }
957
+ /**
958
+ * @method onMessage
959
+ * @async
960
+ * @param {string} message - The message received from a user or shard.
961
+ * @param {Party.Connection} sender - The connection object of the sender.
962
+ * @description Processes incoming messages, handling differently based on if sender is shard or client.
963
+ * @returns {Promise<void>}
964
+ */
614
965
  async onMessage(message, sender) {
966
+ if (sender.state && sender.state.shard) {
967
+ await this.handleShardMessage(message, sender);
968
+ return;
969
+ }
615
970
  let json;
616
971
  try {
617
972
  json = JSON.parse(message);
@@ -625,16 +980,16 @@ var Server = class {
625
980
  const subRoom = await this.getSubRoom();
626
981
  const roomGuards = subRoom.constructor["_roomGuards"] || [];
627
982
  for (const guard of roomGuards) {
628
- const isAuthorized = await guard(sender, result.data.value);
983
+ const isAuthorized = await guard(sender, result.data.value, this.room);
629
984
  if (!isAuthorized) {
630
985
  return;
631
986
  }
632
987
  }
633
988
  const actions = subRoom.constructor["_actionMetadata"];
634
989
  if (actions) {
635
- const signal = this.getUsersProperty(subRoom);
990
+ const signal2 = this.getUsersProperty(subRoom);
636
991
  const { publicId } = sender.state;
637
- const user = signal?.()[publicId];
992
+ const user = signal2?.()[publicId];
638
993
  const actionName = actions.get(result.data.action);
639
994
  if (actionName) {
640
995
  const guards = subRoom.constructor["_actionGuards"]?.get(actionName.key) || [];
@@ -655,6 +1010,173 @@ var Server = class {
655
1010
  }
656
1011
  }
657
1012
  /**
1013
+ * @method handleShardMessage
1014
+ * @private
1015
+ * @async
1016
+ * @param {string} message - The message received from a shard.
1017
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
1018
+ * @description Processes messages from shards, extracting client information.
1019
+ * @returns {Promise<void>}
1020
+ */
1021
+ async handleShardMessage(message, shardConnection) {
1022
+ let parsedMessage;
1023
+ try {
1024
+ parsedMessage = JSON.parse(message);
1025
+ } catch (e) {
1026
+ console.error("Error parsing shard message:", e);
1027
+ return;
1028
+ }
1029
+ const shardState = shardConnection.state;
1030
+ const clients = shardState.clients;
1031
+ switch (parsedMessage.type) {
1032
+ case "shard.clientConnected":
1033
+ await this.handleShardClientConnect(parsedMessage, shardConnection);
1034
+ break;
1035
+ case "shard.clientMessage":
1036
+ await this.handleShardClientMessage(parsedMessage, shardConnection);
1037
+ break;
1038
+ case "shard.clientDisconnected":
1039
+ await this.handleShardClientDisconnect(parsedMessage, shardConnection);
1040
+ break;
1041
+ default:
1042
+ console.warn(`Unknown shard message type: ${parsedMessage.type}`);
1043
+ }
1044
+ }
1045
+ /**
1046
+ * @method handleShardClientConnect
1047
+ * @private
1048
+ * @async
1049
+ * @param {Object} message - The client connection message from a shard.
1050
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
1051
+ * @description Handles a new client connection via a shard.
1052
+ * @returns {Promise<void>}
1053
+ */
1054
+ async handleShardClientConnect(message, shardConnection) {
1055
+ const { privateId, connectionInfo } = message;
1056
+ const shardState = shardConnection.state;
1057
+ const virtualContext = {
1058
+ request: {
1059
+ headers: new Headers({
1060
+ "x-forwarded-for": connectionInfo.ip,
1061
+ "user-agent": connectionInfo.userAgent
1062
+ }),
1063
+ method: "GET",
1064
+ url: ""
1065
+ }
1066
+ };
1067
+ const virtualConnection = {
1068
+ id: privateId,
1069
+ send: /* @__PURE__ */ __name((data) => {
1070
+ shardConnection.send(JSON.stringify({
1071
+ targetClientId: privateId,
1072
+ data
1073
+ }));
1074
+ }, "send"),
1075
+ state: {},
1076
+ setState: /* @__PURE__ */ __name((state) => {
1077
+ const clients = shardState.clients;
1078
+ const currentState = clients.get(privateId) || {};
1079
+ const mergedState = Object.assign({}, currentState, state);
1080
+ clients.set(privateId, mergedState);
1081
+ virtualConnection.state = clients.get(privateId);
1082
+ return virtualConnection.state;
1083
+ }, "setState"),
1084
+ close: /* @__PURE__ */ __name(() => {
1085
+ shardConnection.send(JSON.stringify({
1086
+ type: "shard.closeClient",
1087
+ privateId
1088
+ }));
1089
+ if (shardState.clients) {
1090
+ shardState.clients.delete(privateId);
1091
+ }
1092
+ }, "close")
1093
+ };
1094
+ if (!shardState.clients.has(privateId)) {
1095
+ shardState.clients.set(privateId, {});
1096
+ }
1097
+ await this.onConnectClient(virtualConnection, virtualContext);
1098
+ }
1099
+ /**
1100
+ * @method handleShardClientMessage
1101
+ * @private
1102
+ * @async
1103
+ * @param {Object} message - The client message from a shard.
1104
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
1105
+ * @description Handles a message from a client via a shard.
1106
+ * @returns {Promise<void>}
1107
+ */
1108
+ async handleShardClientMessage(message, shardConnection) {
1109
+ const { privateId, publicId, payload } = message;
1110
+ const shardState = shardConnection.state;
1111
+ const clients = shardState.clients;
1112
+ if (!clients.has(privateId)) {
1113
+ console.warn(`Received message from unknown client ${privateId}, creating virtual connection`);
1114
+ clients.set(privateId, {
1115
+ publicId
1116
+ });
1117
+ }
1118
+ const virtualConnection = {
1119
+ id: privateId,
1120
+ send: /* @__PURE__ */ __name((data) => {
1121
+ shardConnection.send(JSON.stringify({
1122
+ targetClientId: privateId,
1123
+ data
1124
+ }));
1125
+ }, "send"),
1126
+ state: clients.get(privateId),
1127
+ setState: /* @__PURE__ */ __name((state) => {
1128
+ const currentState = clients.get(privateId) || {};
1129
+ const mergedState = Object.assign({}, currentState, state);
1130
+ clients.set(privateId, mergedState);
1131
+ virtualConnection.state = clients.get(privateId);
1132
+ return virtualConnection.state;
1133
+ }, "setState"),
1134
+ close: /* @__PURE__ */ __name(() => {
1135
+ shardConnection.send(JSON.stringify({
1136
+ type: "shard.closeClient",
1137
+ privateId
1138
+ }));
1139
+ if (shardState.clients) {
1140
+ shardState.clients.delete(privateId);
1141
+ }
1142
+ }, "close")
1143
+ };
1144
+ const payloadString = typeof payload === "string" ? payload : JSON.stringify(payload);
1145
+ await this.onMessage(payloadString, virtualConnection);
1146
+ }
1147
+ /**
1148
+ * @method handleShardClientDisconnect
1149
+ * @private
1150
+ * @async
1151
+ * @param {Object} message - The client disconnection message from a shard.
1152
+ * @param {Party.Connection} shardConnection - The connection object of the shard.
1153
+ * @description Handles a client disconnection via a shard.
1154
+ * @returns {Promise<void>}
1155
+ */
1156
+ async handleShardClientDisconnect(message, shardConnection) {
1157
+ const { privateId, publicId } = message;
1158
+ const shardState = shardConnection.state;
1159
+ const clients = shardState.clients;
1160
+ const clientState = clients.get(privateId);
1161
+ if (!clientState) {
1162
+ console.warn(`Disconnection for unknown client ${privateId}`);
1163
+ return;
1164
+ }
1165
+ const virtualConnection = {
1166
+ id: privateId,
1167
+ send: /* @__PURE__ */ __name(() => {
1168
+ }, "send"),
1169
+ state: clientState,
1170
+ setState: /* @__PURE__ */ __name(() => {
1171
+ return {};
1172
+ }, "setState"),
1173
+ close: /* @__PURE__ */ __name(() => {
1174
+ }, "close")
1175
+ };
1176
+ await this.onClose(virtualConnection);
1177
+ clients.delete(privateId);
1178
+ }
1179
+ /**
658
1180
  * @method onClose
659
1181
  * @async
660
1182
  * @param {Party.Connection} conn - The connection object of the disconnecting user.
@@ -674,22 +1196,22 @@ var Server = class {
674
1196
  if (!subRoom) {
675
1197
  return;
676
1198
  }
677
- const signal = this.getUsersProperty(subRoom);
1199
+ const signal2 = this.getUsersProperty(subRoom);
678
1200
  if (!conn.state) {
679
1201
  return;
680
1202
  }
681
1203
  const privateId = conn.id;
682
1204
  const { publicId } = conn.state;
683
- const user = signal?.()[publicId];
1205
+ const user = signal2?.()[publicId];
684
1206
  if (!user) return;
685
1207
  await awaitReturn(subRoom["onLeave"]?.(user, conn));
686
1208
  await this.updateSessionConnection(privateId, false);
687
- this.room.broadcast(JSON.stringify({
1209
+ this.broadcast({
688
1210
  type: "user_disconnected",
689
1211
  value: {
690
1212
  publicId
691
1213
  }
692
- }));
1214
+ }, subRoom);
693
1215
  }
694
1216
  async onAlarm() {
695
1217
  const subRoom = await this.getSubRoom();
@@ -699,41 +1221,440 @@ var Server = class {
699
1221
  const subRoom = await this.getSubRoom();
700
1222
  await awaitReturn(subRoom["onError"]?.(connection, error));
701
1223
  }
1224
+ /**
1225
+ * @method onRequest
1226
+ * @async
1227
+ * @param {Party.Request} req - The HTTP request to handle
1228
+ * @description Handles HTTP requests, either directly from clients or forwarded by shards
1229
+ * @returns {Promise<Response>} The response to return to the client
1230
+ */
702
1231
  async onRequest(req) {
1232
+ const isFromShard = req.headers.has("x-forwarded-by-shard");
1233
+ const shardId = req.headers.get("x-shard-id");
1234
+ const res = new ServerResponse([
1235
+ createCorsInterceptor()
1236
+ ]);
1237
+ if (isFromShard) {
1238
+ return this.handleShardRequest(req, res, shardId);
1239
+ }
1240
+ return this.handleDirectRequest(req, res);
1241
+ }
1242
+ /**
1243
+ * @method handleDirectRequest
1244
+ * @private
1245
+ * @async
1246
+ * @param {Party.Request} req - The HTTP request received directly from a client
1247
+ * @description Processes requests received directly from clients
1248
+ * @returns {Promise<Response>} The response to return to the client
1249
+ */
1250
+ async handleDirectRequest(req, res) {
703
1251
  const subRoom = await this.getSubRoom();
704
- const res = /* @__PURE__ */ __name((body, status) => {
705
- return new Response(JSON.stringify(body), {
706
- status
707
- });
708
- }, "res");
709
1252
  if (!subRoom) {
710
- return res({
711
- error: "Not found"
712
- }, 404);
1253
+ return res.notFound();
1254
+ }
1255
+ const response2 = await this.tryMatchRequestHandler(req, res, subRoom);
1256
+ if (response2) {
1257
+ return response2;
713
1258
  }
714
- const response = await awaitReturn(subRoom["onRequest"]?.(req, this.room));
715
- if (!response) {
716
- return res({
717
- error: "Not found"
718
- }, 404);
1259
+ const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(req, res));
1260
+ if (!legacyResponse) {
1261
+ return res.notFound();
719
1262
  }
720
- if (response instanceof Response) {
721
- return response;
1263
+ if (legacyResponse instanceof Response) {
1264
+ return legacyResponse;
1265
+ }
1266
+ return res.success(legacyResponse);
1267
+ }
1268
+ /**
1269
+ * @method tryMatchRequestHandler
1270
+ * @private
1271
+ * @async
1272
+ * @param {Party.Request} req - The HTTP request to handle
1273
+ * @param {Object} subRoom - The room instance
1274
+ * @description Attempts to match the request to a registered @Request handler
1275
+ * @returns {Promise<Response | null>} The response or null if no handler matched
1276
+ */
1277
+ async tryMatchRequestHandler(req, res, subRoom) {
1278
+ const requestHandlers = subRoom.constructor["_requestMetadata"];
1279
+ if (!requestHandlers) {
1280
+ return null;
722
1281
  }
723
- return res(response, 200);
1282
+ const url = new URL(req.url);
1283
+ const method = req.method;
1284
+ let pathname = url.pathname;
1285
+ pathname = "/" + pathname.split("/").slice(4).join("/");
1286
+ for (const [routeKey, handler] of requestHandlers.entries()) {
1287
+ const firstColonIndex = routeKey.indexOf(":");
1288
+ const handlerMethod = routeKey.substring(0, firstColonIndex);
1289
+ const handlerPath = routeKey.substring(firstColonIndex + 1);
1290
+ if (handlerMethod !== method) {
1291
+ continue;
1292
+ }
1293
+ if (this.pathMatches(pathname, handlerPath)) {
1294
+ const params = this.extractPathParams(pathname, handlerPath);
1295
+ const guards = subRoom.constructor["_actionGuards"]?.get(handler.key) || [];
1296
+ for (const guard of guards) {
1297
+ const isAuthorized = await guard(null, req, this.room);
1298
+ if (isAuthorized instanceof Response) {
1299
+ return isAuthorized;
1300
+ }
1301
+ if (!isAuthorized) {
1302
+ return res.notPermitted();
1303
+ }
1304
+ }
1305
+ let bodyData = null;
1306
+ if (handler.bodyValidation && [
1307
+ "POST",
1308
+ "PUT",
1309
+ "PATCH"
1310
+ ].includes(method)) {
1311
+ try {
1312
+ const contentType = req.headers.get("content-type") || "";
1313
+ if (contentType.includes("application/json")) {
1314
+ const body = await req.json();
1315
+ const validation = handler.bodyValidation.safeParse(body);
1316
+ if (!validation.success) {
1317
+ return res.badRequest("Invalid request body", {
1318
+ details: validation.error
1319
+ });
1320
+ }
1321
+ bodyData = validation.data;
1322
+ }
1323
+ } catch (error) {
1324
+ return res.badRequest("Failed to parse request body");
1325
+ }
1326
+ }
1327
+ try {
1328
+ req["data"] = bodyData;
1329
+ req["params"] = params;
1330
+ const result = await awaitReturn(subRoom[handler.key](req, res));
1331
+ if (result instanceof Response) {
1332
+ return result;
1333
+ }
1334
+ return res.success(result);
1335
+ } catch (error) {
1336
+ console.error("Error executing request handler:", error);
1337
+ return res.serverError();
1338
+ }
1339
+ }
1340
+ }
1341
+ return null;
1342
+ }
1343
+ /**
1344
+ * @method pathMatches
1345
+ * @private
1346
+ * @param {string} requestPath - The path from the request
1347
+ * @param {string} handlerPath - The path pattern from the handler
1348
+ * @description Checks if a request path matches a handler path pattern
1349
+ * @returns {boolean} True if the paths match
1350
+ */
1351
+ pathMatches(requestPath, handlerPath) {
1352
+ const pathRegexString = handlerPath.replace(/\//g, "\\/").replace(/:([^\/]+)/g, "([^/]+)");
1353
+ const pathRegex = new RegExp(`^${pathRegexString}$`);
1354
+ return pathRegex.test(requestPath);
1355
+ }
1356
+ /**
1357
+ * @method extractPathParams
1358
+ * @private
1359
+ * @param {string} requestPath - The path from the request
1360
+ * @param {string} handlerPath - The path pattern from the handler
1361
+ * @description Extracts path parameters from the request path based on the handler pattern
1362
+ * @returns {Object} An object containing the path parameters
1363
+ */
1364
+ extractPathParams(requestPath, handlerPath) {
1365
+ const params = {};
1366
+ const paramNames = [];
1367
+ handlerPath.split("/").forEach((segment) => {
1368
+ if (segment.startsWith(":")) {
1369
+ paramNames.push(segment.substring(1));
1370
+ }
1371
+ });
1372
+ const pathRegexString = handlerPath.replace(/\//g, "\\/").replace(/:([^\/]+)/g, "([^/]+)");
1373
+ const pathRegex = new RegExp(`^${pathRegexString}$`);
1374
+ const matches = requestPath.match(pathRegex);
1375
+ if (matches && matches.length > 1) {
1376
+ for (let i = 0; i < paramNames.length; i++) {
1377
+ params[paramNames[i]] = matches[i + 1];
1378
+ }
1379
+ }
1380
+ return params;
1381
+ }
1382
+ /**
1383
+ * @method handleShardRequest
1384
+ * @private
1385
+ * @async
1386
+ * @param {Party.Request} req - The HTTP request forwarded by a shard
1387
+ * @param {string | null} shardId - The ID of the shard that forwarded the request
1388
+ * @description Processes requests forwarded by shards, preserving client context
1389
+ * @returns {Promise<Response>} The response to return to the shard (which will forward it to the client)
1390
+ */
1391
+ async handleShardRequest(req, res, shardId) {
1392
+ const subRoom = await this.getSubRoom();
1393
+ if (!subRoom) {
1394
+ return res.notFound();
1395
+ }
1396
+ const originalClientIp = req.headers.get("x-original-client-ip");
1397
+ const enhancedReq = this.createEnhancedRequest(req, originalClientIp);
1398
+ try {
1399
+ const response2 = await this.tryMatchRequestHandler(enhancedReq, res, subRoom);
1400
+ if (response2) {
1401
+ return response2;
1402
+ }
1403
+ const legacyResponse = await awaitReturn(subRoom["onRequest"]?.(enhancedReq, res));
1404
+ if (!legacyResponse) {
1405
+ return res.notFound();
1406
+ }
1407
+ if (legacyResponse instanceof Response) {
1408
+ return legacyResponse;
1409
+ }
1410
+ return res.success(legacyResponse);
1411
+ } catch (error) {
1412
+ console.error(`Error processing request from shard ${shardId}:`, error);
1413
+ return res.serverError();
1414
+ }
1415
+ }
1416
+ /**
1417
+ * @method createEnhancedRequest
1418
+ * @private
1419
+ * @param {Party.Request} originalReq - The original request received from the shard
1420
+ * @param {string | null} originalClientIp - The original client IP, if available
1421
+ * @description Creates an enhanced request object that preserves the original client context
1422
+ * @returns {Party.Request} The enhanced request object
1423
+ */
1424
+ createEnhancedRequest(originalReq, originalClientIp) {
1425
+ const clonedReq = originalReq.clone();
1426
+ clonedReq.viaShard = true;
1427
+ if (originalClientIp) {
1428
+ clonedReq.originalClientIp = originalClientIp;
1429
+ }
1430
+ return clonedReq;
1431
+ }
1432
+ };
1433
+
1434
+ // src/shard.ts
1435
+ var Shard = class {
1436
+ static {
1437
+ __name(this, "Shard");
1438
+ }
1439
+ room;
1440
+ ws;
1441
+ connectionMap;
1442
+ mainServerStub;
1443
+ worldUrl;
1444
+ worldId;
1445
+ lastReportedConnections;
1446
+ statsInterval;
1447
+ statsIntervalId;
1448
+ constructor(room) {
1449
+ this.room = room;
1450
+ this.connectionMap = /* @__PURE__ */ new Map();
1451
+ this.worldUrl = null;
1452
+ this.worldId = "default";
1453
+ this.lastReportedConnections = 0;
1454
+ this.statsInterval = 3e4;
1455
+ this.statsIntervalId = null;
1456
+ }
1457
+ async onStart() {
1458
+ const roomId = this.room.id.split(":")[0];
1459
+ const roomStub = this.room.context.parties.main.get(roomId);
1460
+ if (!roomStub) {
1461
+ console.warn("No room room stub found in main party context");
1462
+ return;
1463
+ }
1464
+ this.mainServerStub = roomStub;
1465
+ this.ws = await roomStub.socket({
1466
+ headers: {
1467
+ "x-shard-id": this.room.id
1468
+ }
1469
+ });
1470
+ this.ws.addEventListener("message", (event) => {
1471
+ try {
1472
+ const message = JSON.parse(event.data);
1473
+ if (message.targetClientId) {
1474
+ const clientConn = this.connectionMap.get(message.targetClientId);
1475
+ if (clientConn) {
1476
+ delete message.targetClientId;
1477
+ clientConn.send(message.data);
1478
+ }
1479
+ } else {
1480
+ this.room.broadcast(event.data);
1481
+ }
1482
+ } catch (error) {
1483
+ console.error("Error processing message from main server:", error);
1484
+ }
1485
+ });
1486
+ await this.updateWorldStats();
1487
+ this.startPeriodicStatsUpdates();
1488
+ }
1489
+ startPeriodicStatsUpdates() {
1490
+ if (!this.worldUrl) {
1491
+ return;
1492
+ }
1493
+ if (this.statsIntervalId) {
1494
+ clearInterval(this.statsIntervalId);
1495
+ }
1496
+ this.statsIntervalId = setInterval(() => {
1497
+ this.updateWorldStats().catch((error) => {
1498
+ console.error("Error in periodic stats update:", error);
1499
+ });
1500
+ }, this.statsInterval);
1501
+ }
1502
+ stopPeriodicStatsUpdates() {
1503
+ if (this.statsIntervalId) {
1504
+ clearInterval(this.statsIntervalId);
1505
+ this.statsIntervalId = null;
1506
+ }
1507
+ }
1508
+ onConnect(conn, ctx) {
1509
+ this.connectionMap.set(conn.id, conn);
1510
+ this.ws.send(JSON.stringify({
1511
+ type: "shard.clientConnected",
1512
+ privateId: conn.id,
1513
+ connectionInfo: {
1514
+ ip: ctx.request?.headers.get("x-forwarded-for") || "unknown",
1515
+ userAgent: ctx.request?.headers.get("user-agent") || "unknown"
1516
+ }
1517
+ }));
1518
+ this.updateWorldStats();
1519
+ }
1520
+ onMessage(message, sender) {
1521
+ try {
1522
+ const parsedMessage = typeof message === "string" ? JSON.parse(message) : message;
1523
+ const wrappedMessage = JSON.stringify({
1524
+ type: "shard.clientMessage",
1525
+ privateId: sender.id,
1526
+ publicId: sender.state?.publicId,
1527
+ payload: parsedMessage
1528
+ });
1529
+ this.ws.send(wrappedMessage);
1530
+ } catch (error) {
1531
+ console.error("Error forwarding message to main server:", error);
1532
+ }
1533
+ }
1534
+ onClose(conn) {
1535
+ this.connectionMap.delete(conn.id);
1536
+ this.ws.send(JSON.stringify({
1537
+ type: "shard.clientDisconnected",
1538
+ privateId: conn.id,
1539
+ publicId: conn.state?.publicId
1540
+ }));
1541
+ this.updateWorldStats();
1542
+ }
1543
+ async updateWorldStats() {
1544
+ const currentConnections = this.connectionMap.size;
1545
+ if (currentConnections === this.lastReportedConnections) {
1546
+ return true;
1547
+ }
1548
+ try {
1549
+ const worldRoom = this.room.context.parties.world.get("world-default");
1550
+ const response2 = await worldRoom.fetch("/update-shard", {
1551
+ method: "POST",
1552
+ headers: {
1553
+ "Content-Type": "application/json",
1554
+ "x-access-shard": this.room.env.SHARD_SECRET
1555
+ },
1556
+ body: JSON.stringify({
1557
+ shardId: this.room.id,
1558
+ connections: currentConnections
1559
+ })
1560
+ });
1561
+ if (!response2.ok) {
1562
+ const errorData = await response2.json().catch(() => ({
1563
+ error: "Unknown error"
1564
+ }));
1565
+ console.error(`Failed to update World stats: ${response2.status} - ${errorData.error || "Unknown error"}`);
1566
+ return false;
1567
+ }
1568
+ this.lastReportedConnections = currentConnections;
1569
+ return true;
1570
+ } catch (error) {
1571
+ console.error("Error updating World stats:", error);
1572
+ return false;
1573
+ }
1574
+ }
1575
+ /**
1576
+ * @method onRequest
1577
+ * @async
1578
+ * @param {Party.Request} req - The HTTP request to handle
1579
+ * @description Forwards HTTP requests to the main server, preserving client context
1580
+ * @returns {Promise<Response>} The response from the main server
1581
+ */
1582
+ async onRequest(req) {
1583
+ if (!this.mainServerStub) {
1584
+ return response(503, {
1585
+ error: "Shard not connected to main server"
1586
+ });
1587
+ }
1588
+ try {
1589
+ const url = new URL(req.url);
1590
+ const path = url.pathname;
1591
+ const method = req.method;
1592
+ let body = null;
1593
+ if (method !== "GET" && method !== "HEAD") {
1594
+ body = await req.text();
1595
+ }
1596
+ const headers = new Headers();
1597
+ req.headers.forEach((value, key) => {
1598
+ headers.set(key, value);
1599
+ });
1600
+ headers.set("x-shard-id", this.room.id);
1601
+ headers.set("x-forwarded-by-shard", "true");
1602
+ const clientIp = req.headers.get("x-forwarded-for") || "unknown";
1603
+ if (clientIp) {
1604
+ headers.set("x-original-client-ip", clientIp);
1605
+ }
1606
+ const requestInit = {
1607
+ method,
1608
+ headers,
1609
+ body
1610
+ };
1611
+ const response2 = await this.mainServerStub.fetch(path, requestInit);
1612
+ return response2;
1613
+ } catch (error) {
1614
+ return response(500, {
1615
+ error: "Error forwarding request"
1616
+ });
1617
+ }
1618
+ }
1619
+ /**
1620
+ * @method onAlarm
1621
+ * @async
1622
+ * @description Executed periodically, used to perform maintenance tasks
1623
+ */
1624
+ async onAlarm() {
1625
+ await this.updateWorldStats();
724
1626
  }
725
1627
  };
726
1628
 
727
1629
  // 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
- ];
1630
+ async function testRoom(Room3, options = {}) {
1631
+ const createServer = /* @__PURE__ */ __name((io2) => {
1632
+ const server2 = new Server(io2);
1633
+ server2.rooms = [
1634
+ Room3
1635
+ ];
1636
+ return server2;
1637
+ }, "createServer");
1638
+ const isShard = options.shard || false;
1639
+ const io = new ServerIo(Room3.path, isShard ? {
1640
+ parties: {
1641
+ game: createServer
1642
+ },
1643
+ env: options.env
1644
+ } : {
1645
+ env: options.env
1646
+ });
1647
+ Room3.prototype.throttleSync = 0;
1648
+ Room3.prototype.throttleStorage = 0;
1649
+ Room3.prototype.options = options;
1650
+ let server;
1651
+ if (options.shard) {
1652
+ const shardServer = new Shard(io);
1653
+ shardServer.subRoom = null;
1654
+ server = shardServer;
1655
+ } else {
1656
+ server = await createServer(io);
1657
+ }
737
1658
  await server.onStart();
738
1659
  return {
739
1660
  server,
@@ -745,15 +1666,682 @@ async function testRoom(Room2, options = {}) {
745
1666
  };
746
1667
  }
747
1668
  __name(testRoom, "testRoom");
1669
+ async function request(room, path, options = {
1670
+ method: "GET"
1671
+ }) {
1672
+ const url = new URL("http://localhost" + path);
1673
+ const request1 = new Request(url.toString(), options);
1674
+ const response2 = await room.onRequest(request1);
1675
+ return response2;
1676
+ }
1677
+ __name(request, "request");
1678
+
1679
+ // src/world.ts
1680
+ import { signal } from "@signe/reactive";
1681
+ import { sync, id, persist } from "@signe/sync";
1682
+ import { z as z2 } from "zod";
1683
+
1684
+ // src/types/party.ts
1685
+ var party_exports = {};
1686
+
1687
+ // src/jwt.ts
1688
+ var JWTAuth = class {
1689
+ static {
1690
+ __name(this, "JWTAuth");
1691
+ }
1692
+ secret;
1693
+ encoder;
1694
+ decoder;
1695
+ /**
1696
+ * Constructor for the JWTAuth class
1697
+ * @param {string} secret - The secret key used for signing and verifying tokens
1698
+ */
1699
+ constructor(secret) {
1700
+ if (!secret || typeof secret !== "string") {
1701
+ throw new Error("Secret is required and must be a string");
1702
+ }
1703
+ this.secret = secret;
1704
+ this.encoder = new TextEncoder();
1705
+ this.decoder = new TextDecoder();
1706
+ }
1707
+ /**
1708
+ * Convert the secret to a CryptoKey for HMAC operations
1709
+ * @returns {Promise<CryptoKey>} - The CryptoKey for HMAC operations
1710
+ */
1711
+ async getSecretKey() {
1712
+ const keyData = this.encoder.encode(this.secret);
1713
+ return await crypto.subtle.importKey(
1714
+ "raw",
1715
+ keyData,
1716
+ {
1717
+ name: "HMAC",
1718
+ hash: {
1719
+ name: "SHA-256"
1720
+ }
1721
+ },
1722
+ false,
1723
+ [
1724
+ "sign",
1725
+ "verify"
1726
+ ]
1727
+ // key usages
1728
+ );
1729
+ }
1730
+ /**
1731
+ * Base64Url encode a buffer
1732
+ * @param {ArrayBuffer} buffer - The buffer to encode
1733
+ * @returns {string} - The base64url encoded string
1734
+ */
1735
+ base64UrlEncode(buffer) {
1736
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
1737
+ return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
1738
+ }
1739
+ /**
1740
+ * Base64Url decode a string
1741
+ * @param {string} base64Url - The base64url encoded string
1742
+ * @returns {ArrayBuffer} - The decoded buffer
1743
+ */
1744
+ base64UrlDecode(base64Url) {
1745
+ const padding = "=".repeat((4 - base64Url.length % 4) % 4);
1746
+ const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/") + padding;
1747
+ const rawData = atob(base64);
1748
+ const buffer = new Uint8Array(rawData.length);
1749
+ for (let i = 0; i < rawData.length; i++) {
1750
+ buffer[i] = rawData.charCodeAt(i);
1751
+ }
1752
+ return buffer.buffer;
1753
+ }
1754
+ /**
1755
+ * Sign a payload and create a JWT token
1756
+ * @param {JWTPayload} payload - The payload to include in the token
1757
+ * @param {JWTOptions} [options={}] - Options for the token
1758
+ * @param {string | number} [options.expiresIn='1h'] - Token expiration time
1759
+ * @returns {Promise<string>} - The JWT token
1760
+ */
1761
+ async sign(payload, options = {}) {
1762
+ if (!payload || typeof payload !== "object") {
1763
+ throw new Error("Payload must be an object");
1764
+ }
1765
+ const expiresIn = options.expiresIn || "1h";
1766
+ let exp;
1767
+ if (typeof expiresIn === "number") {
1768
+ exp = Math.floor(Date.now() / 1e3) + expiresIn;
1769
+ } else if (typeof expiresIn === "string") {
1770
+ const match = expiresIn.match(/^(\d+)([smhd])$/);
1771
+ if (match) {
1772
+ const value = parseInt(match[1]);
1773
+ const unit = match[2];
1774
+ const seconds = {
1775
+ "s": value,
1776
+ "m": value * 60,
1777
+ "h": value * 60 * 60,
1778
+ "d": value * 60 * 60 * 24
1779
+ }[unit];
1780
+ exp = Math.floor(Date.now() / 1e3) + seconds;
1781
+ } else {
1782
+ throw new Error('Invalid expiresIn format. Use a number (seconds) or a string like "1h", "30m", etc.');
1783
+ }
1784
+ }
1785
+ const fullPayload = {
1786
+ ...payload,
1787
+ iat: Math.floor(Date.now() / 1e3),
1788
+ exp
1789
+ };
1790
+ const header = {
1791
+ alg: "HS256",
1792
+ typ: "JWT"
1793
+ };
1794
+ const encodedHeader = this.base64UrlEncode(this.encoder.encode(JSON.stringify(header)));
1795
+ const encodedPayload = this.base64UrlEncode(this.encoder.encode(JSON.stringify(fullPayload)));
1796
+ const signatureBase = `${encodedHeader}.${encodedPayload}`;
1797
+ const key = await this.getSecretKey();
1798
+ const signature = await crypto.subtle.sign({
1799
+ name: "HMAC"
1800
+ }, key, this.encoder.encode(signatureBase));
1801
+ const encodedSignature = this.base64UrlEncode(signature);
1802
+ return `${signatureBase}.${encodedSignature}`;
1803
+ }
1804
+ /**
1805
+ * Verify a JWT token and return the decoded payload
1806
+ * @param {string} token - The JWT token to verify
1807
+ * @returns {Promise<JWTPayload>} - The decoded payload if verification succeeds
1808
+ * @throws {Error} - If verification fails
1809
+ */
1810
+ async verify(token) {
1811
+ if (!token || typeof token !== "string") {
1812
+ throw new Error("Token is required and must be a string");
1813
+ }
1814
+ const parts = token.split(".");
1815
+ if (parts.length !== 3) {
1816
+ throw new Error("Invalid token format");
1817
+ }
1818
+ const [encodedHeader, encodedPayload, encodedSignature] = parts;
1819
+ try {
1820
+ const header = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedHeader)));
1821
+ const payload = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedPayload)));
1822
+ if (header.alg !== "HS256") {
1823
+ throw new Error(`Unsupported algorithm: ${header.alg}`);
1824
+ }
1825
+ const now = Math.floor(Date.now() / 1e3);
1826
+ if (payload.exp && payload.exp < now) {
1827
+ throw new Error("Token has expired");
1828
+ }
1829
+ const key = await this.getSecretKey();
1830
+ const signatureBase = `${encodedHeader}.${encodedPayload}`;
1831
+ const signature = this.base64UrlDecode(encodedSignature);
1832
+ const isValid = await crypto.subtle.verify({
1833
+ name: "HMAC"
1834
+ }, key, signature, this.encoder.encode(signatureBase));
1835
+ if (!isValid) {
1836
+ throw new Error("Invalid signature");
1837
+ }
1838
+ return payload;
1839
+ } catch (error) {
1840
+ if (error instanceof Error) {
1841
+ throw new Error(`Token verification failed: ${error.message}`);
1842
+ }
1843
+ throw new Error("Token verification failed: Unknown error");
1844
+ }
1845
+ }
1846
+ };
1847
+
1848
+ // src/world.guard.ts
1849
+ var guardManageWorld = /* @__PURE__ */ __name(async (_, req, room) => {
1850
+ const tokenShard = req.headers.get("x-access-shard");
1851
+ if (tokenShard) {
1852
+ if (tokenShard !== room.env.SHARD_SECRET) {
1853
+ return false;
1854
+ }
1855
+ return true;
1856
+ }
1857
+ const url = new URL(req.url);
1858
+ const token = req.headers.get("Authorization") ?? url.searchParams.get("world-auth-token");
1859
+ if (!token) {
1860
+ return false;
1861
+ }
1862
+ const jwt = new JWTAuth(room.env.AUTH_JWT_SECRET);
1863
+ try {
1864
+ const payload = await jwt.verify(token);
1865
+ if (!payload) {
1866
+ return false;
1867
+ }
1868
+ } catch (error) {
1869
+ return false;
1870
+ }
1871
+ return true;
1872
+ }, "guardManageWorld");
1873
+
1874
+ // src/world.ts
1875
+ function _ts_decorate(decorators, target, key, desc) {
1876
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1877
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1878
+ 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;
1879
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1880
+ }
1881
+ __name(_ts_decorate, "_ts_decorate");
1882
+ function _ts_metadata(k, v) {
1883
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1884
+ }
1885
+ __name(_ts_metadata, "_ts_metadata");
1886
+ var RoomConfigSchema = z2.object({
1887
+ name: z2.string(),
1888
+ balancingStrategy: z2.enum([
1889
+ "round-robin",
1890
+ "least-connections",
1891
+ "random"
1892
+ ]),
1893
+ public: z2.boolean(),
1894
+ maxPlayersPerShard: z2.number().int().positive(),
1895
+ minShards: z2.number().int().min(0),
1896
+ maxShards: z2.number().int().positive().optional()
1897
+ });
1898
+ var RegisterShardSchema = z2.object({
1899
+ shardId: z2.string(),
1900
+ roomId: z2.string(),
1901
+ url: z2.string().url(),
1902
+ maxConnections: z2.number().int().positive()
1903
+ });
1904
+ var UpdateShardStatsSchema = z2.object({
1905
+ connections: z2.number().int().min(0),
1906
+ status: z2.enum([
1907
+ "active",
1908
+ "maintenance",
1909
+ "draining"
1910
+ ]).optional()
1911
+ });
1912
+ var ScaleRoomSchema = z2.object({
1913
+ roomId: z2.string(),
1914
+ targetShardCount: z2.number().int().positive(),
1915
+ shardTemplate: z2.object({
1916
+ urlTemplate: z2.string(),
1917
+ maxConnections: z2.number().int().positive()
1918
+ }).optional()
1919
+ });
1920
+ var RoomConfig = class RoomConfig2 {
1921
+ static {
1922
+ __name(this, "RoomConfig");
1923
+ }
1924
+ id;
1925
+ name = signal("");
1926
+ balancingStrategy = signal("round-robin");
1927
+ public = signal(true);
1928
+ maxPlayersPerShard = signal(100);
1929
+ minShards = signal(1);
1930
+ maxShards = signal(void 0);
1931
+ };
1932
+ _ts_decorate([
1933
+ id(),
1934
+ _ts_metadata("design:type", String)
1935
+ ], RoomConfig.prototype, "id", void 0);
1936
+ _ts_decorate([
1937
+ sync()
1938
+ ], RoomConfig.prototype, "name", void 0);
1939
+ _ts_decorate([
1940
+ sync()
1941
+ ], RoomConfig.prototype, "balancingStrategy", void 0);
1942
+ _ts_decorate([
1943
+ sync()
1944
+ ], RoomConfig.prototype, "public", void 0);
1945
+ _ts_decorate([
1946
+ sync()
1947
+ ], RoomConfig.prototype, "maxPlayersPerShard", void 0);
1948
+ _ts_decorate([
1949
+ sync()
1950
+ ], RoomConfig.prototype, "minShards", void 0);
1951
+ _ts_decorate([
1952
+ sync()
1953
+ ], RoomConfig.prototype, "maxShards", void 0);
1954
+ var ShardInfo = class ShardInfo2 {
1955
+ static {
1956
+ __name(this, "ShardInfo");
1957
+ }
1958
+ id;
1959
+ roomId = signal("");
1960
+ url = signal("");
1961
+ currentConnections = signal(0);
1962
+ maxConnections = signal(100);
1963
+ status = signal("active");
1964
+ lastHeartbeat = signal(0);
1965
+ };
1966
+ _ts_decorate([
1967
+ id(),
1968
+ _ts_metadata("design:type", String)
1969
+ ], ShardInfo.prototype, "id", void 0);
1970
+ _ts_decorate([
1971
+ sync()
1972
+ ], ShardInfo.prototype, "roomId", void 0);
1973
+ _ts_decorate([
1974
+ sync()
1975
+ ], ShardInfo.prototype, "url", void 0);
1976
+ _ts_decorate([
1977
+ sync({
1978
+ persist: false
1979
+ })
1980
+ ], ShardInfo.prototype, "currentConnections", void 0);
1981
+ _ts_decorate([
1982
+ sync()
1983
+ ], ShardInfo.prototype, "maxConnections", void 0);
1984
+ _ts_decorate([
1985
+ sync()
1986
+ ], ShardInfo.prototype, "status", void 0);
1987
+ _ts_decorate([
1988
+ sync()
1989
+ ], ShardInfo.prototype, "lastHeartbeat", void 0);
1990
+ var WorldRoom = class {
1991
+ static {
1992
+ __name(this, "WorldRoom");
1993
+ }
1994
+ room;
1995
+ // Synchronized state
1996
+ rooms;
1997
+ shards;
1998
+ // Only persisted state (not synced to clients)
1999
+ rrCounters;
2000
+ // Configuration
2001
+ defaultShardUrlTemplate;
2002
+ defaultMaxConnectionsPerShard;
2003
+ constructor(room) {
2004
+ this.room = room;
2005
+ this.rooms = signal({});
2006
+ this.shards = signal({});
2007
+ this.rrCounters = signal({});
2008
+ this.defaultShardUrlTemplate = signal("{shardId}");
2009
+ this.defaultMaxConnectionsPerShard = signal(100);
2010
+ const { AUTH_JWT_SECRET, SHARD_SECRET } = this.room.env;
2011
+ if (!AUTH_JWT_SECRET) {
2012
+ throw new Error("AUTH_JWT_SECRET env variable is not set");
2013
+ }
2014
+ if (!SHARD_SECRET) {
2015
+ throw new Error("SHARD_SECRET env variable is not set");
2016
+ }
2017
+ setTimeout(() => this.cleanupInactiveShards(), 6e4);
2018
+ }
2019
+ async onJoin(user, conn, ctx) {
2020
+ const canConnect = await guardManageWorld(user, ctx.request, this.room);
2021
+ conn.setState({
2022
+ ...conn.state,
2023
+ isAdmin: canConnect
2024
+ });
2025
+ }
2026
+ interceptorPacket(_, obj, conn) {
2027
+ if (!conn.state["isAdmin"]) {
2028
+ return null;
2029
+ }
2030
+ return obj;
2031
+ }
2032
+ // Helper methods
2033
+ cleanupInactiveShards() {
2034
+ const now = Date.now();
2035
+ const timeout = 5 * 60 * 1e3;
2036
+ const shardsValue = this.shards();
2037
+ let hasChanges = false;
2038
+ Object.values(shardsValue).forEach((shard) => {
2039
+ if (now - shard.lastHeartbeat() > timeout) {
2040
+ delete this.shards()[shard.id];
2041
+ hasChanges = true;
2042
+ }
2043
+ });
2044
+ setTimeout(() => this.cleanupInactiveShards(), 6e4);
2045
+ }
2046
+ // Actions
2047
+ async registerRoom(req) {
2048
+ const roomConfig = await req.json();
2049
+ const roomId = roomConfig.name;
2050
+ if (!this.rooms()[roomId]) {
2051
+ const newRoom = new RoomConfig();
2052
+ newRoom.id = roomId;
2053
+ newRoom.name.set(roomConfig.name);
2054
+ newRoom.balancingStrategy.set(roomConfig.balancingStrategy);
2055
+ newRoom.public.set(roomConfig.public);
2056
+ newRoom.maxPlayersPerShard.set(roomConfig.maxPlayersPerShard);
2057
+ newRoom.minShards.set(roomConfig.minShards);
2058
+ newRoom.maxShards.set(roomConfig.maxShards);
2059
+ this.rooms()[roomId] = newRoom;
2060
+ if (roomConfig.minShards > 0) {
2061
+ for (let i = 0; i < roomConfig.minShards; i++) {
2062
+ await this.createShard(roomId);
2063
+ }
2064
+ }
2065
+ } else {
2066
+ const room = this.rooms()[roomId];
2067
+ room.balancingStrategy.set(roomConfig.balancingStrategy);
2068
+ room.public.set(roomConfig.public);
2069
+ room.maxPlayersPerShard.set(roomConfig.maxPlayersPerShard);
2070
+ room.minShards.set(roomConfig.minShards);
2071
+ room.maxShards.set(roomConfig.maxShards);
2072
+ }
2073
+ }
2074
+ async updateShardStats(req, res) {
2075
+ const body = await req.json();
2076
+ const { shardId, connections, status } = body;
2077
+ const shard = this.shards()[shardId];
2078
+ if (!shard) {
2079
+ return res.notFound(`Shard ${shardId} not found`);
2080
+ }
2081
+ shard.currentConnections.set(connections);
2082
+ if (status) {
2083
+ shard.status.set(status);
2084
+ }
2085
+ shard.lastHeartbeat.set(Date.now());
2086
+ }
2087
+ async scaleRoom(req, res) {
2088
+ const data = await req.json();
2089
+ const { targetShardCount, shardTemplate, roomId } = data;
2090
+ const room = this.rooms()[roomId];
2091
+ if (!room) {
2092
+ return res.notFound(`Room ${roomId} does not exist`);
2093
+ }
2094
+ const roomShards = Object.values(this.shards()).filter((shard) => shard.roomId() === roomId);
2095
+ const previousShardCount = roomShards.length;
2096
+ if (room.maxShards() !== void 0 && targetShardCount > room.maxShards()) {
2097
+ return res.badRequest(`Cannot scale beyond maximum allowed shards (${room.maxShards()})`, {
2098
+ roomId,
2099
+ currentShardCount: previousShardCount
2100
+ });
2101
+ }
2102
+ if (targetShardCount < previousShardCount) {
2103
+ const shardsToRemove = [
2104
+ ...roomShards
2105
+ ].sort((a, b) => {
2106
+ if (a.status() === "draining" && b.status() !== "draining") return -1;
2107
+ if (a.status() !== "draining" && b.status() === "draining") return 1;
2108
+ return a.currentConnections() - b.currentConnections();
2109
+ }).slice(0, previousShardCount - targetShardCount);
2110
+ const shardsToKeep = roomShards.filter((shard) => !shardsToRemove.some((s) => s.id === shard.id));
2111
+ for (const shard of shardsToRemove) {
2112
+ delete this.shards()[shard.id];
2113
+ }
2114
+ return;
2115
+ }
2116
+ if (targetShardCount > previousShardCount) {
2117
+ const newShards = [];
2118
+ for (let i = 0; i < targetShardCount - previousShardCount; i++) {
2119
+ const newShard = await this.createShard(roomId, shardTemplate?.urlTemplate, shardTemplate?.maxConnections);
2120
+ if (newShard) {
2121
+ newShards.push(newShard);
2122
+ }
2123
+ }
2124
+ }
2125
+ }
2126
+ async connect(req, res) {
2127
+ try {
2128
+ let data;
2129
+ try {
2130
+ const body = await req.text();
2131
+ if (!body || body.trim() === "") {
2132
+ return res.badRequest("Request body is empty");
2133
+ }
2134
+ data = JSON.parse(body);
2135
+ } catch (parseError) {
2136
+ return res.badRequest("Invalid JSON in request body");
2137
+ }
2138
+ if (!data.roomId) {
2139
+ return res.badRequest("roomId parameter is required");
2140
+ }
2141
+ const autoCreate = data.autoCreate !== void 0 ? data.autoCreate : true;
2142
+ const result = await this.findOptimalShard(data.roomId, autoCreate);
2143
+ if ("error" in result) {
2144
+ return res.notFound(result.error);
2145
+ }
2146
+ return res.success({
2147
+ success: true,
2148
+ shardId: result.shardId,
2149
+ url: result.url
2150
+ });
2151
+ } catch (error) {
2152
+ console.error("Error connecting to shard:", error);
2153
+ return res.serverError();
2154
+ }
2155
+ }
2156
+ async findOptimalShard(roomId, autoCreate = true) {
2157
+ let room = this.rooms()[roomId];
2158
+ if (!room) {
2159
+ if (autoCreate) {
2160
+ const mockRequest = {
2161
+ json: /* @__PURE__ */ __name(async () => ({
2162
+ name: roomId,
2163
+ balancingStrategy: "round-robin",
2164
+ public: true,
2165
+ maxPlayersPerShard: this.defaultMaxConnectionsPerShard(),
2166
+ minShards: 1,
2167
+ maxShards: void 0
2168
+ }), "json")
2169
+ };
2170
+ await this.registerRoom(mockRequest);
2171
+ room = this.rooms()[roomId];
2172
+ if (!room) {
2173
+ return {
2174
+ error: `Failed to create room ${roomId}`
2175
+ };
2176
+ }
2177
+ } else {
2178
+ return {
2179
+ error: `Room ${roomId} does not exist`
2180
+ };
2181
+ }
2182
+ }
2183
+ const roomShards = Object.values(this.shards()).filter((shard) => shard.roomId() === roomId);
2184
+ if (roomShards.length === 0) {
2185
+ if (autoCreate) {
2186
+ const newShard = await this.createShard(roomId);
2187
+ if (newShard) {
2188
+ return {
2189
+ shardId: newShard.id,
2190
+ url: newShard.url()
2191
+ };
2192
+ } else {
2193
+ return {
2194
+ error: `Failed to create shard for room ${roomId}`
2195
+ };
2196
+ }
2197
+ } else {
2198
+ return {
2199
+ error: `No shards available for room ${roomId}`
2200
+ };
2201
+ }
2202
+ }
2203
+ const activeShards = roomShards.filter((shard) => shard && shard.status() === "active");
2204
+ if (activeShards.length === 0) {
2205
+ return {
2206
+ error: `No active shards available for room ${roomId}`
2207
+ };
2208
+ }
2209
+ const balancingStrategy = room.balancingStrategy();
2210
+ let selectedShard;
2211
+ switch (balancingStrategy) {
2212
+ case "least-connections":
2213
+ selectedShard = activeShards.reduce((min, shard) => shard.currentConnections() < min.currentConnections() ? shard : min, activeShards[0]);
2214
+ break;
2215
+ case "random":
2216
+ selectedShard = activeShards[Math.floor(Math.random() * activeShards.length)];
2217
+ break;
2218
+ case "round-robin":
2219
+ default:
2220
+ const counter = this.rrCounters()[roomId] || 0;
2221
+ const nextCounter = (counter + 1) % activeShards.length;
2222
+ this.rrCounters()[roomId] = nextCounter;
2223
+ selectedShard = activeShards[counter];
2224
+ break;
2225
+ }
2226
+ return {
2227
+ shardId: selectedShard.id,
2228
+ url: selectedShard.url()
2229
+ };
2230
+ }
2231
+ // Private methods
2232
+ async createShard(roomId, urlTemplate, maxConnections) {
2233
+ const room = this.rooms()[roomId];
2234
+ if (!room) {
2235
+ console.error(`Cannot create shard for non-existent room: ${roomId}`);
2236
+ return null;
2237
+ }
2238
+ const shardId = `${roomId}:${Date.now()}-${Math.floor(Math.random() * 1e4)}`;
2239
+ const template = urlTemplate || this.defaultShardUrlTemplate();
2240
+ const url = template.replace("{shardId}", shardId).replace("{roomId}", roomId);
2241
+ const max = maxConnections || room.maxPlayersPerShard();
2242
+ const newShard = new ShardInfo();
2243
+ newShard.id = shardId;
2244
+ newShard.roomId.set(roomId);
2245
+ newShard.url.set(url);
2246
+ newShard.maxConnections.set(max);
2247
+ newShard.currentConnections.set(0);
2248
+ newShard.status.set("active");
2249
+ newShard.lastHeartbeat.set(Date.now());
2250
+ this.shards()[shardId] = newShard;
2251
+ return newShard;
2252
+ }
2253
+ };
2254
+ _ts_decorate([
2255
+ sync(RoomConfig)
2256
+ ], WorldRoom.prototype, "rooms", void 0);
2257
+ _ts_decorate([
2258
+ sync(ShardInfo)
2259
+ ], WorldRoom.prototype, "shards", void 0);
2260
+ _ts_decorate([
2261
+ persist()
2262
+ ], WorldRoom.prototype, "rrCounters", void 0);
2263
+ _ts_decorate([
2264
+ Request2({
2265
+ path: "register-room",
2266
+ method: "POST"
2267
+ }),
2268
+ Guard([
2269
+ guardManageWorld
2270
+ ]),
2271
+ _ts_metadata("design:type", Function),
2272
+ _ts_metadata("design:paramtypes", [
2273
+ typeof party_exports === "undefined" || typeof void 0 === "undefined" ? Object : void 0
2274
+ ]),
2275
+ _ts_metadata("design:returntype", Promise)
2276
+ ], WorldRoom.prototype, "registerRoom", null);
2277
+ _ts_decorate([
2278
+ Request2({
2279
+ path: "update-shard",
2280
+ method: "POST"
2281
+ }),
2282
+ Guard([
2283
+ guardManageWorld
2284
+ ]),
2285
+ _ts_metadata("design:type", Function),
2286
+ _ts_metadata("design:paramtypes", [
2287
+ typeof party_exports === "undefined" || typeof void 0 === "undefined" ? Object : void 0,
2288
+ typeof ServerResponse === "undefined" ? Object : ServerResponse
2289
+ ]),
2290
+ _ts_metadata("design:returntype", Promise)
2291
+ ], WorldRoom.prototype, "updateShardStats", null);
2292
+ _ts_decorate([
2293
+ Request2({
2294
+ path: "scale-room",
2295
+ method: "POST"
2296
+ }),
2297
+ Guard([
2298
+ guardManageWorld
2299
+ ]),
2300
+ _ts_metadata("design:type", Function),
2301
+ _ts_metadata("design:paramtypes", [
2302
+ typeof party_exports === "undefined" || typeof void 0 === "undefined" ? Object : void 0,
2303
+ typeof ServerResponse === "undefined" ? Object : ServerResponse
2304
+ ]),
2305
+ _ts_metadata("design:returntype", Promise)
2306
+ ], WorldRoom.prototype, "scaleRoom", null);
2307
+ _ts_decorate([
2308
+ Request2({
2309
+ path: "connect",
2310
+ method: "POST"
2311
+ }),
2312
+ _ts_metadata("design:type", Function),
2313
+ _ts_metadata("design:paramtypes", [
2314
+ typeof party_exports === "undefined" || typeof void 0 === "undefined" ? Object : void 0,
2315
+ typeof ServerResponse === "undefined" ? Object : ServerResponse
2316
+ ]),
2317
+ _ts_metadata("design:returntype", Promise)
2318
+ ], WorldRoom.prototype, "connect", null);
2319
+ WorldRoom = _ts_decorate([
2320
+ Room({
2321
+ path: "world-{worldId}",
2322
+ maxUsers: 100,
2323
+ throttleStorage: 2e3,
2324
+ throttleSync: 500
2325
+ }),
2326
+ _ts_metadata("design:type", Function),
2327
+ _ts_metadata("design:paramtypes", [
2328
+ typeof party_exports === "undefined" || typeof void 0 === "undefined" ? Object : void 0
2329
+ ])
2330
+ ], WorldRoom);
748
2331
  export {
749
2332
  Action,
750
2333
  ClientIo,
751
2334
  Guard,
752
2335
  MockConnection,
2336
+ Request2 as Request,
753
2337
  Room,
754
2338
  RoomGuard,
755
2339
  Server,
756
2340
  ServerIo,
2341
+ ServerResponse,
2342
+ Shard,
2343
+ WorldRoom,
2344
+ request,
757
2345
  testRoom
758
2346
  };
759
2347
  //# sourceMappingURL=index.js.map