@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
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
  /**
@@ -61,6 +61,24 @@ export interface ApplicationOptions {
61
61
  * 框架内部会自动转换为 Bun.serve 的秒单位
62
62
  */
63
63
  idleTimeout?: number;
64
+
65
+ /**
66
+ * SSE 保活配置
67
+ *
68
+ * 框架自动检测 `Content-Type: text/event-stream` 的响应,
69
+ * 对该请求禁用 Bun TCP 空闲超时(`server.timeout(req, 0)`),
70
+ * 并按配置间隔向客户端发送 SSE 注释心跳(`: keepalive\n\n`)。
71
+ *
72
+ * 心跳可防止中间代理(nginx / 云 LB)因空闲而断开连接。
73
+ *
74
+ * @default `{ enabled: true, intervalMs: 15000 }`
75
+ */
76
+ sseKeepAlive?: {
77
+ /** 是否启用,默认 true */
78
+ enabled?: boolean;
79
+ /** 心跳间隔(毫秒),默认 15000 */
80
+ intervalMs?: number;
81
+ };
64
82
  }
65
83
 
66
84
  /**
@@ -150,6 +168,7 @@ export class Application {
150
168
  hostname: finalHostname,
151
169
  reusePort: this.options.reusePort,
152
170
  idleTimeout: this.options.idleTimeout,
171
+ sseKeepAlive: this.options.sseKeepAlive,
153
172
  fetch: this.handleRequest.bind(this),
154
173
  websocketRegistry: this.websocketRegistry,
155
174
  gracefulShutdownTimeout: this.options.gracefulShutdownTimeout,
@@ -324,6 +343,7 @@ export class Application {
324
343
  */
