@dangao/bun-server 2.1.0 → 2.3.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 (45) hide show
  1. package/dist/ai/providers/anthropic-provider.d.ts.map +1 -1
  2. package/dist/ai/providers/google-provider.d.ts.map +1 -1
  3. package/dist/ai/providers/ollama-provider.d.ts.map +1 -1
  4. package/dist/ai/providers/openai-provider.d.ts.map +1 -1
  5. package/dist/ai/service.d.ts.map +1 -1
  6. package/dist/ai/types.d.ts +5 -0
  7. package/dist/ai/types.d.ts.map +1 -1
  8. package/dist/core/application.d.ts +17 -0
  9. package/dist/core/application.d.ts.map +1 -1
  10. package/dist/core/context.d.ts +5 -0
  11. package/dist/core/context.d.ts.map +1 -1
  12. package/dist/core/server.d.ts +17 -0
  13. package/dist/core/server.d.ts.map +1 -1
  14. package/dist/di/container.d.ts +16 -0
  15. package/dist/di/container.d.ts.map +1 -1
  16. package/dist/di/lifecycle.d.ts +48 -0
  17. package/dist/di/lifecycle.d.ts.map +1 -1
  18. package/dist/di/module-registry.d.ts +10 -6
  19. package/dist/di/module-registry.d.ts.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +316 -108
  23. package/dist/mcp/server.d.ts +5 -2
  24. package/dist/mcp/server.d.ts.map +1 -1
  25. package/docs/idle-timeout.md +99 -8
  26. package/docs/lifecycle.md +74 -4
  27. package/docs/zh/idle-timeout.md +97 -6
  28. package/docs/zh/lifecycle.md +47 -8
  29. package/package.json +1 -1
  30. package/src/ai/providers/anthropic-provider.ts +5 -2
  31. package/src/ai/providers/google-provider.ts +3 -0
  32. package/src/ai/providers/ollama-provider.ts +3 -0
  33. package/src/ai/providers/openai-provider.ts +5 -2
  34. package/src/ai/service.ts +17 -5
  35. package/src/ai/types.ts +5 -0
  36. package/src/core/application.ts +55 -23
  37. package/src/core/context.ts +7 -0
  38. package/src/core/server.ts +121 -18
  39. package/src/di/container.ts +55 -1
  40. package/src/di/lifecycle.ts +114 -0
  41. package/src/di/module-registry.ts +58 -10
  42. package/src/index.ts +4 -0
  43. package/src/mcp/server.ts +6 -15
  44. package/tests/di/lifecycle.test.ts +102 -1
  45. package/tests/di/scoped-lifecycle.test.ts +61 -0
