@signe/room 1.4.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +258 -22
- package/dist/index.js +1447 -60
- package/dist/index.js.map +1 -1
- package/examples/game/app/client.tsx +2 -2
- package/examples/game/app/components/Admin.tsx +1089 -0
- package/examples/game/app/components/Room.tsx +158 -0
- package/examples/game/party/server.ts +3 -2
- package/examples/game/party/shard.ts +5 -0
- package/examples/game/partykit.json +5 -1
- package/package.json +2 -2
- package/readme.md +226 -2
- package/src/decorators.ts +34 -2
- package/src/index.ts +4 -1
- package/src/interfaces.ts +13 -0
- package/src/jwt.ts +217 -0
- package/src/mock.ts +39 -3
- package/src/server.ts +595 -79
- package/src/shard.ts +244 -0
- package/src/testing.ts +47 -6
- package/src/utils.ts +7 -0
- package/src/world.guard.ts +28 -0
- package/src/world.ts +448 -0
- package/examples/game/app/components/Counter.tsx +0 -82
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(
|
|
120
|
-
this.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 =
|
|
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(
|
|
140
|
-
return this.clients.get(
|
|
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.
|
|
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
|
|
635
|
+
const signal2 = this.getUsersProperty(subRoom);
|
|
569
636
|
const usersPropName = this.getUsersPropName(subRoom);
|
|
570
|
-
if (
|
|
571
|
-
const { classType } =
|
|
637
|
+
if (signal2) {
|
|
638
|
+
const { classType } = signal2.options;
|
|
572
639
|
if (!existingSession?.publicId) {
|
|
573
640
|
user = isClass(classType) ? new classType() : classType(conn, ctx);
|
|
574
|
-
|
|
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
|
-
|
|
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
|
|
667
|
+
* @method onConnect
|
|
600
668
|
* @async
|
|
601
|
-
* @param {
|
|
602
|
-
* @param {Party.
|
|
603
|
-
* @description
|
|
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.
|
|
609
|
-
* await server.
|
|
610
|
-
* console.log("
|
|
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
|
|
739
|
+
const signal2 = this.getUsersProperty(subRoom);
|
|
636
740
|
const { publicId } = sender.state;
|
|
637
|
-
const user =
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
|
715
|
-
if (
|
|
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 (
|
|
721
|
-
return
|
|
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
|
-
|
|
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(
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|