@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.
Files changed (166) hide show
  1. package/README.md +81 -3
  2. package/dist/ai/providers/anthropic-provider.d.ts.map +1 -1
  3. package/dist/ai/providers/google-provider.d.ts.map +1 -1
  4. package/dist/ai/providers/ollama-provider.d.ts.map +1 -1
  5. package/dist/ai/providers/openai-provider.d.ts.map +1 -1
  6. package/dist/ai/service.d.ts.map +1 -1
  7. package/dist/ai/types.d.ts +5 -0
  8. package/dist/ai/types.d.ts.map +1 -1
  9. package/dist/auth/jwt.d.ts.map +1 -1
  10. package/dist/config/service.d.ts +0 -1
  11. package/dist/config/service.d.ts.map +1 -1
  12. package/dist/core/application.d.ts +30 -0
  13. package/dist/core/application.d.ts.map +1 -1
  14. package/dist/core/cluster.d.ts.map +1 -1
  15. package/dist/core/context.d.ts +5 -0
  16. package/dist/core/context.d.ts.map +1 -1
  17. package/dist/core/server.d.ts +29 -9
  18. package/dist/core/server.d.ts.map +1 -1
  19. package/dist/dashboard/controller.d.ts.map +1 -1
  20. package/dist/database/connection-pool.d.ts +3 -3
  21. package/dist/database/connection-pool.d.ts.map +1 -1
  22. package/dist/database/sql-manager.d.ts +8 -4
  23. package/dist/database/sql-manager.d.ts.map +1 -1
  24. package/dist/database/sqlite-adapter.d.ts +7 -3
  25. package/dist/database/sqlite-adapter.d.ts.map +1 -1
  26. package/dist/debug/recorder.d.ts +0 -1
  27. package/dist/debug/recorder.d.ts.map +1 -1
  28. package/dist/files/static-middleware.d.ts.map +1 -1
  29. package/dist/files/storage.d.ts.map +1 -1
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +40335 -3523
  33. package/dist/index.node.mjs +17689 -0
  34. package/dist/mcp/server.d.ts +5 -2
  35. package/dist/mcp/server.d.ts.map +1 -1
  36. package/dist/middleware/builtin/static-file.d.ts +4 -2
  37. package/dist/middleware/builtin/static-file.d.ts.map +1 -1
  38. package/dist/platform/bun/crypto.d.ts +3 -0
  39. package/dist/platform/bun/crypto.d.ts.map +1 -0
  40. package/dist/platform/bun/fs.d.ts +3 -0
  41. package/dist/platform/bun/fs.d.ts.map +1 -0
  42. package/dist/platform/bun/http.d.ts +15 -0
  43. package/dist/platform/bun/http.d.ts.map +1 -0
  44. package/dist/platform/bun/index.d.ts +3 -0
  45. package/dist/platform/bun/index.d.ts.map +1 -0
  46. package/dist/platform/bun/parser.d.ts +3 -0
  47. package/dist/platform/bun/parser.d.ts.map +1 -0
  48. package/dist/platform/bun/process.d.ts +3 -0
  49. package/dist/platform/bun/process.d.ts.map +1 -0
  50. package/dist/platform/detector.d.ts +9 -0
  51. package/dist/platform/detector.d.ts.map +1 -0
  52. package/dist/platform/index.d.ts +4 -0
  53. package/dist/platform/index.d.ts.map +1 -0
  54. package/dist/platform/node/crypto.d.ts +3 -0
  55. package/dist/platform/node/crypto.d.ts.map +1 -0
  56. package/dist/platform/node/fs.d.ts +3 -0
  57. package/dist/platform/node/fs.d.ts.map +1 -0
  58. package/dist/platform/node/http.d.ts +3 -0
  59. package/dist/platform/node/http.d.ts.map +1 -0
  60. package/dist/platform/node/index.d.ts +3 -0
  61. package/dist/platform/node/index.d.ts.map +1 -0
  62. package/dist/platform/node/parser.d.ts +3 -0
  63. package/dist/platform/node/parser.d.ts.map +1 -0
  64. package/dist/platform/node/process.d.ts +3 -0
  65. package/dist/platform/node/process.d.ts.map +1 -0
  66. package/dist/platform/runtime.d.ts +14 -0
  67. package/dist/platform/runtime.d.ts.map +1 -0
  68. package/dist/platform/types.d.ts +139 -0
  69. package/dist/platform/types.d.ts.map +1 -0
  70. package/dist/prompt/stores/file-store.d.ts.map +1 -1
  71. package/dist/rag/service.d.ts.map +1 -1
  72. package/dist/request/response.d.ts +3 -1
  73. package/dist/request/response.d.ts.map +1 -1
  74. package/dist/security/guards/execution-context.d.ts +2 -2
  75. package/dist/security/guards/execution-context.d.ts.map +1 -1
  76. package/dist/security/guards/types.d.ts +2 -2
  77. package/dist/security/guards/types.d.ts.map +1 -1
  78. package/dist/swagger/generator.d.ts.map +1 -1
  79. package/dist/websocket/registry.d.ts +4 -4
  80. package/dist/websocket/registry.d.ts.map +1 -1
  81. package/docs/deployment.md +31 -7
  82. package/docs/design/query-interceptor-design.md +381 -0
  83. package/docs/idle-timeout.md +101 -8
  84. package/docs/migration.md +43 -0
  85. package/docs/platform.md +299 -0
  86. package/docs/testing.md +60 -0
  87. package/docs/zh/deployment.md +30 -7
  88. package/docs/zh/idle-timeout.md +99 -6
  89. package/docs/zh/migration.md +42 -0
  90. package/docs/zh/platform.md +299 -0
  91. package/docs/zh/testing.md +60 -0
  92. package/package.json +24 -6
  93. package/src/ai/providers/anthropic-provider.ts +5 -2
  94. package/src/ai/providers/google-provider.ts +3 -0
  95. package/src/ai/providers/ollama-provider.ts +3 -0
  96. package/src/ai/providers/openai-provider.ts +5 -2
  97. package/src/ai/service.ts +17 -5
  98. package/src/ai/types.ts +5 -0
  99. package/src/auth/jwt.ts +4 -3
  100. package/src/config/service.ts +7 -6
  101. package/src/core/application.ts +38 -1
  102. package/src/core/cluster.ts +16 -14
  103. package/src/core/context.ts +7 -0
  104. package/src/core/server.ts +162 -46
  105. package/src/dashboard/controller.ts +3 -2
  106. package/src/database/connection-pool.ts +32 -20
  107. package/src/database/database-module.ts +1 -1
  108. package/src/database/db-proxy.ts +2 -2
  109. package/src/database/orm/transaction-manager.ts +1 -1
  110. package/src/database/sql-manager.ts +48 -13
  111. package/src/database/sqlite-adapter.ts +45 -12
  112. package/src/debug/recorder.ts +4 -3
  113. package/src/files/static-middleware.ts +3 -2
  114. package/src/files/storage.ts +2 -1
  115. package/src/index.ts +13 -0
  116. package/src/mcp/server.ts +6 -15
  117. package/src/middleware/builtin/static-file.ts +8 -5
  118. package/src/platform/bun/crypto.ts +30 -0
  119. package/src/platform/bun/fs.ts +52 -0
  120. package/src/platform/bun/http.ts +106 -0
  121. package/src/platform/bun/index.ts +17 -0
  122. package/src/platform/bun/parser.ts +19 -0
  123. package/src/platform/bun/process.ts +37 -0
  124. package/src/platform/detector.ts +36 -0
  125. package/src/platform/index.ts +20 -0
  126. package/src/platform/node/crypto.ts +40 -0
  127. package/src/platform/node/fs.ts +115 -0
  128. package/src/platform/node/http.ts +196 -0
  129. package/src/platform/node/index.ts +17 -0
  130. package/src/platform/node/parser.ts +34 -0
  131. package/src/platform/node/process.ts +51 -0
  132. package/src/platform/runtime.ts +50 -0
  133. package/src/platform/types.ts +150 -0
  134. package/src/prompt/stores/file-store.ts +6 -5
  135. package/src/rag/service.ts +2 -1
  136. package/src/request/response.ts +7 -4
  137. package/src/security/guards/execution-context.ts +4 -4
  138. package/src/security/guards/types.ts +2 -2
  139. package/src/swagger/generator.ts +2 -1
  140. package/src/websocket/registry.ts +6 -7
  141. package/tests/controller/path-combination.test.ts +196 -2
  142. package/tests/files/static-middleware.test.ts +5 -2
  143. package/tests/middleware/static-file.test.ts +5 -2
  144. package/tests/platform/bun/crypto.test.ts +8 -0
  145. package/tests/platform/bun/database.test.ts +8 -0
  146. package/tests/platform/bun/fs.test.ts +8 -0
  147. package/tests/platform/bun/parser.test.ts +8 -0
  148. package/tests/platform/bun/process.test.ts +8 -0
  149. package/tests/platform/bun/websocket.test.ts +8 -0
  150. package/tests/platform/detector.test.ts +57 -0
  151. package/tests/platform/node/build-smoke.test.ts +92 -0
  152. package/tests/platform/node/crypto.test.ts +9 -0
  153. package/tests/platform/node/database.test.ts +9 -0
  154. package/tests/platform/node/fs.test.ts +9 -0
  155. package/tests/platform/node/parser.test.ts +9 -0
  156. package/tests/platform/node/process.test.ts +9 -0
  157. package/tests/platform/node/websocket.test.ts +9 -0
  158. package/tests/platform/shared/crypto.cases.ts +49 -0
  159. package/tests/platform/shared/database.cases.ts +43 -0
  160. package/tests/platform/shared/fs.cases.ts +82 -0
  161. package/tests/platform/shared/parser.cases.ts +55 -0
  162. package/tests/platform/shared/process.cases.ts +26 -0
  163. package/tests/platform/shared/suite.ts +33 -0
  164. package/tests/platform/shared/websocket.cases.ts +61 -0
  165. package/tests/request/response.test.ts +5 -2
  166. 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 适配实现 |
@@ -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": "2.2.0",
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
- "@types/bun": "^1.3.10"
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>): Promise<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>): Promise<OpenAiChatCompletionResponse> {
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 = new Bun.CryptoHasher('sha256');
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 = new Bun.CryptoHasher('sha256');
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 = new Bun.CryptoHasher('sha256');
153
+ const outerHasher = getRuntime().crypto.createHasher('sha256');
153
154
  outerHasher.update(outerData);
154
155
 
155
156
  return new Uint8Array(outerHasher.digest());