@@ -20,8 +20,11 @@ export declare class McpServer {
20
20
  */
21
21
  handleHttp(req: Request): Promise<Response>;
22
22
  /**
23
- * Create an SSE response that keeps the connection open for streaming
24
- * This is the SSE transport endpoint
23
+ * Create an SSE response that keeps the connection open for streaming.
24
+ *
25
+ * Heartbeat / keep-alive pings are handled automatically by the framework's
26
+ * SSE post-processor (`sseKeepAlive`), so this method no longer injects its
27
+ * own `setInterval`.
25
28
  */
26
29
  createSseResponse(): Response;
27
30
  private dispatch;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;gBAExB,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa;IAKnE;;OAEG;IACU,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAgBtE;;;OAGG;IACU,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAQxD;;;OAGG;IACI,iBAAiB,IAAI,QAAQ;YAmCtB,QAAQ;CAkEvB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;gBAExB,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa;IAKnE;;OAEG;IACU,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAgBtE;;;OAGG;IACU,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAQxD;;;;;;OAMG;IACI,iBAAiB,IAAI,QAAQ;YAuBtB,QAAQ;CAkEvB"}
@@ -1,22 +1,33 @@
1
- # idleTimeout
1
+ # idleTimeout & SSE Keep-Alive
2
2
 
3
- `idleTimeout` now supports both global and per-route configuration.
3
+ ## Two layers of timeout
4
+
5
+ Bun Server has **two independent timeout mechanisms** — understanding their difference is critical for SSE / streaming use cases.
6
+
7
+ | Layer | Config | Scope | Mechanism |
8
+ |-------|--------|-------|-----------|
9
+ | **TCP connection** | `Application({ idleTimeout })` | Bun.serve level | Bun kernel closes the TCP socket when no bytes flow for N seconds |
10
+ | **Handler logic** | `@IdleTimeout(ms)` decorator | Per-route `Promise.race` | Returns `408 Request Timeout` if the handler doesn't resolve in time |
11
+
12
+ > **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.
13
+
14
+ ---
4
15
 
5
16
  ## Global idle timeout (milliseconds)
6
17
 
7
- Set in `Application` options using milliseconds.
8
- Framework converts internally before passing to `Bun.serve`.
18
+ Set in `Application` options using milliseconds.
19
+ Framework converts internally before passing to `Bun.serve` (`Math.ceil(ms / 1000)` → seconds).
9
20
 
10
21
  ```ts
11
22
  const app = new Application({
12
23
  port: 3000,
13
- idleTimeout: 15000, // ms
24
+ idleTimeout: 15000, // 15 s — applies to all non-SSE connections
14
25
  });
15
26
  ```
16
27
 
17
- ## Per-route timeout (milliseconds)
28
+ ## Per-route timeout — `@IdleTimeout(ms)`
18
29
 
19
- Use `@IdleTimeout(ms)` on controller class or handler method.
30
+ Use `@IdleTimeout(ms)` on a controller class or a handler method.
20
31
 
21
32
  ```ts
22
33
  import { Controller, GET, IdleTimeout } from '@dangao/bun-server';
@@ -38,5 +49,85 @@ class ApiController {
38
49
  }
39
50
  ```
40
51
 
41
- When timeout is reached, Bun Server throws `HttpException(408, "Request Timeout")`.
52
+ ### Matching & precedence
53
+
54
+ 1. **Method-level** `@IdleTimeout` is checked first — if present, it wins.
55
+ 2. **Class-level** `@IdleTimeout` is used as fallback.
56
+ 3. If neither is set, no handler-level timeout is applied (the route runs until the TCP timeout or until it completes).
57
+
58
+ When the handler-level timeout fires, the framework throws `HttpException(408, "Request Timeout")`.
59
+
60
+ ---
61
+
62
+ ## SSE Keep-Alive (automatic)
63
+
64
+ When the framework detects a response with `Content-Type: text/event-stream`, it automatically:
65
+
66
+ 1. **Disables the TCP idle timeout** for that request via `server.timeout(req, 0)`, preventing Bun from killing the long-lived connection.
67
+ 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.
68
+
69
+ ### Configuration
70
+
71
+ ```ts
72
+ const app = new Application({
73
+ port: 3000,
74
+ idleTimeout: 10000, // normal requests: 10 s
75
+
76
+ // SSE keep-alive — defaults shown below
77
+ sseKeepAlive: {
78
+ enabled: true, // auto-detect SSE and inject heartbeat
79
+ intervalMs: 15000, // heartbeat every 15 s
80
+ },
81
+ });
82
+ ```
83
+
84
+ | Option | Type | Default | Description |
85
+ |--------|------|---------|-------------|
86
+ | `sseKeepAlive.enabled` | `boolean` | `true` | Enable automatic SSE detection, TCP timeout reset, and heartbeat injection |
87
+ | `sseKeepAlive.intervalMs` | `number` | `15000` | Heartbeat interval in milliseconds |
88
+
89
+ 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.
90
+
91
+ When `enabled` is `false`, no SSE-specific processing is applied. You would need to manage keep-alive and `server.timeout` yourself.
92
+
93
+ ---
94
+
95
+ ## Signal cascading (`ctx.signal`)
96
+
97
+ `Context` exposes the client's `AbortSignal` via `ctx.signal`. When the client disconnects (network failure, browser tab closed, `curl` interrupted), this signal aborts.
98
+
99
+ For AI streaming endpoints, pass `ctx.signal` to `AiService` so the upstream API request is cancelled immediately — **stopping token consumption**:
100
+
101
+ ```ts
102
+ import { Controller, GET, Context as Ctx } from '@dangao/bun-server';
103
+ import type { Context } from '@dangao/bun-server';
104
+
105
+ @Controller('/chat')
106
+ class ChatController {
107
+ constructor(private readonly ai: AiService) {}
108
+
109
+ @GET('/stream')
110
+ public stream(@Ctx() ctx: Context) {
111
+ const stream = this.ai.stream({
112
+ messages: [{ role: 'user', content: 'Hello' }],
113
+ signal: ctx.signal, // ← cascade client disconnect
114
+ });
115
+ return new Response(stream, {
116
+ headers: { 'Content-Type': 'text/event-stream' },
117
+ });
118
+ }
119
+ }
120
+ ```
121
+
122
+ > **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.
123
+
124
+ The full cancellation chain:
42
125
 
126
+ ```
127
+ Client disconnects
128
+ → request.signal aborts
129
+ → heartbeat timer cleared
130
+ → wrapped stream cancelled
131
+ → AI provider fetch() aborted
132
+ → upstream API connection closed (tokens saved)
133
+ ```
package/docs/lifecycle.md CHANGED
@@ -1,21 +1,29 @@
1
1
  # Lifecycle Hooks
2
2
 
3
- Bun Server supports lifecycle hooks that let providers participate in application startup and shutdown. Implement the appropriate interface on your injectable services.
3
+ Bun Server supports lifecycle hooks that let components (`@Injectable` / `@Controller`) participate from creation to destruction.
4
4
 
5
5
  ## Interfaces
6
6
 
7
7
  | Interface | Method | When Called |
8
8
  |-----------|--------|-------------|
9
+ | `ComponentClassBeforeCreate` | `static onBeforeCreate()` | Right before the component instance is created |
10
+ | `OnAfterCreate` | `onAfterCreate()` | Right after instance creation and post processors |
9
11
  | `OnModuleInit` | `onModuleInit()` | After all module providers are registered |
10
12
  | `OnModuleDestroy` | `onModuleDestroy()` | During shutdown (reverse order) |
13
+ | `OnBeforeDestroy` | `onBeforeDestroy()` | Before `onModuleDestroy` during shutdown (reverse order) |
14
+ | `OnAfterDestroy` | `onAfterDestroy()` | After `onModuleDestroy` during shutdown (reverse order) |
11
15
  | `OnApplicationBootstrap` | `onApplicationBootstrap()` | After all modules init, before server listens |
12
16
  | `OnApplicationShutdown` | `onApplicationShutdown(signal?)` | When graceful shutdown begins |
13
17
 
14
18
  ## Execution Order
15
19
 
20
+ **Creation (per component instance)**: `onBeforeCreate` (static) -> instantiate -> `onAfterCreate`
21
+
16
22
  **Startup**: `onModuleInit` (all modules) -> `onApplicationBootstrap` (all modules) -> server starts
17
23
 
18
- **Shutdown**: `onApplicationShutdown` (reverse order) -> `onModuleDestroy` (reverse order)
24
+ **Shutdown**: `onApplicationShutdown` (reverse order) -> `onBeforeDestroy` (reverse order) -> `onModuleDestroy` (reverse order) -> `onAfterDestroy` (reverse order)
25
+
26
+ For `Lifecycle.Scoped` components, destroy hooks are executed automatically at the end of each request context.
19
27
 
20
28
  ## Provider Deduplication
21
29
 
@@ -23,19 +31,41 @@ When the same provider instance is exported or registered by multiple tokens,
23
31
  `onModuleInit` now runs only once for that instance. This avoids duplicate
24
32
  initialization side effects in shared singleton objects.
25
33
 
26
- ## Example: DatabaseService with init/destroy
34
+ ## Example: Component hooks from create to destroy
27
35
 
28
36
  ```ts
29
37
  import {
30
38
  Injectable,
39
+ Controller,
40
+ GET,
41
+ Module,
31
42
  OnModuleInit,
32
43
  OnModuleDestroy,
44
+ type ComponentClassBeforeCreate,
45
+ OnAfterCreate,
46
+ OnBeforeDestroy,
47
+ OnAfterDestroy,
33
48
  } from '@dangao/bun-server';
34
49
 
35
50
  @Injectable()
36
- class DatabaseService implements OnModuleInit, OnModuleDestroy {
51
+ class DatabaseService
52
+ implements
53
+ OnAfterCreate,
54
+ OnModuleInit,
55
+ OnBeforeDestroy,
56
+ OnModuleDestroy,
57
+ OnAfterDestroy
58
+ {
37
59
  private connected = false;
38
60
 
61
+ public static onBeforeCreate(): void {
62
+ console.log('[DatabaseService] Before create');
63
+ }
64
+
65
+ public onAfterCreate(): void {
66
+ console.log('[DatabaseService] After create');
67
+ }
68
+
39
69
  public async onModuleInit(): Promise<void> {
40
70
  console.log('[DatabaseService] Connecting...');
41
71
  await this.connect();
@@ -48,6 +78,14 @@ class DatabaseService implements OnModuleInit, OnModuleDestroy {
48
78
  this.connected = false;
49
79
  }
50
80
 
81
+ public onBeforeDestroy(): void {
82
+ console.log('[DatabaseService] Before destroy');
83
+ }
84
+
85
+ public onAfterDestroy(): void {
86
+ console.log('[DatabaseService] After destroy');
87
+ }
88
+
51
89
  public isConnected(): boolean {
52
90
  return this.connected;
53
91
  }
@@ -60,6 +98,36 @@ class DatabaseService implements OnModuleInit, OnModuleDestroy {
60
98
  // Close DB connection
61
99
  }
62
100
  }
101
+
102
+ @Controller('/health')
103
+ class HealthController implements OnAfterCreate, OnBeforeDestroy, OnAfterDestroy {
104
+ public static onBeforeCreate(): void {
105
+ console.log('[HealthController] Before create');
106
+ }
107
+
108
+ public onAfterCreate(): void {
109
+ console.log('[HealthController] After create');
110
+ }
111
+
112
+ public onBeforeDestroy(): void {
113
+ console.log('[HealthController] Before destroy');
114
+ }
115
+
116
+ public onAfterDestroy(): void {
117
+ console.log('[HealthController] After destroy');
118
+ }
119
+
120
+ @GET('/')
121
+ public get(): object {
122
+ return { ok: true };
123
+ }
124
+ }
125
+
126
+ @Module({
127
+ controllers: [HealthController],
128
+ providers: [DatabaseService],
129
+ })
130
+ class AppModule {}
63
131
  ```
64
132
 
65
133
  ## Example: Application-level hooks
@@ -75,4 +143,6 @@ class AppService implements OnApplicationBootstrap, OnApplicationShutdown {
75
143
  console.log(`Shutting down (signal: ${signal ?? 'none'})`);
76
144
  }
77
145
  }
