@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,381 @@
1
+ # ORM 查询拦截器设计方案
2
+
3
+ > 日期:2026-03-17
4
+ > 状态:方案评估阶段
5
+
6
+ ## 一、现状分析
7
+
8
+ ### 1.1 当前 SQL 执行链路
9
+
10
+ ```
11
+ Repository.findAll() / create() / update() / delete()
12
+ → 手动拼接 SQL 字符串
13
+ → BaseRepository.executeQuery(sql, params)
14
+ → DatabaseService.query(sql, params)
15
+ → getCurrentSession() 获取连接
16
+ → 直接执行 SQL(无任何拦截点)
17
+ ```
18
+
19
+ 三条独立的 SQL 执行路径:
20
+
21
+ | 路径 | 入口 | 使用者 |
22
+ |---|---|---|
23
+ | 路径 A | `BaseRepository.executeQuery()` → `DatabaseService.query()` | Repository 模式用户 |
24
+ | 路径 B | `DatabaseService.query()` 直接调用 | Service 层手写 SQL 用户 |
25
+ | 路径 C | `` db`SELECT ...` `` → `BunSQLManager` | db proxy 用户 |
26
+
27
+ ### 1.2 关键代码位置
28
+
29
+ | 组件 | 文件路径 |
30
+ |---|---|
31
+ | DatabaseService | `src/database/service.ts` |
32
+ | BaseRepository | `src/database/orm/repository.ts` |
33
+ | DrizzleBaseRepository | `src/database/orm/drizzle-repository.ts` |
34
+ | db proxy | `src/database/db-proxy.ts` |
35
+ | DatabaseModule | `src/database/database-module.ts` |
36
+ | OrmService | `src/database/orm/service.ts` |
37
+ | Entity/Column 装饰器 | `src/database/orm/decorators.ts` |
38
+ | Repository 装饰器 | `src/database/orm/repository-decorator.ts` |
39
+ | TransactionInterceptor | `src/database/orm/transaction-interceptor.ts` |
40
+ | TransactionManager | `src/database/orm/transaction-manager.ts` |
41
+ | BunSQLManager | `src/database/sql-manager.ts` |
42
+ | SqliteAdapter/Manager | `src/database/sqlite-adapter.ts` |
43
+ | DatabaseSession 上下文 | `src/database/database-context.ts` |
44
+
45
+ ### 1.3 缺失的扩展点
46
+
47
+ | 缺失能力 | 说明 |
48
+ |---|---|
49
+ | SQL 拦截器链 | `DatabaseService.query()` 前后无 `beforeQuery` / `afterQuery` 钩子 |
50
+ | Repository 层钩子 | `BaseRepository.executeQuery()` 直接调用 service,无法注入自定义逻辑 |
51
+ | 查询构建管道 | SQL 全部手写字符串拼接,无 QueryBuilder 或 AST 抽象层 |
52
+ | 插件注册机制 | ORM 层无插件 API,无法通过 `DatabaseModule.forRoot` 注册拦截器 |
53
+
54
+ ### 1.4 Drizzle 集成现状
55
+
56
+ Drizzle 在当前项目中**几乎没有实际集成**:
57
+
58
+ - `DrizzleBaseRepository` 全是 abstract 方法,零实现
59
+ - `OrmModuleOptions.drizzle` 类型是 `unknown`
60
+ - `package.json` 没有 drizzle 依赖
61
+ - `OrmService` 只是一个 drizzle 实例的容器,无查询逻辑
62
+
63
+ 结论:**"与 Drizzle 重叠"是伪问题**,不存在真正需要兼容的 Drizzle 代码。
64
+
65
+ ### 1.5 多租户现状
66
+
67
+ 当前多租户策略是**连接级隔离**(每个租户一个独立数据库连接),不支持**行级隔离**(同库通过 `WHERE tenant_id = ?` 过滤)。
68
+
69
+ ---
70
+
71
+ ## 二、初筛方案(已淘汰)
72
+
73
+ ### 方案 X1:DatabaseService.query() 拦截器链
74
+
75
+ 在 `DatabaseService.query()` 中引入 `QueryInterceptor` 链。
76
+
77
+ - 覆盖路径 A + B,不覆盖路径 C(db proxy)
78
+ - 改动量小(~50 行)
79
+ - SQL 是原始字符串,复杂改写不安全
80
+ - 同步/异步混合签名增加复杂度
81
+
82
+ **淘汰原因**:字符串级操作不安全,路径 C 未覆盖。
83
+
84
+ ### 方案 X2:BaseRepository.executeQuery() 钩子
85
+
86
+ 在 `BaseRepository` 中暴露 `beforeExecute` / `afterExecute` 钩子。
87
+
88
+ - 仅覆盖路径 A
89
+ - 改动量最小(~40 行)
90
+ - SQL 仍是字符串
91
+ - 多 Repository 需逐个继承
92
+
93
+ **淘汰原因**:覆盖面最窄,仅 Repository 模式受益。
94
+
95
+ ---
96
+
97
+ ## 三、候选方案(升级方案,不考虑向下兼容)
98
+
99
+ ### 方案 A:Kysely 接管查询层(推荐)
100
+
101
+ #### 核心思路
102
+
103
+ 引入 Kysely 作为查询引擎,利用其原生 Plugin 系统实现拦截器。
104
+
105
+ #### 解决 db proxy 兼容
106
+
107
+ **路径 1 — 替代**:Kysely 自带 `sql` 模板标签,功能等价于当前 db proxy:
108
+
109
+ ```typescript
110
+ // 旧 API
111
+ const users = await db`SELECT * FROM users WHERE id = ${id}`;
112
+
113
+ // 新 API(Kysely raw SQL)
114
+ const users = await db.execute(sql`SELECT * FROM users WHERE id = ${id}`);
115
+
116
+ // 新 API(QueryBuilder)
117
+ const users = await db.selectFrom('users').where('id', '=', id).execute();
118
+ ```
119
+
120
+ **路径 2 — 融合**:保留 `db` 的模板字符串调用风格,底层替换为 Kysely 实例执行:
121
+
122
+ ```typescript
123
+ // 用户代码不变
124
+ const users = await db`SELECT * FROM users`;
125
+
126
+ // 内部: db proxy → Kysely.executeRaw(sql, params) → Plugin chain → 执行
127
+ ```
128
+
129
+ #### 解决 Drizzle 重叠
130
+
131
+ - 移除 `DrizzleBaseRepository`(空壳,无实际用户)
132
+ - 移除 `OrmService` 的 drizzle 相关代码
133
+ - Kysely 完全覆盖查询构建能力
134
+ - 用户如需 schema migration,可独立使用 Drizzle Kit CLI
135
+
136
+ #### 拦截器实现
137
+
138
+ Kysely 原生 `KyselyPlugin` 接口:
139
+
140
+ ```typescript
141
+ interface KyselyPlugin {
142
+ transformQuery(args: PluginTransformQueryArgs): RootOperationNode;
143
+ transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
144
+ }
145
+ ```
146
+
147
+ 多租户拦截器示例:
148
+
149
+ ```typescript
150
+ class TenantPlugin implements KyselyPlugin {
151
+ transformQuery({ node }): RootOperationNode {
152
+ // 在 AST 层面往所有 SELECT/UPDATE/DELETE 追加 WHERE tenant_id = ?
153
+ // 在 INSERT 中自动注入 tenant_id 列
154
+ return addTenantFilter(node, getCurrentTenantId());
155
+ }
156
+ transformResult({ result }) { return result; }
157
+ }
158
+ ```
159
+
160
+ 操作的是 AST 节点而非 SQL 字符串,安全可靠。
161
+
162
+ #### 需要的额外工作
163
+
164
+ - 编写 `BunSQLDialect`(适配 `Bun.SQL`)
165
+ - 编写 `BunSQLiteDialect`(适配 `bun:sqlite`)
166
+ - 重写 `BaseRepository` 使用 Kysely 查询
167
+
168
+ #### 评估
169
+
170
+ | 维度 | 评价 |
171
+ |---|---|
172
+ | 拦截器能力 | **最强** — 原生 AST 级插件,非字符串操作 |
173
+ | 开发量 | 中等(~3 天) |
174
+ | 新依赖 | 1 个(kysely,轻量) |
175
+ | 类型安全 | 强 — Kysely 天生类型安全 |
176
+ | 风险 | 低 — Kysely 成熟稳定,社区活跃 |
177
+
178
+ ---
179
+
180
+ ### 方案 B:Drizzle 深度集成
181
+
182
+ #### 核心思路
183
+
184
+ 将 Drizzle 从 `unknown` 占位符升级为核心引擎,自建拦截层包裹 Drizzle 查询执行。
185
+
186
+ #### 解决 db proxy 兼容
187
+
188
+ `db` proxy 重定义为 Drizzle 实例的包装:
189
+
190
+ ```typescript
191
+ // Drizzle QueryBuilder 风格
192
+ const users = await db.select().from(usersTable).where(eq(usersTable.id, 1));
193
+
194
+ // 原始 SQL 风格
195
+ const users = await db.execute(sql`SELECT * FROM users WHERE id = ${id}`);
196
+ ```
197
+
198
+ #### 解决 Drizzle 重叠
199
+
200
+ 不存在重叠 — Drizzle 就是唯一的查询层。`@Entity` / `@Column` 可保留为装饰器语法糖,也可废弃转向 Drizzle 原生 schema。
201
+
202
+ #### 拦截器实现
203
+
204
+ Drizzle 无原生插件系统,有两个技术路线:
205
+
206
+ **路线 1 — 自定义 Driver**:
207
+
208
+ ```typescript
209
+ class InterceptableDriver implements Driver {
210
+ async execute(query: Query): Promise<Result> {
211
+ let { sql, params } = query;
212
+ for (const interceptor of this.interceptors) {
213
+ ({ sql, params } = interceptor.beforeQuery(sql, params, context));
214
+ }
215
+ return this.realDriver.execute({ sql, params });
216
+ }
217
+ }
218
+ ```
219
+
220
+ **路线 2 — Proxy 包裹 Drizzle 实例**:用 Proxy 拦截所有查询方法。
221
+
222
+ 两种路线的问题:Drizzle 在 Driver 层已编译为 SQL 字符串,拦截器仍然是字符串级操作。
223
+
224
+ #### 评估
225
+
226
+ | 维度 | 评价 |
227
+ |---|---|
228
+ | 拦截器能力 | 中等 — Driver 层拦截是字符串级别,非 AST |
229
+ | 开发量 | 中大(~4 天) |
230
+ | 新依赖 | 1 个(drizzle-orm,较重) |
231
+ | 类型安全 | 最强 — Drizzle 类型推导业界领先 |
232
+ | 风险 | 中 — 拦截器实现依赖 Drizzle 内部结构 |
233
+ | 额外收益 | migration / studio / schema push 生态 |
234
+
235
+ ---
236
+
237
+ ### 方案 C:零依赖自研查询协议
238
+
239
+ #### 核心思路
240
+
241
+ 定义框架自有的 `QueryIntent` 结构化查询描述,拦截器在对象层面修改查询,最终编译为 SQL。
242
+
243
+ #### 核心设计
244
+
245
+ ```typescript
246
+ interface QueryIntent {
247
+ type: 'select' | 'insert' | 'update' | 'delete';
248
+ table: string;
249
+ columns?: string[];
250
+ where?: WhereClause[];
251
+ joins?: JoinClause[];
252
+ values?: Record<string, unknown>;
253
+ orderBy?: OrderByClause[];
254
+ limit?: number;
255
+ offset?: number;
256
+ raw?: { sql: string; params: unknown[] }; // 原始 SQL 逃生舱
257
+ }
258
+
259
+ interface QueryInterceptor {
260
+ intercept(intent: QueryIntent, context: QueryContext): QueryIntent;
261
+ }
262
+ ```
263
+
264
+ #### 解决 db proxy 兼容
265
+
266
+ `db` proxy 模板字符串调用产出 `raw` 类型的 QueryIntent:
267
+
268
+ ```typescript
269
+ // 用户写法不变
270
+ const users = await db`SELECT * FROM users WHERE id = ${id}`;
271
+
272
+ // 内部流程:
273
+ // 1. 解析模板字符串 → QueryIntent { raw: { sql, params } }
274
+ // 2. 过拦截器链
275
+ // 3. 编译执行
276
+ ```
277
+
278
+ 同时提供结构化 API:
279
+
280
+ ```typescript
281
+ const users = await db.from('users').where('id', '=', id).select();
282
+ ```
283
+
284
+ #### 解决 Drizzle 重叠
285
+
286
+ 完全移除 Drizzle 相关代码,框架自主可控。
287
+
288
+ #### 拦截器实现
289
+
290
+ ```typescript
291
+ class TenantInterceptor implements QueryInterceptor {
292
+ intercept(intent: QueryIntent, ctx: QueryContext): QueryIntent {
293
+ if (intent.raw) {
294
+ return { ...intent, raw: appendTenantFilter(intent.raw, ctx.tenantId) };
295
+ }
296
+ return {
297
+ ...intent,
298
+ where: [...(intent.where ?? []), { column: 'tenant_id', op: '=', value: ctx.tenantId }],
299
+ };
300
+ }
301
+ }
302
+ ```
303
+
304
+ #### 评估
305
+
306
+ | 维度 | 评价 |
307
+ |---|---|
308
+ | 拦截器能力 | 高 — 结构化查询是 AST 级;raw SQL 退化为字符串级 |
309
+ | 开发量 | 大(~5-7 天) |
310
+ | 新依赖 | 0 |
311
+ | 类型安全 | 中 — 需额外投入 |
312
+ | 风险 | 中 — QueryBuilder 完备性需打磨 |
313
+ | 额外收益 | 架构完全自主,未来可对接任意后端 |
314
+
315
+ ---
316
+
317
+ ## 四、三方案横向对比
318
+
319
+ | 维度 | A. Kysely | B. Drizzle | C. 自研 |
320
+ |---|---|---|---|
321
+ | **拦截器级别** | AST 原生 | SQL 字符串(Driver 层) | 结构化对象 + raw 退化 |
322
+ | **db proxy 方案** | 替代或融合均可 | 重定义为 Drizzle 包装 | 保留风格,内部走协议 |
323
+ | **Drizzle 处理** | 移除(空壳) | 升级为核心 | 移除 |
324
+ | **新增依赖** | kysely(轻量) | drizzle-orm(较重) | 无 |
325
+ | **类型安全** | 强 | 最强 | 需额外投入 |
326
+ | **拦截器实现难度** | 低(原生支持) | 中(需 hack) | 中(需自建) |
327
+ | **生态/附加能力** | 纯查询 | migration/studio | 无 |
328
+ | **总开发量** | ~3 天 | ~4 天 | ~5-7 天 |
329
+ | **长期维护成本** | 低 | 中 | 高 |
330
+
331
+ ---
332
+
333
+ ## 五、待清理的遗留代码
334
+
335
+ 无论选择哪个方案,以下代码应当移除或重构:
336
+
337
+ | 文件 | 处理方式 |
338
+ |---|---|
339
+ | `src/database/orm/drizzle-repository.ts` | 移除(空壳,全 abstract) |
340
+ | `src/database/orm/service.ts` | 移除或重构(仅是 drizzle 实例容器) |
341
+ | `src/database/orm/types.ts` 中 `OrmModuleOptions.drizzle` | 移除 |
342
+ | `src/database/service.ts` 中 `DatabaseService.query()` | 重构为走新查询引擎 |
343
+ | `src/database/db-proxy.ts` | 重构底层执行路径 |
344
+ | `src/database/orm/repository.ts` 中手写 SQL | 重构为查询构建器 |
345
+
346
+ ---
347
+
348
+ ## 六、实施建议
349
+
350
+ ### 推荐:方案 A(Kysely)
351
+
352
+ 理由:
353
+ 1. 原生 AST 级 Plugin 是为拦截器场景设计的,不需要任何 hack
354
+ 2. Kysely 足够轻量(纯查询构建器),与框架"Bun 原生、轻量"定位一致
355
+ 3. 开发量适中,风险低
356
+ 4. 社区活跃,Bun 兼容性好
357
+
358
+ ### 实施步骤(草案)
359
+
360
+ 1. **Phase 1 — 基础设施**
361
+ - 引入 kysely 依赖
362
+ - 编写 BunSQLDialect(适配 Bun.SQL)
363
+ - 编写 BunSQLiteDialect(适配 bun:sqlite)
364
+ - 在 DatabaseModule 中创建 Kysely 实例
365
+
366
+ 2. **Phase 2 — 查询层迁移**
367
+ - 重写 BaseRepository 使用 Kysely 查询
368
+ - 重构 db proxy 底层走 Kysely 执行
369
+ - 移除 DrizzleBaseRepository、OrmService 的 drizzle 代码
370
+
371
+ 3. **Phase 3 — 拦截器体系**
372
+ - 定义 QueryPlugin 接口(包装 KyselyPlugin)
373
+ - 实现 TenantPlugin(多租户行级过滤)
374
+ - 在 DatabaseModule.forRoot() 中支持 plugins 配置
375
+ - 编写示例和文档
376
+
377
+ 4. **Phase 4 — 清理和测试**
378
+ - 移除遗留代码
379
+ - 编写单元测试
380
+ - 更新示例应用
381
+ - 更新文档
@@ -1,22 +1,35 @@
1
- # idleTimeout
1
+ # idleTimeout & SSE Keep-Alive
2
2
 
