@signe/room 2.9.0 → 2.9.2
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 +9 -0
- package/LICENSE +21 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +537 -678
- package/dist/index.js.map +1 -1
- package/package.json +10 -4
- package/readme.md +7 -6
- package/src/jwt.ts +1 -5
- package/src/server.ts +40 -27
- package/src/world.ts +28 -19
package/package.json
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signe/room",
|
|
3
|
-
"version": "2.9.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "2.9.2",
|
|
4
|
+
"description": "PartyKit room primitives with synchronized state, sessions, guards, and HTTP handlers.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
|
-
"keywords": [
|
|
6
|
+
"keywords": [
|
|
7
|
+
"partykit",
|
|
8
|
+
"realtime",
|
|
9
|
+
"websocket",
|
|
10
|
+
"cloudflare",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
7
13
|
"author": "Samuel Ronce",
|
|
8
14
|
"type": "module",
|
|
9
15
|
"exports": {
|
|
@@ -17,7 +23,7 @@
|
|
|
17
23
|
"dset": "^3.1.3",
|
|
18
24
|
"partysocket": "^1.0.1",
|
|
19
25
|
"zod": "^3.23.8",
|
|
20
|
-
"@signe/sync": "2.9.
|
|
26
|
+
"@signe/sync": "2.9.2"
|
|
21
27
|
},
|
|
22
28
|
"publishConfig": {
|
|
23
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 { Room, Request,
|
|
126
|
+
import { Guard, Room, Request, ServerResponse } from "@signe/room";
|
|
127
127
|
|
|
128
128
|
@Room({
|
|
129
129
|
path: "api"
|
|
@@ -169,11 +169,12 @@ class ApiRoom {
|
|
|
169
169
|
}
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
-
Request handler methods receive
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
Request handler methods receive:
|
|
173
|
+
|
|
174
|
+
1. `req`: the original `Party.Request`, extended with `req.params` and
|
|
175
|
+
`req.data` when a validation schema is provided.
|
|
176
|
+
2. `res`: a `ServerResponse` helper for JSON, text, redirects, and common
|
|
177
|
+
error responses.
|
|
177
178
|
|
|
178
179
|
You can return:
|
|
179
180
|
- A Response object for complete control
|
package/src/jwt.ts
CHANGED
|
@@ -132,9 +132,7 @@ 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
|
|
136
135
|
const encodedHeader: string = this.base64UrlEncode(this.encoder.encode(JSON.stringify(header)));
|
|
137
|
-
// @ts-expect-error - TS doesn't have a built-in TextEncoder
|
|
138
136
|
const encodedPayload: string = this.base64UrlEncode(this.encoder.encode(JSON.stringify(fullPayload)));
|
|
139
137
|
|
|
140
138
|
// Create signature base
|
|
@@ -174,9 +172,7 @@ export class JWTAuth {
|
|
|
174
172
|
|
|
175
173
|
// Decode header and payload
|
|
176
174
|
try {
|
|
177
|
-
// @ts-expect-error - TS doesn't have a built-in TextDecoder
|
|
178
175
|
const header: JWTHeader = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedHeader)));
|
|
179
|
-
// @ts-expect-error - TS doesn't have a built-in TextDecoder
|
|
180
176
|
const payload: JWTPayload = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedPayload)));
|
|
181
177
|
|
|
182
178
|
// Check algorithm
|
|
@@ -214,4 +210,4 @@ export class JWTAuth {
|
|
|
214
210
|
throw new Error('Token verification failed: Unknown error');
|
|
215
211
|
}
|
|
216
212
|
}
|
|
217
|
-
}
|
|
213
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -615,7 +615,10 @@ export class Server implements Party.Server {
|
|
|
615
615
|
return;
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
-
const sessionExpiryTime =
|
|
618
|
+
const sessionExpiryTime =
|
|
619
|
+
subRoom.sessionExpiryTime ??
|
|
620
|
+
subRoom.constructor.sessionExpiryTime ??
|
|
621
|
+
5 * 60 * 1000;
|
|
619
622
|
await this.garbageCollector({ sessionExpiryTime });
|
|
620
623
|
|
|
621
624
|
// Check room guards
|
|
@@ -1289,9 +1292,7 @@ export class Server implements Party.Server {
|
|
|
1289
1292
|
|
|
1290
1293
|
const url = new URL(req.url);
|
|
1291
1294
|
const method = req.method;
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
pathname = '/' + pathname.split('/').slice(4).join('/');
|
|
1295
|
+
const pathname = this.normalizeRequestPath(url.pathname);
|
|
1295
1296
|
|
|
1296
1297
|
// Check each registered handler
|
|
1297
1298
|
for (const [routeKey, handler] of requestHandlers.entries()) {
|
|
@@ -1325,16 +1326,18 @@ export class Server implements Party.Server {
|
|
|
1325
1326
|
if (handler.bodyValidation && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
1326
1327
|
try {
|
|
1327
1328
|
const contentType = req.headers.get('content-type') || '';
|
|
1328
|
-
if (contentType.includes('application/json')) {
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1329
|
+
if (!contentType.includes('application/json')) {
|
|
1330
|
+
return res.badRequest("Content-Type must be application/json");
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const body = await req.json();
|
|
1334
|
+
const validation = handler.bodyValidation.safeParse(body);
|
|
1335
|
+
if (!validation.success) {
|
|
1336
|
+
return res.badRequest("Invalid request body", {
|
|
1337
|
+
details: validation.error
|
|
1338
|
+
});
|
|
1337
1339
|
}
|
|
1340
|
+
bodyData = validation.data;
|
|
1338
1341
|
} catch (error) {
|
|
1339
1342
|
return res.badRequest("Failed to parse request body");
|
|
1340
1343
|
}
|
|
@@ -1372,14 +1375,7 @@ export class Server implements Party.Server {
|
|
|
1372
1375
|
* @returns {boolean} True if the paths match
|
|
1373
1376
|
*/
|
|
1374
1377
|
private pathMatches(requestPath: string, handlerPath: string): boolean {
|
|
1375
|
-
|
|
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);
|
|
1378
|
+
return this.pathPatternToRegex(handlerPath).test(requestPath);
|
|
1383
1379
|
}
|
|
1384
1380
|
|
|
1385
1381
|
/**
|
|
@@ -1401,12 +1397,7 @@ export class Server implements Party.Server {
|
|
|
1401
1397
|
}
|
|
1402
1398
|
});
|
|
1403
1399
|
|
|
1404
|
-
|
|
1405
|
-
const pathRegexString = handlerPath
|
|
1406
|
-
.replace(/\//g, '\\/') // Escape slashes
|
|
1407
|
-
.replace(/:([^\/]+)/g, '([^/]+)'); // Convert :params to capture groups
|
|
1408
|
-
|
|
1409
|
-
const pathRegex = new RegExp(`^${pathRegexString}`);
|
|
1400
|
+
const pathRegex = this.pathPatternToRegex(handlerPath);
|
|
1410
1401
|
const matches = requestPath.match(pathRegex);
|
|
1411
1402
|
|
|
1412
1403
|
if (matches && matches.length > 1) {
|
|
@@ -1419,6 +1410,28 @@ export class Server implements Party.Server {
|
|
|
1419
1410
|
return params;
|
|
1420
1411
|
}
|
|
1421
1412
|
|
|
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
|
+
|
|
1422
1435
|
/**
|
|
1423
1436
|
* @method handleShardRequest
|
|
1424
1437
|
* @private
|
package/src/world.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { signal } from "@signe/reactive";
|
|
2
|
-
import { Room,
|
|
2
|
+
import { Room, 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";
|
|
8
7
|
import { RoomInterceptorPacket, RoomOnJoin } from "./interfaces";
|
|
9
8
|
import { ServerResponse } from "./request/response";
|
|
10
9
|
|
|
@@ -24,14 +23,8 @@ const RoomConfigSchema = z.object({
|
|
|
24
23
|
maxShards: z.number().int().positive().optional(),
|
|
25
24
|
});
|
|
26
25
|
|
|
27
|
-
const RegisterShardSchema = z.object({
|
|
28
|
-
shardId: z.string(),
|
|
29
|
-
roomId: z.string(),
|
|
30
|
-
url: z.string().url(),
|
|
31
|
-
maxConnections: z.number().int().positive(),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
26
|
const UpdateShardStatsSchema = z.object({
|
|
27
|
+
shardId: z.string(),
|
|
35
28
|
connections: z.number().int().min(0),
|
|
36
29
|
status: z.enum(['active', 'maintenance', 'draining']).optional(),
|
|
37
30
|
});
|
|
@@ -139,8 +132,15 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
139
132
|
method: 'POST',
|
|
140
133
|
})
|
|
141
134
|
@Guard([guardManageWorld])
|
|
142
|
-
async registerRoom(req: Party.Request) {
|
|
143
|
-
const
|
|
135
|
+
async registerRoom(req: Party.Request, res?: ServerResponse) {
|
|
136
|
+
const roomConfigResult = RoomConfigSchema.safeParse(await req.json());
|
|
137
|
+
if (!roomConfigResult.success) {
|
|
138
|
+
return res?.badRequest("Invalid room configuration", {
|
|
139
|
+
details: roomConfigResult.error
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const roomConfig = roomConfigResult.data;
|
|
144
144
|
const roomId = roomConfig.name;
|
|
145
145
|
|
|
146
146
|
if (!this.rooms()[roomId]) {
|
|
@@ -178,7 +178,14 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
178
178
|
})
|
|
179
179
|
@Guard([guardManageWorld])
|
|
180
180
|
async updateShardStats(req: Party.Request, res: ServerResponse) {
|
|
181
|
-
const
|
|
181
|
+
const bodyResult = UpdateShardStatsSchema.safeParse(await req.json());
|
|
182
|
+
if (!bodyResult.success) {
|
|
183
|
+
return res.badRequest("Invalid shard statistics", {
|
|
184
|
+
details: bodyResult.error
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const body = bodyResult.data;
|
|
182
189
|
const { shardId, connections, status } = body;
|
|
183
190
|
const shard = this.shards()[shardId];
|
|
184
191
|
|
|
@@ -199,7 +206,14 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
199
206
|
})
|
|
200
207
|
@Guard([guardManageWorld])
|
|
201
208
|
async scaleRoom(req: Party.Request, res: ServerResponse) {
|
|
202
|
-
const
|
|
209
|
+
const dataResult = ScaleRoomSchema.safeParse(await req.json());
|
|
210
|
+
if (!dataResult.success) {
|
|
211
|
+
return res.badRequest("Invalid scale request", {
|
|
212
|
+
details: dataResult.error
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const data = dataResult.data;
|
|
203
217
|
const { targetShardCount, shardTemplate, roomId } = data;
|
|
204
218
|
|
|
205
219
|
// Validate room exists
|
|
@@ -235,11 +249,6 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
235
249
|
})
|
|
236
250
|
.slice(0, previousShardCount - targetShardCount);
|
|
237
251
|
|
|
238
|
-
// Remove the selected shards
|
|
239
|
-
const shardsToKeep = roomShards.filter(
|
|
240
|
-
shard => !shardsToRemove.some(s => s.id === shard.id)
|
|
241
|
-
);
|
|
242
|
-
|
|
243
252
|
// Update shards
|
|
244
253
|
for (const shard of shardsToRemove) {
|
|
245
254
|
delete this.shards()[shard.id];
|
|
@@ -448,4 +457,4 @@ export class WorldRoom implements RoomInterceptorPacket, RoomOnJoin {
|
|
|
448
457
|
this.shards()[shardId] = newShard;
|
|
449
458
|
return newShard;
|
|
450
459
|
}
|
|
451
|
-
}
|
|
460
|
+
}
|