@dangao/bun-server 1.5.0 → 1.6.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 +72 -21
- package/dist/websocket/registry.d.ts +19 -4
- package/dist/websocket/registry.d.ts.map +1 -1
- package/docs/zh/guide.md +23 -18
- package/docs/zh/migration.md +8 -2
- package/package.json +1 -1
- package/src/core/server.ts +15 -7
- package/src/websocket/registry.ts +113 -19
- package/tests/websocket/gateway.test.ts +1024 -0
|
@@ -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;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"}
|
package/dist/index.js
CHANGED
|
@@ -3264,8 +3264,14 @@ class BunServer {
|
|
|
3264
3264
|
if (!this.options.websocketRegistry.hasGateway(url.pathname)) {
|
|
3265
3265
|
return new Response("WebSocket gateway not found", { status: 404 });
|
|
3266
3266
|
}
|
|
3267
|
+
const context2 = new Context(request);
|
|
3268
|
+
const queryParams = new URLSearchParams(url.searchParams);
|
|
3267
3269
|
const upgraded = server.upgrade(request, {
|
|
3268
|
-
data: {
|
|
3270
|
+
data: {
|
|
3271
|
+
path: url.pathname,
|
|
3272
|
+
query: queryParams,
|
|
3273
|
+
context: context2
|
|
3274
|
+
}
|
|
3269
3275
|
});
|
|
3270
3276
|
if (upgraded) {
|
|
3271
3277
|
return;
|
|
@@ -3291,14 +3297,14 @@ class BunServer {
|
|
|
3291
3297
|
return responsePromise;
|
|
3292
3298
|
},
|
|
3293
3299
|
websocket: {
|
|
3294
|
-
open: (ws) => {
|
|
3295
|
-
this.options.websocketRegistry?.handleOpen(ws);
|
|
3300
|
+
open: async (ws) => {
|
|
3301
|
+
await this.options.websocketRegistry?.handleOpen(ws);
|
|
3296
3302
|
},
|
|
3297
|
-
message: (ws, message) => {
|
|
3298
|
-
this.options.websocketRegistry?.handleMessage(ws, message);
|
|
3303
|
+
message: async (ws, message) => {
|
|
3304
|
+
await this.options.websocketRegistry?.handleMessage(ws, message);
|
|
3299
3305
|
},
|
|
3300
|
-
close: (ws, code, reason) => {
|
|
3301
|
-
this.options.websocketRegistry?.handleClose(ws, code, reason);
|
|
3306
|
+
close: async (ws, code, reason) => {
|
|
3307
|
+
await this.options.websocketRegistry?.handleClose(ws, code, reason);
|
|
3302
3308
|
}
|
|
3303
3309
|
}
|
|
3304
3310
|
});
|
|
@@ -3403,6 +3409,8 @@ function getHandlerMetadata(target) {
|
|
|
3403
3409
|
}
|
|
3404
3410
|
|
|
3405
3411
|
// src/websocket/registry.ts
|
|
3412
|
+
init_param_binder();
|
|
3413
|
+
init_decorators2();
|
|
3406
3414
|
class WebSocketGatewayRegistry {
|
|
3407
3415
|
static instance;
|
|
3408
3416
|
container;
|
|
@@ -3443,47 +3451,90 @@ class WebSocketGatewayRegistry {
|
|
|
3443
3451
|
getGateway(path) {
|
|
3444
3452
|
return this.gateways.get(path);
|
|
3445
3453
|
}
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
definition.instance = this.container.resolve(definition.gatewayClass);
|
|
3449
|
-
}
|
|
3450
|
-
return definition.instance;
|
|
3454
|
+
createGatewayInstance(definition) {
|
|
3455
|
+
return this.container.resolve(definition.gatewayClass);
|
|
3451
3456
|
}
|
|
3452
|
-
invokeHandler(ws, definition, handlerName, ...args) {
|
|
3457
|
+
async invokeHandler(ws, definition, handlerName, ...args) {
|
|
3453
3458
|
if (!handlerName) {
|
|
3454
3459
|
return;
|
|
3455
3460
|
}
|
|
3456
|
-
const instance = this.
|
|
3461
|
+
const instance = this.createGatewayInstance(definition);
|
|
3457
3462
|
const handler = instance[handlerName];
|
|
3458
|
-
if (typeof handler
|
|
3463
|
+
if (typeof handler !== "function") {
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
3466
|
+
const prototype = definition.gatewayClass.prototype;
|
|
3467
|
+
const paramMetadata = getParamMetadata(prototype, handlerName);
|
|
3468
|
+
if (paramMetadata.length > 0) {
|
|
3469
|
+
let context = ws.data?.context;
|
|
3470
|
+
if (!context) {
|
|
3471
|
+
const url = new URL(ws.data.path || "/", "http://localhost");
|
|
3472
|
+
if (ws.data.query) {
|
|
3473
|
+
ws.data.query.forEach((value, key) => {
|
|
3474
|
+
url.searchParams.set(key, value);
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
3477
|
+
const request = new Request(url.toString(), {
|
|
3478
|
+
method: "GET",
|
|
3479
|
+
headers: new Headers
|
|
3480
|
+
});
|
|
3481
|
+
context = new Context(request);
|
|
3482
|
+
ws.data.context = context;
|
|
3483
|
+
}
|
|
3484
|
+
const boundParams = await ParamBinder.bind(prototype, handlerName, context, this.container);
|
|
3485
|
+
const finalArgs = [];
|
|
3486
|
+
const maxParamIndex = paramMetadata.length > 0 ? Math.max(...paramMetadata.map((m) => m.index)) : -1;
|
|
3487
|
+
const totalParamCount = Math.max(maxParamIndex + 1, args.length + 1);
|
|
3488
|
+
for (let i = 0;i < totalParamCount; i++) {
|
|
3489
|
+
const meta = paramMetadata.find((m) => m.index === i);
|
|
3490
|
+
if (meta) {
|
|
3491
|
+
finalArgs[i] = boundParams[i];
|
|
3492
|
+
} else if (i === 0) {
|
|
3493
|
+
finalArgs[i] = ws;
|
|
3494
|
+
} else {
|
|
3495
|
+
const argIndex = i - 1;
|
|
3496
|
+
if (argIndex < args.length) {
|
|
3497
|
+
finalArgs[i] = args[argIndex];
|
|
3498
|
+
} else {
|
|
3499
|
+
finalArgs[i] = undefined;
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
if (args.length > maxParamIndex) {
|
|
3504
|
+
for (let i = maxParamIndex + 1;i < args.length; i++) {
|
|
3505
|
+
finalArgs.push(args[i]);
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
handler.apply(instance, finalArgs);
|
|
3509
|
+
} else {
|
|
3459
3510
|
handler.apply(instance, [ws, ...args]);
|
|
3460
3511
|
}
|
|
3461
3512
|
}
|
|
3462
|
-
handleOpen(ws) {
|
|
3513
|
+
async handleOpen(ws) {
|
|
3463
3514
|
const path = ws.data?.path;
|
|
3464
3515
|
const definition = path ? this.getGateway(path) : undefined;
|
|
3465
3516
|
if (!definition) {
|
|
3466
3517
|
ws.close(1008, "Gateway not found");
|
|
3467
3518
|
return;
|
|
3468
3519
|
}
|
|
3469
|
-
this.invokeHandler(ws, definition, definition.handlers.open);
|
|
3520
|
+
await this.invokeHandler(ws, definition, definition.handlers.open);
|
|
3470
3521
|
}
|
|
3471
|
-
handleMessage(ws, message) {
|
|
3522
|
+
async handleMessage(ws, message) {
|
|
3472
3523
|
const path = ws.data?.path;
|
|
3473
3524
|
const definition = path ? this.getGateway(path) : undefined;
|
|
3474
3525
|
if (!definition) {
|
|
3475
3526
|
ws.close(1008, "Gateway not found");
|
|
3476
3527
|
return;
|
|
3477
3528
|
}
|
|
3478
|
-
this.invokeHandler(ws, definition, definition.handlers.message, message);
|
|
3529
|
+
await this.invokeHandler(ws, definition, definition.handlers.message, message);
|
|
3479
3530
|
}
|
|
3480
|
-
handleClose(ws, code, reason) {
|
|
3531
|
+
async handleClose(ws, code, reason) {
|
|
3481
3532
|
const path = ws.data?.path;
|
|
3482
3533
|
const definition = path ? this.getGateway(path) : undefined;
|
|
3483
3534
|
if (!definition) {
|
|
3484
3535
|
return;
|
|
3485
3536
|
}
|
|
3486
|
-
this.invokeHandler(ws, definition, definition.handlers.close, code, reason);
|
|
3537
|
+
await this.invokeHandler(ws, definition, definition.handlers.close, code, reason);
|
|
3487
3538
|
}
|
|
3488
3539
|
}
|
|
3489
3540
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ServerWebSocket } from 'bun';
|
|
2
|
+
import { Context } from '../core/context';
|
|
2
3
|
import type { Constructor } from '@/core/types';
|
|
3
4
|
export interface WebSocketConnectionData {
|
|
4
5
|
path: string;
|
|
6
|
+
query?: URLSearchParams;
|
|
7
|
+
context?: Context;
|
|
5
8
|
}
|
|
6
9
|
export declare class WebSocketGatewayRegistry {
|
|
7
10
|
private static instance;
|
|
@@ -13,10 +16,22 @@ export declare class WebSocketGatewayRegistry {
|
|
|
13
16
|
hasGateway(path: string): boolean;
|
|
14
17
|
clear(): void;
|
|
15
18
|
private getGateway;
|
|
16
|
-
|
|
19
|
+
/**
|
|
20
|
+
* 动态创建网关实例(每次连接创建新实例)
|
|
21
|
+
* @param definition - 网关定义
|
|
22
|
+
* @returns 网关实例
|
|
23
|
+
*/
|
|
24
|
+
private createGatewayInstance;
|
|
25
|
+
/**
|
|
26
|
+
* 调用处理器,支持参数绑定
|
|
27
|
+
* @param ws - WebSocket 连接
|
|
28
|
+
* @param definition - 网关定义
|
|
29
|
+
* @param handlerName - 处理器方法名
|
|
30
|
+
* @param args - 原始参数(message, code, reason 等,不包括 ws)
|
|
31
|
+
*/
|
|
17
32
|
private invokeHandler;
|
|
18
|
-
handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): void
|
|
19
|
-
handleMessage(ws: ServerWebSocket<WebSocketConnectionData>, message: string | ArrayBuffer | ArrayBufferView): void
|
|
20
|
-
handleClose(ws: ServerWebSocket<WebSocketConnectionData>, code: number, reason: string): void
|
|
33
|
+
handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): Promise<void>;
|
|
34
|
+
handleMessage(ws: ServerWebSocket<WebSocketConnectionData>, message: string | ArrayBuffer | ArrayBufferView): Promise<void>;
|
|
35
|
+
handleClose(ws: ServerWebSocket<WebSocketConnectionData>, code: number, reason: string): Promise<void>;
|
|
21
36
|
}
|
|
22
37
|
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -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;
|
|
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"}
|
package/docs/zh/guide.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
8
|
import "reflect-metadata";
|
|
9
|
-
import { Application } from "
|
|
9
|
+
import { Application } from "@dangao/bun-server";
|
|
10
10
|
|
|
11
11
|
const app = new Application({ port: 3000 });
|
|
12
12
|
app.listen();
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
Injectable,
|
|
27
27
|
Param,
|
|
28
28
|
POST,
|
|
29
|
-
} from "
|
|
29
|
+
} from "@dangao/bun-server";
|
|
30
30
|
|
|
31
31
|
@Injectable()
|
|
32
32
|
class UserService {
|
|
@@ -64,7 +64,7 @@ app.listen();
|
|
|
64
64
|
## 3. 使用中间件
|
|
65
65
|
|
|
66
66
|
```ts
|
|
67
|
-
import { createCorsMiddleware, createLoggerMiddleware } from "
|
|
67
|
+
import { createCorsMiddleware, createLoggerMiddleware } from "@dangao/bun-server";
|
|
68
68
|
|
|
69
69
|
const app = new Application();
|
|
70
70
|
app.use(createLoggerMiddleware({ prefix: "[Example]" }));
|
|
@@ -74,7 +74,7 @@ app.use(createCorsMiddleware({ origin: "*" }));
|
|
|
74
74
|
`@UseMiddleware()` 可作用于单个控制器或方法:
|
|
75
75
|
|
|
76
76
|
```ts
|
|
77
|
-
import { UseMiddleware } from
|
|
77
|
+
import { UseMiddleware } from "@dangao/bun-server";
|
|
78
78
|
|
|
79
79
|
const auth = async (ctx, next) => {
|
|
80
80
|
if (ctx.getHeader('authorization') !== 'token') {
|
|
@@ -92,7 +92,7 @@ class SecureController { ... }
|
|
|
92
92
|
## 4. 参数验证
|
|
93
93
|
|
|
94
94
|
```ts
|
|
95
|
-
import { Validate, IsEmail, MinLength } from
|
|
95
|
+
import { Validate, IsEmail, MinLength } from "@dangao/bun-server";
|
|
96
96
|
|
|
97
97
|
@POST('/register')
|
|
98
98
|
public register(
|
|
@@ -108,32 +108,37 @@ public register(
|
|
|
108
108
|
## 5. WebSocket 网关
|
|
109
109
|
|
|
110
110
|
```ts
|
|
111
|
-
import { OnMessage, WebSocketGateway } from "
|
|
111
|
+
import { OnMessage, WebSocketGateway } from "@dangao/bun-server";
|
|
112
|
+
import type { ServerWebSocket } from "bun";
|
|
112
113
|
|
|
113
|
-
@WebSocketGateway("/ws
|
|
114
|
+
@WebSocketGateway("/ws")
|
|
114
115
|
class ChatGateway {
|
|
115
116
|
@OnMessage
|
|
116
|
-
public
|
|
117
|
-
ws.send(`
|
|
117
|
+
public handleMessage(ws: ServerWebSocket, message: string) {
|
|
118
|
+
ws.send(`Echo: ${message}`);
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
const app = new Application({ port: 3000 });
|
|
121
123
|
app.registerWebSocketGateway(ChatGateway);
|
|
124
|
+
app.listen();
|
|
122
125
|
```
|
|
123
126
|
|
|
124
127
|
## 6. 文件上传与静态资源
|
|
125
128
|
|
|
126
129
|
```ts
|
|
127
|
-
import {
|
|
130
|
+
import {
|
|
131
|
+
createFileUploadMiddleware,
|
|
132
|
+
createStaticFileMiddleware,
|
|
133
|
+
} from "@dangao/bun-server";
|
|
128
134
|
|
|
135
|
+
const app = new Application({ port: 3000 });
|
|
136
|
+
|
|
137
|
+
// File upload
|
|
129
138
|
app.use(createFileUploadMiddleware({ maxSize: 5 * 1024 * 1024 }));
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
prefix: "/assets",
|
|
134
|
-
enableCache: true,
|
|
135
|
-
}),
|
|
136
|
-
);
|
|
139
|
+
|
|
140
|
+
// Static files
|
|
141
|
+
app.use(createStaticFileMiddleware({ root: "./public", prefix: "/assets" }));
|
|
137
142
|
```
|
|
138
143
|
|
|
139
144
|
上传后的文件可在 `context.body.files` 中读取;静态资源请求会自动设置
|
|
@@ -142,7 +147,7 @@ Content-Type 与缓存头。
|
|
|
142
147
|
## 7. 错误处理与自定义过滤器
|
|
143
148
|
|
|
144
149
|
```ts
|
|
145
|
-
import { ExceptionFilterRegistry, HttpException } from "
|
|
150
|
+
import { ExceptionFilterRegistry, HttpException } from "@dangao/bun-server";
|
|
146
151
|
|
|
147
152
|
ExceptionFilterRegistry.getInstance().register({
|
|
148
153
|
catch(error, context) {
|
package/docs/zh/migration.md
CHANGED
|
@@ -63,9 +63,15 @@
|
|
|
63
63
|
|
|
64
64
|
- 将原本在 `Bun.serve({ websocket })` 中手写的逻辑迁移到 `@WebSocketGateway`:
|
|
65
65
|
```ts
|
|
66
|
-
@
|
|
66
|
+
import { OnMessage, WebSocketGateway } from "@dangao/bun-server";
|
|
67
|
+
import type { ServerWebSocket } from "bun";
|
|
68
|
+
|
|
69
|
+
@WebSocketGateway('/ws')
|
|
67
70
|
class ChatGateway {
|
|
68
|
-
@OnMessage
|
|
71
|
+
@OnMessage
|
|
72
|
+
public handleMessage(ws: ServerWebSocket, message: string) {
|
|
73
|
+
ws.send(`Echo: ${message}`);
|
|
74
|
+
}
|
|
69
75
|
}
|
|
70
76
|
app.registerWebSocketGateway(ChatGateway);
|
|
71
77
|
```
|
package/package.json
CHANGED
package/src/core/server.ts
CHANGED
|
@@ -89,8 +89,16 @@ export class BunServer {
|
|
|
89
89
|
if (!this.options.websocketRegistry.hasGateway(url.pathname)) {
|
|
90
90
|
return new Response("WebSocket gateway not found", { status: 404 });
|
|
91
91
|
}
|
|
92
|
+
// 创建 Context 以便在 WebSocket 处理器中使用
|
|
93
|
+
const context = new Context(request);
|
|
94
|
+
// 创建 Bun 兼容的 URLSearchParams(需要 toJSON 方法)
|
|
95
|
+
const queryParams = new URLSearchParams(url.searchParams);
|
|
92
96
|
const upgraded = server.upgrade(request, {
|
|
93
|
-
data: {
|
|
97
|
+
data: {
|
|
98
|
+
path: url.pathname,
|
|
99
|
+
query: queryParams,
|
|
100
|
+
context,
|
|
101
|
+
},
|
|
94
102
|
});
|
|
95
103
|
if (upgraded) {
|
|
96
104
|
return undefined;
|
|
@@ -128,14 +136,14 @@ export class BunServer {
|
|
|
128
136
|
return responsePromise;
|
|
129
137
|
},
|
|
130
138
|
websocket: {
|
|
131
|
-
open: (ws) => {
|
|
132
|
-
this.options.websocketRegistry?.handleOpen(ws);
|
|
139
|
+
open: async (ws) => {
|
|
140
|
+
await this.options.websocketRegistry?.handleOpen(ws);
|
|
133
141
|
},
|
|
134
|
-
message: (ws, message) => {
|
|
135
|
-
this.options.websocketRegistry?.handleMessage(ws, message);
|
|
142
|
+
message: async (ws, message) => {
|
|
143
|
+
await this.options.websocketRegistry?.handleMessage(ws, message);
|
|
136
144
|
},
|
|
137
|
-
close: (ws, code, reason) => {
|
|
138
|
-
this.options.websocketRegistry?.handleClose(ws, code, reason);
|
|
145
|
+
close: async (ws, code, reason) => {
|
|
146
|
+
await this.options.websocketRegistry?.handleClose(ws, code, reason);
|
|
139
147
|
},
|
|
140
148
|
},
|
|
141
149
|
});
|
|
@@ -3,7 +3,10 @@ import type { ServerWebSocket } from 'bun';
|
|
|
3
3
|
import { Container } from '../di/container';
|
|
4
4
|
import { ControllerRegistry } from '../controller/controller';
|
|
5
5
|
import { getGatewayMetadata, getHandlerMetadata } from './decorators';
|
|
6
|
-
import
|
|
6
|
+
import { ParamBinder } from '../controller/param-binder';
|
|
7
|
+
import { getParamMetadata } from '../controller/decorators';
|
|
8
|
+
import { Context } from '../core/context';
|
|
9
|
+
import type { Constructor } from '@/core/types';
|
|
7
10
|
|
|
8
11
|
interface GatewayDefinition {
|
|
9
12
|
path: string;
|
|
@@ -13,11 +16,12 @@ interface GatewayDefinition {
|
|
|
13
16
|
message?: string;
|
|
14
17
|
close?: string;
|
|
15
18
|
};
|
|
16
|
-
instance?: unknown;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface WebSocketConnectionData {
|
|
20
22
|
path: string;
|
|
23
|
+
query?: URLSearchParams;
|
|
24
|
+
context?: Context;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export class WebSocketGatewayRegistry {
|
|
@@ -70,63 +74,153 @@ export class WebSocketGatewayRegistry {
|
|
|
70
74
|
return this.gateways.get(path);
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
/**
|
|
78
|
+
* 动态创建网关实例(每次连接创建新实例)
|
|
79
|
+
* @param definition - 网关定义
|
|
80
|
+
* @returns 网关实例
|
|
81
|
+
*/
|
|
82
|
+
private createGatewayInstance(definition: GatewayDefinition): unknown {
|
|
83
|
+
return this.container.resolve(definition.gatewayClass);
|
|
78
84
|
}
|
|
79
85
|
|
|
80
|
-
|
|
86
|
+
/**
|
|
87
|
+
* 调用处理器,支持参数绑定
|
|
88
|
+
* @param ws - WebSocket 连接
|
|
89
|
+
* @param definition - 网关定义
|
|
90
|
+
* @param handlerName - 处理器方法名
|
|
91
|
+
* @param args - 原始参数(message, code, reason 等,不包括 ws)
|
|
92
|
+
*/
|
|
93
|
+
private async invokeHandler(
|
|
81
94
|
ws: ServerWebSocket<WebSocketConnectionData>,
|
|
82
95
|
definition: GatewayDefinition,
|
|
83
96
|
handlerName: string | undefined,
|
|
84
97
|
...args: unknown[]
|
|
85
|
-
): void {
|
|
98
|
+
): Promise<void> {
|
|
86
99
|
if (!handlerName) {
|
|
87
100
|
return;
|
|
88
101
|
}
|
|
89
|
-
|
|
102
|
+
|
|
103
|
+
// 动态创建实例(每次连接创建新实例)
|
|
104
|
+
const instance = this.createGatewayInstance(definition);
|
|
90
105
|
const handler = (instance as Record<string, unknown>)[handlerName];
|
|
91
|
-
|
|
106
|
+
|
|
107
|
+
if (typeof handler !== 'function') {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 获取参数元数据
|
|
112
|
+
const prototype = definition.gatewayClass.prototype;
|
|
113
|
+
const paramMetadata = getParamMetadata(prototype, handlerName);
|
|
114
|
+
|
|
115
|
+
// 如果有参数装饰器,使用参数绑定
|
|
116
|
+
if (paramMetadata.length > 0) {
|
|
117
|
+
// 创建或获取 Context
|
|
118
|
+
let context = ws.data?.context;
|
|
119
|
+
if (!context) {
|
|
120
|
+
// 从 WebSocket 连接数据创建 Context
|
|
121
|
+
const url = new URL(ws.data.path || '/', 'http://localhost');
|
|
122
|
+
if (ws.data.query) {
|
|
123
|
+
ws.data.query.forEach((value, key) => {
|
|
124
|
+
url.searchParams.set(key, value);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const request = new Request(url.toString(), {
|
|
128
|
+
method: 'GET',
|
|
129
|
+
headers: new Headers(),
|
|
130
|
+
});
|
|
131
|
+
context = new Context(request);
|
|
132
|
+
ws.data.context = context;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 使用 ParamBinder 绑定参数
|
|
136
|
+
const boundParams = await ParamBinder.bind(
|
|
137
|
+
prototype,
|
|
138
|
+
handlerName,
|
|
139
|
+
context,
|
|
140
|
+
this.container,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// 构建最终参数数组
|
|
144
|
+
// WebSocket 处理器的参数顺序:ws, [message/code/reason], [装饰器参数...]
|
|
145
|
+
const finalArgs: unknown[] = [];
|
|
146
|
+
const maxParamIndex = paramMetadata.length > 0
|
|
147
|
+
? Math.max(...paramMetadata.map((m) => m.index))
|
|
148
|
+
: -1;
|
|
149
|
+
|
|
150
|
+
// 按方法签名顺序填充参数
|
|
151
|
+
// 首先确定方法的总参数数量(包括 ws 和原始参数)
|
|
152
|
+
const totalParamCount = Math.max(
|
|
153
|
+
maxParamIndex + 1,
|
|
154
|
+
args.length + 1, // +1 是 ws 参数
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
for (let i = 0; i < totalParamCount; i++) {
|
|
158
|
+
const meta = paramMetadata.find((m) => m.index === i);
|
|
159
|
+
if (meta) {
|
|
160
|
+
// 使用绑定参数(从 ParamBinder 获取)
|
|
161
|
+
finalArgs[i] = boundParams[i];
|
|
162
|
+
} else if (i === 0) {
|
|
163
|
+
// 第一个参数是 ws
|
|
164
|
+
finalArgs[i] = ws;
|
|
165
|
+
} else {
|
|
166
|
+
// 其他参数是原始参数(message, code, reason 等)
|
|
167
|
+
const argIndex = i - 1;
|
|
168
|
+
if (argIndex < args.length) {
|
|
169
|
+
finalArgs[i] = args[argIndex];
|
|
170
|
+
} else {
|
|
171
|
+
finalArgs[i] = undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 如果方法签名参数少于实际参数,添加剩余参数
|
|
177
|
+
if (args.length > maxParamIndex) {
|
|
178
|
+
for (let i = maxParamIndex + 1; i < args.length; i++) {
|
|
179
|
+
finalArgs.push(args[i]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
handler.apply(instance, finalArgs);
|
|
184
|
+
} else {
|
|
185
|
+
// 没有参数装饰器,直接调用:ws, ...args
|
|
92
186
|
handler.apply(instance, [ws, ...args]);
|
|
93
187
|
}
|
|
94
188
|
}
|
|
95
189
|
|
|
96
|
-
public handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): void {
|
|
190
|
+
public async handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): Promise<void> {
|
|
97
191
|
const path = ws.data?.path;
|
|
98
192
|
const definition = path ? this.getGateway(path) : undefined;
|
|
99
193
|
if (!definition) {
|
|
100
194
|
ws.close(1008, 'Gateway not found');
|
|
101
195
|
return;
|
|
102
196
|
}
|
|
103
|
-
this.invokeHandler(ws, definition, definition.handlers.open);
|
|
197
|
+
await this.invokeHandler(ws, definition, definition.handlers.open);
|
|
104
198
|
}
|
|
105
199
|
|
|
106
|
-
public handleMessage(
|
|
200
|
+
public async handleMessage(
|
|
107
201
|
ws: ServerWebSocket<WebSocketConnectionData>,
|
|
108
202
|
message: string | ArrayBuffer | ArrayBufferView,
|
|
109
|
-
): void {
|
|
203
|
+
): Promise<void> {
|
|
110
204
|
const path = ws.data?.path;
|
|
111
205
|
const definition = path ? this.getGateway(path) : undefined;
|
|
112
206
|
if (!definition) {
|
|
113
207
|
ws.close(1008, 'Gateway not found');
|
|
114
208
|
return;
|
|
115
209
|
}
|
|
116
|
-
this.invokeHandler(ws, definition, definition.handlers.message, message);
|
|
210
|
+
await this.invokeHandler(ws, definition, definition.handlers.message, message);
|
|
117
211
|
}
|
|
118
212
|
|
|
119
|
-
public handleClose(
|
|
213
|
+
public async handleClose(
|
|
120
214
|
ws: ServerWebSocket<WebSocketConnectionData>,
|
|
121
215
|
code: number,
|
|
122
216
|
reason: string,
|
|
123
|
-
): void {
|
|
217
|
+
): Promise<void> {
|
|
124
218
|
const path = ws.data?.path;
|
|
125
219
|
const definition = path ? this.getGateway(path) : undefined;
|
|
126
220
|
if (!definition) {
|
|
127
221
|
return;
|
|
128
222
|
}
|
|
129
|
-
this.invokeHandler(ws, definition, definition.handlers.close, code, reason);
|
|
223
|
+
await this.invokeHandler(ws, definition, definition.handlers.close, code, reason);
|
|
130
224
|
}
|
|
131
225
|
}
|
|
132
226
|
|