146
+
147
+ const _beforeCreateHook: ComponentClassBeforeCreate = DatabaseService;
78
148
  ```
@@ -1,19 +1,30 @@
1
- # idleTimeout
1
+ # idleTimeout 与 SSE 保活
2
2
 
3
- `idleTimeout` 现已支持全局与路由级两种配置方式。
3
+ ## 两层超时机制
4
+
5
+ Bun Server 有**两套独立的超时机制**——理解它们的区别对 SSE / 流式场景至关重要。
6
+
7
+ | 层面 | 配置方式 | 作用域 | 机制 |
8
+ |------|----------|--------|------|
9
+ | **TCP 连接级** | `Application({ idleTimeout })` | Bun.serve 底层 | Bun 内核在连接无数据流动 N 秒后直接断开 socket |
10
+ | **Handler 逻辑级** | `@IdleTimeout(ms)` 装饰器 | 路由粒度的 `Promise.race` | handler 未在指定时间内 resolve 则返回 `408 Request Timeout` |
11
+
12
+ > **关键:** 对于 SSE 响应,handler 会立即返回一个带流式 body 的 `Response`。此时 handler 层面的 `@IdleTimeout` 已经 resolve,**不会**保护或终止该流。只有 TCP 级别的 `idleTimeout` 才能断开 SSE 连接。
13
+
14
+ ---
4
15
 
5
16
  ## 全局 idleTimeout(毫秒)
6
17
 
7
- 在 `Application` 中直接按毫秒设置,框架内部会转换后传给 `Bun.serve`。
18
+ 在 `Application` 中按毫秒设置,框架内部自动转换后传给 `Bun.serve`(`Math.ceil(ms / 1000)` → 秒)。
8
19
 
9
20
  ```ts
