@signe/room 2.9.4 → 2.10.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/CHANGELOG.md +6 -0
- package/dist/index.d.ts +1 -3
- package/dist/index.js +32 -57
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +6 -7
- package/src/jwt.ts +5 -1
- package/src/server.ts +27 -40
- package/src/world.ts +70 -79
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signe/room",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "PartyKit room primitives with synchronized state, sessions, guards, and HTTP handlers.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"dset": "^3.1.3",
|
|
24
24
|
"partysocket": "^1.0.1",
|
|
25
25
|
"zod": "^3.23.8",
|
|
26
|
-
"@signe/sync": "2.
|
|
26
|
+
"@signe/sync": "2.10.0"
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
package/readme.md
CHANGED
|
@@ -123,7 +123,7 @@ The `@Request` decorator allows you to handle HTTP requests with specific routes
|
|
|
123
123
|
|
|
124
124
|
```ts
|
|
125
125
|
import { z } from "zod";
|
|
126
|
-
import {
|
|
126
|
+
import { Room, Request, RequestGuard, ServerResponse } from "@signe/room";
|
|
127
127
|
|
|
128
128
|
@Room({
|
|
129
129
|
path: "api"
|
|
@@ -169,12 +169,11 @@ class ApiRoom {
|
|
|
169
169
|
}
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
-
Request handler methods receive:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
error responses.
|
|
172
|
+
Request handler methods receive these parameters:
|
|
173
|
+
1. `req`: The original Party.Request object
|
|
174
|
+
2. `body`: The validated request body (if validation schema was provided)
|
|
175
|
+
3. `params`: An object containing any path parameters
|
|
176
|
+
4. `room`: The Party.Room instance
|
|
178
177
|
|
|
179
178
|
You can return:
|
|
180
179
|
- A Response object for complete control
|
package/src/jwt.ts
CHANGED
|
@@ -132,7 +132,9 @@ export class JWTAuth {
|
|
|
132
132
|
};
|
|
133
133
|
|
|
134
134
|
// Encode header and payload
|
|
135
|
+
// @ts-expect-error - TS doesn't have a built-in TextEncoder
|
|
135
136
|
const encodedHeader: string = this.base64UrlEncode(this.encoder.encode(JSON.stringify(header)));
|
|
137
|
+
// @ts-expect-error - TS doesn't have a built-in TextEncoder
|
|
136
138
|
const encodedPayload: string = this.base64UrlEncode(this.encoder.encode(JSON.stringify(fullPayload)));
|
|
137
139
|
|
|
138
140
|
// Create signature base
|
|
@@ -172,7 +174,9 @@ export class JWTAuth {
|
|
|
172
174
|
|
|
173
175
|
// Decode header and payload
|
|
174
176
|
try {
|
|
177
|
+
// @ts-expect-error - TS doesn't have a built-in TextDecoder
|
|
175
178
|
const header: JWTHeader = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedHeader)));
|
|
179
|
+
// @ts-expect-error - TS doesn't have a built-in TextDecoder
|
|
176
180
|
const payload: JWTPayload = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedPayload)));
|
|
177
181
|
|
|
178
182
|
// Check algorithm
|
|
@@ -210,4 +214,4 @@ export class JWTAuth {
|
|
|
210
214
|
throw new Error('Token verification failed: Unknown error');
|
|
211
215
|
}
|
|
212
216
|
}
|
|
213
|
-
}
|
|
217
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -615,10 +615,7 @@ export class Server implements Party.Server {
|
|
|
615
615
|
return;
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
-
const sessionExpiryTime =
|
|
619
|
-
subRoom.sessionExpiryTime ??
|
|
620
|
-
subRoom.constructor.sessionExpiryTime ??
|
|
621
|
-
5 * 60 * 1000;
|
|
618
|
+
const sessionExpiryTime = subRoom.constructor.sessionExpiryTime;
|
|
622
619
|
await this.garbageCollector({ sessionExpiryTime });
|
|
623
620
|
|
|
624
621
|
// Check room guards
|
|
@@ -1292,7 +1289,9 @@ export class Server implements Party.Server {
|
|
|
1292
1289
|
|
|
1293
1290
|
const url = new URL(req.url);
|
|
1294
1291
|
const method = req.method;
|
|
1295
|
-
|
|
1292
|
+
let pathname = url.pathname;
|
|
1293
|
+
|
|
1294
|
+
pathname = '/' + pathname.split('/').slice(4).join('/');
|
|
1296
1295
|
|
|
1297
1296
|
// Check each registered handler
|
|
1298
1297
|
for (const [routeKey, handler] of requestHandlers.entries()) {
|
|
@@ -1326,18 +1325,16 @@ export class Server implements Party.Server {
|
|
|
1326
1325
|
if (handler.bodyValidation && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
1327
1326
|
try {
|
|
1328
1327
|
const contentType = req.headers.get('content-type') || '';
|
|
1329
|
-
if (
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
});
|
|
1328
|
+
if (contentType.includes('application/json')) {
|
|
1329
|
+
const body = await req.json();
|
|
1330
|
+
const validation = handler.bodyValidation.safeParse(body);
|
|
1331
|
+
if (!validation.success) {
|
|
1332
|
+
return res.badRequest("Invalid request body", {
|
|
1333
|
+
details: validation.error
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
bodyData = validation.data;
|
|
1339
1337
|
}
|
|
1340
|
-
bodyData = validation.data;
|
|
1341
1338
|
} catch (error) {
|
|
1342
1339
|
return res.badRequest("Failed to parse request body");
|
|
1343
1340
|
}
|
|
@@ -1375,7 +1372,14 @@ export class Server implements Party.Server {
|
|
|
1375
1372
|
* @returns {boolean} True if the paths match
|
|
1376
1373
|
*/
|
|
1377
1374
|
private pathMatches(requestPath: string, handlerPath: string): boolean {
|
|
1378
|
-
|
|
1375
|
+
// Convert handler path pattern to regex
|
|
1376
|
+
// Replace :param with named capture groups
|
|
1377
|
+
const pathRegexString = handlerPath
|
|
1378
|
+
.replace(/\//g, '\\/') // Escape slashes
|
|
1379
|
+
.replace(/:([^\/]+)/g, '([^/]+)'); // Convert :params to capture groups
|
|
1380
|
+
|
|
1381
|
+
const pathRegex = new RegExp(`^${pathRegexString}`);
|
|
1382
|
+
return pathRegex.test(requestPath);
|
|
1379
1383
|
}
|
|
1380
1384
|
|
|
1381
1385
|
/**
|
|
@@ -1397,7 +1401,12 @@ export class Server implements Party.Server {
|
|
|
1397
1401
|
}
|
|
1398
1402
|
});
|
|
1399
1403
|
|
|
1400
|
-
|
|
1404
|
+
// Extract parameter values from request path
|
|
1405
|
+
const pathRegexString = handlerPath
|
|
1406
|
+
.replace(/\//g, '\\/') // Escape slashes
|
|
1407
|
+
.replace(/:([^\/]+)/g, '([^/]+)'); // Convert :params to capture groups
|
|
1408
|
+
|
|
1409
|
+
const pathRegex = new RegExp(`^${pathRegexString}`);
|
|
1401
1410
|
const matches = requestPath.match(pathRegex);
|
|
1402
1411
|
|
|
1403
1412
|
if (matches && matches.length > 1) {
|
|
@@ -1410,28 +1419,6 @@ export class Server implements Party.Server {
|
|
|
1410
1419
|
return params;
|
|
1411
1420
|
}
|
|
1412
1421
|
|
|
1413
|
-
private normalizeRequestPath(pathname: string): string {
|
|
1414
|
-
const parts = pathname.split('/').filter(Boolean);
|
|
1415
|
-
if (parts[0] === 'parties' && parts.length >= 3) {
|
|
1416
|
-
const routePath = parts.slice(3).join('/');
|
|
1417
|
-
return routePath ? `/${routePath}` : '/';
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
return pathname || '/';
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
private pathPatternToRegex(handlerPath: string): RegExp {
|
|
1424
|
-
const segments = handlerPath.split('/').map(segment => {
|
|
1425
|
-
if (segment.startsWith(':')) {
|
|
1426
|
-
return '([^/]+)';
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
return segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1430
|
-
});
|
|
1431
|
-
|
|
1432
|
-
return new RegExp(`^${segments.join('/')}$`);
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
1422
|
/**
|
|
1436
1423
|
* @method handleShardRequest
|
|
1437
1424
|
* @private
|
package/src/world.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { signal } from "@signe/reactive";
|
|
2
|
-
import { Room, Guard, Request } from "./decorators";
|
|
2
|
+
import { Room, Action, Guard, Request } from "./decorators";
|
|
3
3
|
import { sync, id, persist } from "@signe/sync";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import * as Party from "./types/party";
|
|
6
6
|
import { guardManageWorld } from "./world.guard";
|
|
7
|
+
import { response } from "./utils";
|
|
7
8
|
import { RoomInterceptorPacket, RoomOnJoin } from "./interfaces";
|
|
8
9
|
import { ServerResponse } from "./request/response";
|
|
9
10
|
|
|
@@ -23,8 +24,14 @@ const RoomConfigSchema = z.object({
|
|
|
23
24
|
maxShards: z.number().int().positive().optional(),
|
|
24
25
|
});
|
|
25
26
|
|
|
26
|
-
const
|
|
27
|
+
const RegisterShardSchema = z.object({
|
|
27
28
|
shardId: z.string(),
|
|
29
|
+
roomId: z.string(),
|
|
30
|
+
url: z.string().url(),
|
|
31
|
+
maxConnections: z.number().int().positive(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const UpdateShardStatsSchema = z.object({
|
|
28
35
|
connections: z.number().int().min(0),
|
|
29
36
|
status: z.enum(['active', 'maintenance', 'draining']).optional(),
|
|
30
37
|
});
|
|
@@ -72,10 +79,10 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
72
79
|
// Synchronized state
|
|
73
80
|
@sync(RoomConfig) rooms = signal<Record<string, RoomConfig>>({});
|
|
74
81
|
@sync(ShardInfo) shards = signal<Record<string, ShardInfo>>({});
|
|
75
|
-
|
|
82
|
+
|
|
76
83
|
// Only persisted state (not synced to clients)
|
|
77
84
|
@persist() rrCounters = signal<Record<string, number>>({});
|
|
78
|
-
|
|
85
|
+
|
|
79
86
|
// Configuration
|
|
80
87
|
defaultShardUrlTemplate = signal("{shardId}");
|
|
81
88
|
defaultMaxConnectionsPerShard = signal(MAX_PLAYERS_PER_SHARD);
|
|
@@ -106,43 +113,36 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
106
113
|
}
|
|
107
114
|
return obj;
|
|
108
115
|
}
|
|
109
|
-
|
|
116
|
+
|
|
110
117
|
// Helper methods
|
|
111
118
|
private cleanupInactiveShards() {
|
|
112
119
|
const now = Date.now();
|
|
113
120
|
const timeout = 5 * 60 * 1000; // 5 minutes timeout
|
|
114
121
|
const shardsValue = this.shards();
|
|
115
|
-
|
|
122
|
+
|
|
116
123
|
let hasChanges = false;
|
|
117
124
|
Object.values(shardsValue).forEach(shard => {
|
|
118
125
|
if (now - shard.lastHeartbeat() > timeout) {
|
|
119
126
|
delete this.shards()[shard.id];
|
|
120
|
-
|
|
127
|
+
|
|
121
128
|
hasChanges = true;
|
|
122
129
|
}
|
|
123
130
|
});
|
|
124
|
-
|
|
131
|
+
|
|
125
132
|
// Schedule next cleanup
|
|
126
133
|
setTimeout(() => this.cleanupInactiveShards(), 60000);
|
|
127
134
|
}
|
|
128
|
-
|
|
135
|
+
|
|
129
136
|
// Actions
|
|
130
137
|
@Request({
|
|
131
138
|
path: 'register-room',
|
|
132
139
|
method: 'POST',
|
|
133
140
|
})
|
|
134
141
|
@Guard([guardManageWorld])
|
|
135
|
-
async registerRoom(req: Party.Request
|
|
136
|
-
const
|
|
137
|
-
if (!roomConfigResult.success) {
|
|
138
|
-
return res?.badRequest("Invalid room configuration", {
|
|
139
|
-
details: roomConfigResult.error
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const roomConfig = roomConfigResult.data;
|
|
142
|
+
async registerRoom(req: Party.Request) {
|
|
143
|
+
const roomConfig: z.infer<typeof RoomConfigSchema> = await req.json();
|
|
144
144
|
const roomId = roomConfig.name;
|
|
145
|
-
|
|
145
|
+
|
|
146
146
|
if (!this.rooms()[roomId]) {
|
|
147
147
|
const newRoom = new RoomConfig();
|
|
148
148
|
newRoom.id = roomId;
|
|
@@ -152,9 +152,9 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
152
152
|
newRoom.maxPlayersPerShard.set(roomConfig.maxPlayersPerShard);
|
|
153
153
|
newRoom.minShards.set(roomConfig.minShards);
|
|
154
154
|
newRoom.maxShards.set(roomConfig.maxShards);
|
|
155
|
-
|
|
155
|
+
|
|
156
156
|
this.rooms()[roomId] = newRoom;
|
|
157
|
-
|
|
157
|
+
|
|
158
158
|
// Ensure minimum shards are created
|
|
159
159
|
if (roomConfig.minShards > 0) {
|
|
160
160
|
for (let i = 0; i < roomConfig.minShards; i++) {
|
|
@@ -171,62 +171,48 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
171
171
|
room.maxShards.set(roomConfig.maxShards);
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
-
|
|
174
|
+
|
|
175
175
|
@Request({
|
|
176
176
|
path: 'update-shard',
|
|
177
177
|
method: 'POST',
|
|
178
178
|
})
|
|
179
179
|
@Guard([guardManageWorld])
|
|
180
180
|
async updateShardStats(req: Party.Request, res: ServerResponse) {
|
|
181
|
-
const
|
|
182
|
-
if (!bodyResult.success) {
|
|
183
|
-
return res.badRequest("Invalid shard statistics", {
|
|
184
|
-
details: bodyResult.error
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const body = bodyResult.data;
|
|
181
|
+
const body: { shardId: string; connections: number; status: ShardStatus } = await req.json();
|
|
189
182
|
const { shardId, connections, status } = body;
|
|
190
183
|
const shard = this.shards()[shardId];
|
|
191
184
|
|
|
192
185
|
if (!shard) {
|
|
193
186
|
return res.notFound(`Shard ${shardId} not found`);
|
|
194
187
|
}
|
|
195
|
-
|
|
188
|
+
|
|
196
189
|
shard.currentConnections.set(connections);
|
|
197
190
|
if (status) {
|
|
198
191
|
shard.status.set(status);
|
|
199
192
|
}
|
|
200
193
|
shard.lastHeartbeat.set(Date.now());
|
|
201
194
|
}
|
|
202
|
-
|
|
195
|
+
|
|
203
196
|
@Request({
|
|
204
197
|
path: 'scale-room',
|
|
205
198
|
method: 'POST',
|
|
206
199
|
})
|
|
207
200
|
@Guard([guardManageWorld])
|
|
208
201
|
async scaleRoom(req: Party.Request, res: ServerResponse) {
|
|
209
|
-
const
|
|
210
|
-
if (!dataResult.success) {
|
|
211
|
-
return res.badRequest("Invalid scale request", {
|
|
212
|
-
details: dataResult.error
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const data = dataResult.data;
|
|
202
|
+
const data: z.infer<typeof ScaleRoomSchema> = await req.json();
|
|
217
203
|
const { targetShardCount, shardTemplate, roomId } = data;
|
|
218
|
-
|
|
204
|
+
|
|
219
205
|
// Validate room exists
|
|
220
206
|
const room = this.rooms()[roomId];
|
|
221
207
|
if (!room) {
|
|
222
208
|
return res.notFound(`Room ${roomId} does not exist`);
|
|
223
209
|
}
|
|
224
|
-
|
|
210
|
+
|
|
225
211
|
const roomShards = Object.values(this.shards())
|
|
226
212
|
.filter(shard => shard.roomId() === roomId);
|
|
227
|
-
|
|
213
|
+
|
|
228
214
|
const previousShardCount = roomShards.length;
|
|
229
|
-
|
|
215
|
+
|
|
230
216
|
// Check max shards constraint
|
|
231
217
|
if (room.maxShards() !== undefined && targetShardCount > room.maxShards()!) {
|
|
232
218
|
return res.badRequest(`Cannot scale beyond maximum allowed shards (${room.maxShards()})`, {
|
|
@@ -234,7 +220,7 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
234
220
|
currentShardCount: previousShardCount
|
|
235
221
|
});
|
|
236
222
|
}
|
|
237
|
-
|
|
223
|
+
|
|
238
224
|
// Handle scaling down
|
|
239
225
|
if (targetShardCount < previousShardCount) {
|
|
240
226
|
// Find candidates for removal (prioritize draining or low-connection shards)
|
|
@@ -243,24 +229,29 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
243
229
|
// Prioritize draining status
|
|
244
230
|
if (a.status() === 'draining' && b.status() !== 'draining') return -1;
|
|
245
231
|
if (a.status() !== 'draining' && b.status() === 'draining') return 1;
|
|
246
|
-
|
|
232
|
+
|
|
247
233
|
// Then by connection count (ascending)
|
|
248
234
|
return a.currentConnections() - b.currentConnections();
|
|
249
235
|
})
|
|
250
236
|
.slice(0, previousShardCount - targetShardCount);
|
|
251
|
-
|
|
237
|
+
|
|
238
|
+
// Remove the selected shards
|
|
239
|
+
const shardsToKeep = roomShards.filter(
|
|
240
|
+
shard => !shardsToRemove.some(s => s.id === shard.id)
|
|
241
|
+
);
|
|
242
|
+
|
|
252
243
|
// Update shards
|
|
253
244
|
for (const shard of shardsToRemove) {
|
|
254
245
|
delete this.shards()[shard.id];
|
|
255
246
|
}
|
|
256
|
-
|
|
247
|
+
|
|
257
248
|
return;
|
|
258
249
|
}
|
|
259
|
-
|
|
250
|
+
|
|
260
251
|
// Handle scaling up
|
|
261
252
|
if (targetShardCount > previousShardCount) {
|
|
262
253
|
const newShards = [];
|
|
263
|
-
|
|
254
|
+
|
|
264
255
|
// Create new shards
|
|
265
256
|
for (let i = 0; i < targetShardCount - previousShardCount; i++) {
|
|
266
257
|
const newShard = await this.createShard(
|
|
@@ -268,7 +259,7 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
268
259
|
shardTemplate?.urlTemplate,
|
|
269
260
|
shardTemplate?.maxConnections
|
|
270
261
|
);
|
|
271
|
-
|
|
262
|
+
|
|
272
263
|
if (newShard) {
|
|
273
264
|
newShards.push(newShard);
|
|
274
265
|
}
|
|
@@ -284,35 +275,35 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
284
275
|
try {
|
|
285
276
|
// Extract request data
|
|
286
277
|
let data: { roomId: string; autoCreate?: boolean };
|
|
287
|
-
|
|
278
|
+
|
|
288
279
|
try {
|
|
289
280
|
// Handle potential empty body or malformed JSON
|
|
290
281
|
const body = await req.text();
|
|
291
282
|
if (!body || body.trim() === '') {
|
|
292
283
|
return res.badRequest("Request body is empty");
|
|
293
284
|
}
|
|
294
|
-
|
|
285
|
+
|
|
295
286
|
data = JSON.parse(body);
|
|
296
287
|
} catch (parseError) {
|
|
297
288
|
return res.badRequest("Invalid JSON in request body");
|
|
298
289
|
}
|
|
299
|
-
|
|
290
|
+
|
|
300
291
|
// Verify roomId is provided
|
|
301
292
|
if (!data.roomId) {
|
|
302
293
|
return res.badRequest("roomId parameter is required");
|
|
303
294
|
}
|
|
304
|
-
|
|
295
|
+
|
|
305
296
|
// Determine if auto-creation is enabled (default to true)
|
|
306
297
|
const autoCreate = data.autoCreate !== undefined ? data.autoCreate : true;
|
|
307
|
-
|
|
298
|
+
|
|
308
299
|
// Find optimal shard
|
|
309
300
|
const result = await this.findOptimalShard(data.roomId, autoCreate);
|
|
310
|
-
|
|
301
|
+
|
|
311
302
|
// Check for errors
|
|
312
303
|
if ('error' in result) {
|
|
313
304
|
return res.notFound(result.error);
|
|
314
305
|
}
|
|
315
|
-
|
|
306
|
+
|
|
316
307
|
// Return shard information to the client
|
|
317
308
|
return res.success({
|
|
318
309
|
success: true,
|
|
@@ -324,9 +315,9 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
324
315
|
return res.serverError();
|
|
325
316
|
}
|
|
326
317
|
}
|
|
327
|
-
|
|
318
|
+
|
|
328
319
|
private async findOptimalShard(
|
|
329
|
-
roomId: string,
|
|
320
|
+
roomId: string,
|
|
330
321
|
autoCreate: boolean = true
|
|
331
322
|
): Promise<{ shardId: string; url: string } | { error: string }> {
|
|
332
323
|
// Ensure room exists
|
|
@@ -343,11 +334,11 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
343
334
|
maxShards: undefined
|
|
344
335
|
})
|
|
345
336
|
} as Party.Request;
|
|
346
|
-
|
|
337
|
+
|
|
347
338
|
await this.registerRoom(mockRequest);
|
|
348
|
-
|
|
339
|
+
|
|
349
340
|
room = this.rooms()[roomId];
|
|
350
|
-
|
|
341
|
+
|
|
351
342
|
if (!room) {
|
|
352
343
|
return { error: `Failed to create room ${roomId}` };
|
|
353
344
|
}
|
|
@@ -355,11 +346,11 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
355
346
|
return { error: `Room ${roomId} does not exist` };
|
|
356
347
|
}
|
|
357
348
|
}
|
|
358
|
-
|
|
349
|
+
|
|
359
350
|
// Get shards for this room
|
|
360
351
|
const roomShards = Object.values(this.shards())
|
|
361
352
|
.filter(shard => shard.roomId() === roomId);
|
|
362
|
-
|
|
353
|
+
|
|
363
354
|
if (roomShards.length === 0) {
|
|
364
355
|
if (autoCreate) {
|
|
365
356
|
// Auto-create a shard
|
|
@@ -376,51 +367,51 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
376
367
|
return { error: `No shards available for room ${roomId}` };
|
|
377
368
|
}
|
|
378
369
|
}
|
|
379
|
-
|
|
370
|
+
|
|
380
371
|
// Get active shards
|
|
381
372
|
const activeShards = roomShards
|
|
382
373
|
.filter(shard => shard && shard.status() === 'active');
|
|
383
|
-
|
|
374
|
+
|
|
384
375
|
if (activeShards.length === 0) {
|
|
385
376
|
return { error: `No active shards available for room ${roomId}` };
|
|
386
377
|
}
|
|
387
|
-
|
|
378
|
+
|
|
388
379
|
// Apply balancing strategy
|
|
389
380
|
const balancingStrategy = room.balancingStrategy();
|
|
390
381
|
let selectedShard: ShardInfo;
|
|
391
|
-
|
|
382
|
+
|
|
392
383
|
switch (balancingStrategy) {
|
|
393
384
|
case 'least-connections':
|
|
394
385
|
// Choose shard with fewest connections
|
|
395
386
|
selectedShard = activeShards.reduce(
|
|
396
|
-
(min, shard) =>
|
|
387
|
+
(min, shard) =>
|
|
397
388
|
shard.currentConnections() < min.currentConnections() ? shard : min,
|
|
398
389
|
activeShards[0]
|
|
399
390
|
);
|
|
400
391
|
break;
|
|
401
|
-
|
|
392
|
+
|
|
402
393
|
case 'random':
|
|
403
394
|
// Choose random shard
|
|
404
395
|
selectedShard = activeShards[Math.floor(Math.random() * activeShards.length)];
|
|
405
396
|
break;
|
|
406
|
-
|
|
397
|
+
|
|
407
398
|
case 'round-robin':
|
|
408
399
|
default:
|
|
409
400
|
// Round-robin selection
|
|
410
401
|
const counter = this.rrCounters()[roomId] || 0;
|
|
411
402
|
const nextCounter = (counter + 1) % activeShards.length;
|
|
412
403
|
this.rrCounters()[roomId] = nextCounter;
|
|
413
|
-
|
|
404
|
+
|
|
414
405
|
selectedShard = activeShards[counter];
|
|
415
406
|
break;
|
|
416
407
|
}
|
|
417
|
-
|
|
408
|
+
|
|
418
409
|
return {
|
|
419
410
|
shardId: selectedShard.id,
|
|
420
411
|
url: selectedShard.url()
|
|
421
412
|
};
|
|
422
413
|
}
|
|
423
|
-
|
|
414
|
+
|
|
424
415
|
// Private methods
|
|
425
416
|
private async createShard(
|
|
426
417
|
roomId: string,
|
|
@@ -432,17 +423,17 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
432
423
|
console.error(`Cannot create shard for non-existent room: ${roomId}`);
|
|
433
424
|
return null;
|
|
434
425
|
}
|
|
435
|
-
|
|
426
|
+
|
|
436
427
|
// Generate shard ID
|
|
437
428
|
const shardId = `${roomId}:${Date.now()}-${Math.floor(Math.random() * 10000)}`;
|
|
438
|
-
|
|
429
|
+
|
|
439
430
|
// Generate URL from template
|
|
440
431
|
const template = urlTemplate || this.defaultShardUrlTemplate();
|
|
441
432
|
const url = template.replace('{shardId}', shardId).replace('{roomId}', roomId);
|
|
442
433
|
|
|
443
434
|
// Set max connections
|
|
444
435
|
const max = maxConnections || room.maxPlayersPerShard();
|
|
445
|
-
|
|
436
|
+
|
|
446
437
|
// Create the shard
|
|
447
438
|
const newShard = new ShardInfo();
|
|
448
439
|
newShard.id = shardId;
|
|
@@ -452,7 +443,7 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
452
443
|
newShard.currentConnections.set(0);
|
|
453
444
|
newShard.status.set("active");
|
|
454
445
|
newShard.lastHeartbeat.set(Date.now());
|
|
455
|
-
|
|
446
|
+
|
|
456
447
|
// Update shards collection
|
|
457
448
|
this.shards()[shardId] = newShard;
|
|
458
449
|
return newShard;
|