@dangao/bun-server 1.9.0 → 1.12.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/README.md +79 -6
- package/dist/cache/cache-module.d.ts +6 -0
- package/dist/cache/cache-module.d.ts.map +1 -1
- package/dist/client/generator.d.ts +16 -0
- package/dist/client/generator.d.ts.map +1 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/runtime.d.ts +15 -0
- package/dist/client/runtime.d.ts.map +1 -0
- package/dist/client/types.d.ts +36 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/config/config-module.d.ts +7 -0
- package/dist/config/config-module.d.ts.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/service.d.ts +13 -0
- package/dist/config/service.d.ts.map +1 -1
- package/dist/config/types.d.ts +10 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/core/application.d.ts +7 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/apply-decorators.d.ts +6 -0
- package/dist/core/apply-decorators.d.ts.map +1 -0
- package/dist/core/cluster.d.ts +47 -0
- package/dist/core/cluster.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/server.d.ts +8 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/dashboard/controller.d.ts +55 -0
- package/dist/dashboard/controller.d.ts.map +1 -0
- package/dist/dashboard/dashboard-extension.d.ts +20 -0
- package/dist/dashboard/dashboard-extension.d.ts.map +1 -0
- package/dist/dashboard/dashboard-module.d.ts +13 -0
- package/dist/dashboard/dashboard-module.d.ts.map +1 -0
- package/dist/dashboard/index.d.ts +4 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/types.d.ts +16 -0
- package/dist/dashboard/types.d.ts.map +1 -0
- package/dist/dashboard/ui.d.ts +7 -0
- package/dist/dashboard/ui.d.ts.map +1 -0
- package/dist/database/database-module.d.ts +7 -0
- package/dist/database/database-module.d.ts.map +1 -1
- package/dist/debug/debug-module.d.ts +13 -0
- package/dist/debug/debug-module.d.ts.map +1 -0
- package/dist/debug/debug-ui-middleware.d.ts +8 -0
- package/dist/debug/debug-ui-middleware.d.ts.map +1 -0
- package/dist/debug/index.d.ts +5 -0
- package/dist/debug/index.d.ts.map +1 -0
- package/dist/debug/middleware.d.ts +12 -0
- package/dist/debug/middleware.d.ts.map +1 -0
- package/dist/debug/recorder.d.ts +61 -0
- package/dist/debug/recorder.d.ts.map +1 -0
- package/dist/debug/types.d.ts +48 -0
- package/dist/debug/types.d.ts.map +1 -0
- package/dist/debug/ui.d.ts +6 -0
- package/dist/debug/ui.d.ts.map +1 -0
- package/dist/di/async-module.d.ts +49 -0
- package/dist/di/async-module.d.ts.map +1 -0
- package/dist/di/lifecycle.d.ts +49 -0
- package/dist/di/lifecycle.d.ts.map +1 -0
- package/dist/di/module-registry.d.ts +24 -0
- package/dist/di/module-registry.d.ts.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1887 -35
- package/dist/router/route.d.ts +5 -7
- package/dist/router/route.d.ts.map +1 -1
- package/dist/swagger/generator.d.ts +10 -0
- package/dist/swagger/generator.d.ts.map +1 -1
- package/dist/testing/test-client.d.ts +49 -0
- package/dist/testing/test-client.d.ts.map +1 -0
- package/dist/testing/testing-module.d.ts +90 -0
- package/dist/testing/testing-module.d.ts.map +1 -0
- package/dist/websocket/registry.d.ts +1 -6
- package/dist/websocket/registry.d.ts.map +1 -1
- package/docs/async-module.md +59 -0
- package/docs/client-generation.md +100 -0
- package/docs/cluster.md +81 -0
- package/docs/custom-decorators.md +1 -7
- package/docs/dashboard.md +54 -0
- package/docs/debug.md +58 -0
- package/docs/extensions.md +0 -2
- package/docs/guide.md +0 -1
- package/docs/lifecycle.md +72 -0
- package/docs/testing.md +110 -0
- package/docs/zh/async-module.md +98 -0
- package/docs/zh/client-generation.md +92 -0
- package/docs/zh/cluster.md +74 -0
- package/docs/zh/custom-decorators.md +1 -7
- package/docs/zh/dashboard.md +69 -0
- package/docs/zh/debug.md +81 -0
- package/docs/zh/extensions.md +0 -2
- package/docs/zh/guide.md +0 -1
- package/docs/zh/lifecycle.md +87 -0
- package/docs/zh/migration.md +0 -5
- package/docs/zh/testing.md +119 -0
- package/package.json +4 -4
- package/src/cache/cache-module.ts +25 -0
- package/src/client/generator.ts +36 -0
- package/src/client/index.ts +8 -0
- package/src/client/runtime.ts +101 -0
- package/src/client/types.ts +38 -0
- package/src/config/config-module.ts +44 -4
- package/src/config/index.ts +1 -0
- package/src/config/service.ts +50 -0
- package/src/config/types.ts +12 -0
- package/src/core/application.ts +37 -0
- package/src/core/apply-decorators.ts +31 -0
- package/src/core/cluster.ts +143 -0
- package/src/core/index.ts +1 -0
- package/src/core/server.ts +14 -1
- package/src/dashboard/controller.ts +227 -0
- package/src/dashboard/dashboard-extension.ts +26 -0
- package/src/dashboard/dashboard-module.ts +38 -0
- package/src/dashboard/index.ts +3 -0
- package/src/dashboard/types.ts +16 -0
- package/src/dashboard/ui.ts +219 -0
- package/src/database/database-module.ts +20 -0
- package/src/debug/debug-module.ts +70 -0
- package/src/debug/debug-ui-middleware.ts +110 -0
- package/src/debug/index.ts +9 -0
- package/src/debug/middleware.ts +126 -0
- package/src/debug/recorder.ts +141 -0
- package/src/debug/types.ts +49 -0
- package/src/debug/ui.ts +393 -0
- package/src/di/async-module.ts +141 -0
- package/src/di/lifecycle.ts +117 -0
- package/src/di/module-registry.ts +75 -0
- package/src/index.ts +35 -0
- package/src/router/route.ts +20 -20
- package/src/swagger/generator.ts +100 -0
- package/src/testing/test-client.ts +112 -0
- package/src/testing/testing-module.ts +238 -0
- package/src/websocket/registry.ts +3 -16
- package/tests/auth/auth-decorators.test.ts +0 -1
- package/tests/auth/oauth2-service.test.ts +0 -1
- package/tests/cache/cache-decorators-extended.test.ts +0 -1
- package/tests/cache/cache-decorators.test.ts +0 -1
- package/tests/cache/cache-interceptors.test.ts +0 -1
- package/tests/cache/cache-module.test.ts +0 -1
- package/tests/cache/cache-service-proxy.test.ts +0 -1
- package/tests/client/client-generator.test.ts +142 -0
- package/tests/config/config-center-integration.test.ts +0 -1
- package/tests/config/config-module-extended.test.ts +0 -1
- package/tests/config/config-module.test.ts +0 -1
- package/tests/controller/controller.test.ts +0 -1
- package/tests/controller/param-binder.test.ts +0 -1
- package/tests/controller/path-combination.test.ts +0 -1
- package/tests/core/application.test.ts +34 -0
- package/tests/core/apply-decorators.test.ts +109 -0
- package/tests/core/cluster.test.ts +32 -0
- package/tests/dashboard/dashboard-module.test.ts +85 -0
- package/tests/database/database-module.test.ts +0 -1
- package/tests/database/orm.test.ts +0 -1
- package/tests/database/postgres-mysql-integration.test.ts +0 -1
- package/tests/database/transaction.test.ts +0 -1
- package/tests/debug/debug-module.test.ts +141 -0
- package/tests/di/async-module.test.ts +125 -0
- package/tests/di/container.test.ts +0 -1
- package/tests/di/lifecycle.test.ts +140 -0
- package/tests/error/error-handler.test.ts +0 -1
- package/tests/events/event-decorators.test.ts +0 -1
- package/tests/events/event-listener-scanner.test.ts +0 -1
- package/tests/events/event-module.test.ts +0 -1
- package/tests/extensions/logger-module.test.ts +0 -1
- package/tests/health/health-module.test.ts +0 -1
- package/tests/integration/oauth2-e2e.test.ts +0 -1
- package/tests/integration/session-e2e.test.ts +0 -1
- package/tests/interceptor/base-interceptor.test.ts +0 -1
- package/tests/interceptor/builtin/cache-interceptor.test.ts +0 -1
- package/tests/interceptor/builtin/log-interceptor.test.ts +0 -1
- package/tests/interceptor/builtin/permission-interceptor.test.ts +0 -1
- package/tests/interceptor/interceptor-advanced-integration.test.ts +0 -1
- package/tests/interceptor/interceptor-chain.test.ts +0 -1
- package/tests/interceptor/interceptor-integration.test.ts +0 -1
- package/tests/interceptor/interceptor-metadata.test.ts +0 -1
- package/tests/interceptor/interceptor-registry.test.ts +0 -1
- package/tests/interceptor/perf/interceptor-performance.test.ts +0 -1
- package/tests/metrics/metrics-module.test.ts +0 -1
- package/tests/microservice/config-center.test.ts +0 -1
- package/tests/microservice/service-client-decorators.test.ts +0 -1
- package/tests/microservice/service-registry-decorators.test.ts +0 -1
- package/tests/microservice/service-registry.test.ts +0 -1
- package/tests/middleware/builtin/middleware-builtin-extended.test.ts +0 -1
- package/tests/middleware/builtin/rate-limit.test.ts +0 -1
- package/tests/middleware/middleware-decorators.test.ts +0 -1
- package/tests/middleware/middleware-pipeline.test.ts +0 -1
- package/tests/middleware/middleware.test.ts +0 -1
- package/tests/perf/optimization.test.ts +0 -1
- package/tests/queue/queue-decorators.test.ts +0 -1
- package/tests/queue/queue-module.test.ts +0 -1
- package/tests/queue/queue-service.test.ts +0 -1
- package/tests/router/router-decorators.test.ts +0 -1
- package/tests/router/router-extended.test.ts +0 -1
- package/tests/security/guards/guards-integration.test.ts +0 -1
- package/tests/security/guards/guards.test.ts +0 -1
- package/tests/security/guards/reflector.test.ts +0 -1
- package/tests/security/security-filter.test.ts +0 -1
- package/tests/security/security-module-extended.test.ts +0 -1
- package/tests/security/security-module.test.ts +0 -1
- package/tests/session/session-decorators.test.ts +0 -1
- package/tests/session/session-module.test.ts +0 -1
- package/tests/swagger/decorators.test.ts +0 -1
- package/tests/swagger/swagger-module.test.ts +0 -1
- package/tests/swagger/ui.test.ts +0 -1
- package/tests/testing/testing-module.test.ts +129 -0
- package/tests/validation/class-validator.test.ts +0 -1
- package/tests/validation/controller-validation.test.ts +0 -1
package/src/core/application.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { ConfigModule } from '../config/config-module';
|
|
|
19
19
|
import { CacheModule, CACHE_POST_PROCESSOR_TOKEN } from '../cache';
|
|
20
20
|
import { LoggerManager } from '@dangao/logsmith';
|
|
21
21
|
import { EventModule } from '../events/event-module';
|
|
22
|
+
import { AsyncProviderRegistry } from '../di/async-module';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* 应用配置选项
|
|
@@ -45,6 +46,14 @@ export interface ApplicationOptions {
|
|
|
45
46
|
* 默认 true
|
|
46
47
|
*/
|
|
47
48
|
enableSignalHandlers?: boolean;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 是否启用 SO_REUSEPORT
|
|
52
|
+
* 允许多进程绑定同一端口,用于多进程负载均衡
|
|
53
|
+
* 仅 Linux 有效,macOS/Windows 会忽略
|
|
54
|
+
* @default false
|
|
55
|
+
*/
|
|
56
|
+
reusePort?: boolean;
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
/**
|
|
@@ -97,6 +106,12 @@ export class Application {
|
|
|
97
106
|
throw new Error('Application is already running');
|
|
98
107
|
}
|
|
99
108
|
|
|
109
|
+
// 初始化异步 providers(forRootAsync 注册的工厂)
|
|
110
|
+
const asyncRegistry = AsyncProviderRegistry.getInstance();
|
|
111
|
+
if (asyncRegistry.hasPending()) {
|
|
112
|
+
await asyncRegistry.initializeAll(this.getContainer());
|
|
113
|
+
}
|
|
114
|
+
|
|
100
115
|
// 初始化所有扩展(包括数据库连接等)
|
|
101
116
|
await this.initializeExtensions();
|
|
102
117
|
|
|
@@ -106,12 +121,20 @@ export class Application {
|
|
|
106
121
|
// 自动初始化事件监听器(如果 EventModule 已注册且启用了 autoScan)
|
|
107
122
|
this.initializeEventListeners();
|
|
108
123
|
|
|
124
|
+
// 调用生命周期钩子:onModuleInit
|
|
125
|
+
const registry = ModuleRegistry.getInstance();
|
|
126
|
+
await registry.callModuleInitHooks();
|
|
127
|
+
|
|
128
|
+
// 调用生命周期钩子:onApplicationBootstrap
|
|
129
|
+
await registry.callBootstrapHooks();
|
|
130
|
+
|
|
109
131
|
const finalPort = port ?? this.options.port ?? 3000;
|
|
110
132
|
const finalHostname = hostname ?? this.options.hostname;
|
|
111
133
|
|
|
112
134
|
const serverOptions: ServerOptions = {
|
|
113
135
|
port: finalPort,
|
|
114
136
|
hostname: finalHostname,
|
|
137
|
+
reusePort: this.options.reusePort,
|
|
115
138
|
fetch: this.handleRequest.bind(this),
|
|
116
139
|
websocketRegistry: this.websocketRegistry,
|
|
117
140
|
gracefulShutdownTimeout: this.options.gracefulShutdownTimeout,
|
|
@@ -214,6 +237,13 @@ export class Application {
|
|
|
214
237
|
// 移除信号处理器
|
|
215
238
|
this.removeSignalHandlers();
|
|
216
239
|
|
|
240
|
+
// 调用生命周期钩子:onApplicationShutdown
|
|
241
|
+
const moduleRegistry = ModuleRegistry.getInstance();
|
|
242
|
+
await moduleRegistry.callShutdownHooks();
|
|
243
|
+
|
|
244
|
+
// 调用生命周期钩子:onModuleDestroy
|
|
245
|
+
await moduleRegistry.callModuleDestroyHooks();
|
|
246
|
+
|
|
217
247
|
// 自动注销服务(如果使用了 @ServiceRegistry 装饰器)
|
|
218
248
|
await this.deregisterServices();
|
|
219
249
|
|
|
@@ -232,6 +262,13 @@ export class Application {
|
|
|
232
262
|
// 移除信号处理器
|
|
233
263
|
this.removeSignalHandlers();
|
|
234
264
|
|
|
265
|
+
// 调用生命周期钩子:onApplicationShutdown
|
|
266
|
+
const moduleRegistry = ModuleRegistry.getInstance();
|
|
267
|
+
await moduleRegistry.callShutdownHooks();
|
|
268
|
+
|
|
269
|
+
// 调用生命周期钩子:onModuleDestroy
|
|
270
|
+
await moduleRegistry.callModuleDestroyHooks();
|
|
271
|
+
|
|
235
272
|
// 自动注销服务(如果使用了 @ServiceRegistry 装饰器)
|
|
236
273
|
await this.deregisterServices();
|
|
237
274
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 装饰器组合工具
|
|
3
|
+
* 将多个装饰器合并为一个可复用的装饰器
|
|
4
|
+
*/
|
|
5
|
+
export function applyDecorators(
|
|
6
|
+
...decorators: Array<ClassDecorator | MethodDecorator | PropertyDecorator>
|
|
7
|
+
): ClassDecorator & MethodDecorator & PropertyDecorator {
|
|
8
|
+
return ((
|
|
9
|
+
target: unknown,
|
|
10
|
+
propertyKey?: string | symbol,
|
|
11
|
+
descriptor?: PropertyDescriptor,
|
|
12
|
+
) => {
|
|
13
|
+
for (const decorator of decorators.reverse()) {
|
|
14
|
+
if (descriptor) {
|
|
15
|
+
const result = (decorator as MethodDecorator)(
|
|
16
|
+
target as object,
|
|
17
|
+
propertyKey!,
|
|
18
|
+
descriptor,
|
|
19
|
+
);
|
|
20
|
+
if (result) {
|
|
21
|
+
descriptor = result as PropertyDescriptor;
|
|
22
|
+
}
|
|
23
|
+
} else if (propertyKey) {
|
|
24
|
+
(decorator as PropertyDecorator)(target as object, propertyKey);
|
|
25
|
+
} else {
|
|
26
|
+
(decorator as ClassDecorator)(target as Function);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return descriptor;
|
|
30
|
+
}) as ClassDecorator & MethodDecorator & PropertyDecorator;
|
|
31
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { spawn } from 'bun';
|
|
2
|
+
import { LoggerManager } from '@dangao/logsmith';
|
|
3
|
+
|
|
4
|
+
export interface ClusterOptions {
|
|
5
|
+
/**
|
|
6
|
+
* 是否启用集群模式
|
|
7
|
+
* @default false
|
|
8
|
+
*/
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Worker 数量。'auto' 表示使用 CPU 核心数
|
|
12
|
+
* @default 'auto'
|
|
13
|
+
*/
|
|
14
|
+
workers?: number | 'auto';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 集群管理器
|
|
19
|
+
* 自动派生 worker 进程,每个 worker 使用 reusePort 绑定相同端口
|
|
20
|
+
*/
|
|
21
|
+
export class ClusterManager {
|
|
22
|
+
private readonly workerCount: number;
|
|
23
|
+
private readonly workers: ReturnType<typeof spawn>[] = [];
|
|
24
|
+
private readonly scriptPath: string;
|
|
25
|
+
private readonly port: number;
|
|
26
|
+
private readonly hostname?: string;
|
|
27
|
+
|
|
28
|
+
public constructor(options: {
|
|
29
|
+
workers: number | 'auto';
|
|
30
|
+
scriptPath: string;
|
|
31
|
+
port: number;
|
|
32
|
+
hostname?: string;
|
|
33
|
+
}) {
|
|
34
|
+
this.workerCount =
|
|
35
|
+
options.workers === 'auto' ? navigator.hardwareConcurrency : options.workers;
|
|
36
|
+
this.scriptPath = options.scriptPath;
|
|
37
|
+
this.port = options.port;
|
|
38
|
+
this.hostname = options.hostname;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 启动所有 worker 进程
|
|
43
|
+
*/
|
|
44
|
+
public start(): void {
|
|
45
|
+
const logger = LoggerManager.getLogger();
|
|
46
|
+
logger.info(
|
|
47
|
+
`[Cluster] Starting ${this.workerCount} workers on port ${this.port}`,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < this.workerCount; i++) {
|
|
51
|
+
const worker = spawn({
|
|
52
|
+
cmd: ['bun', 'run', this.scriptPath],
|
|
53
|
+
env: {
|
|
54
|
+
...process.env,
|
|
55
|
+
PORT: String(this.port),
|
|
56
|
+
REUSE_PORT: '1',
|
|
57
|
+
CLUSTER_WORKER: '1',
|
|
58
|
+
WORKER_ID: String(i),
|
|
59
|
+
...(this.hostname ? { HOSTNAME: this.hostname } : {}),
|
|
60
|
+
},
|
|
61
|
+
stdout: 'inherit',
|
|
62
|
+
stderr: 'inherit',
|
|
63
|
+
});
|
|
64
|
+
this.workers.push(worker);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
logger.info(
|
|
68
|
+
`[Cluster] ${this.workerCount} workers started (reusePort mode)`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Monitor workers and auto-restart on crash
|
|
72
|
+
this.monitorWorkers();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 停止所有 worker 进程
|
|
77
|
+
*/
|
|
78
|
+
public async stop(): Promise<void> {
|
|
79
|
+
const logger = LoggerManager.getLogger();
|
|
80
|
+
logger.info('[Cluster] Stopping all workers...');
|
|
81
|
+
|
|
82
|
+
for (const worker of this.workers) {
|
|
83
|
+
worker.kill('SIGTERM');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Wait up to 5 seconds for workers to exit
|
|
87
|
+
const timeout = setTimeout(() => {
|
|
88
|
+
for (const worker of this.workers) {
|
|
89
|
+
worker.kill('SIGKILL');
|
|
90
|
+
}
|
|
91
|
+
}, 5000);
|
|
92
|
+
|
|
93
|
+
await Promise.all(this.workers.map((w) => w.exited));
|
|
94
|
+
clearTimeout(timeout);
|
|
95
|
+
|
|
96
|
+
this.workers.length = 0;
|
|
97
|
+
logger.info('[Cluster] All workers stopped');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private monitorWorkers(): void {
|
|
101
|
+
// For each worker, restart if it crashes unexpectedly
|
|
102
|
+
for (let i = 0; i < this.workers.length; i++) {
|
|
103
|
+
const index = i;
|
|
104
|
+
this.workers[index].exited.then((exitCode) => {
|
|
105
|
+
if (exitCode !== 0 && exitCode !== null) {
|
|
106
|
+
const logger = LoggerManager.getLogger();
|
|
107
|
+
logger.warn(
|
|
108
|
+
`[Cluster] Worker ${index} exited with code ${exitCode}, restarting...`,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const newWorker = spawn({
|
|
112
|
+
cmd: ['bun', 'run', this.scriptPath],
|
|
113
|
+
env: {
|
|
114
|
+
...process.env,
|
|
115
|
+
PORT: String(this.port),
|
|
116
|
+
REUSE_PORT: '1',
|
|
117
|
+
CLUSTER_WORKER: '1',
|
|
118
|
+
WORKER_ID: String(index),
|
|
119
|
+
...(this.hostname ? { HOSTNAME: this.hostname } : {}),
|
|
120
|
+
},
|
|
121
|
+
stdout: 'inherit',
|
|
122
|
+
stderr: 'inherit',
|
|
123
|
+
});
|
|
124
|
+
this.workers[index] = newWorker;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 检查当前进程是否为 cluster worker
|
|
132
|
+
*/
|
|
133
|
+
public static isWorker(): boolean {
|
|
134
|
+
return process.env.CLUSTER_WORKER === '1';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 获取当前 worker ID(仅在 worker 进程中有效)
|
|
139
|
+
*/
|
|
140
|
+
public static getWorkerId(): number {
|
|
141
|
+
return Number(process.env.WORKER_ID ?? -1);
|
|
142
|
+
}
|
|
143
|
+
}
|
package/src/core/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { Application, type ApplicationOptions } from './application';
|
|
2
2
|
export { BunServer, type ServerOptions } from './server';
|
|
3
|
+
export { ClusterManager, type ClusterOptions } from './cluster';
|
|
3
4
|
export { Context } from './context';
|
|
4
5
|
export { ContextService, CONTEXT_SERVICE_TOKEN, contextStore } from './context-service';
|
|
5
6
|
|
package/src/core/server.ts
CHANGED
|
@@ -33,6 +33,14 @@ export interface ServerOptions {
|
|
|
33
33
|
* 默认 30 秒
|
|
34
34
|
*/
|
|
35
35
|
gracefulShutdownTimeout?: number;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 是否启用 SO_REUSEPORT
|
|
39
|
+
* 允许多进程绑定同一端口,用于多进程负载均衡
|
|
40
|
+
* 仅 Linux 有效,macOS/Windows 会忽略
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
43
|
+
reusePort?: boolean;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
/**
|
|
@@ -70,6 +78,7 @@ export class BunServer {
|
|
|
70
78
|
this.server = Bun.serve({
|
|
71
79
|
port: this.options.port ?? 3000,
|
|
72
80
|
hostname: this.options.hostname,
|
|
81
|
+
reusePort: this.options.reusePort,
|
|
73
82
|
fetch: (
|
|
74
83
|
request: Request,
|
|
75
84
|
server: Server<WebSocketConnectionData>,
|
|
@@ -150,7 +159,7 @@ export class BunServer {
|
|
|
150
159
|
});
|
|
151
160
|
|
|
152
161
|
const hostname = this.options.hostname ?? "localhost";
|
|
153
|
-
const port = this.
|
|
162
|
+
const port = this.server.port;
|
|
154
163
|
logger.info(`Server started at http://${hostname}:${port}`);
|
|
155
164
|
}
|
|
156
165
|
|
|
@@ -249,8 +258,12 @@ export class BunServer {
|
|
|
249
258
|
|
|
250
259
|
/**
|
|
251
260
|
* 获取服务器端口
|
|
261
|
+
* 如果服务器正在运行,返回实际绑定的端口(支持 port:0 场景)
|
|
252
262
|
*/
|
|
253
263
|
public getPort(): number {
|
|
264
|
+
if (this.server) {
|
|
265
|
+
return this.server.port ?? this.options.port ?? 3000;
|
|
266
|
+
}
|
|
254
267
|
return this.options.port ?? 3000;
|
|
255
268
|
}
|
|
256
269
|
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { RouteRegistry } from '../router/registry';
|
|
2
|
+
import type { Context } from '../core/context';
|
|
3
|
+
import { createDashboardHTML } from './ui';
|
|
4
|
+
import { ControllerRegistry } from '../controller/controller';
|
|
5
|
+
import { HEALTH_INDICATORS_TOKEN } from '../health/types';
|
|
6
|
+
import type { HealthIndicator } from '../health/types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Dashboard 服务
|
|
10
|
+
* 提供监控 UI 和 API 端点
|
|
11
|
+
*/
|
|
12
|
+
export class DashboardService {
|
|
13
|
+
private readonly basePath: string;
|
|
14
|
+
private readonly auth?: { username: string; password: string };
|
|
15
|
+
private readonly startTime: number = Date.now();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 创建 Dashboard 服务实例
|
|
19
|
+
* @param basePath - Dashboard 基础路径
|
|
20
|
+
* @param auth - Basic Auth 认证配置(可选)
|
|
21
|
+
*/
|
|
22
|
+
public constructor(
|
|
23
|
+
basePath: string,
|
|
24
|
+
auth?: { username: string; password: string },
|
|
25
|
+
) {
|
|
26
|
+
this.basePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
|
27
|
+
this.auth = auth;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 注册 Dashboard 路由
|
|
32
|
+
*/
|
|
33
|
+
public registerRoutes(): void {
|
|
34
|
+
const registry = RouteRegistry.getInstance();
|
|
35
|
+
|
|
36
|
+
registry.register('GET', this.basePath, (ctx) => this.handleDashboard(ctx));
|
|
37
|
+
registry.register(
|
|
38
|
+
'GET',
|
|
39
|
+
`${this.basePath}/api/system`,
|
|
40
|
+
(ctx) => this.handleSystem(ctx),
|
|
41
|
+
);
|
|
42
|
+
registry.register(
|
|
43
|
+
'GET',
|
|
44
|
+
`${this.basePath}/api/routes`,
|
|
45
|
+
(ctx) => this.handleRoutes(ctx),
|
|
46
|
+
);
|
|
47
|
+
registry.register(
|
|
48
|
+
'GET',
|
|
49
|
+
`${this.basePath}/api/health`,
|
|
50
|
+
(ctx) => this.handleHealth(ctx),
|
|
51
|
+
);
|
|
52
|
+
registry.register(
|
|
53
|
+
'POST',
|
|
54
|
+
`${this.basePath}/api/markdown`,
|
|
55
|
+
(ctx) => this.handleMarkdownRender(ctx),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 检查 Basic Auth
|
|
61
|
+
* @param ctx - 请求上下文
|
|
62
|
+
* @returns 是否通过认证,未配置 auth 时返回 true
|
|
63
|
+
*/
|
|
64
|
+
private checkAuth(ctx: Context): boolean {
|
|
65
|
+
if (!this.auth) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
const authHeader = ctx.getHeader('Authorization');
|
|
69
|
+
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const decoded = atob(authHeader.slice(6));
|
|
74
|
+
const [username, password] = decoded.split(':');
|
|
75
|
+
return (
|
|
76
|
+
username === this.auth.username && password === this.auth.password
|
|
77
|
+
);
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 返回 401 Unauthorized 响应
|
|
85
|
+
*/
|
|
86
|
+
private unauthorizedResponse(): Response {
|
|
87
|
+
return new Response('Unauthorized', {
|
|
88
|
+
status: 401,
|
|
89
|
+
headers: {
|
|
90
|
+
'WWW-Authenticate': 'Basic realm="Dashboard"',
|
|
91
|
+
'Content-Type': 'text/plain',
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 处理 Dashboard 主页面
|
|
98
|
+
*/
|
|
99
|
+
private async handleDashboard(ctx: Context): Promise<Response> {
|
|
100
|
+
if (!this.checkAuth(ctx)) {
|
|
101
|
+
return this.unauthorizedResponse();
|
|
102
|
+
}
|
|
103
|
+
const html = createDashboardHTML(this.basePath);
|
|
104
|
+
return new Response(html, {
|
|
105
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 处理系统信息 API
|
|
111
|
+
*/
|
|
112
|
+
private async handleSystem(ctx: Context): Promise<Response> {
|
|
113
|
+
if (!this.checkAuth(ctx)) {
|
|
114
|
+
return this.unauthorizedResponse();
|
|
115
|
+
}
|
|
116
|
+
const mem = process.memoryUsage();
|
|
117
|
+
const data = {
|
|
118
|
+
uptime: Math.floor(process.uptime()),
|
|
119
|
+
memory: {
|
|
120
|
+
rss: mem.rss,
|
|
121
|
+
heapUsed: mem.heapUsed,
|
|
122
|
+
heapTotal: mem.heapTotal,
|
|
123
|
+
},
|
|
124
|
+
platform: process.platform,
|
|
125
|
+
bunVersion: typeof Bun !== 'undefined' ? Bun.version : undefined,
|
|
126
|
+
};
|
|
127
|
+
return new Response(JSON.stringify(data), {
|
|
128
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 处理路由列表 API
|
|
134
|
+
*/
|
|
135
|
+
private async handleRoutes(ctx: Context): Promise<Response> {
|
|
136
|
+
if (!this.checkAuth(ctx)) {
|
|
137
|
+
return this.unauthorizedResponse();
|
|
138
|
+
}
|
|
139
|
+
const registry = RouteRegistry.getInstance();
|
|
140
|
+
const router = registry.getRouter();
|
|
141
|
+
const routes = router.getRoutes();
|
|
142
|
+
const data = routes.map((r) => ({
|
|
143
|
+
method: r.method,
|
|
144
|
+
path: r.path,
|
|
145
|
+
controller: r.controllerClass?.name ?? undefined,
|
|
146
|
+
methodName: r.methodName ?? undefined,
|
|
147
|
+
}));
|
|
148
|
+
return new Response(JSON.stringify(data), {
|
|
149
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 将 Markdown 文本渲染为 HTML
|
|
155
|
+
* 利用 Bun 1.3.8+ 内置 Bun.markdown 解析器(Zig 实现,支持 GFM)
|
|
156
|
+
*/
|
|
157
|
+
private async handleMarkdownRender(ctx: Context): Promise<Response> {
|
|
158
|
+
if (!this.checkAuth(ctx)) {
|
|
159
|
+
return this.unauthorizedResponse();
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const body = await ctx.request.json() as { content?: string };
|
|
163
|
+
if (!body.content) {
|
|
164
|
+
return new Response(JSON.stringify({ error: 'content field is required' }), {
|
|
165
|
+
status: 400,
|
|
166
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const html = Bun.markdown.html(body.content, { headings: true });
|
|
170
|
+
return new Response(JSON.stringify({ html }), {
|
|
171
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
172
|
+
});
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return new Response(JSON.stringify({ error: (err as Error).message }), {
|
|
175
|
+
status: 500,
|
|
176
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 处理健康检查 API
|
|
183
|
+
* 如果 HealthModule 已注册则运行健康指示器,否则返回基本状态
|
|
184
|
+
*/
|
|
185
|
+
private async handleHealth(ctx: Context): Promise<Response> {
|
|
186
|
+
if (!this.checkAuth(ctx)) {
|
|
187
|
+
return this.unauthorizedResponse();
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const container = ControllerRegistry.getInstance().getContainer();
|
|
191
|
+
const indicators = container.resolve<HealthIndicator[]>(
|
|
192
|
+
HEALTH_INDICATORS_TOKEN,
|
|
193
|
+
);
|
|
194
|
+
const details: Record<string, { status: string; details?: unknown }> = {};
|
|
195
|
+
for (const indicator of indicators) {
|
|
196
|
+
try {
|
|
197
|
+
const result = await indicator.check();
|
|
198
|
+
details[indicator.name] = result;
|
|
199
|
+
} catch (err) {
|
|
200
|
+
details[indicator.name] = {
|
|
201
|
+
status: 'down',
|
|
202
|
+
details: { error: (err as Error).message },
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const allUp =
|
|
207
|
+
Object.keys(details).length === 0 ||
|
|
208
|
+
Object.values(details).every((r) => r.status === 'up');
|
|
209
|
+
const data = {
|
|
210
|
+
status: allUp ? 'up' : 'down',
|
|
211
|
+
timestamp: Date.now(),
|
|
212
|
+
details,
|
|
213
|
+
};
|
|
214
|
+
return new Response(JSON.stringify(data), {
|
|
215
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
216
|
+
});
|
|
217
|
+
} catch {
|
|
218
|
+
const data = {
|
|
219
|
+
status: 'up',
|
|
220
|
+
timestamp: Date.now(),
|
|
221
|
+
};
|
|
222
|
+
return new Response(JSON.stringify(data), {
|
|
223
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ApplicationExtension } from '../extensions/types';
|
|
2
|
+
import type { Container } from '../di/container';
|
|
3
|
+
import { DashboardService } from './controller';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Dashboard 扩展
|
|
7
|
+
* 在应用初始化时注册 Dashboard 路由
|
|
8
|
+
*/
|
|
9
|
+
export class DashboardExtension implements ApplicationExtension {
|
|
10
|
+
private readonly service: DashboardService;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 创建 Dashboard 扩展
|
|
14
|
+
* @param service - Dashboard 服务实例
|
|
15
|
+
*/
|
|
16
|
+
public constructor(service: DashboardService) {
|
|
17
|
+
this.service = service;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 注册扩展,将 Dashboard 路由注册到 RouteRegistry
|
|
22
|
+
*/
|
|
23
|
+
public register(_container: Container): void {
|
|
24
|
+
this.service.registerRoutes();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Module, MODULE_METADATA_KEY } from '../di/module';
|
|
2
|
+
import { DashboardService } from './controller';
|
|
3
|
+
import { DashboardExtension } from './dashboard-extension';
|
|
4
|
+
import type { DashboardModuleOptions } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dashboard 模块
|
|
8
|
+
* 提供监控 Web UI,可视化指标、健康状态、路由和系统信息
|
|
9
|
+
*/
|
|
10
|
+
@Module({
|
|
11
|
+
extensions: [],
|
|
12
|
+
controllers: [],
|
|
13
|
+
providers: [],
|
|
14
|
+
})
|
|
15
|
+
export class DashboardModule {
|
|
16
|
+
/**
|
|
17
|
+
* 创建 Dashboard 模块
|
|
18
|
+
* @param options - 模块配置
|
|
19
|
+
*/
|
|
20
|
+
public static forRoot(
|
|
21
|
+
options: DashboardModuleOptions = {},
|
|
22
|
+
): typeof DashboardModule {
|
|
23
|
+
const path = options.path ?? '/_dashboard';
|
|
24
|
+
const basePath = path.endsWith('/') ? path.slice(0, -1) : path;
|
|
25
|
+
const service = new DashboardService(basePath, options.auth);
|
|
26
|
+
const extension = new DashboardExtension(service);
|
|
27
|
+
|
|
28
|
+
const existingMetadata =
|
|
29
|
+
Reflect.getMetadata(MODULE_METADATA_KEY, DashboardModule) || {};
|
|
30
|
+
const metadata = {
|
|
31
|
+
...existingMetadata,
|
|
32
|
+
extensions: [extension],
|
|
33
|
+
};
|
|
34
|
+
Reflect.defineMetadata(MODULE_METADATA_KEY, metadata, DashboardModule);
|
|
35
|
+
|
|
36
|
+
return DashboardModule;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface DashboardModuleOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Dashboard UI 路径
|
|
4
|
+
* @default '/_dashboard'
|
|
5
|
+
*/
|
|
6
|
+
path?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Basic Auth 认证配置
|
|
9
|
+
*/
|
|
10
|
+
auth?: {
|
|
11
|
+
username: string;
|
|
12
|
+
password: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const DASHBOARD_OPTIONS_TOKEN = Symbol('@dangao/bun-server:dashboard:options');
|