10
21
  const app = new Application({
11
22
  port: 3000,
12
- idleTimeout: 15000, // ms
23
+ idleTimeout: 15000, // 15 秒 — 对所有非 SSE 连接生效
13
24
  });
14
25
  ```
15
26
 
16
- ## 路由级超时(毫秒)
27
+ ## 路由级超时 — `@IdleTimeout(ms)`
17
28
 
18
29
  使用 `@IdleTimeout(ms)` 装饰器配置控制器级或方法级超时。
19
30
 
@@ -37,5 +48,85 @@ class ApiController {
37
48
  }
38
49
  ```
39
50
 
40
- 超时后会抛出 `HttpException(408, "Request Timeout")`。
51
+ ### 匹配与生效规则
52
+
53
+ 1. **方法级** `@IdleTimeout` 优先检测——存在即生效。
54
+ 2. **类级** `@IdleTimeout` 作为兜底。
55
+ 3. 若均未设置,则不应用 handler 级超时(路由将持续运行直到 TCP 超时或自然完成)。
56
+
57
+ handler 级超时触发时,框架抛出 `HttpException(408, "Request Timeout")`。
58
+
59
+ ---
60
+
61
+ ## SSE 保活(自动)
62
+
63
+ 当框架检测到响应的 `Content-Type` 包含 `text/event-stream` 时,会自动执行:
64
+
65
+ 1. **禁用该请求的 TCP 空闲超时** —— 通过 `server.timeout(req, 0)` 阻止 Bun 断开长连接。
66
+ 2. **注入 SSE 注释心跳** —— 按配置间隔发送 `: keepalive\n\n`,防止中间代理(nginx、云 LB)因空闲而断连。
67
+
68
+ ### 配置
69
+
70
+ ```ts
71
+ const app = new Application({
72
+ port: 3000,
73
+ idleTimeout: 10000, // 普通请求:10 秒
74
+
75
+ // SSE 保活 — 以下为默认值
76
+ sseKeepAlive: {
77
+ enabled: true, // 自动检测 SSE 并注入心跳
78
+ intervalMs: 15000, // 每 15 秒一次心跳
79
+ },
80
+ });
81
+ ```
82
+
83
+ | 选项 | 类型 | 默认值 | 说明 |
84
+ |------|------|--------|------|
85
+ | `sseKeepAlive.enabled` | `boolean` | `true` | 启用 SSE 自动检测、TCP 超时解除和心跳注入 |
86
+ | `sseKeepAlive.intervalMs` | `number` | `15000` | 心跳间隔(毫秒) |
87
+
88
+ 当 `enabled` 为 `true`(默认)时,任何 `Content-Type` 头包含 `text/event-stream` 的响应都会触发 SSE 后处理器。**无需任何特殊装饰器或注解**——纯粹基于响应头自动检测。
89
+
90
+ 当 `enabled` 为 `false` 时,框架不做任何 SSE 特殊处理。你需要自行管理 keep-alive 和 `server.timeout`。
91
+
92
+ ---
93
+
94
+ ## 信号级联(`ctx.signal`)
95
+
96
+ `Context` 通过 `ctx.signal` 暴露客户端的 `AbortSignal`。当客户端断连(网络故障、关闭浏览器标签、中断 `curl`)时,该信号会 abort。
97
+
98
+ 对于 AI 流式端点,将 `ctx.signal` 传递给 `AiService`,可立即取消上游 API 请求——**停止 token 消耗**:
99
+
100
+ ```ts
101
+ import { Controller, GET, Context as Ctx } from '@dangao/bun-server';
102
+ import type { Context } from '@dangao/bun-server';
103
+
104
+ @Controller('/chat')
105
+ class ChatController {
106
+ constructor(private readonly ai: AiService) {}
107
+
108
+ @GET('/stream')
109
+ public stream(@Ctx() ctx: Context) {
110
+ const stream = this.ai.stream({
111
+ messages: [{ role: 'user', content: 'Hello' }],
112
+ signal: ctx.signal, // ← 级联客户端断连
113
+ });
114
+ return new Response(stream, {
115
+ headers: { 'Content-Type': 'text/event-stream' },
116
+ });
117
+ }
118
+ }
119
+ ```
120
+
121
+ > **注意:** `Context` 参数装饰器既可通过 `Context`(从 `'./controller'`)导入,也可通过包根导出的别名 `ContextParam` 导入。推荐使用 `ContextParam` 或 `Context as Ctx` 以避免与 `Context` 类型名冲突。
122
+
123
+ 完整取消链路:
41
124
 
