@dangao/bun-server 1.4.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/application.d.ts +27 -1
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/server.d.ts +25 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/index.js +179 -23
- 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/application.ts +89 -1
- package/src/core/server.ts +129 -9
- package/src/websocket/registry.ts +113 -19
- package/tests/core/graceful-shutdown.test.ts +321 -0
- package/tests/websocket/gateway.test.ts +1024 -0
|
@@ -15,6 +15,16 @@ export interface ApplicationOptions {
|
|
|
15
15
|
* 主机名
|
|
16
16
|
*/
|
|
17
17
|
hostname?: string;
|
|
18
|
+
/**
|
|
19
|
+
* 优雅停机超时时间(毫秒)
|
|
20
|
+
* 默认 30 秒
|
|
21
|
+
*/
|
|
22
|
+
gracefulShutdownTimeout?: number;
|
|
23
|
+
/**
|
|
24
|
+
* 是否启用信号监听(SIGTERM、SIGINT)
|
|
25
|
+
* 默认 true
|
|
26
|
+
*/
|
|
27
|
+
enableSignalHandlers?: boolean;
|
|
18
28
|
}
|
|
19
29
|
/**
|
|
20
30
|
* 应用主类
|
|
@@ -26,6 +36,7 @@ export declare class Application {
|
|
|
26
36
|
private readonly middlewarePipeline;
|
|
27
37
|
private readonly websocketRegistry;
|
|
28
38
|
private readonly extensions;
|
|
39
|
+
private signalHandlersInstalled;
|
|
29
40
|
constructor(options?: ApplicationOptions);
|
|
30
41
|
/**
|
|
31
42
|
* 注册全局中间件
|
|
@@ -46,9 +57,16 @@ export declare class Application {
|
|
|
46
57
|
*/
|
|
47
58
|
private initializeConfigCenter;
|
|
48
59
|
/**
|
|
49
|
-
*
|
|
60
|
+
* 停止应用(立即停止,不等待请求完成)
|
|
50
61
|
*/
|
|
51
62
|
stop(): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* 优雅停机
|
|
65
|
+
* 停止接受新请求,等待正在处理的请求完成,然后关闭应用
|
|
66
|
+
* @param timeout - 超时时间(毫秒),默认使用配置的 gracefulShutdownTimeout 或 30000
|
|
67
|
+
* @returns Promise,在停机完成时 resolve
|
|
68
|
+
*/
|
|
69
|
+
gracefulShutdown(timeout?: number): Promise<void>;
|
|
52
70
|
/**
|
|
53
71
|
* 关闭所有扩展
|
|
54
72
|
*/
|
|
@@ -99,5 +117,13 @@ export declare class Application {
|
|
|
99
117
|
* @returns DI 容器
|
|
100
118
|
*/
|
|
101
119
|
getContainer(): import("..").Container;
|
|
120
|
+
/**
|
|
121
|
+
* 安装信号处理器(SIGTERM、SIGINT)
|
|
122
|
+
*/
|
|
123
|
+
private installSignalHandlers;
|
|
124
|
+
/**
|
|
125
|
+
* 移除信号处理器
|
|
126
|
+
*/
|
|
127
|
+
private removeSignalHandlers;
|
|
102
128
|
}
|
|
103
129
|
//# sourceMappingURL=application.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../../src/core/application.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAsB,MAAM,UAAU,CAAC;AAMzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAGhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../../src/core/application.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAsB,MAAM,UAAU,CAAC;AAMzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAGhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAO3C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAEjC;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAC,CAAY;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA2B;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8B;IACzD,OAAO,CAAC,uBAAuB,CAAkB;gBAE9B,OAAO,GAAE,kBAAuB;IAsBnD;;;OAGG;IACI,GAAG,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI;IAIxC;;OAEG;IACU,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkCpE;;OAEG;YACW,oBAAoB;IAqBlC;;;OAGG;YACW,sBAAsB;IAyBpC;;OAEG;IACU,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAYlC;;;;;OAKG;IACU,gBAAgB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB9D;;OAEG;YACW,eAAe;IAkB7B;;;;OAIG;YACW,aAAa;IA8B3B;;;OAGG;IACI,kBAAkB,CAAC,eAAe,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,IAAI;IAKtE;;;OAGG;IACI,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI;IAgBrD;;;OAGG;IACI,wBAAwB,CAAC,YAAY,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,IAAI;IAIzE;;;OAGG;IACI,iBAAiB,CAAC,SAAS,EAAE,oBAAoB,GAAG,IAAI;IAK/D;;;OAGG;IACI,SAAS,IAAI,SAAS,GAAG,SAAS;IAIzC;;;OAGG;YACW,gBAAgB;IAsB9B;;;OAGG;YACW,kBAAkB;IAwBhC;;;OAGG;IACI,YAAY;IAInB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA2B7B;;OAEG;IACH,OAAO,CAAC,oBAAoB;CAS7B"}
|
package/dist/core/server.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export interface ServerOptions {
|
|
|
22
22
|
* WebSocket 网关注册表
|
|
23
23
|
*/
|
|
24
24
|
websocketRegistry?: WebSocketGatewayRegistry;
|
|
25
|
+
/**
|
|
26
|
+
* 优雅停机超时时间(毫秒)
|
|
27
|
+
* 默认 30 秒
|
|
28
|
+
*/
|
|
29
|
+
gracefulShutdownTimeout?: number;
|
|
25
30
|
}
|
|
26
31
|
/**
|
|
27
32
|
* 服务器封装类
|
|
@@ -30,15 +35,34 @@ export interface ServerOptions {
|
|
|
30
35
|
export declare class BunServer {
|
|
31
36
|
private server?;
|
|
32
37
|
private readonly options;
|
|
38
|
+
private activeRequests;
|
|
39
|
+
private isShuttingDown;
|
|
40
|
+
private shutdownPromise?;
|
|
41
|
+
private shutdownResolve?;
|
|
33
42
|
constructor(options: ServerOptions);
|
|
34
43
|
/**
|
|
35
44
|
* 启动服务器
|
|
36
45
|
*/
|
|
37
46
|
start(): void;
|
|
38
47
|
/**
|
|
39
|
-
*
|
|
48
|
+
* 停止服务器(立即停止,不等待请求完成)
|
|
40
49
|
*/
|
|
41
50
|
stop(): void;
|
|
51
|
+
/**
|
|
52
|
+
* 优雅停机
|
|
53
|
+
* 停止接受新请求,等待正在处理的请求完成
|
|
54
|
+
* @param timeout - 超时时间(毫秒),默认使用配置的 gracefulShutdownTimeout 或 30000
|
|
55
|
+
* @returns Promise,在停机完成时 resolve
|
|
56
|
+
*/
|
|
57
|
+
gracefulShutdown(timeout?: number): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* 获取当前活跃请求数
|
|
60
|
+
*/
|
|
61
|
+
getActiveRequests(): number;
|
|
62
|
+
/**
|
|
63
|
+
* 检查是否正在关闭
|
|
64
|
+
*/
|
|
65
|
+
isShuttingDownState(): boolean;
|
|
42
66
|
/**
|
|
43
67
|
* 获取服务器实例
|
|
44
68
|
* @returns Bun Server 实例
|
|
@@ -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;
|
|
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
|
@@ -3235,6 +3235,10 @@ import { LoggerManager } from "@dangao/logsmith";
|
|
|
3235
3235
|
class BunServer {
|
|
3236
3236
|
server;
|
|
3237
3237
|
options;
|
|
3238
|
+
activeRequests = 0;
|
|
3239
|
+
isShuttingDown = false;
|
|
3240
|
+
shutdownPromise;
|
|
3241
|
+
shutdownResolve;
|
|
3238
3242
|
constructor(options) {
|
|
3239
3243
|
this.options = options;
|
|
3240
3244
|
}
|
|
@@ -3243,36 +3247,64 @@ class BunServer {
|
|
|
3243
3247
|
throw new Error("Server is already running");
|
|
3244
3248
|
}
|
|
3245
3249
|
const logger = LoggerManager.getLogger();
|
|
3250
|
+
this.activeRequests = 0;
|
|
3251
|
+
this.isShuttingDown = false;
|
|
3252
|
+
this.shutdownPromise = undefined;
|
|
3253
|
+
this.shutdownResolve = undefined;
|
|
3246
3254
|
this.server = Bun.serve({
|
|
3247
3255
|
port: this.options.port ?? 3000,
|
|
3248
3256
|
hostname: this.options.hostname,
|
|
3249
3257
|
fetch: (request, server) => {
|
|
3258
|
+
if (this.isShuttingDown) {
|
|
3259
|
+
return new Response("Server is shutting down", { status: 503 });
|
|
3260
|
+
}
|
|
3250
3261
|
const upgradeHeader = request.headers.get("upgrade");
|
|
3251
3262
|
if (this.options.websocketRegistry && upgradeHeader && upgradeHeader.toLowerCase() === "websocket") {
|
|
3252
3263
|
const url = new URL(request.url);
|
|
3253
3264
|
if (!this.options.websocketRegistry.hasGateway(url.pathname)) {
|
|
3254
3265
|
return new Response("WebSocket gateway not found", { status: 404 });
|
|
3255
3266
|
}
|
|
3267
|
+
const context2 = new Context(request);
|
|
3268
|
+
const queryParams = new URLSearchParams(url.searchParams);
|
|
3256
3269
|
const upgraded = server.upgrade(request, {
|
|
3257
|
-
data: {
|
|
3270
|
+
data: {
|
|
3271
|
+
path: url.pathname,
|
|
3272
|
+
query: queryParams,
|
|
3273
|
+
context: context2
|
|
3274
|
+
}
|
|
3258
3275
|
});
|
|
3259
3276
|
if (upgraded) {
|
|
3260
3277
|
return;
|
|
3261
3278
|
}
|
|
3262
3279
|
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
3263
3280
|
}
|
|
3281
|
+
this.activeRequests++;
|
|
3264
3282
|
const context = new Context(request);
|
|
3265
|
-
|
|
3283
|
+
const responsePromise = this.options.fetch(context);
|
|
3284
|
+
if (responsePromise instanceof Promise) {
|
|
3285
|
+
responsePromise.finally(() => {
|
|
3286
|
+
this.activeRequests--;
|
|
3287
|
+
if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
|
|
3288
|
+
this.shutdownResolve();
|
|
3289
|
+
}
|
|
3290
|
+
}).catch(() => {});
|
|
3291
|
+
} else {
|
|
3292
|
+
this.activeRequests--;
|
|
3293
|
+
if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
|
|
3294
|
+
this.shutdownResolve();
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
return responsePromise;
|
|
3266
3298
|
},
|
|
3267
3299
|
websocket: {
|
|
3268
|
-
open: (ws) => {
|
|
3269
|
-
this.options.websocketRegistry?.handleOpen(ws);
|
|
3300
|
+
open: async (ws) => {
|
|
3301
|
+
await this.options.websocketRegistry?.handleOpen(ws);
|
|
3270
3302
|
},
|
|
3271
|
-
message: (ws, message) => {
|
|
3272
|
-
this.options.websocketRegistry?.handleMessage(ws, message);
|
|
3303
|
+
message: async (ws, message) => {
|
|
3304
|
+
await this.options.websocketRegistry?.handleMessage(ws, message);
|
|
3273
3305
|
},
|
|
3274
|
-
close: (ws, code, reason) => {
|
|
3275
|
-
this.options.websocketRegistry?.handleClose(ws, code, reason);
|
|
3306
|
+
close: async (ws, code, reason) => {
|
|
3307
|
+
await this.options.websocketRegistry?.handleClose(ws, code, reason);
|
|
3276
3308
|
}
|
|
3277
3309
|
}
|
|
3278
3310
|
});
|
|
@@ -3285,9 +3317,45 @@ class BunServer {
|
|
|
3285
3317
|
const logger = LoggerManager.getLogger();
|
|
3286
3318
|
this.server.stop();
|
|
3287
3319
|
this.server = undefined;
|
|
3320
|
+
this.isShuttingDown = false;
|
|
3321
|
+
this.activeRequests = 0;
|
|
3288
3322
|
logger.info("Server stopped");
|
|
3289
3323
|
}
|
|
3290
3324
|
}
|
|
3325
|
+
async gracefulShutdown(timeout) {
|
|
3326
|
+
if (!this.server || this.isShuttingDown) {
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
const logger = LoggerManager.getLogger();
|
|
3330
|
+
const shutdownTimeout = timeout ?? this.options.gracefulShutdownTimeout ?? 30000;
|
|
3331
|
+
logger.info(`Starting graceful shutdown (timeout: ${shutdownTimeout}ms, active requests: ${this.activeRequests})`);
|
|
3332
|
+
this.isShuttingDown = true;
|
|
3333
|
+
if (this.activeRequests === 0) {
|
|
3334
|
+
this.stop();
|
|
3335
|
+
logger.info("Graceful shutdown completed (no active requests)");
|
|
3336
|
+
return;
|
|
3337
|
+
}
|
|
3338
|
+
if (!this.shutdownPromise) {
|
|
3339
|
+
this.shutdownPromise = new Promise((resolve) => {
|
|
3340
|
+
this.shutdownResolve = resolve;
|
|
3341
|
+
});
|
|
3342
|
+
}
|
|
3343
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
3344
|
+
setTimeout(() => {
|
|
3345
|
+
logger.warn(`Graceful shutdown timeout (${shutdownTimeout}ms), forcing shutdown`);
|
|
3346
|
+
resolve();
|
|
3347
|
+
}, shutdownTimeout);
|
|
3348
|
+
});
|
|
3349
|
+
await Promise.race([this.shutdownPromise, timeoutPromise]);
|
|
3350
|
+
this.stop();
|
|
3351
|
+
logger.info(`Graceful shutdown completed (remaining active requests: ${this.activeRequests})`);
|
|
3352
|
+
}
|
|
3353
|
+
getActiveRequests() {
|
|
3354
|
+
return this.activeRequests;
|
|
3355
|
+
}
|
|
3356
|
+
isShuttingDownState() {
|
|
3357
|
+
return this.isShuttingDown;
|
|
3358
|
+
}
|
|
3291
3359
|
getServer() {
|
|
3292
3360
|
return this.server;
|
|
3293
3361
|
}
|
|
@@ -3341,6 +3409,8 @@ function getHandlerMetadata(target) {
|
|
|
3341
3409
|
}
|
|
3342
3410
|
|
|
3343
3411
|
// src/websocket/registry.ts
|
|
3412
|
+
init_param_binder();
|
|
3413
|
+
init_decorators2();
|
|
3344
3414
|
class WebSocketGatewayRegistry {
|
|
3345
3415
|
static instance;
|
|
3346
3416
|
container;
|
|
@@ -3381,47 +3451,90 @@ class WebSocketGatewayRegistry {
|
|
|
3381
3451
|
getGateway(path) {
|
|
3382
3452
|
return this.gateways.get(path);
|
|
3383
3453
|
}
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
definition.instance = this.container.resolve(definition.gatewayClass);
|
|
3387
|
-
}
|
|
3388
|
-
return definition.instance;
|
|
3454
|
+
createGatewayInstance(definition) {
|
|
3455
|
+
return this.container.resolve(definition.gatewayClass);
|
|
3389
3456
|
}
|
|
3390
|
-
invokeHandler(ws, definition, handlerName, ...args) {
|
|
3457
|
+
async invokeHandler(ws, definition, handlerName, ...args) {
|
|
3391
3458
|
if (!handlerName) {
|
|
3392
3459
|
return;
|
|
3393
3460
|
}
|
|
3394
|
-
const instance = this.
|
|
3461
|
+
const instance = this.createGatewayInstance(definition);
|
|
3395
3462
|
const handler = instance[handlerName];
|
|
3396
|
-
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 {
|
|
3397
3510
|
handler.apply(instance, [ws, ...args]);
|
|
3398
3511
|
}
|
|
3399
3512
|
}
|
|
3400
|
-
handleOpen(ws) {
|
|
3513
|
+
async handleOpen(ws) {
|
|
3401
3514
|
const path = ws.data?.path;
|
|
3402
3515
|
const definition = path ? this.getGateway(path) : undefined;
|
|
3403
3516
|
if (!definition) {
|
|
3404
3517
|
ws.close(1008, "Gateway not found");
|
|
3405
3518
|
return;
|
|
3406
3519
|
}
|
|
3407
|
-
this.invokeHandler(ws, definition, definition.handlers.open);
|
|
3520
|
+
await this.invokeHandler(ws, definition, definition.handlers.open);
|
|
3408
3521
|
}
|
|
3409
|
-
handleMessage(ws, message) {
|
|
3522
|
+
async handleMessage(ws, message) {
|
|
3410
3523
|
const path = ws.data?.path;
|
|
3411
3524
|
const definition = path ? this.getGateway(path) : undefined;
|
|
3412
3525
|
if (!definition) {
|
|
3413
3526
|
ws.close(1008, "Gateway not found");
|
|
3414
3527
|
return;
|
|
3415
3528
|
}
|
|
3416
|
-
this.invokeHandler(ws, definition, definition.handlers.message, message);
|
|
3529
|
+
await this.invokeHandler(ws, definition, definition.handlers.message, message);
|
|
3417
3530
|
}
|
|
3418
|
-
handleClose(ws, code, reason) {
|
|
3531
|
+
async handleClose(ws, code, reason) {
|
|
3419
3532
|
const path = ws.data?.path;
|
|
3420
3533
|
const definition = path ? this.getGateway(path) : undefined;
|
|
3421
3534
|
if (!definition) {
|
|
3422
3535
|
return;
|
|
3423
3536
|
}
|
|
3424
|
-
this.invokeHandler(ws, definition, definition.handlers.close, code, reason);
|
|
3537
|
+
await this.invokeHandler(ws, definition, definition.handlers.close, code, reason);
|
|
3425
3538
|
}
|
|
3426
3539
|
}
|
|
3427
3540
|
|
|
@@ -3840,12 +3953,15 @@ ConfigModule = __legacyDecorateClassTS([
|
|
|
3840
3953
|
], ConfigModule);
|
|
3841
3954
|
|
|
3842
3955
|
// src/core/application.ts
|
|
3956
|
+
import { LoggerManager as LoggerManager8 } from "@dangao/logsmith";
|
|
3957
|
+
|
|
3843
3958
|
class Application {
|
|
3844
3959
|
server;
|
|
3845
3960
|
options;
|
|
3846
3961
|
middlewarePipeline;
|
|
3847
3962
|
websocketRegistry;
|
|
3848
3963
|
extensions = [];
|
|
3964
|
+
signalHandlersInstalled = false;
|
|
3849
3965
|
constructor(options = {}) {
|
|
3850
3966
|
this.options = options;
|
|
3851
3967
|
this.middlewarePipeline = new MiddlewarePipeline([createErrorHandlingMiddleware()]);
|
|
@@ -3874,10 +3990,14 @@ class Application {
|
|
|
3874
3990
|
port: finalPort,
|
|
3875
3991
|
hostname: finalHostname,
|
|
3876
3992
|
fetch: this.handleRequest.bind(this),
|
|
3877
|
-
websocketRegistry: this.websocketRegistry
|
|
3993
|
+
websocketRegistry: this.websocketRegistry,
|
|
3994
|
+
gracefulShutdownTimeout: this.options.gracefulShutdownTimeout
|
|
3878
3995
|
};
|
|
3879
3996
|
this.server = new BunServer(serverOptions);
|
|
3880
3997
|
this.server.start();
|
|
3998
|
+
if (this.options.enableSignalHandlers !== false) {
|
|
3999
|
+
this.installSignalHandlers();
|
|
4000
|
+
}
|
|
3881
4001
|
await this.registerServices(finalPort, finalHostname);
|
|
3882
4002
|
}
|
|
3883
4003
|
async initializeExtensions() {
|
|
@@ -3905,10 +4025,19 @@ class Application {
|
|
|
3905
4025
|
}
|
|
3906
4026
|
}
|
|
3907
4027
|
async stop() {
|
|
4028
|
+
this.removeSignalHandlers();
|
|
3908
4029
|
await this.deregisterServices();
|
|
3909
4030
|
await this.closeExtensions();
|
|
3910
4031
|
this.server?.stop();
|
|
3911
4032
|
}
|
|
4033
|
+
async gracefulShutdown(timeout) {
|
|
4034
|
+
this.removeSignalHandlers();
|
|
4035
|
+
await this.deregisterServices();
|
|
4036
|
+
await this.closeExtensions();
|
|
4037
|
+
if (this.server) {
|
|
4038
|
+
await this.server.gracefulShutdown(timeout);
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
3912
4041
|
async closeExtensions() {
|
|
3913
4042
|
const container = this.getContainer();
|
|
3914
4043
|
for (let i = this.extensions.length - 1;i >= 0; i--) {
|
|
@@ -3995,6 +4124,33 @@ class Application {
|
|
|
3995
4124
|
getContainer() {
|
|
3996
4125
|
return ControllerRegistry.getInstance().getContainer();
|
|
3997
4126
|
}
|
|
4127
|
+
installSignalHandlers() {
|
|
4128
|
+
if (this.signalHandlersInstalled) {
|
|
4129
|
+
return;
|
|
4130
|
+
}
|
|
4131
|
+
const logger = LoggerManager8.getLogger();
|
|
4132
|
+
const shutdownHandler = async (signal) => {
|
|
4133
|
+
logger.info(`Received ${signal}, starting graceful shutdown...`);
|
|
4134
|
+
try {
|
|
4135
|
+
await this.gracefulShutdown();
|
|
4136
|
+
process.exit(0);
|
|
4137
|
+
} catch (error) {
|
|
4138
|
+
logger.error(`Error during graceful shutdown:`, error);
|
|
4139
|
+
process.exit(1);
|
|
4140
|
+
}
|
|
4141
|
+
};
|
|
4142
|
+
process.on("SIGTERM", () => shutdownHandler("SIGTERM"));
|
|
4143
|
+
process.on("SIGINT", () => shutdownHandler("SIGINT"));
|
|
4144
|
+
this.signalHandlersInstalled = true;
|
|
4145
|
+
}
|
|
4146
|
+
removeSignalHandlers() {
|
|
4147
|
+
if (!this.signalHandlersInstalled) {
|
|
4148
|
+
return;
|
|
4149
|
+
}
|
|
4150
|
+
process.removeAllListeners("SIGTERM");
|
|
4151
|
+
process.removeAllListeners("SIGINT");
|
|
4152
|
+
this.signalHandlersInstalled = false;
|
|
4153
|
+
}
|
|
3998
4154
|
}
|
|
3999
4155
|
|
|
4000
4156
|
// src/index.ts
|
|
@@ -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
|
```
|