325
344
  private async handleRequest(context: Context): Promise<Response> {
326
345
  const logger = LoggerManager.getLogger();
346
+ const moduleRegistry = ModuleRegistry.getInstance();
327
347
  logger.debug('[Request] Incoming', {
328
348
  method: context.method,
329
349
  path: context.path,
@@ -332,34 +352,46 @@ export class Application {
332
352
 
333
353
  // 使用 AsyncLocalStorage 包裹请求处理,确保所有中间件和控制器都在请求上下文中执行
334
354
  return await contextStore.run(context, async () => {
335
- // 对于 POST、PUT、PATCH 请求,提前解析 body 并缓存
336
- // 这样可以确保 Request.body 流只读取一次
337
- if (['POST', 'PUT', 'PATCH'].includes(context.method)) {
338
- await context.getBody();
339
- }
355
+ try {
356
+ // 对于 POST、PUT、PATCH 请求,提前解析 body 并缓存
357
+ // 这样可以确保 Request.body 流只读取一次
358
+ if (['POST', 'PUT', 'PATCH'].includes(context.method)) {
359
+ await context.getBody();
360
+ }
340
361
 
341
- // 先通过路由解析出处理器信息,便于安全中间件等基于路由元数据做决策
342
- const registry = RouteRegistry.getInstance();
343
- const router = registry.getRouter();
362
+ // 先通过路由解析出处理器信息,便于安全中间件等基于路由元数据做决策
363
+ const registry = RouteRegistry.getInstance();
364
+ const router = registry.getRouter();
344
365
 
345
- // 预解析路由,仅设置上下文信息,不执行处理器
346
- await router.preHandle(context);
366
+ // 预解析路由,仅设置上下文信息,不执行处理器
367
+ await router.preHandle(context);
347
368
 
348
- // 再进入中间件管道,由中间件(如安全过滤器)根据 routeHandler 和 Auth 元数据做校验,
349
- // 最后再由路由真正执行控制器方法
350
- return await this.middlewarePipeline.run(context, async () => {
351
- const response = await router.handle(context);
352
- if (response) {
353
- return response;
354
- }
369
+ // 再进入中间件管道,由中间件(如安全过滤器)根据 routeHandler 和 Auth 元数据做校验,
370
+ // 最后再由路由真正执行控制器方法
371
+ return await this.middlewarePipeline.run(context, async () => {
372
+ const response = await router.handle(context);
373
+ if (response) {
374
+ return response;
375
+ }
355
376
 
356
- logger.debug('[Router] No route matched', {
357
- method: context.method,
358
- path: context.path,
377
+ logger.debug('[Router] No route matched', {
378
+ method: context.method,
379
+ path: context.path,
380
+ });
381
+ context.setStatus(404);
382
+ return context.createErrorResponse({ error: 'Not Found' });
359
383
  });
360
- context.setStatus(404);
361
- return context.createErrorResponse({ error: 'Not Found' });
362
- });
384
+ } finally {
385
+ try {
386
+ await moduleRegistry.disposeScopedInstances(context);
387
+ } catch (error) {
388
+ logger.warn('[Application] Failed to dispose scoped instances', {
389
+ path: context.path,
390
+ method: context.method,
391
+ errorMessage: error instanceof Error ? error.message : String(error),
392
+ });
393
+ }
394
+ }
363
395
  });
364
396
  }
365
397
 
@@ -74,6 +74,12 @@ export class Context {
74
74
  */
75
75
  private _bodyParsed: boolean = false;
76
76
 
77
+ /**
78
+ * 客户端断连信号
79
+ * 当客户端主动关闭连接时 abort,可用于级联取消内部请求
80
+ */
81
+ public readonly signal: AbortSignal;
82
+
77
83
  public constructor(request: Request) {
78
84
  this.request = request;
79
85
  this.url = new URL(request.url);
@@ -82,6 +88,7 @@ export class Context {
82
88
  this.query = this.url.searchParams;
83
89
  this.headers = request.headers;
84
90
  this.responseHeaders = new Headers();
91
+ this.signal = request.signal;
85
92
  }
86
93
 
87
94
  /**
@@ -47,6 +47,15 @@ export interface ServerOptions {
47
47
  * 框架内部会转换为 Bun.serve 所需的秒
48
48
  */
49
49
  idleTimeout?: number;
50
+
51
+ /**
52
+ * SSE 保活配置
53
+ * @see ApplicationOptions.sseKeepAlive
54
+ */
55
+ sseKeepAlive?: {
56
+ enabled?: boolean;
57
+ intervalMs?: number;
58
+ };
50
59
  }
51
60
 
52
61
  /**
@@ -81,9 +90,44 @@ export class BunServer {
81
90
  this.shutdownPromise = undefined;
82
91
  this.shutdownResolve = undefined;
83
92
 
93
+ const sseKeepAlive = this.options.sseKeepAlive;
94
+ const sseHeartbeatEnabled = sseKeepAlive?.enabled !== false;
95
+ const sseHeartbeatIntervalMs = sseKeepAlive?.intervalMs ?? 15_000;
96
+
97
+ const postProcessSse = (
98
+ response: Response,
99
+ request: Request,
100
+ bunServer: Server<WebSocketConnectionData>,
101
+ ): Response => {
102
+ const ct = response.headers.get('content-type');
103
+ if (!ct?.includes('text/event-stream')) {
104
+ return response;
105
+ }
106
+
107
+ // SSE detected — always disable Bun TCP idle timeout for this connection
108
+ bunServer.timeout(request, 0);
109
+
110
+ if (sseHeartbeatEnabled && response.body) {
111
+ return BunServer.wrapSseWithHeartbeat(
112
+ response,
113
+ sseHeartbeatIntervalMs,
114
+ request.signal,
115
+ );
116
+ }
117
+
118
+ return response;
119
+ };
120
+
121
+ const decrementAndMaybeShutdown = () => {
122
+ this.activeRequests--;
123
+ if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
124
+ this.shutdownResolve();
125
+ }
126
+ };
127
+
84
128
  const fetchHandler = (
85
129
  request: Request,
86
- server: Server<WebSocketConnectionData>,
130
+ bunServer: Server<WebSocketConnectionData>,
87
131
  ): Response | Promise<Response> | undefined => {
88
132
  if (this.isShuttingDown) {
89
133
  return new Response("Server is shutting down", { status: 503 });
@@ -101,7 +145,7 @@ export class BunServer {
101
145
  }
102
146
  const context = new Context(request);
103
147
  const queryParams = new URLSearchParams(url.searchParams);
104
- const upgraded = server.upgrade(request, {
148
+ const upgraded = bunServer.upgrade(request, {
105
149
  data: {
106
150
  path: url.pathname,
107
151
  query: queryParams,
@@ -120,24 +164,17 @@ export class BunServer {
120
164
  const responsePromise = this.options.fetch(context);
121
165
 
122
166
  if (responsePromise instanceof Promise) {
123
- responsePromise
124
- .finally(() => {
125
- this.activeRequests--;
126
- if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
127
- this.shutdownResolve();
128
- }
129
- })
130
- .catch(() => {
131
- // errors handled by middleware pipeline
132
- });
133
- } else {
134
- this.activeRequests--;
135
- if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
136
- this.shutdownResolve();
137
- }
167
+ const processed = responsePromise.then(
168
+ (response) => postProcessSse(response, request, bunServer),
169
+ );
170
+ processed
171
+ .finally(decrementAndMaybeShutdown)
172
+ .catch(() => { /* errors handled by middleware pipeline */ });
173
+ return processed;
138
174
  }
139
175
 
140
- return responsePromise;
176
+ decrementAndMaybeShutdown();
177
+ return postProcessSse(responsePromise, request, bunServer);
141
178
  };
142
179
 
143
180
  const websocketHandlers = {
@@ -296,4 +333,70 @@ export class BunServer {
296
333
  public getHostname(): string | undefined {
297
334
  return this.options.hostname;
298
335
  }
336
+
337
+ /**
338
+ * 将 SSE Response 的 body 包裹一层心跳注入流。
339
+ *
340
+ * 原始流的数据原样透传;在数据间隙中按 intervalMs 发送
341
+ * SSE 注释帧 `: keepalive\n\n`(客户端会忽略注释帧)。
342
+ *
343
+ * 当 signal abort / 原始流结束 / 客户端断连时自动清理定时器。
344
+ */
345
+ private static wrapSseWithHeartbeat(
346
+ original: Response,
347
+ intervalMs: number,
348
+ signal: AbortSignal,
349
+ ): Response {
350
+ const encoder = new TextEncoder();
351
+ const keepaliveChunk = encoder.encode(': keepalive\n\n');
352
+ const originalBody = original.body!;
353
+ let heartbeat: ReturnType<typeof setInterval> | undefined;
354
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
355
+
356
+ const wrapped = new ReadableStream<Uint8Array>({
357
+ start(controller) {
358
+ reader = originalBody.getReader() as ReadableStreamDefaultReader<Uint8Array>;
359
+
360
+ heartbeat = setInterval(() => {
361
+ try {
362
+ controller.enqueue(keepaliveChunk);
363
+ } catch {
364
+ clearInterval(heartbeat);
365
+ heartbeat = undefined;
366
+ }
367
+ }, intervalMs);
368
+
369
+ const onAbort = () => {
370
+ if (heartbeat) { clearInterval(heartbeat); heartbeat = undefined; }
371
+ };
372
+ signal.addEventListener('abort', onAbort, { once: true });
373
+
374
+ const pump = async () => {
375
+ try {
376
+ while (true) {
377
+ const { done, value } = await reader!.read();
378
+ if (done) break;
379
+ controller.enqueue(value);
380
+ }
381
+ controller.close();
382
+ } catch (err) {
383
+ try { controller.error(err); } catch { /* already closed */ }
384
+ } finally {
385
+ if (heartbeat) { clearInterval(heartbeat); heartbeat = undefined; }
386
+ signal.removeEventListener('abort', onAbort);
387
+ }
388
+ };
389
+ pump();
390
+ },
391
+ cancel() {
392
+ if (heartbeat) { clearInterval(heartbeat); heartbeat = undefined; }
393
+ reader?.cancel();
394
+ },
395
+ });
396
+
397
+ return new Response(wrapped, {
398
+ status: original.status,
399
+ headers: original.headers,
400
+ });
401
+ }
299
402
  }
@@ -14,6 +14,13 @@ import {
14
14
  import { LoggerManager } from "@dangao/logsmith";
15
15
  import type { Constructor } from "@/core/types";
16
16
  import { contextStore } from "../core/context-service";
17
+ import {
18
+ callComponentBeforeCreate,
19
+ callOnAfterCreate,
20
+ callOnBeforeDestroy,
21
+ callOnModuleDestroy,
22
+ callOnAfterDestroy,
23
+ } from "./lifecycle";
17
24
 
18
25
  /**
19
26
  * 依赖注入容器
@@ -365,6 +372,7 @@ export class Container {
365
372
  * @returns 实例
366
373
  */
367
374
  private instantiate<T>(constructor: Constructor<T>): T {
375
+ callComponentBeforeCreate(constructor);
368
376
  const plan = this.getDependencyPlan(constructor);
369
377
 
370
378
  let instance: T;
@@ -379,7 +387,9 @@ export class Container {
379
387
  }
380
388
 
381
389
  // 应用后处理器
382
- return this.applyPostProcessors(instance, constructor);
390
+ const processed = this.applyPostProcessors(instance, constructor);
391
+ callOnAfterCreate(processed);
392
+ return processed;
383
393
  }
384
394
 
385
395
  /**
@@ -409,6 +419,50 @@ export class Container {
409
419
  // scopedInstances 使用 WeakMap,当 Context 对象被 GC 时会自动清理
410
420
  }
411
421
 
422
+ /**
423
+ * 获取指定请求上下文下的 scoped 实例
424
+ * @param context - 请求上下文对象
425
+ * @returns 去重后的 scoped 实例列表
426
+ */
427
+ public getScopedInstances(context: object): unknown[] {
428
+ const scopedMap = this.scopedInstances.get(context);
429
+ if (!scopedMap || scopedMap.size === 0) {
430
+ return [];
431
+ }
432
+ const seen = new Set<unknown>();
433
+ const instances: unknown[] = [];
434
+ for (const instance of scopedMap.values()) {
435
+ if (!seen.has(instance)) {
436
+ seen.add(instance);
437
+ instances.push(instance);
438
+ }
439
+ }
440
+ return instances;
441
+ }
442
+
443
+ /**
444
+ * 清理指定请求上下文的 scoped 实例缓存
445
+ * @param context - 请求上下文对象
446
+ */
447
+ public clearScopedInstances(context: object): void {
448
+ this.scopedInstances.delete(context);
449
+ }
450
+
451
+ /**
452
+ * 触发指定请求上下文下 scoped 实例的销毁钩子并清理缓存
453
+ * @param context - 请求上下文对象
454
+ */
455
+ public async disposeScopedInstances(context: object): Promise<void> {
456
+ const instances = this.getScopedInstances(context);
457
+ if (instances.length === 0) {
458
+ return;
459
+ }
460
+ await callOnBeforeDestroy(instances);
461
+ await callOnModuleDestroy(instances);
462
+ await callOnAfterDestroy(instances);
463
+ this.clearScopedInstances(context);
464
+ }
465
+
412
466
  /**
413
467
  * 检查是否已注册
414
468
  * @param token - 提供者标识符
@@ -30,6 +30,38 @@ export interface OnApplicationShutdown {
30
30
  onApplicationShutdown(signal?: string): Promise<void> | void;
31
31
  }
32
32
 
33
+ /**
34
+ * 组件创建前钩子(静态类方法)
35
+ * 在实例化前调用,适用于 Controller / Injectable 类
36
+ */
37
+ export type ComponentClassBeforeCreate = {
38
+ onBeforeCreate(): void;
39
+ };
40
+
41
+ /**
42
+ * 组件创建后钩子(实例)
43
+ * 在实例化并完成后处理后调用
44
+ */
45
+ export interface OnAfterCreate {
46
+ onAfterCreate(): void;
47
+ }
48
+
49
+ /**
50
+ * 组件销毁前钩子(实例)
51
+ * 在 onModuleDestroy 之前调用(反向顺序)
52
+ */
53
+ export interface OnBeforeDestroy {
54
+ onBeforeDestroy(): Promise<void> | void;
55
+ }
56
+
57
+ /**
58
+ * 组件销毁后钩子(实例)
59
+ * 在 onModuleDestroy 之后调用(反向顺序)
60
+ */
61
+ export interface OnAfterDestroy {
62
+ onAfterDestroy(): Promise<void> | void;
63
+ }
64
+
33
65
  export function hasOnModuleInit(instance: unknown): instance is OnModuleInit {
34
66
  return (
35
67
  instance !== null &&
@@ -70,6 +102,64 @@ export function hasOnApplicationShutdown(instance: unknown): instance is OnAppli
70
102
  );
71
103
  }
72
104
 
105
+ export function hasComponentBeforeCreate(target: unknown): target is ComponentClassBeforeCreate {
106
+ return (
107
+ target !== null &&
108
+ target !== undefined &&
109
+ typeof target === 'function' &&
110
+ 'onBeforeCreate' in target &&
111
+ typeof (target as ComponentClassBeforeCreate).onBeforeCreate === 'function'
112
+ );
113
+ }
114
+
115
+ export function hasOnAfterCreate(instance: unknown): instance is OnAfterCreate {
116
+ return (
117
+ instance !== null &&
118
+ instance !== undefined &&
119
+ typeof instance === 'object' &&
120
+ 'onAfterCreate' in instance &&
121
+ typeof (instance as OnAfterCreate).onAfterCreate === 'function'
122
+ );
123
+ }
124
+
125
+ export function hasOnBeforeDestroy(instance: unknown): instance is OnBeforeDestroy {
126
+ return (
127
+ instance !== null &&
128
+ instance !== undefined &&
129
+ typeof instance === 'object' &&
130
+ 'onBeforeDestroy' in instance &&
131
+ typeof (instance as OnBeforeDestroy).onBeforeDestroy === 'function'
132
+ );
133
+ }
134
+
135
+ export function hasOnAfterDestroy(instance: unknown): instance is OnAfterDestroy {
136
+ return (
137
+ instance !== null &&
138
+ instance !== undefined &&
139
+ typeof instance === 'object' &&
140
+ 'onAfterDestroy' in instance &&
141
+ typeof (instance as OnAfterDestroy).onAfterDestroy === 'function'
142
+ );
143
+ }
144
+
145
+ /**
146
+ * 调用组件类静态 onBeforeCreate
147
+ */
148
+ export function callComponentBeforeCreate(target: unknown): void {
149
+ if (hasComponentBeforeCreate(target)) {
150
+ target.onBeforeCreate();
151
+ }
152
+ }
153
+
154
+ /**
155
+ * 调用组件实例 onAfterCreate
156
+ */
157
+ export function callOnAfterCreate(instance: unknown): void {
158
+ if (hasOnAfterCreate(instance)) {
159
+ instance.onAfterCreate();
160
+ }
161
+ }
162
+
73
163
  /**
74
164
  * 按顺序调用 onModuleInit
75
165
  */
@@ -115,3 +205,27 @@ export async function callOnApplicationShutdown(instances: unknown[], signal?: s
115
205
  }
116
206
  }
117
207
  }
208
+
209
+ /**
210
+ * 按反向顺序调用 onBeforeDestroy
211
+ */
212
+ export async function callOnBeforeDestroy(instances: unknown[]): Promise<void> {
213
+ for (let i = instances.length - 1; i >= 0; i--) {
214
+ const instance = instances[i];
215
+ if (hasOnBeforeDestroy(instance)) {
216
+ await instance.onBeforeDestroy();
217
+ }
218
+ }
219
+ }
220
+
221
+ /**
222
+ * 按反向顺序调用 onAfterDestroy
223
+ */
224
+ export async function callOnAfterDestroy(instances: unknown[]): Promise<void> {
225
+ for (let i = instances.length - 1; i >= 0; i--) {
226
+ const instance = instances[i];
227
+ if (hasOnAfterDestroy(instance)) {
228
+ await instance.onAfterDestroy();
229
+ }
230
+ }
231
+ }
@@ -11,6 +11,8 @@ import {
11
11
  callOnModuleDestroy,
12
12
  callOnApplicationBootstrap,
13
13
  callOnApplicationShutdown,
14
+ callOnBeforeDestroy,
15
+ callOnAfterDestroy,
14
16
  } from './lifecycle';
15
17
 
16
18
  interface ModuleRef {
@@ -225,9 +227,9 @@ export class ModuleRegistry {
225
227
  }
226
228
 
227
229
  /**
228
- * 解析所有模块中的 provider 实例,用于生命周期钩子调用
230
+ * 解析所有模块中的组件实例(providers + controllers),用于生命周期钩子调用
229
231
  */
230
- public resolveAllProviderInstances(): unknown[] {
232
+ public resolveAllComponentInstances(): unknown[] {
231
233
  const instances: unknown[] = [];
232
234
  const seen = new Set<unknown>();
233
235
  for (const [, ref] of this.moduleRefs) {
@@ -262,42 +264,88 @@ export class ModuleRegistry {
262
264
  // skip providers that can't be resolved (e.g. pending async providers)
263
265
  }
264
266
  }
267
+ for (const controller of ref.metadata.controllers) {
268
+ try {
269
+ const instance = ref.container.resolve(controller);
270
+ if (!seen.has(instance)) {
271
+ seen.add(instance);
272
+ instances.push(instance);
273
+ }
274
+ } catch (_error) {
275
+ // skip controllers that can't be resolved
276
+ }
277
+ }
265
278
  }
266
279
  return instances;
267
280
  }
268
281
 
269
282
  /**
270
- * 调用所有 provider onModuleInit 钩子
283
+ * 调用所有组件(providers + controllers)的 onModuleInit 钩子
271
284
  */
272
285
  public async callModuleInitHooks(): Promise<void> {
273
- const instances = this.resolveAllProviderInstances();
286
+ const instances = this.resolveAllComponentInstances();
274
287
  await callOnModuleInit(instances);
275
288
  }
276
289
 
277
290
  /**
278
- * 调用所有 provider onApplicationBootstrap 钩子
291
+ * 调用所有组件(providers + controllers)的 onApplicationBootstrap 钩子
279
292
  */
280
293
  public async callBootstrapHooks(): Promise<void> {
281
- const instances = this.resolveAllProviderInstances();
294
+ const instances = this.resolveAllComponentInstances();
282
295
  await callOnApplicationBootstrap(instances);
283
296
  }
284
297
 
285
298
  /**
286
- * 调用所有 provider onModuleDestroy 钩子
299
+ * 调用所有组件(providers + controllers)的 onModuleDestroy 钩子
287
300
  */
288
301
  public async callModuleDestroyHooks(): Promise<void> {
289
- const instances = this.resolveAllProviderInstances();
302
+ const instances = this.resolveAllComponentInstances();
303
+ await callOnBeforeDestroy(instances);
290
304
  await callOnModuleDestroy(instances);
305
+ await callOnAfterDestroy(instances);
291
306
  }
292
307
 
293
308
  /**
294
- * 调用所有 provider onApplicationShutdown 钩子
309
+ * 调用所有组件(providers + controllers)的 onApplicationShutdown 钩子
295
310
  */
296
311
  public async callShutdownHooks(signal?: string): Promise<void> {
297
- const instances = this.resolveAllProviderInstances();
312
+ const instances = this.resolveAllComponentInstances();
298
313
  await callOnApplicationShutdown(instances, signal);
299
314
  }
300
315
 
316
+ /**
317
+ * 调用当前请求上下文下 scoped 组件的销毁钩子并清理缓存
318
+ */
319
+ public async disposeScopedInstances(context: object): Promise<void> {
320
+ const containers: Container[] = [];
321
+ if (this.rootContainer) {
322
+ containers.push(this.rootContainer);
323
+ }
324
+ containers.push(...this.getAllModuleContainers());
325
+
326
+ const instances: unknown[] = [];
327
+ const seen = new Set<unknown>();
328
+ for (const container of containers) {
329
+ const scopedInstances = container.getScopedInstances(context);
330
+ for (const instance of scopedInstances) {
331
+ if (!seen.has(instance)) {
332
+ seen.add(instance);
333
+ instances.push(instance);
334
+ }
335
+ }
336
+ }
337
+
338
+ if (instances.length > 0) {
339
+ await callOnBeforeDestroy(instances);
340
+ await callOnModuleDestroy(instances);
341
+ await callOnAfterDestroy(instances);
342
+ }
343
+
344
+ for (const container of containers) {
345
+ container.clearScopedInstances(context);
346
+ }
347
+ }
348
+
301
349
  private registerExport(parentContainer: Container, moduleRef: ModuleRef, token: ProviderToken): void {
302
350
  if (!moduleRef.container.isRegistered(token)) {
303
351
  throw new Error(