125
+ ```
126
+ 客户端断连
127
+ → request.signal abort
128
+ → 心跳定时器清理
129
+ → 包裹流取消
130
+ → AI Provider 内部 fetch() abort
131
+ → 上游 API 连接关闭(节省 token)
132
+ ```
@@ -1,24 +1,31 @@
1
1
  # 生命周期钩子
2
2
 
3
- Bun Server 提供四个生命周期接口,用于在模块和应用启动、关闭时执行初始化或清理逻辑。
3
+ Bun Server 支持组件级生命周期钩子,可让 `@Injectable` / `@Controller` 从创建前到销毁后执行自定义逻辑。
4
4
 
5
5
  ## 接口定义
6
6
 
7
- - **OnModuleInit**:`onModuleInit()`,在模块所有 providers 注册完成后调用
8
- - **OnModuleDestroy**:`onModuleDestroy()`,在应用关闭时调用(反向顺序)
7
+ - **ComponentClassBeforeCreate**:`static onBeforeCreate()`,组件实例创建前调用
8
+ - **OnAfterCreate**:`onAfterCreate()`,组件实例创建并完成后处理后调用
9
+ - **OnModuleInit**:`onModuleInit()`,在模块所有组件初始化阶段调用
9
10
  - **OnApplicationBootstrap**:`onApplicationBootstrap()`,在所有模块初始化完成后、服务器开始监听前调用
