@dangao/bun-server 2.2.0 → 3.0.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 +81 -3
- package/dist/ai/providers/anthropic-provider.d.ts.map +1 -1
- package/dist/ai/providers/google-provider.d.ts.map +1 -1
- package/dist/ai/providers/ollama-provider.d.ts.map +1 -1
- package/dist/ai/providers/openai-provider.d.ts.map +1 -1
- package/dist/ai/service.d.ts.map +1 -1
- package/dist/ai/types.d.ts +5 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/auth/jwt.d.ts.map +1 -1
- package/dist/config/service.d.ts +0 -1
- package/dist/config/service.d.ts.map +1 -1
- package/dist/core/application.d.ts +30 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/context.d.ts +5 -0
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/server.d.ts +29 -9
- package/dist/core/server.d.ts.map +1 -1
- package/dist/dashboard/controller.d.ts.map +1 -1
- package/dist/database/connection-pool.d.ts +3 -3
- package/dist/database/connection-pool.d.ts.map +1 -1
- package/dist/database/sql-manager.d.ts +8 -4
- package/dist/database/sql-manager.d.ts.map +1 -1
- package/dist/database/sqlite-adapter.d.ts +7 -3
- package/dist/database/sqlite-adapter.d.ts.map +1 -1
- package/dist/debug/recorder.d.ts +0 -1
- package/dist/debug/recorder.d.ts.map +1 -1
- package/dist/files/static-middleware.d.ts.map +1 -1
- package/dist/files/storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40335 -3523
- package/dist/index.node.mjs +17689 -0
- package/dist/mcp/server.d.ts +5 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/middleware/builtin/static-file.d.ts +4 -2
- package/dist/middleware/builtin/static-file.d.ts.map +1 -1
- package/dist/platform/bun/crypto.d.ts +3 -0
- package/dist/platform/bun/crypto.d.ts.map +1 -0
- package/dist/platform/bun/fs.d.ts +3 -0
- package/dist/platform/bun/fs.d.ts.map +1 -0
- package/dist/platform/bun/http.d.ts +15 -0
- package/dist/platform/bun/http.d.ts.map +1 -0
- package/dist/platform/bun/index.d.ts +3 -0
- package/dist/platform/bun/index.d.ts.map +1 -0
- package/dist/platform/bun/parser.d.ts +3 -0
- package/dist/platform/bun/parser.d.ts.map +1 -0
- package/dist/platform/bun/process.d.ts +3 -0
- package/dist/platform/bun/process.d.ts.map +1 -0
- package/dist/platform/detector.d.ts +9 -0
- package/dist/platform/detector.d.ts.map +1 -0
- package/dist/platform/index.d.ts +4 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/node/crypto.d.ts +3 -0
- package/dist/platform/node/crypto.d.ts.map +1 -0
- package/dist/platform/node/fs.d.ts +3 -0
- package/dist/platform/node/fs.d.ts.map +1 -0
- package/dist/platform/node/http.d.ts +3 -0
- package/dist/platform/node/http.d.ts.map +1 -0
- package/dist/platform/node/index.d.ts +3 -0
- package/dist/platform/node/index.d.ts.map +1 -0
- package/dist/platform/node/parser.d.ts +3 -0
- package/dist/platform/node/parser.d.ts.map +1 -0
- package/dist/platform/node/process.d.ts +3 -0
- package/dist/platform/node/process.d.ts.map +1 -0
- package/dist/platform/runtime.d.ts +14 -0
- package/dist/platform/runtime.d.ts.map +1 -0
- package/dist/platform/types.d.ts +139 -0
- package/dist/platform/types.d.ts.map +1 -0
- package/dist/prompt/stores/file-store.d.ts.map +1 -1
- package/dist/rag/service.d.ts.map +1 -1
- package/dist/request/response.d.ts +3 -1
- package/dist/request/response.d.ts.map +1 -1
- package/dist/security/guards/execution-context.d.ts +2 -2
- package/dist/security/guards/execution-context.d.ts.map +1 -1
- package/dist/security/guards/types.d.ts +2 -2
- package/dist/security/guards/types.d.ts.map +1 -1
- package/dist/swagger/generator.d.ts.map +1 -1
- package/dist/websocket/registry.d.ts +4 -4
- package/dist/websocket/registry.d.ts.map +1 -1
- package/docs/deployment.md +31 -7
- package/docs/design/query-interceptor-design.md +381 -0
- package/docs/idle-timeout.md +101 -8
- package/docs/migration.md +43 -0
- package/docs/platform.md +299 -0
- package/docs/testing.md +60 -0
- package/docs/zh/deployment.md +30 -7
- package/docs/zh/idle-timeout.md +99 -6
- package/docs/zh/migration.md +42 -0
- package/docs/zh/platform.md +299 -0
- package/docs/zh/testing.md +60 -0
- package/package.json +24 -6
- package/src/ai/providers/anthropic-provider.ts +5 -2
- package/src/ai/providers/google-provider.ts +3 -0
- package/src/ai/providers/ollama-provider.ts +3 -0
- package/src/ai/providers/openai-provider.ts +5 -2
- package/src/ai/service.ts +17 -5
- package/src/ai/types.ts +5 -0
- package/src/auth/jwt.ts +4 -3
- package/src/config/service.ts +7 -6
- package/src/core/application.ts +38 -1
- package/src/core/cluster.ts +16 -14
- package/src/core/context.ts +7 -0
- package/src/core/server.ts +162 -46
- package/src/dashboard/controller.ts +3 -2
- package/src/database/connection-pool.ts +32 -20
- package/src/database/database-module.ts +1 -1
- package/src/database/db-proxy.ts +2 -2
- package/src/database/orm/transaction-manager.ts +1 -1
- package/src/database/sql-manager.ts +48 -13
- package/src/database/sqlite-adapter.ts +45 -12
- package/src/debug/recorder.ts +4 -3
- package/src/files/static-middleware.ts +3 -2
- package/src/files/storage.ts +2 -1
- package/src/index.ts +13 -0
- package/src/mcp/server.ts +6 -15
- package/src/middleware/builtin/static-file.ts +8 -5
- package/src/platform/bun/crypto.ts +30 -0
- package/src/platform/bun/fs.ts +52 -0
- package/src/platform/bun/http.ts +106 -0
- package/src/platform/bun/index.ts +17 -0
- package/src/platform/bun/parser.ts +19 -0
- package/src/platform/bun/process.ts +37 -0
- package/src/platform/detector.ts +36 -0
- package/src/platform/index.ts +20 -0
- package/src/platform/node/crypto.ts +40 -0
- package/src/platform/node/fs.ts +115 -0
- package/src/platform/node/http.ts +196 -0
- package/src/platform/node/index.ts +17 -0
- package/src/platform/node/parser.ts +34 -0
- package/src/platform/node/process.ts +51 -0
- package/src/platform/runtime.ts +50 -0
- package/src/platform/types.ts +150 -0
- package/src/prompt/stores/file-store.ts +6 -5
- package/src/rag/service.ts +2 -1
- package/src/request/response.ts +7 -4
- package/src/security/guards/execution-context.ts +4 -4
- package/src/security/guards/types.ts +2 -2
- package/src/swagger/generator.ts +2 -1
- package/src/websocket/registry.ts +6 -7
- package/tests/controller/path-combination.test.ts +196 -2
- package/tests/files/static-middleware.test.ts +5 -2
- package/tests/middleware/static-file.test.ts +5 -2
- package/tests/platform/bun/crypto.test.ts +8 -0
- package/tests/platform/bun/database.test.ts +8 -0
- package/tests/platform/bun/fs.test.ts +8 -0
- package/tests/platform/bun/parser.test.ts +8 -0
- package/tests/platform/bun/process.test.ts +8 -0
- package/tests/platform/bun/websocket.test.ts +8 -0
- package/tests/platform/detector.test.ts +57 -0
- package/tests/platform/node/build-smoke.test.ts +92 -0
- package/tests/platform/node/crypto.test.ts +9 -0
- package/tests/platform/node/database.test.ts +9 -0
- package/tests/platform/node/fs.test.ts +9 -0
- package/tests/platform/node/parser.test.ts +9 -0
- package/tests/platform/node/process.test.ts +9 -0
- package/tests/platform/node/websocket.test.ts +9 -0
- package/tests/platform/shared/crypto.cases.ts +49 -0
- package/tests/platform/shared/database.cases.ts +43 -0
- package/tests/platform/shared/fs.cases.ts +82 -0
- package/tests/platform/shared/parser.cases.ts +55 -0
- package/tests/platform/shared/process.cases.ts +26 -0
- package/tests/platform/shared/suite.ts +33 -0
- package/tests/platform/shared/websocket.cases.ts +61 -0
- package/tests/request/response.test.ts +5 -2
- package/tests/router/router-extended.test.ts +53 -0
package/src/config/service.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConfigFileFormat } from './types';
|
|
2
|
+
import { getRuntime } from '../platform/runtime';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 配置服务
|
|
@@ -7,16 +8,16 @@ import type { ConfigFileFormat } from './types';
|
|
|
7
8
|
export class ConfigService<TConfig extends Record<string, unknown> = Record<string, unknown>> {
|
|
8
9
|
/**
|
|
9
10
|
* 解析配置内容,按 JSON -> JSONC -> JSON5 顺序自动尝试
|
|
10
|
-
* 利用 Bun 1.3.6+ 的 Bun.JSONC 和 Bun 1.3.7+ 的 Bun.JSON5
|
|
11
11
|
* @param content - 配置文本内容
|
|
12
12
|
* @param format - 强制指定格式(可选),省略则自动检测
|
|
13
13
|
*/
|
|
14
14
|
public static parseConfigContent(content: string, format?: ConfigFileFormat): unknown {
|
|
15
|
+
const parser = getRuntime().parser;
|
|
15
16
|
if (format === 'jsonc') {
|
|
16
|
-
return
|
|
17
|
+
return parser.parseJSONC(content);
|
|
17
18
|
}
|
|
18
19
|
if (format === 'json5') {
|
|
19
|
-
return
|
|
20
|
+
return parser.parseJSON5(content);
|
|
20
21
|
}
|
|
21
22
|
if (format === 'json') {
|
|
22
23
|
return JSON.parse(content);
|
|
@@ -26,9 +27,9 @@ export class ConfigService<TConfig extends Record<string, unknown> = Record<stri
|
|
|
26
27
|
return JSON.parse(content);
|
|
27
28
|
} catch (_error) {
|
|
28
29
|
try {
|
|
29
|
-
return
|
|
30
|
+
return parser.parseJSONC(content);
|
|
30
31
|
} catch (_innerError) {
|
|
31
|
-
return
|
|
32
|
+
return parser.parseJSON5(content);
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
}
|
|
@@ -38,7 +39,7 @@ export class ConfigService<TConfig extends Record<string, unknown> = Record<stri
|
|
|
38
39
|
* @param filePath - 配置文件路径(.json / .jsonc / .json5)
|
|
39
40
|
*/
|
|
40
41
|
public static async loadConfigFile(filePath: string): Promise<Record<string, unknown>> {
|
|
41
|
-
const file =
|
|
42
|
+
const file = getRuntime().fs.file(filePath);
|
|
42
43
|
const content = await file.text();
|
|
43
44
|
|
|
44
45
|
let format: ConfigFileFormat | undefined;
|
package/src/core/application.ts
CHANGED
|
@@ -21,6 +21,8 @@ import { LoggerManager } from '@dangao/logsmith';
|
|
|
21
21
|
import { EventModule } from '../events/event-module';
|
|
22
22
|
import { AsyncProviderRegistry } from '../di/async-module';
|
|
23
23
|
import { ServiceRegistryModule } from '../microservice/service-registry/service-registry-module';
|
|
24
|
+
import type { PlatformEngine } from '../platform/types';
|
|
25
|
+
import { initRuntime } from '../platform/runtime';
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* 应用配置选项
|
|
@@ -61,6 +63,37 @@ export interface ApplicationOptions {
|
|
|
61
63
|
* 框架内部会自动转换为 Bun.serve 的秒单位
|
|
62
64
|
*/
|
|
63
65
|
idleTimeout?: number;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* SSE 保活配置
|
|
69
|
+
*
|
|
70
|
+
* 框架自动检测 `Content-Type: text/event-stream` 的响应,
|
|
71
|
+
* 对该请求禁用 Bun TCP 空闲超时(`server.timeout(req, 0)`),
|
|
72
|
+
* 并按配置间隔向客户端发送 SSE 注释心跳(`: keepalive\n\n`)。
|
|
73
|
+
*
|
|
74
|
+
* 心跳可防止中间代理(nginx / 云 LB)因空闲而断开连接。
|
|
75
|
+
*
|
|
76
|
+
* @default `{ enabled: true, intervalMs: 15000 }`
|
|
77
|
+
*/
|
|
78
|
+
sseKeepAlive?: {
|
|
79
|
+
/** 是否启用,默认 true */
|
|
80
|
+
enabled?: boolean;
|
|
81
|
+
/** 心跳间隔(毫秒),默认 15000 */
|
|
82
|
+
intervalMs?: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 运行时平台选择
|
|
87
|
+
*
|
|
88
|
+
* 优先级(从高到低):
|
|
89
|
+
* 1. 此字段(构造函数选项)
|
|
90
|
+
* 2. CLI 参数 `--platform=node`
|
|
91
|
+
* 3. 环境变量 `BUN_SERVER_PLATFORM=node`
|
|
92
|
+
* 4. 自动检测(有 Bun 全局对象则使用 bun,否则使用 node)
|
|
93
|
+
*
|
|
94
|
+
* @default 自动检测
|
|
95
|
+
*/
|
|
96
|
+
platform?: PlatformEngine;
|
|
64
97
|
}
|
|
65
98
|
|
|
66
99
|
/**
|
|
@@ -76,6 +109,9 @@ export class Application {
|
|
|
76
109
|
private signalHandlersInstalled: boolean = false;
|
|
77
110
|
|
|
78
111
|
public constructor(options: ApplicationOptions = {}) {
|
|
112
|
+
// Initialize platform runtime first — must be the very first operation
|
|
113
|
+
initRuntime(options.platform);
|
|
114
|
+
|
|
79
115
|
this.options = options;
|
|
80
116
|
this.middlewarePipeline = new MiddlewarePipeline([createErrorHandlingMiddleware()]);
|
|
81
117
|
this.websocketRegistry = WebSocketGatewayRegistry.getInstance();
|
|
@@ -150,13 +186,14 @@ export class Application {
|
|
|
150
186
|
hostname: finalHostname,
|
|
151
187
|
reusePort: this.options.reusePort,
|
|
152
188
|
idleTimeout: this.options.idleTimeout,
|
|
189
|
+
sseKeepAlive: this.options.sseKeepAlive,
|
|
153
190
|
fetch: this.handleRequest.bind(this),
|
|
154
191
|
websocketRegistry: this.websocketRegistry,
|
|
155
192
|
gracefulShutdownTimeout: this.options.gracefulShutdownTimeout,
|
|
156
193
|
};
|
|
157
194
|
|
|
158
195
|
this.server = new BunServer(serverOptions);
|
|
159
|
-
this.server.start();
|
|
196
|
+
await this.server.start();
|
|
160
197
|
|
|
161
198
|
// 安装信号处理器(如果启用)
|
|
162
199
|
if (this.options.enableSignalHandlers !== false) {
|
package/src/core/cluster.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { spawn } from 'bun';
|
|
2
1
|
import { LoggerManager } from '@dangao/logsmith';
|
|
3
2
|
import { tmpdir } from 'os';
|
|
4
3
|
import { join } from 'path';
|
|
5
4
|
import { mkdirSync, rmSync, existsSync } from 'fs';
|
|
5
|
+
import type { IServerHandle, IChildProcess } from '../platform/types';
|
|
6
|
+
import { getRuntime } from '../platform/runtime';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* 集群模式
|
|
@@ -38,12 +39,12 @@ export interface ClusterOptions {
|
|
|
38
39
|
*/
|
|
39
40
|
export class ClusterManager {
|
|
40
41
|
private readonly workerCount: number;
|
|
41
|
-
private readonly workers:
|
|
42
|
+
private readonly workers: IChildProcess[] = [];
|
|
42
43
|
private readonly scriptPath: string;
|
|
43
44
|
private readonly port: number;
|
|
44
45
|
private readonly hostname?: string;
|
|
45
46
|
private readonly mode: 'reusePort' | 'proxy';
|
|
46
|
-
private proxyServer?:
|
|
47
|
+
private proxyServer?: IServerHandle;
|
|
47
48
|
private socketPaths: string[] = [];
|
|
48
49
|
private roundRobinIndex = 0;
|
|
49
50
|
private socketDir?: string;
|
|
@@ -145,8 +146,8 @@ export class ClusterManager {
|
|
|
145
146
|
this.monitorReusePortWorkers();
|
|
146
147
|
}
|
|
147
148
|
|
|
148
|
-
private spawnReusePortWorker(index: number):
|
|
149
|
-
return spawn({
|
|
149
|
+
private spawnReusePortWorker(index: number): IChildProcess {
|
|
150
|
+
return getRuntime().process.spawn({
|
|
150
151
|
cmd: ['bun', 'run', this.scriptPath],
|
|
151
152
|
env: {
|
|
152
153
|
...process.env,
|
|
@@ -197,7 +198,7 @@ export class ClusterManager {
|
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
await this.waitForWorkerSockets();
|
|
200
|
-
this.startProxyServer();
|
|
201
|
+
await this.startProxyServer();
|
|
201
202
|
|
|
202
203
|
logger.info(
|
|
203
204
|
`[Cluster] ${this.workerCount} workers ready (proxy mode, unix sockets)`,
|
|
@@ -206,8 +207,8 @@ export class ClusterManager {
|
|
|
206
207
|
this.monitorProxyWorkers();
|
|
207
208
|
}
|
|
208
209
|
|
|
209
|
-
private spawnProxyWorker(index: number, socketPath: string):
|
|
210
|
-
return spawn({
|
|
210
|
+
private spawnProxyWorker(index: number, socketPath: string): IChildProcess {
|
|
211
|
+
return getRuntime().process.spawn({
|
|
211
212
|
cmd: ['bun', 'run', this.scriptPath],
|
|
212
213
|
env: {
|
|
213
214
|
...process.env,
|
|
@@ -239,11 +240,11 @@ export class ClusterManager {
|
|
|
239
240
|
|
|
240
241
|
if (allReady) {
|
|
241
242
|
// Give workers a moment to finish bind+listen after file creation
|
|
242
|
-
await
|
|
243
|
+
await getRuntime().process.sleep(200);
|
|
243
244
|
return;
|
|
244
245
|
}
|
|
245
246
|
|
|
246
|
-
await
|
|
247
|
+
await getRuntime().process.sleep(100);
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
const ready = this.socketPaths.filter((p) => existsSync(p)).length;
|
|
@@ -252,11 +253,11 @@ export class ClusterManager {
|
|
|
252
253
|
);
|
|
253
254
|
}
|
|
254
255
|
|
|
255
|
-
private startProxyServer(): void {
|
|
256
|
+
private async startProxyServer(): Promise<void> {
|
|
256
257
|
const sockets = this.socketPaths;
|
|
257
258
|
const count = sockets.length;
|
|
258
259
|
|
|
259
|
-
this.proxyServer =
|
|
260
|
+
this.proxyServer = await getRuntime().http.serve({
|
|
260
261
|
port: this.port,
|
|
261
262
|
hostname: this.hostname,
|
|
262
263
|
fetch: async (req) => {
|
|
@@ -269,6 +270,7 @@ export class ClusterManager {
|
|
|
269
270
|
headers: req.headers,
|
|
270
271
|
body: req.body,
|
|
271
272
|
redirect: 'manual',
|
|
273
|
+
// @ts-ignore — unix fetch is Bun-specific; Node fallback skips this
|
|
272
274
|
unix: socketPath,
|
|
273
275
|
});
|
|
274
276
|
} catch (_error) {
|
|
@@ -296,11 +298,11 @@ export class ClusterManager {
|
|
|
296
298
|
const deadline = Date.now() + 15_000;
|
|
297
299
|
while (Date.now() < deadline) {
|
|
298
300
|
if (existsSync(socketPath)) {
|
|
299
|
-
await
|
|
301
|
+
await getRuntime().process.sleep(200);
|
|
300
302
|
logger.info(`[Cluster] Worker ${index} restarted (unix socket)`);
|
|
301
303
|
return;
|
|
302
304
|
}
|
|
303
|
-
await
|
|
305
|
+
await getRuntime().process.sleep(100);
|
|
304
306
|
}
|
|
305
307
|
|
|
306
308
|
logger.error(`[Cluster] Worker ${index} failed to restart within 15s`);
|
package/src/core/context.ts
CHANGED
|
@@ -74,6 +74,12 @@ export class Context {
|
|
|
74
74
|
*/
|
|
75
75
|
private _bodyParsed: boolean = false;
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* 客户端断连信号
|
|
79
|
+
* 当客户端主动关闭连接时 abort,可用于级联取消内部请求
|
|
80
|
+
*/
|
|
81
|
+
public readonly signal: AbortSignal;
|
|
82
|
+
|
|
77
83
|
public constructor(request: Request) {
|
|
78
84
|
this.request = request;
|
|
79
85
|
this.url = new URL(request.url);
|
|
@@ -82,6 +88,7 @@ export class Context {
|
|
|
82
88
|
this.query = this.url.searchParams;
|
|
83
89
|
this.headers = request.headers;
|
|
84
90
|
this.responseHeaders = new Headers();
|
|
91
|
+
this.signal = request.signal;
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
/**
|
package/src/core/server.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { Server } from "bun";
|
|
2
1
|
import { Context } from "./context";
|
|
3
2
|
import { LoggerManager } from "@dangao/logsmith";
|
|
4
3
|
import type { WebSocketGatewayRegistry } from "../websocket/registry";
|
|
5
4
|
import type { WebSocketConnectionData } from "../websocket/registry";
|
|
5
|
+
import type { IServerHandle, IWebSocket } from "../platform/types";
|
|
6
|
+
import { getRuntime } from "../platform/runtime";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* 服务器配置选项
|
|
@@ -44,17 +45,26 @@ export interface ServerOptions {
|
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* 连接空闲超时时间(毫秒)
|
|
47
|
-
*
|
|
48
|
+
* Bun 平台下自动转换为秒单位;Node.js 平台下静默忽略
|
|
48
49
|
*/
|
|
49
50
|
idleTimeout?: number;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* SSE 保活配置
|
|
54
|
+
* @see ApplicationOptions.sseKeepAlive
|
|
55
|
+
*/
|
|
56
|
+
sseKeepAlive?: {
|
|
57
|
+
enabled?: boolean;
|
|
58
|
+
intervalMs?: number;
|
|
59
|
+
};
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
/**
|
|
53
63
|
* 服务器封装类
|
|
54
|
-
*
|
|
64
|
+
* 基于平台适配层构建,支持 Bun 和 Node.js
|
|
55
65
|
*/
|
|
56
66
|
export class BunServer {
|
|
57
|
-
private server?:
|
|
67
|
+
private server?: IServerHandle;
|
|
58
68
|
private readonly options: ServerOptions;
|
|
59
69
|
private activeRequests: number = 0;
|
|
60
70
|
private isShuttingDown: boolean = false;
|
|
@@ -68,7 +78,7 @@ export class BunServer {
|
|
|
68
78
|
/**
|
|
69
79
|
* 启动服务器
|
|
70
80
|
*/
|
|
71
|
-
public start(): void {
|
|
81
|
+
public async start(): Promise<void> {
|
|
72
82
|
if (this.server) {
|
|
73
83
|
throw new Error("Server is already running");
|
|
74
84
|
}
|
|
@@ -81,10 +91,45 @@ export class BunServer {
|
|
|
81
91
|
this.shutdownPromise = undefined;
|
|
82
92
|
this.shutdownResolve = undefined;
|
|
83
93
|
|
|
94
|
+
const sseKeepAlive = this.options.sseKeepAlive;
|
|
95
|
+
const sseHeartbeatEnabled = sseKeepAlive?.enabled !== false;
|
|
96
|
+
const sseHeartbeatIntervalMs = sseKeepAlive?.intervalMs ?? 15_000;
|
|
97
|
+
|
|
98
|
+
const postProcessSse = (
|
|
99
|
+
response: Response,
|
|
100
|
+
request: Request,
|
|
101
|
+
serverHandle: IServerHandle,
|
|
102
|
+
): Response => {
|
|
103
|
+
const ct = response.headers.get('content-type');
|
|
104
|
+
if (!ct?.includes('text/event-stream')) {
|
|
105
|
+
return response;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// SSE detected — disable idle timeout for this connection (Bun-only, no-op on Node)
|
|
109
|
+
serverHandle.timeout?.(request, 0);
|
|
110
|
+
|
|
111
|
+
if (sseHeartbeatEnabled && response.body) {
|
|
112
|
+
return BunServer.wrapSseWithHeartbeat(
|
|
113
|
+
response,
|
|
114
|
+
sseHeartbeatIntervalMs,
|
|
115
|
+
request.signal,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return response;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const decrementAndMaybeShutdown = () => {
|
|
123
|
+
this.activeRequests--;
|
|
124
|
+
if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
|
|
125
|
+
this.shutdownResolve();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
84
129
|
const fetchHandler = (
|
|
85
130
|
request: Request,
|
|
86
|
-
|
|
87
|
-
): Response | Promise<Response> | undefined => {
|
|
131
|
+
serverHandle: IServerHandle,
|
|
132
|
+
): Response | Promise<Response | undefined> | undefined => {
|
|
88
133
|
if (this.isShuttingDown) {
|
|
89
134
|
return new Response("Server is shutting down", { status: 503 });
|
|
90
135
|
}
|
|
@@ -101,7 +146,7 @@ export class BunServer {
|
|
|
101
146
|
}
|
|
102
147
|
const context = new Context(request);
|
|
103
148
|
const queryParams = new URLSearchParams(url.searchParams);
|
|
104
|
-
const upgraded =
|
|
149
|
+
const upgraded = serverHandle.upgrade?.(request, {
|
|
105
150
|
data: {
|
|
106
151
|
path: url.pathname,
|
|
107
152
|
query: queryParams,
|
|
@@ -120,57 +165,55 @@ export class BunServer {
|
|
|
120
165
|
const responsePromise = this.options.fetch(context);
|
|
121
166
|
|
|
122
167
|
if (responsePromise instanceof Promise) {
|
|
123
|
-
responsePromise
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
.catch(() => {
|
|
131
|
-
// errors handled by middleware pipeline
|
|
132
|
-
});
|
|
133
|
-
} else {
|
|
134
|
-
this.activeRequests--;
|
|
135
|
-
if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
|
|
136
|
-
this.shutdownResolve();
|
|
137
|
-
}
|
|
168
|
+
const processed = responsePromise.then(
|
|
169
|
+
(response) => postProcessSse(response, request, serverHandle),
|
|
170
|
+
);
|
|
171
|
+
processed
|
|
172
|
+
.finally(decrementAndMaybeShutdown)
|
|
173
|
+
.catch(() => { /* errors handled by middleware pipeline */ });
|
|
174
|
+
return processed;
|
|
138
175
|
}
|
|
139
176
|
|
|
140
|
-
|
|
177
|
+
decrementAndMaybeShutdown();
|
|
178
|
+
return postProcessSse(responsePromise, request, serverHandle);
|
|
141
179
|
};
|
|
142
180
|
|
|
143
|
-
const websocketHandlers =
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
181
|
+
const websocketHandlers = this.options.websocketRegistry
|
|
182
|
+
? {
|
|
183
|
+
open: async (ws: IWebSocket<WebSocketConnectionData>) => {
|
|
184
|
+
await this.options.websocketRegistry?.handleOpen(ws);
|
|
185
|
+
},
|
|
186
|
+
message: async (ws: IWebSocket<WebSocketConnectionData>, message: string | Buffer) => {
|
|
187
|
+
await this.options.websocketRegistry?.handleMessage(ws, message);
|
|
188
|
+
},
|
|
189
|
+
close: async (ws: IWebSocket<WebSocketConnectionData>, code: number, reason: string) => {
|
|
190
|
+
await this.options.websocketRegistry?.handleClose(ws, code, reason);
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
: undefined;
|
|
154
194
|
|
|
195
|
+
const runtime = getRuntime();
|
|
155
196
|
const socketFile = process.env.CLUSTER_SOCKET_FILE;
|
|
156
197
|
|
|
157
198
|
if (socketFile) {
|
|
158
199
|
// Unix socket mode for cluster proxy workers
|
|
159
|
-
this.server =
|
|
200
|
+
this.server = await runtime.http.serve({
|
|
160
201
|
unix: socketFile,
|
|
161
202
|
fetch: fetchHandler,
|
|
162
203
|
websocket: websocketHandlers,
|
|
163
204
|
});
|
|
164
205
|
logger.info(`Server started at unix://${socketFile}`);
|
|
165
206
|
} else {
|
|
166
|
-
|
|
207
|
+
const idleTimeoutSec =
|
|
208
|
+
typeof this.options.idleTimeout === 'number'
|
|
209
|
+
? Math.max(0, Math.ceil(this.options.idleTimeout / 1000))
|
|
210
|
+
: undefined;
|
|
211
|
+
|
|
212
|
+
this.server = await runtime.http.serve({
|
|
167
213
|
port: this.options.port ?? 3000,
|
|
168
214
|
hostname: this.options.hostname,
|
|
169
215
|
reusePort: this.options.reusePort,
|
|
170
|
-
idleTimeout:
|
|
171
|
-
typeof this.options.idleTimeout === 'number'
|
|
172
|
-
? Math.max(0, Math.ceil(this.options.idleTimeout / 1000))
|
|
173
|
-
: undefined,
|
|
216
|
+
idleTimeout: idleTimeoutSec,
|
|
174
217
|
fetch: fetchHandler,
|
|
175
218
|
websocket: websocketHandlers,
|
|
176
219
|
});
|
|
@@ -181,7 +224,7 @@ export class BunServer {
|
|
|
181
224
|
// In proxy cluster mode (TCP fallback), report port to master
|
|
182
225
|
const portFile = process.env.CLUSTER_PORT_FILE;
|
|
183
226
|
if (portFile) {
|
|
184
|
-
|
|
227
|
+
runtime.fs.write(portFile, String(port));
|
|
185
228
|
}
|
|
186
229
|
}
|
|
187
230
|
}
|
|
@@ -264,16 +307,23 @@ export class BunServer {
|
|
|
264
307
|
}
|
|
265
308
|
|
|
266
309
|
/**
|
|
267
|
-
*
|
|
268
|
-
* @returns Bun Server 实例
|
|
310
|
+
* 获取平台中立的服务器句柄(推荐)
|
|
269
311
|
*/
|
|
270
|
-
public getServer():
|
|
312
|
+
public getServer(): IServerHandle | undefined {
|
|
271
313
|
return this.server;
|
|
272
314
|
}
|
|
273
315
|
|
|
316
|
+
/**
|
|
317
|
+
* 获取底层原生服务器实例(不推荐,类型为 unknown)
|
|
318
|
+
* - Bun 平台:Bun.Server<WebSocketConnectionData>
|
|
319
|
+
* - Node.js 平台:node:http.Server
|
|
320
|
+
*/
|
|
321
|
+
public getNativeServer(): unknown {
|
|
322
|
+
return this.server?.getNative();
|
|
323
|
+
}
|
|
324
|
+
|
|
274
325
|
/**
|
|
275
326
|
* 检查服务器是否运行中
|
|
276
|
-
* @returns 是否运行中
|
|
277
327
|
*/
|
|
278
328
|
public isRunning(): boolean {
|
|
279
329
|
return this.server !== undefined;
|
|
@@ -296,4 +346,70 @@ export class BunServer {
|
|
|
296
346
|
public getHostname(): string | undefined {
|
|
297
347
|
return this.options.hostname;
|
|
298
348
|
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 将 SSE Response 的 body 包裹一层心跳注入流。
|
|
352
|
+
*
|
|
353
|
+
* 原始流的数据原样透传;在数据间隙中按 intervalMs 发送
|
|
354
|
+
* SSE 注释帧 `: keepalive\n\n`(客户端会忽略注释帧)。
|
|
355
|
+
*
|
|
356
|
+
* 当 signal abort / 原始流结束 / 客户端断连时自动清理定时器。
|
|
357
|
+
*/
|
|
358
|
+
private static wrapSseWithHeartbeat(
|
|
359
|
+
original: Response,
|
|
360
|
+
intervalMs: number,
|
|
361
|
+
signal: AbortSignal,
|
|
362
|
+
): Response {
|
|
363
|
+
const encoder = new TextEncoder();
|
|
364
|
+
const keepaliveChunk = encoder.encode(': keepalive\n\n');
|
|
365
|
+
const originalBody = original.body!;
|
|
366
|
+
let heartbeat: ReturnType<typeof setInterval> | undefined;
|
|
367
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
368
|
+
|
|
369
|
+
const wrapped = new ReadableStream<Uint8Array>({
|
|
370
|
+
start(controller) {
|
|
371
|
+
reader = originalBody.getReader() as ReadableStreamDefaultReader<Uint8Array>;
|
|
372
|
+
|
|
373
|
+
heartbeat = setInterval(() => {
|
|
374
|
+
try {
|
|
375
|
+
controller.enqueue(keepaliveChunk);
|
|
376
|
+
} catch {
|
|
377
|
+
clearInterval(heartbeat);
|
|
378
|
+
heartbeat = undefined;
|
|
379
|
+
}
|
|
380
|
+
}, intervalMs);
|
|
381
|
+
|
|
382
|
+
const onAbort = () => {
|
|
383
|
+
if (heartbeat) { clearInterval(heartbeat); heartbeat = undefined; }
|
|
384
|
+
};
|
|
385
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
386
|
+
|
|
387
|
+
const pump = async () => {
|
|
388
|
+
try {
|
|
389
|
+
while (true) {
|
|
390
|
+
const { done, value } = await reader!.read();
|
|
391
|
+
if (done) break;
|
|
392
|
+
controller.enqueue(value);
|
|
393
|
+
}
|
|
394
|
+
controller.close();
|
|
395
|
+
} catch (err) {
|
|
396
|
+
try { controller.error(err); } catch { /* already closed */ }
|
|
397
|
+
} finally {
|
|
398
|
+
if (heartbeat) { clearInterval(heartbeat); heartbeat = undefined; }
|
|
399
|
+
signal.removeEventListener('abort', onAbort);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
pump();
|
|
403
|
+
},
|
|
404
|
+
cancel() {
|
|
405
|
+
if (heartbeat) { clearInterval(heartbeat); heartbeat = undefined; }
|
|
406
|
+
reader?.cancel();
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
return new Response(wrapped, {
|
|
411
|
+
status: original.status,
|
|
412
|
+
headers: original.headers,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
299
415
|
}
|
|
@@ -4,6 +4,7 @@ import { createDashboardHTML } from './ui';
|
|
|
4
4
|
import { ControllerRegistry } from '../controller/controller';
|
|
5
5
|
import { HEALTH_INDICATORS_TOKEN } from '../health/types';
|
|
6
6
|
import type { HealthIndicator } from '../health/types';
|
|
7
|
+
import { getRuntime } from '../platform/runtime';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Dashboard 服务
|
|
@@ -122,7 +123,7 @@ export class DashboardService {
|
|
|
122
123
|
heapTotal: mem.heapTotal,
|
|
123
124
|
},
|
|
124
125
|
platform: process.platform,
|
|
125
|
-
bunVersion: typeof Bun !== 'undefined' ? Bun.version : undefined,
|
|
126
|
+
bunVersion: getRuntime().engine === 'bun' && typeof Bun !== 'undefined' ? Bun.version : undefined,
|
|
126
127
|
};
|
|
127
128
|
return new Response(JSON.stringify(data), {
|
|
128
129
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
@@ -166,7 +167,7 @@ export class DashboardService {
|
|
|
166
167
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
167
168
|
});
|
|
168
169
|
}
|
|
169
|
-
const html =
|
|
170
|
+
const html = getRuntime().parser.renderMarkdown(body.content);
|
|
170
171
|
return new Response(JSON.stringify({ html }), {
|
|
171
172
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
172
173
|
});
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
DatabaseConfig,
|
|
4
4
|
DatabaseType,
|
|
5
5
|
} from './types';
|
|
6
|
+
import { getRuntime } from '../platform/runtime';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* 连接池中的连接项
|
|
@@ -175,19 +176,22 @@ export class ConnectionPool {
|
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
/**
|
|
178
|
-
* 创建 SQLite
|
|
179
|
+
* 创建 SQLite 连接(自动感知运行时)
|
|
179
180
|
*/
|
|
180
181
|
private async createSqliteConnection(
|
|
181
182
|
config: { path: string },
|
|
182
183
|
): Promise<unknown> {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
if (getRuntime().engine === 'bun') {
|
|
185
|
+
const { Database } = await import('bun:sqlite');
|
|
186
|
+
return new Database(config.path);
|
|
187
|
+
}
|
|
188
|
+
// Node.js:使用 better-sqlite3
|
|
189
|
+
const BetterSqlite3 = require('better-sqlite3') as typeof import('better-sqlite3');
|
|
190
|
+
return BetterSqlite3(config.path);
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
/**
|
|
190
|
-
* 创建 PostgreSQL
|
|
194
|
+
* 创建 PostgreSQL 连接(自动感知运行时)
|
|
191
195
|
*/
|
|
192
196
|
private async createPostgresConnection(
|
|
193
197
|
config: {
|
|
@@ -199,18 +203,18 @@ export class ConnectionPool {
|
|
|
199
203
|
ssl?: boolean;
|
|
200
204
|
},
|
|
201
205
|
): Promise<unknown> {
|
|
202
|
-
// 使用 Bun.SQL API
|
|
203
|
-
// Bun.SQL 支持 postgres:// URL
|
|
204
206
|
const url = `postgres://${config.user}:${config.password}@${config.host}:${config.port}/${config.database}`;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
max: 1,
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
if (getRuntime().engine === 'bun') {
|
|
208
|
+
const { SQL } = await import('bun');
|
|
209
|
+
return new SQL(url, { max: 1, tls: config.ssl ?? false });
|
|
210
|
+
}
|
|
211
|
+
// Node.js:使用 postgres 包
|
|
212
|
+
const postgres = require('postgres') as typeof import('postgres');
|
|
213
|
+
return postgres(url, { max: 1, ssl: config.ssl ? 'require' : false });
|
|
210
214
|
}
|
|
211
215
|
|
|
212
216
|
/**
|
|
213
|
-
* 创建 MySQL
|
|
217
|
+
* 创建 MySQL 连接(自动感知运行时)
|
|
214
218
|
*/
|
|
215
219
|
private async createMysqlConnection(
|
|
216
220
|
config: {
|
|
@@ -221,13 +225,21 @@ export class ConnectionPool {
|
|
|
221
225
|
password: string;
|
|
222
226
|
},
|
|
223
227
|
): Promise<unknown> {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
if (getRuntime().engine === 'bun') {
|
|
229
|
+
const url = `mysql://${config.user}:${config.password}@${config.host}:${config.port}/${config.database}`;
|
|
230
|
+
const { SQL } = await import('bun');
|
|
231
|
+
return new SQL(url, { max: 1 });
|
|
232
|
+
}
|
|
233
|
+
// Node.js:使用 mysql2 包
|
|
234
|
+
const mysql2 = require('mysql2/promise') as typeof import('mysql2/promise');
|
|
235
|
+
const conn = await mysql2.createConnection({
|
|
236
|
+
host: config.host,
|
|
237
|
+
port: config.port,
|
|
238
|
+
database: config.database,
|
|
239
|
+
user: config.user,
|
|
240
|
+
password: config.password,
|
|
230
241
|
});
|
|
242
|
+
return conn;
|
|
231
243
|
}
|
|
232
244
|
|
|
233
245
|
/**
|
|
@@ -185,7 +185,7 @@ export class DatabaseModule {
|
|
|
185
185
|
tenantId: selected.tenantId,
|
|
186
186
|
lazyReserve: async () => {
|
|
187
187
|
if (!reserved) {
|
|
188
|
-
reserved = await sql.reserve();
|
|
188
|
+
reserved = await (sql as any).reserve();
|
|
189
189
|
session.reserved = reserved;
|
|
190
190
|
}
|
|
191
191
|
return reserved;
|