@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
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# 平台适配指南
|
|
2
|
+
|
|
3
|
+
[English](../platform.md) | **中文**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Bun Server 通过内部的 Platform Adapter Layer,原生支持在 **Bun** 和 **Node.js 22+** 上运行。所有运行时相关的 API(HTTP 服务器、文件 I/O、加密、解析器、进程管理、WebSocket)均被抽象到统一的 TypeScript 接口后。你编写一套代码,框架在启动时自动选择正确的实现——无需额外配置。
|
|
8
|
+
|
|
9
|
+
## 目录
|
|
10
|
+
|
|
11
|
+
- [架构](#架构)
|
|
12
|
+
- [运行时检测](#运行时检测)
|
|
13
|
+
- [平台配置](#平台配置)
|
|
14
|
+
- [支持矩阵](#支持矩阵)
|
|
15
|
+
- [数据库自动适配](#数据库自动适配)
|
|
16
|
+
- [公开 API 变更](#公开-api-变更)
|
|
17
|
+
- [Bun 独有特性](#bun-独有特性)
|
|
18
|
+
- [Node.js 启动指南](#nodejs-启动指南)
|
|
19
|
+
- [多运行时测试](#多运行时测试)
|
|
20
|
+
- [已知限制](#已知限制)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 架构
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
┌──────────────────────────────────────────────────────┐
|
|
28
|
+
│ 应用层 │
|
|
29
|
+
│ Controllers / Services / Modules / Middleware │
|
|
30
|
+
└──────────────────────────┬───────────────────────────┘
|
|
31
|
+
│ getRuntime()
|
|
32
|
+
┌──────────────────────────▼───────────────────────────┐
|
|
33
|
+
│ Platform Adapter Layer │
|
|
34
|
+
│ IFsAdapter · ICryptoAdapter · IParserAdapter │
|
|
35
|
+
│ IProcessAdapter · IHttpDriver · IWebSocket │
|
|
36
|
+
└──────┬───────────────────────────────┬───────────────┘
|
|
37
|
+
│ │
|
|
38
|
+
┌──────▼──────┐ ┌──────▼──────┐
|
|
39
|
+
│ BunPlatform │ │ NodePlatform│
|
|
40
|
+
│ Bun.serve │ │ node:http │
|
|
41
|
+
│ Bun.file │ │ node:fs │
|
|
42
|
+
│ Bun.Crypto │ │ node:crypto │
|
|
43
|
+
│ spawn(bun) │ │ ws package │
|
|
44
|
+
└─────────────┘ └─────────────┘
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
每个适配器接口及其对应实现:
|
|
48
|
+
|
|
49
|
+
| 接口 | Bun 实现 | Node.js 实现 |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `IFsAdapter` | `Bun.file`、`Bun.write`、`Bun.Glob` | `node:fs/promises`、`node:fs` glob |
|
|
52
|
+
| `ICryptoAdapter` | `Bun.CryptoHasher` | `node:crypto` HMAC/hash |
|
|
53
|
+
| `IParserAdapter` | `Bun.JSONC`、`Bun.JSON5`、`Bun.JSONL`、`Bun.markdown` | `jsonc-parser`、`json5`、自定义 JSONL、`marked` |
|
|
54
|
+
| `IProcessAdapter` | `spawn`(bun)、`Bun.sleep` | `node:child_process`、`setTimeout` |
|
|
55
|
+
| `IHttpDriver` | `Bun.serve` | `node:http.createServer` |
|
|
56
|
+
| `IWebSocket<T>` | `Bun.ServerWebSocket<T>` | `ws` 包 |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 运行时检测
|
|
61
|
+
|
|
62
|
+
平台在 `Application` 构造时按以下优先级链解析:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
1. Bootstrap 配置 → new Application({ platform: 'node' })
|
|
66
|
+
2. CLI 参数 → --platform=node
|
|
67
|
+
3. 环境变量 → BUN_SERVER_PLATFORM=node
|
|
68
|
+
4. 自动检测 → typeof Bun !== 'undefined' ? 'bun' : 'node'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
一旦解析完成,单例在进程生命周期内保持不变。后续调用 `getRuntime()` 始终返回同一实例。
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 平台配置
|
|
76
|
+
|
|
77
|
+
### 方式一 — 代码配置(最高优先级)
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { Application } from '@dangao/bun-server';
|
|
81
|
+
|
|
82
|
+
const app = new Application({ platform: 'node' }); // 'bun' | 'node'
|
|
83
|
+
app.registerModule(AppModule);
|
|
84
|
+
await app.listen(3000);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 方式二 — CLI 参数
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Bun 运行时,但强制使用 Node.js 适配器
|
|
91
|
+
bun run src/main.ts --platform=node
|
|
92
|
+
|
|
93
|
+
# Node.js 运行时(无需参数,自动检测)
|
|
94
|
+
node dist/main.js
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 方式三 — 环境变量
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
BUN_SERVER_PLATFORM=node node dist/main.js
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 方式四 — 自动检测(默认,无需配置)
|
|
104
|
+
|
|
105
|
+
无需任何配置。如果 `typeof Bun !== 'undefined'`,选择 `BunPlatform`;否则自动选择 `NodePlatform`。
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 支持矩阵
|
|
110
|
+
|
|
111
|
+
| 特性 | Bun | Node.js 22+ |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| HTTP 服务器 | `Bun.serve`(原生) | `node:http` |
|
|
114
|
+
| WebSocket | `Bun.ServerWebSocket`(原生) | `ws` 包 |
|
|
115
|
+
| 文件 I/O | `Bun.file / Bun.write` | `node:fs/promises` |
|
|
116
|
+
| Crypto / JWT | `Bun.CryptoHasher` | `node:crypto` |
|
|
117
|
+
| JSONC 解析 | `Bun.JSONC` | `jsonc-parser` 包 |
|
|
118
|
+
| JSON5 解析 | `Bun.JSON5` | `json5` 包 |
|
|
119
|
+
| JSONL 解析 | `Bun.JSONL` | 自定义流式解析器 |
|
|
120
|
+
| Markdown 渲染 | `Bun.markdown` | `marked` 包 |
|
|
121
|
+
| Cluster spawn | Bun `spawn` | `node:child_process` |
|
|
122
|
+
| SQLite | `bun:sqlite` | `better-sqlite3` |
|
|
123
|
+
| PostgreSQL | `Bun.SQL` | `postgres` 包 |
|
|
124
|
+
| MySQL | `Bun.SQL` | `mysql2` 包 |
|
|
125
|
+
| `idleTimeout` | 支持 | 静默忽略 |
|
|
126
|
+
| `reusePort` | 支持 | 静默忽略 |
|
|
127
|
+
| SSE TCP keepalive | 支持(via `server.timeout`) | 不可用 |
|
|
128
|
+
| 整体性能 | 最优 | 良好 |
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 数据库自动适配
|
|
133
|
+
|
|
134
|
+
`DatabaseModule` 根据检测到的平台自动选择数据库驱动,**无需用户额外配置**。
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
Bun 平台 Node.js 平台
|
|
138
|
+
───────────────────────────── ─────────────────────────────
|
|
139
|
+
SQLite → bun:sqlite SQLite → better-sqlite3
|
|
140
|
+
PostgreSQL → Bun.SQL PostgreSQL → postgres 包
|
|
141
|
+
MySQL → Bun.SQL MySQL → mysql2 包
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
你的 `DatabaseModule` 配置在两个平台间完全一致:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
DatabaseModule.forRoot({
|
|
148
|
+
connections: [
|
|
149
|
+
{
|
|
150
|
+
name: 'default',
|
|
151
|
+
type: 'sqlite',
|
|
152
|
+
database: './data/app.db',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'pg',
|
|
156
|
+
type: 'postgres',
|
|
157
|
+
url: process.env.DATABASE_URL,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 公开 API 变更
|
|
166
|
+
|
|
167
|
+
### `BunServer.getServer()`
|
|
168
|
+
|
|
169
|
+
返回 `IServerHandle | undefined`(原来为 `Bun.Server | undefined`)。
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
const handle: IServerHandle | undefined = app.getServer();
|
|
173
|
+
handle?.port; // number
|
|
174
|
+
handle?.hostname; // string
|
|
175
|
+
await handle?.stop();
|
|
176
|
+
|
|
177
|
+
// 访问底层原生服务器实例(不推荐,类型为 unknown)
|
|
178
|
+
const native: unknown = app.getNativeServer();
|
|
179
|
+
// Bun: native as Bun.Server<any>
|
|
180
|
+
// Node.js: native as import('node:http').Server
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### `WsArgumentsHost.getClient()`
|
|
184
|
+
|
|
185
|
+
返回 `IWebSocket<T>` 而非 Bun 的 `ServerWebSocket<T>`。这是 WebSocket 公开 API 中的**唯一破坏性变更**。
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import type { IWebSocket } from '@dangao/bun-server';
|
|
189
|
+
|
|
190
|
+
@WebSocketGateway()
|
|
191
|
+
class ChatGateway {
|
|
192
|
+
@OnMessage('chat')
|
|
193
|
+
onChat(client: IWebSocket<unknown>, data: unknown) {
|
|
194
|
+
client.send('pong');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`IWebSocket<T>` 暴露与 `ServerWebSocket<T>` 相同的核心方法:
|
|
200
|
+
`send`、`close`、`data`、`readyState`、`remoteAddress`。
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Bun 独有特性
|
|
205
|
+
|
|
206
|
+
以下选项在 `ApplicationOptions` 中接受,但仅在 Bun 上生效,在 Node.js 上静默忽略。
|
|
207
|
+
|
|
208
|
+
| 选项 | Bun 效果 | Node.js |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| `idleTimeout` | 通过 `Bun.serve` 设置 TCP 空闲超时 | 忽略 |
|
|
211
|
+
| `reusePort` | 通过 `Bun.serve` 启用端口复用 | 忽略 |
|
|
212
|
+
| `sseKeepAlive` | 使用 `server.timeout(req, 0)` | 仅注入心跳 |
|
|
213
|
+
|
|
214
|
+
如果你的应用依赖这些特性,请在文档中明确说明对 Bun 运行时的要求。
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Node.js 启动指南
|
|
219
|
+
|
|
220
|
+
### 1. 安装
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
npm install @dangao/bun-server
|
|
224
|
+
# Node.js 所需的对等依赖会自动安装
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### 2. 构建
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# 使用 bun build 输出面向 Node.js 的 JS 文件
|
|
231
|
+
bun build src/main.ts --target=node --outdir=dist
|
|
232
|
+
|
|
233
|
+
# 或使用 tsc
|
|
234
|
+
npx tsc
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 3. 运行
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
node dist/main.js
|
|
241
|
+
# 平台自动检测为 'node',无需额外参数
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### 4. 冒烟测试(验证 bun build 输出能被 Node.js 原生运行)
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
bun run test:node
|
|
248
|
+
# 等价于:vitest run tests/platform/node
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 多运行时测试
|
|
254
|
+
|
|
255
|
+
共享测试用例位于 `tests/platform/shared/*.cases.ts`。Bun 和 Node.js 的测试运行器导入相同的断言,保证测试覆盖完全一致。
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
tests/platform/
|
|
259
|
+
├── shared/
|
|
260
|
+
│ ├── suite.ts ← TestSuite 接口(test / expect / beforeEach)
|
|
261
|
+
│ ├── fs.cases.ts
|
|
262
|
+
│ ├── crypto.cases.ts
|
|
263
|
+
│ ├── parser.cases.ts
|
|
264
|
+
│ ├── process.cases.ts
|
|
265
|
+
│ ├── websocket.cases.ts
|
|
266
|
+
│ └── database.cases.ts
|
|
267
|
+
├── bun/ ← bun:test 运行器
|
|
268
|
+
│ └── *.test.ts
|
|
269
|
+
├── node/ ← vitest 运行器
|
|
270
|
+
│ ├── *.test.ts
|
|
271
|
+
│ └── build-smoke.test.ts ← 验证 bun build --target=node 输出
|
|
272
|
+
└── detector.test.ts ← 优先级链单元测试
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### 执行测试
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
# Bun 平台测试
|
|
279
|
+
bun run test:bun
|
|
280
|
+
|
|
281
|
+
# Node.js 平台测试
|
|
282
|
+
bun run test:node
|
|
283
|
+
|
|
284
|
+
# 两个平台全部测试
|
|
285
|
+
bun run test:platform
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 已知限制
|
|
291
|
+
|
|
292
|
+
| 限制 | 说明 |
|
|
293
|
+
|---|---|
|
|
294
|
+
| `idleTimeout` / `reusePort` | Bun 独有,Node.js 无对等实现 |
|
|
295
|
+
| SSE TCP keepalive(`server.timeout`) | Bun 独有 API;Node.js 仅支持心跳注入 |
|
|
296
|
+
| `Bun.SQL` 高级特性 | 部分 Bun.SQL 选项(如预处理语句缓存)在 `postgres`/`mysql2` 中不可用 |
|
|
297
|
+
| Node.js 上的 Cluster 模式 | 使用 `node:child_process`,行为可能与 Bun cluster 存在差异 |
|
|
298
|
+
| `bun:sqlite` 扩展 | Bun SQLite 支持扩展加载;`better-sqlite3` 的扩展机制不同 |
|
|
299
|
+
| 整体性能 | Bun 原生 API 在文件 I/O 和加密等方面性能优于 Node.js 适配实现 |
|
package/docs/zh/testing.md
CHANGED
|
@@ -61,6 +61,66 @@ const client = await module.createHttpClient();
|
|
|
61
61
|
|
|
62
62
|
`options` 支持 `headers`、`body`、`query`。响应对象包含 `status`、`headers`、`body`、`text`、`ok`。
|
|
63
63
|
|
|
64
|
+
## 多运行时测试
|
|
65
|
+
|
|
66
|
+
框架内置的共享测试策略,能在 Bun 和 Node.js 上运行完全相同的测试用例。
|
|
67
|
+
|
|
68
|
+
### 测试目录结构
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
tests/platform/
|
|
72
|
+
├── shared/ ← 平台无关的断言辅助函数
|
|
73
|
+
│ ├── suite.ts ← TestSuite 接口(test / expect / beforeEach)
|
|
74
|
+
│ ├── fs.cases.ts
|
|
75
|
+
│ ├── crypto.cases.ts
|
|
76
|
+
│ ├── parser.cases.ts
|
|
77
|
+
│ ├── process.cases.ts
|
|
78
|
+
│ ├── websocket.cases.ts
|
|
79
|
+
│ └── database.cases.ts
|
|
80
|
+
├── bun/ ← bun:test 运行器(initRuntime('bun'))
|
|
81
|
+
│ └── *.test.ts
|
|
82
|
+
└── node/ ← vitest 运行器(initRuntime('node'))
|
|
83
|
+
├── *.test.ts
|
|
84
|
+
└── build-smoke.test.ts
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 执行平台测试
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# 仅 Bun 平台测试
|
|
91
|
+
bun run test:bun
|
|
92
|
+
|
|
93
|
+
# 仅 Node.js 平台测试(使用 vitest)
|
|
94
|
+
bun run test:node
|
|
95
|
+
|
|
96
|
+
# 两个平台全部测试
|
|
97
|
+
bun run test:platform
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 编写跨平台测试
|
|
101
|
+
|
|
102
|
+
在测试文件开头调用 `initRuntime()`:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// tests/platform/bun/fs.test.ts
|
|
106
|
+
import { test, expect, beforeEach } from 'bun:test';
|
|
107
|
+
import { initRuntime, _resetRuntime } from '../../../src/platform/runtime';
|
|
108
|
+
import { runFsCases } from '../shared/fs.cases';
|
|
109
|
+
|
|
110
|
+
initRuntime('bun');
|
|
111
|
+
runFsCases({ test, expect, beforeEach });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// tests/platform/node/fs.test.ts
|
|
116
|
+
import { test, expect, beforeEach } from 'vitest';
|
|
117
|
+
import { initRuntime, _resetRuntime } from '../../../src/platform/runtime';
|
|
118
|
+
import { runFsCases } from '../shared/fs.cases';
|
|
119
|
+
|
|
120
|
+
beforeEach(() => { _resetRuntime(); initRuntime('node'); });
|
|
121
|
+
runFsCases({ test, expect, beforeEach });
|
|
122
|
+
```
|
|
123
|
+
|
|
64
124
|
## 配合 bun:test 使用
|
|
65
125
|
|
|
66
126
|
```ts
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dangao/bun-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
|
+
"node": "./dist/index.node.mjs",
|
|
9
10
|
"types": "./dist/index.d.ts",
|
|
10
11
|
"import": "./dist/index.js",
|
|
11
12
|
"default": "./dist/index.js"
|
|
@@ -38,27 +39,44 @@
|
|
|
38
39
|
"url": "https://github.com/dangaogit/bun-server/issues"
|
|
39
40
|
},
|
|
40
41
|
"engines": {
|
|
41
|
-
"bun": ">=1.3.10"
|
|
42
|
+
"bun": ">=1.3.10",
|
|
43
|
+
"node": ">=22.0.0"
|
|
42
44
|
},
|
|
43
45
|
"scripts": {
|
|
44
46
|
"dev": "bun --watch src/index.ts",
|
|
45
47
|
"test": "bun test",
|
|
48
|
+
"test:bun": "bun test tests/platform/bun",
|
|
49
|
+
"test:node": "vitest run tests/platform/node",
|
|
50
|
+
"test:platform": "bun run test:bun && bun run test:node",
|
|
46
51
|
"type-check": "tsc --noEmit",
|
|
47
52
|
"clean": "rm -rf dist",
|
|
48
53
|
"bundle": "bun build src/index.ts --target=bun --format=esm --outfile=dist/index.js --external reflect-metadata --external @dangao/logsmith --external @dangao/nacos-client",
|
|
54
|
+
"bundle:node": "bun build src/index.ts --target=node --packages=external --outfile=dist/index.node.mjs",
|
|
49
55
|
"dts": "tsc -p tsconfig.build.json",
|
|
50
|
-
"build": "bun run clean && bun run bundle && bun run dts",
|
|
56
|
+
"build": "bun run clean && bun run bundle && bun run bundle:node && bun run dts",
|
|
51
57
|
"prepublishOnly": "bun run build",
|
|
52
58
|
"publish:package": "cp -r ../../README.md ../../LICENSE ../../docs . && bun publish --access public && rm -rf ./README.md ./LICENSE ./docs",
|
|
53
59
|
"publish:package:github": "cp -r ../../README.md ../../LICENSE ../../docs . && bun pm pack --access public && npm publish *.tgz --provenance --access public && rm -rf ./README.md ./LICENSE ./docs"
|
|
54
60
|
},
|
|
55
61
|
"devDependencies": {
|
|
62
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
63
|
+
"@types/bun": "^1.3.10",
|
|
64
|
+
"@types/mime-types": "^3.0.1",
|
|
65
|
+
"@types/ws": "^8.18.1",
|
|
56
66
|
"typescript": "^5.9.3",
|
|
57
|
-
"
|
|
67
|
+
"vitest": "^4.1.4"
|
|
58
68
|
},
|
|
59
69
|
"dependencies": {
|
|
60
|
-
"reflect-metadata": "^0.2.2",
|
|
61
70
|
"@dangao/logsmith": "0.2.0",
|
|
62
|
-
"@dangao/nacos-client": "0.1.1"
|
|
71
|
+
"@dangao/nacos-client": "0.1.1",
|
|
72
|
+
"better-sqlite3": "^12.8.0",
|
|
73
|
+
"json5": "^2.2.3",
|
|
74
|
+
"jsonc-parser": "^3.3.1",
|
|
75
|
+
"marked": "^18.0.0",
|
|
76
|
+
"mime-types": "^3.0.2",
|
|
77
|
+
"mysql2": "^3.21.0",
|
|
78
|
+
"postgres": "^3.4.9",
|
|
79
|
+
"reflect-metadata": "^0.2.2",
|
|
80
|
+
"ws": "^8.20.0"
|
|
63
81
|
}
|
|
64
82
|
}
|
|
@@ -56,7 +56,7 @@ export class AnthropicProvider implements LlmProvider {
|
|
|
56
56
|
}));
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const response = await this.post('/v1/messages', body);
|
|
59
|
+
const response = await this.post('/v1/messages', body, request.signal);
|
|
60
60
|
const usage = (response['usage'] as { input_tokens: number; output_tokens: number }) ?? { input_tokens: 0, output_tokens: 0 };
|
|
61
61
|
|
|
62
62
|
let content = '';
|
|
@@ -108,6 +108,7 @@ export class AnthropicProvider implements LlmProvider {
|
|
|
108
108
|
const baseUrl = this.baseUrl;
|
|
109
109
|
const anthropicVersion = this.anthropicVersion;
|
|
110
110
|
const encoder = new TextEncoder();
|
|
111
|
+
const signal = request.signal;
|
|
111
112
|
|
|
112
113
|
return new ReadableStream<Uint8Array>({
|
|
113
114
|
async start(controller) {
|
|
@@ -120,6 +121,7 @@ export class AnthropicProvider implements LlmProvider {
|
|
|
120
121
|
'anthropic-version': anthropicVersion,
|
|
121
122
|
},
|
|
122
123
|
body: JSON.stringify(body),
|
|
124
|
+
signal,
|
|
123
125
|
});
|
|
124
126
|
|
|
125
127
|
if (!res.ok || !res.body) {
|
|
@@ -170,7 +172,7 @@ export class AnthropicProvider implements LlmProvider {
|
|
|
170
172
|
return Math.ceil(messages.reduce((sum, m) => sum + m.content.length, 0) / 4);
|
|
171
173
|
}
|
|
172
174
|
|
|
173
|
-
private async post(path: string, body: Record<string, unknown
|
|
175
|
+
private async post(path: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<Record<string, unknown>> {
|
|
174
176
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
175
177
|
method: 'POST',
|
|
176
178
|
headers: {
|
|
@@ -179,6 +181,7 @@ export class AnthropicProvider implements LlmProvider {
|
|
|
179
181
|
'anthropic-version': this.anthropicVersion,
|
|
180
182
|
},
|
|
181
183
|
body: JSON.stringify(body),
|
|
184
|
+
signal,
|
|
182
185
|
});
|
|
183
186
|
|
|
184
187
|
if (res.status === 429) throw new AiRateLimitError(this.name);
|
|
@@ -50,6 +50,7 @@ export class GoogleProvider implements LlmProvider {
|
|
|
50
50
|
method: 'POST',
|
|
51
51
|
headers: { 'Content-Type': 'application/json' },
|
|
52
52
|
body: JSON.stringify(body),
|
|
53
|
+
signal: request.signal,
|
|
53
54
|
},
|
|
54
55
|
);
|
|
55
56
|
|
|
@@ -95,6 +96,7 @@ export class GoogleProvider implements LlmProvider {
|
|
|
95
96
|
const apiKey = this.apiKey;
|
|
96
97
|
const baseUrl = this.baseUrl;
|
|
97
98
|
const encoder = new TextEncoder();
|
|
99
|
+
const signal = request.signal;
|
|
98
100
|
|
|
99
101
|
const body: Record<string, unknown> = {
|
|
100
102
|
contents,
|
|
@@ -111,6 +113,7 @@ export class GoogleProvider implements LlmProvider {
|
|
|
111
113
|
method: 'POST',
|
|
112
114
|
headers: { 'Content-Type': 'application/json' },
|
|
113
115
|
body: JSON.stringify(body),
|
|
116
|
+
signal,
|
|
114
117
|
},
|
|
115
118
|
);
|
|
116
119
|
|
|
@@ -33,6 +33,7 @@ export class OllamaProvider implements LlmProvider {
|
|
|
33
33
|
num_predict: request.maxTokens,
|
|
34
34
|
},
|
|
35
35
|
}),
|
|
36
|
+
signal: request.signal,
|
|
36
37
|
});
|
|
37
38
|
|
|
38
39
|
if (!res.ok) {
|
|
@@ -61,6 +62,7 @@ export class OllamaProvider implements LlmProvider {
|
|
|
61
62
|
const model = request.model ?? this.defaultModel;
|
|
62
63
|
const baseUrl = this.baseUrl;
|
|
63
64
|
const encoder = new TextEncoder();
|
|
65
|
+
const signal = request.signal;
|
|
64
66
|
|
|
65
67
|
return new ReadableStream<Uint8Array>({
|
|
66
68
|
async start(controller) {
|
|
@@ -77,6 +79,7 @@ export class OllamaProvider implements LlmProvider {
|
|
|
77
79
|
num_predict: request.maxTokens,
|
|
78
80
|
},
|
|
79
81
|
}),
|
|
82
|
+
signal,
|
|
80
83
|
});
|
|
81
84
|
|
|
82
85
|
if (!res.ok || !res.body) {
|
|
@@ -79,7 +79,7 @@ export class OpenAIProvider implements LlmProvider {
|
|
|
79
79
|
}));
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const response = await this.post('/chat/completions', body);
|
|
82
|
+
const response = await this.post('/chat/completions', body, request.signal);
|
|
83
83
|
const choice = response.choices?.[0];
|
|
84
84
|
const usage = response.usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
85
85
|
const message = choice?.message;
|
|
@@ -127,6 +127,7 @@ export class OpenAIProvider implements LlmProvider {
|
|
|
127
127
|
const encoder = new TextEncoder();
|
|
128
128
|
const apiKey = this.apiKey;
|
|
129
129
|
const baseUrl = this.baseUrl;
|
|
130
|
+
const signal = request.signal;
|
|
130
131
|
|
|
131
132
|
return new ReadableStream<Uint8Array>({
|
|
132
133
|
async start(controller) {
|
|
@@ -138,6 +139,7 @@ export class OpenAIProvider implements LlmProvider {
|
|
|
138
139
|
'Authorization': `Bearer ${apiKey}`,
|
|
139
140
|
},
|
|
140
141
|
body: JSON.stringify(body),
|
|
142
|
+
signal,
|
|
141
143
|
});
|
|
142
144
|
|
|
143
145
|
if (!res.ok || !res.body) {
|
|
@@ -191,7 +193,7 @@ export class OpenAIProvider implements LlmProvider {
|
|
|
191
193
|
return Math.ceil(messages.reduce((sum, m) => sum + m.content.length, 0) / 4);
|
|
192
194
|
}
|
|
193
195
|
|
|
194
|
-
private async post(path: string, body: Record<string, unknown
|
|
196
|
+
private async post(path: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<OpenAiChatCompletionResponse> {
|
|
195
197
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
196
198
|
method: 'POST',
|
|
197
199
|
headers: {
|
|
@@ -199,6 +201,7 @@ export class OpenAIProvider implements LlmProvider {
|
|
|
199
201
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
200
202
|
},
|
|
201
203
|
body: JSON.stringify(body),
|
|
204
|
+
signal,
|
|
202
205
|
});
|
|
203
206
|
|
|
204
207
|
if (res.status === 429) {
|
package/src/ai/service.ts
CHANGED
|
@@ -120,7 +120,7 @@ export class AiService {
|
|
|
120
120
|
const timeout = this.options.timeout ?? 30000;
|
|
121
121
|
|
|
122
122
|
if (!fallback) {
|
|
123
|
-
return this.withTimeout(this.getProvider(targetName).complete(request), timeout, targetName);
|
|
123
|
+
return this.withTimeout(this.getProvider(targetName).complete(request), timeout, targetName, request.signal);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
// Fallback chain: try target first, then others in order
|
|
@@ -134,7 +134,7 @@ export class AiService {
|
|
|
134
134
|
try {
|
|
135
135
|
const provider = this.providers.get(name);
|
|
136
136
|
if (!provider) continue;
|
|
137
|
-
return await this.withTimeout(provider.complete({ ...request, provider: name }), timeout, name);
|
|
137
|
+
return await this.withTimeout(provider.complete({ ...request, provider: name }), timeout, name, request.signal);
|
|
138
138
|
} catch (err) {
|
|
139
139
|
errors.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
140
140
|
}
|
|
@@ -143,12 +143,24 @@ export class AiService {
|
|
|
143
143
|
throw new AiAllProvidersFailed(errors);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
private withTimeout<T>(promise: Promise<T>, ms: number, providerName: string): Promise<T> {
|
|
146
|
+
private withTimeout<T>(promise: Promise<T>, ms: number, providerName: string, signal?: AbortSignal): Promise<T> {
|
|
147
147
|
return new Promise<T>((resolve, reject) => {
|
|
148
|
+
if (signal?.aborted) {
|
|
149
|
+
reject(signal.reason ?? new Error('Aborted'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
148
153
|
const timer = setTimeout(() => reject(new AiTimeoutError(providerName, ms)), ms);
|
|
154
|
+
|
|
155
|
+
const onAbort = () => {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
reject(signal!.reason ?? new Error('Aborted'));
|
|
158
|
+
};
|
|
159
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
160
|
+
|
|
149
161
|
promise.then(
|
|
150
|
-
(val) => { clearTimeout(timer); resolve(val); },
|
|
151
|
-
(err) => { clearTimeout(timer); reject(err); },
|
|
162
|
+
(val) => { clearTimeout(timer); signal?.removeEventListener('abort', onAbort); resolve(val); },
|
|
163
|
+
(err) => { clearTimeout(timer); signal?.removeEventListener('abort', onAbort); reject(err); },
|
|
152
164
|
);
|
|
153
165
|
});
|
|
154
166
|
}
|
package/src/ai/types.ts
CHANGED
|
@@ -45,6 +45,11 @@ export interface AiRequest {
|
|
|
45
45
|
tools?: AiToolDefinition[];
|
|
46
46
|
/** Provider name override */
|
|
47
47
|
provider?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Abort signal — pass `ctx.signal` to cascade client disconnection
|
|
50
|
+
* to the upstream AI API, stopping token consumption immediately.
|
|
51
|
+
*/
|
|
52
|
+
signal?: AbortSignal;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
/**
|
package/src/auth/jwt.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { JWTConfig, JWTPayload } from './types';
|
|
2
|
+
import { getRuntime } from '../platform/runtime';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* JWT 工具类
|
|
@@ -120,7 +121,7 @@ export class JWTUtil {
|
|
|
120
121
|
|
|
121
122
|
if (key.length > blockSize) {
|
|
122
123
|
// 如果密钥长度超过块大小,先哈希
|
|
123
|
-
const hasher =
|
|
124
|
+
const hasher = getRuntime().crypto.createHasher('sha256');
|
|
124
125
|
hasher.update(key);
|
|
125
126
|
keyBuffer = new Uint8Array(hasher.digest());
|
|
126
127
|
} else {
|
|
@@ -141,7 +142,7 @@ export class JWTUtil {
|
|
|
141
142
|
const innerData = new Uint8Array(iKeyPad.length + data.length);
|
|
142
143
|
innerData.set(iKeyPad);
|
|
143
144
|
innerData.set(data, iKeyPad.length);
|
|
144
|
-
const innerHasher =
|
|
145
|
+
const innerHasher = getRuntime().crypto.createHasher('sha256');
|
|
145
146
|
innerHasher.update(innerData);
|
|
146
147
|
const innerHash = new Uint8Array(innerHasher.digest());
|
|
147
148
|
|
|
@@ -149,7 +150,7 @@ export class JWTUtil {
|
|
|
149
150
|
const outerData = new Uint8Array(oKeyPad.length + innerHash.length);
|
|
150
151
|
outerData.set(oKeyPad);
|
|
151
152
|
outerData.set(innerHash, oKeyPad.length);
|
|
152
|
-
const outerHasher =
|
|
153
|
+
const outerHasher = getRuntime().crypto.createHasher('sha256');
|
|
153
154
|
outerHasher.update(outerData);
|
|
154
155
|
|
|
155
156
|
return new Uint8Array(outerHasher.digest());
|