@dangao/bun-server 1.6.0 → 1.7.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/core/server.d.ts.map +1 -1
- package/dist/index.js +73 -14
- package/dist/websocket/registry.d.ts +19 -0
- package/dist/websocket/registry.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/server.ts +1 -0
- package/src/websocket/registry.ts +114 -14
- package/tests/websocket/gateway.test.ts +207 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/core/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE1D;;OAEG;IACH,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;IAE7C;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;GAGG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAC,CAAkC;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,cAAc,CAAkB;IACxC,OAAO,CAAC,eAAe,CAAC,CAAgB;IACxC,OAAO,CAAC,eAAe,CAAC,CAAa;gBAElB,OAAO,EAAE,aAAa;IAIzC;;OAEG;IACI,KAAK,IAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/core/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE1D;;OAEG;IACH,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;IAE7C;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;GAGG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAC,CAAkC;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,cAAc,CAAkB;IACxC,OAAO,CAAC,eAAe,CAAC,CAAgB;IACxC,OAAO,CAAC,eAAe,CAAC,CAAa;gBAElB,OAAO,EAAE,aAAa;IAIzC;;OAEG;IACI,KAAK,IAAI,IAAI;IAoGpB;;OAEG;IACI,IAAI,IAAI,IAAI;IAWnB;;;;;OAKG;IACU,gBAAgB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA2C9D;;OAEG;IACI,iBAAiB,IAAI,MAAM;IAIlC;;OAEG;IACI,mBAAmB,IAAI,OAAO;IAIrC;;;OAGG;IACI,SAAS,IAAI,MAAM,CAAC,uBAAuB,CAAC,GAAG,SAAS;IAI/D;;;OAGG;IACI,SAAS,IAAI,OAAO;IAI3B;;OAEG;IACI,OAAO,IAAI,MAAM;IAIxB;;OAEG;IACI,WAAW,IAAI,MAAM,GAAG,SAAS;CAGzC"}
|
package/dist/index.js
CHANGED
|
@@ -3415,6 +3415,8 @@ class WebSocketGatewayRegistry {
|
|
|
3415
3415
|
static instance;
|
|
3416
3416
|
container;
|
|
3417
3417
|
gateways = new Map;
|
|
3418
|
+
staticGateways = new Map;
|
|
3419
|
+
dynamicGateways = [];
|
|
3418
3420
|
constructor() {
|
|
3419
3421
|
this.container = ControllerRegistry.getInstance().getContainer();
|
|
3420
3422
|
}
|
|
@@ -3424,6 +3426,15 @@ class WebSocketGatewayRegistry {
|
|
|
3424
3426
|
}
|
|
3425
3427
|
return WebSocketGatewayRegistry.instance;
|
|
3426
3428
|
}
|
|
3429
|
+
parsePath(path) {
|
|
3430
|
+
const paramNames = [];
|
|
3431
|
+
const patternString = path.replace(/:([^/]+)/g, (_, paramName) => {
|
|
3432
|
+
paramNames.push(paramName);
|
|
3433
|
+
return "([^/]+)";
|
|
3434
|
+
}).replace(/\*/g, ".*");
|
|
3435
|
+
const pattern = new RegExp(`^${patternString}$`);
|
|
3436
|
+
return { pattern, paramNames };
|
|
3437
|
+
}
|
|
3427
3438
|
register(gatewayClass) {
|
|
3428
3439
|
const metadata = getGatewayMetadata(gatewayClass);
|
|
3429
3440
|
if (!metadata) {
|
|
@@ -3436,20 +3447,55 @@ class WebSocketGatewayRegistry {
|
|
|
3436
3447
|
if (!this.container.isRegistered(gatewayClass)) {
|
|
3437
3448
|
this.container.register(gatewayClass);
|
|
3438
3449
|
}
|
|
3439
|
-
this.
|
|
3450
|
+
const { pattern, paramNames } = this.parsePath(metadata.path);
|
|
3451
|
+
const isStatic = !metadata.path.includes(":") && !metadata.path.includes("*");
|
|
3452
|
+
const definition = {
|
|
3440
3453
|
path: metadata.path,
|
|
3441
3454
|
gatewayClass,
|
|
3442
|
-
handlers
|
|
3443
|
-
|
|
3455
|
+
handlers,
|
|
3456
|
+
pattern,
|
|
3457
|
+
paramNames,
|
|
3458
|
+
isStatic
|
|
3459
|
+
};
|
|
3460
|
+
this.gateways.set(metadata.path, definition);
|
|
3461
|
+
if (isStatic) {
|
|
3462
|
+
this.staticGateways.set(metadata.path, definition);
|
|
3463
|
+
} else {
|
|
3464
|
+
this.dynamicGateways.push(definition);
|
|
3465
|
+
}
|
|
3444
3466
|
}
|
|
3445
3467
|
hasGateway(path) {
|
|
3446
|
-
|
|
3468
|
+
if (this.staticGateways.has(path)) {
|
|
3469
|
+
return true;
|
|
3470
|
+
}
|
|
3471
|
+
for (const gateway of this.dynamicGateways) {
|
|
3472
|
+
if (gateway.pattern.test(path)) {
|
|
3473
|
+
return true;
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
return false;
|
|
3447
3477
|
}
|
|
3448
3478
|
clear() {
|
|
3449
3479
|
this.gateways.clear();
|
|
3480
|
+
this.staticGateways.clear();
|
|
3481
|
+
this.dynamicGateways.length = 0;
|
|
3450
3482
|
}
|
|
3451
3483
|
getGateway(path) {
|
|
3452
|
-
|
|
3484
|
+
const staticGateway = this.staticGateways.get(path);
|
|
3485
|
+
if (staticGateway) {
|
|
3486
|
+
return { definition: staticGateway, params: {} };
|
|
3487
|
+
}
|
|
3488
|
+
for (const gateway of this.dynamicGateways) {
|
|
3489
|
+
const match = path.match(gateway.pattern);
|
|
3490
|
+
if (match) {
|
|
3491
|
+
const params = {};
|
|
3492
|
+
for (let i = 0;i < gateway.paramNames.length; i++) {
|
|
3493
|
+
params[gateway.paramNames[i]] = match[i + 1] ?? "";
|
|
3494
|
+
}
|
|
3495
|
+
return { definition: gateway, params };
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
return;
|
|
3453
3499
|
}
|
|
3454
3500
|
createGatewayInstance(definition) {
|
|
3455
3501
|
return this.container.resolve(definition.gatewayClass);
|
|
@@ -3479,7 +3525,14 @@ class WebSocketGatewayRegistry {
|
|
|
3479
3525
|
headers: new Headers
|
|
3480
3526
|
});
|
|
3481
3527
|
context = new Context(request);
|
|
3528
|
+
if (ws.data.params) {
|
|
3529
|
+
context.params = ws.data.params;
|
|
3530
|
+
}
|
|
3482
3531
|
ws.data.context = context;
|
|
3532
|
+
} else {
|
|
3533
|
+
if (ws.data.params) {
|
|
3534
|
+
context.params = ws.data.params;
|
|
3535
|
+
}
|
|
3483
3536
|
}
|
|
3484
3537
|
const boundParams = await ParamBinder.bind(prototype, handlerName, context, this.container);
|
|
3485
3538
|
const finalArgs = [];
|
|
@@ -3512,29 +3565,35 @@ class WebSocketGatewayRegistry {
|
|
|
3512
3565
|
}
|
|
3513
3566
|
async handleOpen(ws) {
|
|
3514
3567
|
const path = ws.data?.path;
|
|
3515
|
-
const
|
|
3516
|
-
if (!
|
|
3568
|
+
const match = path ? this.getGateway(path) : undefined;
|
|
3569
|
+
if (!match) {
|
|
3517
3570
|
ws.close(1008, "Gateway not found");
|
|
3518
3571
|
return;
|
|
3519
3572
|
}
|
|
3520
|
-
|
|
3573
|
+
if (match.params && Object.keys(match.params).length > 0) {
|
|
3574
|
+
ws.data.params = match.params;
|
|
3575
|
+
}
|
|
3576
|
+
await this.invokeHandler(ws, match.definition, match.definition.handlers.open);
|
|
3521
3577
|
}
|
|
3522
3578
|
async handleMessage(ws, message) {
|
|
3523
3579
|
const path = ws.data?.path;
|
|
3524
|
-
const
|
|
3525
|
-
if (!
|
|
3580
|
+
const match = path ? this.getGateway(path) : undefined;
|
|
3581
|
+
if (!match) {
|
|
3526
3582
|
ws.close(1008, "Gateway not found");
|
|
3527
3583
|
return;
|
|
3528
3584
|
}
|
|
3529
|
-
|
|
3585
|
+
if (match.params && Object.keys(match.params).length > 0 && !ws.data.params) {
|
|
3586
|
+
ws.data.params = match.params;
|
|
3587
|
+
}
|
|
3588
|
+
await this.invokeHandler(ws, match.definition, match.definition.handlers.message, message);
|
|
3530
3589
|
}
|
|
3531
3590
|
async handleClose(ws, code, reason) {
|
|
3532
3591
|
const path = ws.data?.path;
|
|
3533
|
-
const
|
|
3534
|
-
if (!
|
|
3592
|
+
const match = path ? this.getGateway(path) : undefined;
|
|
3593
|
+
if (!match) {
|
|
3535
3594
|
return;
|
|
3536
3595
|
}
|
|
3537
|
-
await this.invokeHandler(ws, definition, definition.handlers.close, code, reason);
|
|
3596
|
+
await this.invokeHandler(ws, match.definition, match.definition.handlers.close, code, reason);
|
|
3538
3597
|
}
|
|
3539
3598
|
}
|
|
3540
3599
|
|
|
@@ -5,16 +5,35 @@ export interface WebSocketConnectionData {
|
|
|
5
5
|
path: string;
|
|
6
6
|
query?: URLSearchParams;
|
|
7
7
|
context?: Context;
|
|
8
|
+
params?: Record<string, string>;
|
|
8
9
|
}
|
|
9
10
|
export declare class WebSocketGatewayRegistry {
|
|
10
11
|
private static instance;
|
|
11
12
|
private readonly container;
|
|
12
13
|
private readonly gateways;
|
|
14
|
+
private readonly staticGateways;
|
|
15
|
+
private readonly dynamicGateways;
|
|
13
16
|
private constructor();
|
|
14
17
|
static getInstance(): WebSocketGatewayRegistry;
|
|
18
|
+
/**
|
|
19
|
+
* 解析路径,生成匹配模式和参数名列表
|
|
20
|
+
* @param path - 路由路径
|
|
21
|
+
* @returns 匹配模式和参数名列表
|
|
22
|
+
*/
|
|
23
|
+
private parsePath;
|
|
15
24
|
register(gatewayClass: Constructor<unknown>): void;
|
|
25
|
+
/**
|
|
26
|
+
* 检查是否有匹配的网关(支持动态路径匹配)
|
|
27
|
+
* @param path - 请求路径
|
|
28
|
+
* @returns 是否有匹配的网关
|
|
29
|
+
*/
|
|
16
30
|
hasGateway(path: string): boolean;
|
|
17
31
|
clear(): void;
|
|
32
|
+
/**
|
|
33
|
+
* 获取匹配的网关(支持动态路径匹配)
|
|
34
|
+
* @param path - 请求路径
|
|
35
|
+
* @returns 匹配的网关定义和路径参数
|
|
36
|
+
*/
|
|
18
37
|
private getGateway;
|
|
19
38
|
/**
|
|
20
39
|
* 动态创建网关实例(每次连接创建新实例)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/websocket/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,KAAK,CAAC;AAO3C,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/websocket/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,KAAK,CAAC;AAO3C,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAehD,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,qBAAa,wBAAwB;IACnC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA2B;IAClD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwC;IACjE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwC;IACvE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA2B;IAE3D,OAAO;WAIO,WAAW,IAAI,wBAAwB;IAOrD;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAaV,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,IAAI;IAsCzD;;;;OAIG;IACI,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAgBjC,KAAK,IAAI,IAAI;IAMpB;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAuBlB;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAI7B;;;;;;OAMG;YACW,aAAa;IA0Gd,UAAU,CAAC,EAAE,EAAE,eAAe,CAAC,uBAAuB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAcvE,aAAa,CACxB,EAAE,EAAE,eAAe,CAAC,uBAAuB,CAAC,EAC5C,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,eAAe,GAC9C,OAAO,CAAC,IAAI,CAAC;IAcH,WAAW,CACtB,EAAE,EAAE,eAAe,CAAC,uBAAuB,CAAC,EAC5C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC;CAQjB"}
|
package/package.json
CHANGED
package/src/core/server.ts
CHANGED
|
@@ -86,6 +86,7 @@ export class BunServer {
|
|
|
86
86
|
upgradeHeader.toLowerCase() === "websocket"
|
|
87
87
|
) {
|
|
88
88
|
const url = new URL(request.url);
|
|
89
|
+
// 检查是否有匹配的网关(支持动态路径匹配)
|
|
89
90
|
if (!this.options.websocketRegistry.hasGateway(url.pathname)) {
|
|
90
91
|
return new Response("WebSocket gateway not found", { status: 404 });
|
|
91
92
|
}
|
|
@@ -16,18 +16,24 @@ interface GatewayDefinition {
|
|
|
16
16
|
message?: string;
|
|
17
17
|
close?: string;
|
|
18
18
|
};
|
|
19
|
+
pattern: RegExp;
|
|
20
|
+
paramNames: string[];
|
|
21
|
+
isStatic: boolean;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export interface WebSocketConnectionData {
|
|
22
25
|
path: string;
|
|
23
26
|
query?: URLSearchParams;
|
|
24
27
|
context?: Context;
|
|
28
|
+
params?: Record<string, string>;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export class WebSocketGatewayRegistry {
|
|
28
32
|
private static instance: WebSocketGatewayRegistry;
|
|
29
33
|
private readonly container: Container;
|
|
30
34
|
private readonly gateways = new Map<string, GatewayDefinition>();
|
|
35
|
+
private readonly staticGateways = new Map<string, GatewayDefinition>();
|
|
36
|
+
private readonly dynamicGateways: GatewayDefinition[] = [];
|
|
31
37
|
|
|
32
38
|
private constructor() {
|
|
33
39
|
this.container = ControllerRegistry.getInstance().getContainer();
|
|
@@ -40,6 +46,24 @@ export class WebSocketGatewayRegistry {
|
|
|
40
46
|
return WebSocketGatewayRegistry.instance;
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
/**
|
|
50
|
+
* 解析路径,生成匹配模式和参数名列表
|
|
51
|
+
* @param path - 路由路径
|
|
52
|
+
* @returns 匹配模式和参数名列表
|
|
53
|
+
*/
|
|
54
|
+
private parsePath(path: string): { pattern: RegExp; paramNames: string[] } {
|
|
55
|
+
const paramNames: string[] = [];
|
|
56
|
+
const patternString = path
|
|
57
|
+
.replace(/:([^/]+)/g, (_, paramName) => {
|
|
58
|
+
paramNames.push(paramName);
|
|
59
|
+
return '([^/]+)';
|
|
60
|
+
})
|
|
61
|
+
.replace(/\*/g, '.*');
|
|
62
|
+
|
|
63
|
+
const pattern = new RegExp(`^${patternString}$`);
|
|
64
|
+
return { pattern, paramNames };
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
public register(gatewayClass: Constructor<unknown>): void {
|
|
44
68
|
const metadata = getGatewayMetadata(gatewayClass);
|
|
45
69
|
if (!metadata) {
|
|
@@ -55,23 +79,82 @@ export class WebSocketGatewayRegistry {
|
|
|
55
79
|
this.container.register(gatewayClass);
|
|
56
80
|
}
|
|
57
81
|
|
|
58
|
-
|
|
82
|
+
// 解析路径
|
|
83
|
+
const { pattern, paramNames } = this.parsePath(metadata.path);
|
|
84
|
+
const isStatic = !metadata.path.includes(':') && !metadata.path.includes('*');
|
|
85
|
+
|
|
86
|
+
const definition: GatewayDefinition = {
|
|
59
87
|
path: metadata.path,
|
|
60
88
|
gatewayClass,
|
|
61
89
|
handlers,
|
|
62
|
-
|
|
90
|
+
pattern,
|
|
91
|
+
paramNames,
|
|
92
|
+
isStatic,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
this.gateways.set(metadata.path, definition);
|
|
96
|
+
|
|
97
|
+
// 分别存储静态和动态路由
|
|
98
|
+
if (isStatic) {
|
|
99
|
+
this.staticGateways.set(metadata.path, definition);
|
|
100
|
+
} else {
|
|
101
|
+
this.dynamicGateways.push(definition);
|
|
102
|
+
}
|
|
63
103
|
}
|
|
64
104
|
|
|
105
|
+
/**
|
|
106
|
+
* 检查是否有匹配的网关(支持动态路径匹配)
|
|
107
|
+
* @param path - 请求路径
|
|
108
|
+
* @returns 是否有匹配的网关
|
|
109
|
+
*/
|
|
65
110
|
public hasGateway(path: string): boolean {
|
|
66
|
-
|
|
111
|
+
// 先检查静态路由
|
|
112
|
+
if (this.staticGateways.has(path)) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 遍历动态路由
|
|
117
|
+
for (const gateway of this.dynamicGateways) {
|
|
118
|
+
if (gateway.pattern.test(path)) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return false;
|
|
67
124
|
}
|
|
68
125
|
|
|
69
126
|
public clear(): void {
|
|
70
127
|
this.gateways.clear();
|
|
128
|
+
this.staticGateways.clear();
|
|
129
|
+
this.dynamicGateways.length = 0;
|
|
71
130
|
}
|
|
72
131
|
|
|
73
|
-
|
|
74
|
-
|
|
132
|
+
/**
|
|
133
|
+
* 获取匹配的网关(支持动态路径匹配)
|
|
134
|
+
* @param path - 请求路径
|
|
135
|
+
* @returns 匹配的网关定义和路径参数
|
|
136
|
+
*/
|
|
137
|
+
private getGateway(path: string): { definition: GatewayDefinition; params: Record<string, string> } | undefined {
|
|
138
|
+
// 先检查静态路由
|
|
139
|
+
const staticGateway = this.staticGateways.get(path);
|
|
140
|
+
if (staticGateway) {
|
|
141
|
+
return { definition: staticGateway, params: {} };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 遍历动态路由
|
|
145
|
+
for (const gateway of this.dynamicGateways) {
|
|
146
|
+
const match = path.match(gateway.pattern);
|
|
147
|
+
if (match) {
|
|
148
|
+
// 提取路径参数
|
|
149
|
+
const params: Record<string, string> = {};
|
|
150
|
+
for (let i = 0; i < gateway.paramNames.length; i++) {
|
|
151
|
+
params[gateway.paramNames[i]] = match[i + 1] ?? '';
|
|
152
|
+
}
|
|
153
|
+
return { definition: gateway, params };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return undefined;
|
|
75
158
|
}
|
|
76
159
|
|
|
77
160
|
/**
|
|
@@ -129,7 +212,16 @@ export class WebSocketGatewayRegistry {
|
|
|
129
212
|
headers: new Headers(),
|
|
130
213
|
});
|
|
131
214
|
context = new Context(request);
|
|
215
|
+
// 设置路径参数到 Context
|
|
216
|
+
if (ws.data.params) {
|
|
217
|
+
context.params = ws.data.params;
|
|
218
|
+
}
|
|
132
219
|
ws.data.context = context;
|
|
220
|
+
} else {
|
|
221
|
+
// 如果 Context 已存在,更新路径参数
|
|
222
|
+
if (ws.data.params) {
|
|
223
|
+
context.params = ws.data.params;
|
|
224
|
+
}
|
|
133
225
|
}
|
|
134
226
|
|
|
135
227
|
// 使用 ParamBinder 绑定参数
|
|
@@ -189,12 +281,16 @@ export class WebSocketGatewayRegistry {
|
|
|
189
281
|
|
|
190
282
|
public async handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): Promise<void> {
|
|
191
283
|
const path = ws.data?.path;
|
|
192
|
-
const
|
|
193
|
-
if (!
|
|
284
|
+
const match = path ? this.getGateway(path) : undefined;
|
|
285
|
+
if (!match) {
|
|
194
286
|
ws.close(1008, 'Gateway not found');
|
|
195
287
|
return;
|
|
196
288
|
}
|
|
197
|
-
|
|
289
|
+
// 保存路径参数到 WebSocket 连接数据
|
|
290
|
+
if (match.params && Object.keys(match.params).length > 0) {
|
|
291
|
+
ws.data.params = match.params;
|
|
292
|
+
}
|
|
293
|
+
await this.invokeHandler(ws, match.definition, match.definition.handlers.open);
|
|
198
294
|
}
|
|
199
295
|
|
|
200
296
|
public async handleMessage(
|
|
@@ -202,12 +298,16 @@ export class WebSocketGatewayRegistry {
|
|
|
202
298
|
message: string | ArrayBuffer | ArrayBufferView,
|
|
203
299
|
): Promise<void> {
|
|
204
300
|
const path = ws.data?.path;
|
|
205
|
-
const
|
|
206
|
-
if (!
|
|
301
|
+
const match = path ? this.getGateway(path) : undefined;
|
|
302
|
+
if (!match) {
|
|
207
303
|
ws.close(1008, 'Gateway not found');
|
|
208
304
|
return;
|
|
209
305
|
}
|
|
210
|
-
|
|
306
|
+
// 保存路径参数到 WebSocket 连接数据(如果还没有)
|
|
307
|
+
if (match.params && Object.keys(match.params).length > 0 && !ws.data.params) {
|
|
308
|
+
ws.data.params = match.params;
|
|
309
|
+
}
|
|
310
|
+
await this.invokeHandler(ws, match.definition, match.definition.handlers.message, message);
|
|
211
311
|
}
|
|
212
312
|
|
|
213
313
|
public async handleClose(
|
|
@@ -216,11 +316,11 @@ export class WebSocketGatewayRegistry {
|
|
|
216
316
|
reason: string,
|
|
217
317
|
): Promise<void> {
|
|
218
318
|
const path = ws.data?.path;
|
|
219
|
-
const
|
|
220
|
-
if (!
|
|
319
|
+
const match = path ? this.getGateway(path) : undefined;
|
|
320
|
+
if (!match) {
|
|
221
321
|
return;
|
|
222
322
|
}
|
|
223
|
-
await this.invokeHandler(ws, definition, definition.handlers.close, code, reason);
|
|
323
|
+
await this.invokeHandler(ws, match.definition, match.definition.handlers.close, code, reason);
|
|
224
324
|
}
|
|
225
325
|
}
|
|
226
326
|
|
|
@@ -5,7 +5,7 @@ import { WebSocketGateway, OnOpen, OnMessage, OnClose } from '../../src/websocke
|
|
|
5
5
|
import { WebSocketGatewayRegistry } from '../../src/websocket/registry';
|
|
6
6
|
import { ControllerRegistry } from '../../src/controller/controller';
|
|
7
7
|
import { Application } from '../../src/core/application';
|
|
8
|
-
import { Query, Context } from '../../src/controller/decorators';
|
|
8
|
+
import { Query, Context, Param } from '../../src/controller/decorators';
|
|
9
9
|
import { getTestPort } from '../utils/test-port';
|
|
10
10
|
import type { WebSocketConnectionData } from '../../src/websocket/registry';
|
|
11
11
|
import type { Context as RequestContext } from '../../src/core/context';
|
|
@@ -1087,6 +1087,212 @@ describe('WebSocket Gateway Integration', () => {
|
|
|
1087
1087
|
|
|
1088
1088
|
ws.close();
|
|
1089
1089
|
});
|
|
1090
|
+
|
|
1091
|
+
test('should support dynamic path matching with path parameters', async () => {
|
|
1092
|
+
@WebSocketGateway('/ws/rooms/:roomId')
|
|
1093
|
+
class DynamicPathGateway {
|
|
1094
|
+
public static connected = false;
|
|
1095
|
+
public static roomIds: string[] = [];
|
|
1096
|
+
public static params: Record<string, string>[] = [];
|
|
1097
|
+
|
|
1098
|
+
@OnOpen
|
|
1099
|
+
public handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): void {
|
|
1100
|
+
DynamicPathGateway.connected = true;
|
|
1101
|
+
const roomId = ws.data?.params?.roomId;
|
|
1102
|
+
if (roomId) {
|
|
1103
|
+
DynamicPathGateway.roomIds.push(roomId);
|
|
1104
|
+
DynamicPathGateway.params.push(ws.data.params || {});
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
@OnMessage
|
|
1109
|
+
public handleMessage(
|
|
1110
|
+
ws: ServerWebSocket<WebSocketConnectionData>,
|
|
1111
|
+
message: string,
|
|
1112
|
+
): void {
|
|
1113
|
+
const roomId = ws.data?.params?.roomId;
|
|
1114
|
+
if (roomId) {
|
|
1115
|
+
ws.send(`Room ${roomId}: ${message}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
app.registerWebSocketGateway(DynamicPathGateway);
|
|
1121
|
+
await app.listen();
|
|
1122
|
+
|
|
1123
|
+
const registry = WebSocketGatewayRegistry.getInstance();
|
|
1124
|
+
|
|
1125
|
+
// 验证动态路径匹配
|
|
1126
|
+
expect(registry.hasGateway('/ws/rooms/123')).toBe(true);
|
|
1127
|
+
expect(registry.hasGateway('/ws/rooms/abc')).toBe(true);
|
|
1128
|
+
expect(registry.hasGateway('/ws/rooms/room-xyz')).toBe(true);
|
|
1129
|
+
// 不匹配的路径
|
|
1130
|
+
expect(registry.hasGateway('/ws/rooms')).toBe(false);
|
|
1131
|
+
expect(registry.hasGateway('/ws/rooms/123/users')).toBe(false);
|
|
1132
|
+
|
|
1133
|
+
// 测试访问动态路径
|
|
1134
|
+
const roomId1 = 'room-123';
|
|
1135
|
+
const ws1 = new WebSocket(`ws://localhost:${port}/ws/rooms/${roomId1}`);
|
|
1136
|
+
|
|
1137
|
+
await new Promise<void>((resolve, reject) => {
|
|
1138
|
+
const timeout = setTimeout(() => {
|
|
1139
|
+
reject(new Error('WebSocket connection timeout'));
|
|
1140
|
+
}, 2000);
|
|
1141
|
+
|
|
1142
|
+
ws1.onopen = () => {
|
|
1143
|
+
clearTimeout(timeout);
|
|
1144
|
+
resolve();
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
ws1.onerror = (error) => {
|
|
1148
|
+
clearTimeout(timeout);
|
|
1149
|
+
reject(error);
|
|
1150
|
+
};
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1154
|
+
expect(DynamicPathGateway.connected).toBe(true);
|
|
1155
|
+
expect(DynamicPathGateway.roomIds).toContain(roomId1);
|
|
1156
|
+
expect(DynamicPathGateway.params[0]?.roomId).toBe(roomId1);
|
|
1157
|
+
|
|
1158
|
+
// 测试另一个房间 ID
|
|
1159
|
+
const roomId2 = 'room-456';
|
|
1160
|
+
const ws2 = new WebSocket(`ws://localhost:${port}/ws/rooms/${roomId2}`);
|
|
1161
|
+
|
|
1162
|
+
await new Promise<void>((resolve, reject) => {
|
|
1163
|
+
const timeout = setTimeout(() => {
|
|
1164
|
+
reject(new Error('WebSocket connection timeout'));
|
|
1165
|
+
}, 2000);
|
|
1166
|
+
|
|
1167
|
+
ws2.onopen = () => {
|
|
1168
|
+
clearTimeout(timeout);
|
|
1169
|
+
resolve();
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
ws2.onerror = (error) => {
|
|
1173
|
+
clearTimeout(timeout);
|
|
1174
|
+
reject(error);
|
|
1175
|
+
};
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1179
|
+
expect(DynamicPathGateway.roomIds).toContain(roomId2);
|
|
1180
|
+
expect(DynamicPathGateway.params.some((p) => p.roomId === roomId2)).toBe(true);
|
|
1181
|
+
|
|
1182
|
+
ws1.close();
|
|
1183
|
+
ws2.close();
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
test('should support multiple path parameters', async () => {
|
|
1187
|
+
@WebSocketGateway('/ws/users/:userId/rooms/:roomId')
|
|
1188
|
+
class MultiParamGateway {
|
|
1189
|
+
public static connections: Array<{
|
|
1190
|
+
userId: string;
|
|
1191
|
+
roomId: string;
|
|
1192
|
+
}> = [];
|
|
1193
|
+
|
|
1194
|
+
@OnOpen
|
|
1195
|
+
public handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): void {
|
|
1196
|
+
const params = ws.data?.params || {};
|
|
1197
|
+
MultiParamGateway.connections.push({
|
|
1198
|
+
userId: params.userId || '',
|
|
1199
|
+
roomId: params.roomId || '',
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
app.registerWebSocketGateway(MultiParamGateway);
|
|
1205
|
+
await app.listen();
|
|
1206
|
+
|
|
1207
|
+
const registry = WebSocketGatewayRegistry.getInstance();
|
|
1208
|
+
|
|
1209
|
+
// 验证多参数路径匹配
|
|
1210
|
+
expect(registry.hasGateway('/ws/users/user1/rooms/room1')).toBe(true);
|
|
1211
|
+
expect(registry.hasGateway('/ws/users/user2/rooms/room2')).toBe(true);
|
|
1212
|
+
|
|
1213
|
+
const userId = 'user-123';
|
|
1214
|
+
const roomId = 'room-456';
|
|
1215
|
+
const ws = new WebSocket(
|
|
1216
|
+
`ws://localhost:${port}/ws/users/${userId}/rooms/${roomId}`,
|
|
1217
|
+
);
|
|
1218
|
+
|
|
1219
|
+
await new Promise<void>((resolve, reject) => {
|
|
1220
|
+
const timeout = setTimeout(() => {
|
|
1221
|
+
reject(new Error('WebSocket connection timeout'));
|
|
1222
|
+
}, 2000);
|
|
1223
|
+
|
|
1224
|
+
ws.onopen = () => {
|
|
1225
|
+
clearTimeout(timeout);
|
|
1226
|
+
resolve();
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1229
|
+
ws.onerror = (error) => {
|
|
1230
|
+
clearTimeout(timeout);
|
|
1231
|
+
reject(error);
|
|
1232
|
+
};
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1236
|
+
|
|
1237
|
+
// 验证路径参数被正确提取
|
|
1238
|
+
expect(MultiParamGateway.connections.length).toBeGreaterThan(0);
|
|
1239
|
+
const connection = MultiParamGateway.connections[0];
|
|
1240
|
+
expect(connection.userId).toBe(userId);
|
|
1241
|
+
expect(connection.roomId).toBe(roomId);
|
|
1242
|
+
|
|
1243
|
+
ws.close();
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
test('should support @Param decorator with dynamic path parameters', async () => {
|
|
1247
|
+
@WebSocketGateway('/ws/param-test/:id')
|
|
1248
|
+
class ParamDecoratorGateway {
|
|
1249
|
+
public static receivedIds: string[] = [];
|
|
1250
|
+
public static paramValues: string[] = [];
|
|
1251
|
+
|
|
1252
|
+
@OnMessage
|
|
1253
|
+
public handleMessage(
|
|
1254
|
+
_ws: ServerWebSocket<WebSocketConnectionData>,
|
|
1255
|
+
message: string,
|
|
1256
|
+
@Param('id') id: string,
|
|
1257
|
+
): void {
|
|
1258
|
+
ParamDecoratorGateway.receivedIds.push(message);
|
|
1259
|
+
ParamDecoratorGateway.paramValues.push(id);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
app.registerWebSocketGateway(ParamDecoratorGateway);
|
|
1264
|
+
await app.listen();
|
|
1265
|
+
|
|
1266
|
+
const testId = 'test-123';
|
|
1267
|
+
const ws = new WebSocket(`ws://localhost:${port}/ws/param-test/${testId}`);
|
|
1268
|
+
|
|
1269
|
+
await new Promise<void>((resolve, reject) => {
|
|
1270
|
+
const timeout = setTimeout(() => {
|
|
1271
|
+
reject(new Error('WebSocket connection timeout'));
|
|
1272
|
+
}, 2000);
|
|
1273
|
+
|
|
1274
|
+
ws.onopen = () => {
|
|
1275
|
+
clearTimeout(timeout);
|
|
1276
|
+
resolve();
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
ws.onerror = (error) => {
|
|
1280
|
+
clearTimeout(timeout);
|
|
1281
|
+
reject(error);
|
|
1282
|
+
};
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1286
|
+
ws.send('test-message');
|
|
1287
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1288
|
+
|
|
1289
|
+
// 验证消息被接收
|
|
1290
|
+
expect(ParamDecoratorGateway.receivedIds).toContain('test-message');
|
|
1291
|
+
// 验证 @Param 装饰器正确提取了路径参数
|
|
1292
|
+
expect(ParamDecoratorGateway.paramValues).toContain(testId);
|
|
1293
|
+
|
|
1294
|
+
ws.close();
|
|
1295
|
+
});
|
|
1090
1296
|
});
|
|
1091
1297
|
|
|
1092
1298
|
|