3
- `idleTimeout` now supports both global and per-route configuration.
3
+ > **Platform note:** `idleTimeout`, `reusePort`, and SSE TCP keepalive via `server.timeout` are **Bun-exclusive** features. On Node.js they are silently ignored. The `@IdleTimeout(ms)` decorator (handler-level timeout) works on both runtimes. See [Platform Guide](./platform.md#bun-exclusive-features) for the full list.
4
+
5
+ ## Two layers of timeout
6
+
7
+ Bun Server has **two independent timeout mechanisms** — understanding their difference is critical for SSE / streaming use cases.
8
+
9
+ | Layer | Config | Scope | Mechanism | Bun | Node.js |
10
+ |-------|--------|-------|-----------|-----|---------|
11
+ | **TCP connection** | `Application({ idleTimeout })` | Bun.serve level | Bun kernel closes the TCP socket when no bytes flow for N seconds | Yes | Ignored |
12
+ | **Handler logic** | `@IdleTimeout(ms)` decorator | Per-route `Promise.race` | Returns `408 Request Timeout` if the handler doesn't resolve in time | Yes | Yes |
13
+
14
+ > **Key point:** For SSE responses the handler returns a `Response` immediately (with a streaming body). The handler-level `@IdleTimeout` has already resolved at that point and will **not** protect or kill the stream. Only the TCP-level `idleTimeout` can break an SSE connection.
15
+
16
+ ---
4
17
 
5
18
  ## Global idle timeout (milliseconds)
6
19
 
7
- Set in `Application` options using milliseconds.
8
- Framework converts internally before passing to `Bun.serve`.
20
+ Set in `Application` options using milliseconds.
21
+ Framework converts internally before passing to `Bun.serve` (`Math.ceil(ms / 1000)` → seconds).
9
22
 
10
23
  ```ts
11
24
  const app = new Application({
12
25
  port: 3000,
13
- idleTimeout: 15000, // ms
26
+ idleTimeout: 15000, // 15 s — applies to all non-SSE connections
14
27
  });
15
28
  ```
16
29
 
17
- ## Per-route timeout (milliseconds)
30
+ ## Per-route timeout — `@IdleTimeout(ms)`
18
31
 
19
- Use `@IdleTimeout(ms)` on controller class or handler method.
32
+ Use `@IdleTimeout(ms)` on a controller class or a handler method.
20
33
 
21
34
  ```ts
22
35
  import { Controller, GET, IdleTimeout } from '@dangao/bun-server';
@@ -38,5 +51,85 @@ class ApiController {
38
51
  }
39
52
  ```
40
53
 
41
- When timeout is reached, Bun Server throws `HttpException(408, "Request Timeout")`.
54
+ ### Matching & precedence
55
+
56
+ 1. **Method-level** `@IdleTimeout` is checked first — if present, it wins.
57
+ 2. **Class-level** `@IdleTimeout` is used as fallback.
58
+ 3. If neither is set, no handler-level timeout is applied (the route runs until the TCP timeout or until it completes).
59
+
60
+ When the handler-level timeout fires, the framework throws `HttpException(408, "Request Timeout")`.
61
+
62
+ ---
63
+
64
+ ## SSE Keep-Alive (automatic)
65
+
66
+ When the framework detects a response with `Content-Type: text/event-stream`, it automatically:
67
+
68
+ 1. **Disables the TCP idle timeout** for that request via `server.timeout(req, 0)`, preventing Bun from killing the long-lived connection.
69
+ 2. **Injects SSE comment heartbeats** (`: keepalive\n\n`) at a configurable interval, preventing intermediate proxies (nginx, cloud load balancers) from closing the connection due to inactivity.
70
+
71
+ ### Configuration
72
+
73
+ ```ts
74
+ const app = new Application({
75
+ port: 3000,
76
+ idleTimeout: 10000, // normal requests: 10 s
77
+
78
+ // SSE keep-alive — defaults shown below
79
+ sseKeepAlive: {
80
+ enabled: true, // auto-detect SSE and inject heartbeat
81
+ intervalMs: 15000, // heartbeat every 15 s
82
+ },
83
+ });
84
+ ```
42
85
 
86
+ | Option | Type | Default | Description |
87
+ |--------|------|---------|-------------|
88
+ | `sseKeepAlive.enabled` | `boolean` | `true` | Enable automatic SSE detection, TCP timeout reset, and heartbeat injection |
89
+ | `sseKeepAlive.intervalMs` | `number` | `15000` | Heartbeat interval in milliseconds |
90
+
91
+ When `enabled` is `true` (default), any response whose `Content-Type` header contains `text/event-stream` triggers the SSE post-processor. You do **not** need any special decorator or annotation — detection is purely based on the response header.
92
+
93
+ When `enabled` is `false`, no SSE-specific processing is applied. You would need to manage keep-alive and `server.timeout` yourself.
94
+
95
+ ---
96
+
97
+ ## Signal cascading (`ctx.signal`)
98
+
99
+ `Context` exposes the client's `AbortSignal` via `ctx.signal`. When the client disconnects (network failure, browser tab closed, `curl` interrupted), this signal aborts.
100
+
101
+ For AI streaming endpoints, pass `ctx.signal` to `AiService` so the upstream API request is cancelled immediately — **stopping token consumption**:
102
+
103
+ ```ts
104
+ import { Controller, GET, Context as Ctx } from '@dangao/bun-server';
105
+ import type { Context } from '@dangao/bun-server';
106
+
107
+ @Controller('/chat')
108
+ class ChatController {
109
+ constructor(private readonly ai: AiService) {}
110
+
111
+ @GET('/stream')
112
+ public stream(@Ctx() ctx: Context) {
113
+ const stream = this.ai.stream({
114
+ messages: [{ role: 'user', content: 'Hello' }],
115
+ signal: ctx.signal, // ← cascade client disconnect
116
+ });
117
+ return new Response(stream, {
118
+ headers: { 'Content-Type': 'text/event-stream' },
119
+ });
120
+ }
121
+ }
122
+ ```
123
+
124
+ > **Note:** The `Context` parameter decorator is exported as both `Context` (from `'./controller'`) and `ContextParam` (from the package root). Use whichever alias avoids name collision with the `Context` type.
125
+
126
+ The full cancellation chain:
127
+
128
+ ```
129
+ Client disconnects
130
+ → request.signal aborts
131
+ → heartbeat timer cleared
132
+ → wrapped stream cancelled
133
+ → AI provider fetch() aborted
134
+ → upstream API connection closed (tokens saved)
135
+ ```
package/docs/migration.md CHANGED
@@ -4,6 +4,49 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## v2.x → v3.0 (Platform Adapter)
8
+
9
+ v3.0.0 introduces the **Platform Adapter Layer** enabling Node.js 22+ support alongside Bun.
10
+
11
+ ### Breaking Changes
12
+
13
+ | Change | Before | After |
14
+ |---|---|---|
15
+ | `BunServer.getServer()` return type | `Bun.Server \| undefined` | `IServerHandle \| undefined` |
16
+ | `WsArgumentsHost.getClient()` return type | `ServerWebSocket<T>` | `IWebSocket<T>` |
17
+
18
+ ### Migration Steps
19
+
20
+ **1. Update WebSocket guard types**
21
+
22
+ ```typescript
23
+ // Before
24
+ import type { ServerWebSocket } from 'bun';
25
+ getClient(): ServerWebSocket<unknown>
26
+
27
+ // After
28
+ import type { IWebSocket } from '@dangao/bun-server';
29
+ getClient(): IWebSocket<unknown>
30
+ ```
31
+
32
+ **2. Update server handle access**
33
+
34
+ ```typescript
35
+ // Before
36
+ const server: Bun.Server | undefined = app.getServer();
37
+
38
+ // After
39
+ import type { IServerHandle } from '@dangao/bun-server';
40
+ const server: IServerHandle | undefined = app.getServer();
41
+ // Raw native access (not recommended):
42
+ const native: unknown = app.getNativeServer();
43
+ ```
44
+
45
+ **3. Everything else is backward compatible.**
46
+ Database configuration, module APIs, controllers, services, and middleware require no changes.
47
+
48
+ ---
49
+
7
50
  ## v1.x → v2.0
8
51
 
9
52
  v2.0.0 is **fully backward compatible** with v1.x. All existing modules, APIs, and patterns continue to work without changes.