10
11
  - **OnApplicationShutdown**:`onApplicationShutdown(signal?)`,在优雅停机开始时调用
12
+ - **OnBeforeDestroy**:`onBeforeDestroy()`,在 `onModuleDestroy()` 前调用(反向顺序)
13
+ - **OnModuleDestroy**:`onModuleDestroy()`,在应用关闭时调用(反向顺序)
14
+ - **OnAfterDestroy**:`onAfterDestroy()`,在 `onModuleDestroy()` 后调用(反向顺序)
11
15
 
12
16
  ## 执行顺序
13
17
 
18
+ **创建阶段(每个组件实例)**:`onBeforeCreate`(静态)→ 实例化 → `onAfterCreate`
19
+
14
20
  **启动阶段**:`onModuleInit` → `onApplicationBootstrap`
15
21
 
16
- **关闭阶段**:`onApplicationShutdown` → `onModuleDestroy`(均为反向顺序,即后注册的先执行)
22
+ **关闭阶段**:`onApplicationShutdown` → `onBeforeDestroy` → `onModuleDestroy` → `onAfterDestroy`(均为反向顺序,即后注册的先执行)
17
23
 
18
- ## Provider 去重行为
24
+ 对于 `Lifecycle.Scoped` 组件,请求上下文结束时会自动触发其销毁钩子。
19
25
 
