@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.
@@ -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;IAmGpB;;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"}
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.gateways.set(metadata.path, {
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
- return this.gateways.has(path);
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
- return this.gateways.get(path);
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 definition = path ? this.getGateway(path) : undefined;
3516
- if (!definition) {
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
- await this.invokeHandler(ws, definition, definition.handlers.open);
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 definition = path ? this.getGateway(path) : undefined;
3525
- if (!definition) {
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
- await this.invokeHandler(ws, definition, definition.handlers.message, message);
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 definition = path ? this.getGateway(path) : undefined;
3534
- if (!definition) {
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;AAYhD,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,wBAAwB;IACnC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA2B;IAClD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwC;IAEjE,OAAO;WAIO,WAAW,IAAI,wBAAwB;IAO9C,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,IAAI;IAsBlD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIjC,KAAK,IAAI,IAAI;IAIpB,OAAO,CAAC,UAAU;IAIlB;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAI7B;;;;;;OAMG;YACW,aAAa;IAiGd,UAAU,CAAC,EAAE,EAAE,eAAe,CAAC,uBAAuB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAUvE,aAAa,CACxB,EAAE,EAAE,eAAe,CAAC,uBAAuB,CAAC,EAC5C,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,eAAe,GAC9C,OAAO,CAAC,IAAI,CAAC;IAUH,WAAW,CACtB,EAAE,EAAE,eAAe,CAAC,uBAAuB,CAAC,EAC5C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC;CAQjB"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dangao/bun-server",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -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
- this.gateways.set(metadata.path, {
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
- return this.gateways.has(path);
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
- private getGateway(path: string): GatewayDefinition | undefined {
74
- return this.gateways.get(path);
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 definition = path ? this.getGateway(path) : undefined;
193
- if (!definition) {
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
- await this.invokeHandler(ws, definition, definition.handlers.open);
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 definition = path ? this.getGateway(path) : undefined;
206
- if (!definition) {
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
- await this.invokeHandler(ws, definition, definition.handlers.message, message);
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 definition = path ? this.getGateway(path) : undefined;
220
- if (!definition) {
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