20
- 当同一个 provider 实例通过多个 token 重复注册/导出时,`onModuleInit`
21
- 现只会执行一次,避免共享单例出现重复初始化副作用。
26
+ ## 组件去重行为
27
+
28
+ 当同一个组件实例通过多个 token 重复注册/导出时,生命周期钩子只会执行一次,避免共享单例出现重复副作用。
22
29
 
23
30
  ## 示例:DatabaseService 的初始化和销毁
24
31
 
@@ -41,6 +48,14 @@ import type {
41
48
  class DatabaseService implements OnModuleInit, OnModuleDestroy {
42
49
  private connected = false;
43
50
 
51
+ public static onBeforeCreate(): void {
52
+ console.log('[DatabaseService] 创建前');
53
+ }
54
+
55
+ public onAfterCreate(): void {
56
+ console.log('[DatabaseService] 创建后');
57
+ }
58
+
44
59
  public async onModuleInit(): Promise<void> {
45
60
  console.log('[DatabaseService] 正在连接数据库...');
46
61
  await new Promise((resolve) => setTimeout(resolve, 100));
@@ -54,6 +69,14 @@ class DatabaseService implements OnModuleInit, OnModuleDestroy {
54
69
  console.log('[DatabaseService] 已断开');
55
70
  }
56
71
 
72
+ public onBeforeDestroy(): void {
73
+ console.log('[DatabaseService] 销毁前');
74
+ }
75
+
76
+ public onAfterDestroy(): void {
77
+ console.log('[DatabaseService] 销毁后');
78
+ }
79
+
57
80
  public isConnected(): boolean {
58
81
  return this.connected;
59
82
  }
@@ -72,6 +95,22 @@ class AppService implements OnApplicationBootstrap, OnApplicationShutdown {
72
95
 
73
96
  @Controller('/api')
74
97
  class AppController {
98
+ public static onBeforeCreate(): void {
99
+ console.log('[AppController] 创建前');
100
+ }
101
+
102
+ public onAfterCreate(): void {
103
+ console.log('[AppController] 创建后');
104
+ }
105
+
106
+ public onBeforeDestroy(): void {
107
+ console.log('[AppController] 销毁前');
108
+ }
109
+
110
+ public onAfterDestroy(): void {
111
+ console.log('[AppController] 销毁后');
112
+ }
113
+
75
114
  @GET('/status')
76
115
  public status(): object {
77
116
  return { status: 'running', timestamp: Date.now() };
@@ -89,4 +128,4 @@ app.registerModule(AppModule);
89
128
  await app.listen();
90
129
  ```
91
130
 
92
- 按 Ctrl+C 触发关闭时,将依次执行 `onApplicationShutdown` 和 `onModuleDestroy`。
131
+ 按 Ctrl+C 触发关闭时,将依次执行 `onApplicationShutdown`、`onBeforeDestroy`、`onModuleDestroy`、`onAfterDestroy`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dangao/bun-server",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -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) {