@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.
- package/dist/ai/providers/anthropic-provider.d.ts.map +1 -1
- package/dist/ai/providers/google-provider.d.ts.map +1 -1
- package/dist/ai/providers/ollama-provider.d.ts.map +1 -1
- package/dist/ai/providers/openai-provider.d.ts.map +1 -1
- package/dist/ai/service.d.ts.map +1 -1
- package/dist/ai/types.d.ts +5 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/core/application.d.ts +17 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/context.d.ts +5 -0
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/server.d.ts +17 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/di/container.d.ts +16 -0
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/lifecycle.d.ts +48 -0
- package/dist/di/lifecycle.d.ts.map +1 -1
- package/dist/di/module-registry.d.ts +10 -6
- package/dist/di/module-registry.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +316 -108
- package/dist/mcp/server.d.ts +5 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/docs/idle-timeout.md +99 -8
- package/docs/lifecycle.md +74 -4
- package/docs/zh/idle-timeout.md +97 -6
- package/docs/zh/lifecycle.md +47 -8
- package/package.json +1 -1
- package/src/ai/providers/anthropic-provider.ts +5 -2
- package/src/ai/providers/google-provider.ts +3 -0
- package/src/ai/providers/ollama-provider.ts +3 -0
- package/src/ai/providers/openai-provider.ts +5 -2
- package/src/ai/service.ts +17 -5
- package/src/ai/types.ts +5 -0
- package/src/core/application.ts +55 -23
- package/src/core/context.ts +7 -0
- package/src/core/server.ts +121 -18
- package/src/di/container.ts +55 -1
- package/src/di/lifecycle.ts +114 -0
- package/src/di/module-registry.ts +58 -10
- package/src/index.ts +4 -0
- package/src/mcp/server.ts +6 -15
- package/tests/di/lifecycle.test.ts +102 -1
- 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
|
/**
|
package/src/core/application.ts
CHANGED
|
@@ -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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
343
|
-
|
|
362
|
+
// 先通过路由解析出处理器信息,便于安全中间件等基于路由元数据做决策
|
|
363
|
+
const registry = RouteRegistry.getInstance();
|
|
364
|
+
const router = registry.getRouter();
|
|
344
365
|
|
|
345
|
-
|
|
346
|
-
|
|
366
|
+
// 预解析路由,仅设置上下文信息,不执行处理器
|
|
367
|
+
await router.preHandle(context);
|
|
347
368
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
361
|
-
|
|
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
|
|
package/src/core/context.ts
CHANGED
|
@@ -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
|
/**
|
package/src/core/server.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/di/container.ts
CHANGED
|
@@ -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
|
-
|
|
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 - 提供者标识符
|
package/src/di/lifecycle.ts
CHANGED
|
@@ -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
|
-
*
|
|
230
|
+
* 解析所有模块中的组件实例(providers + controllers),用于生命周期钩子调用
|
|
229
231
|
*/
|
|
230
|
-
public
|
|
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
|
-
*
|
|
283
|
+
* 调用所有组件(providers + controllers)的 onModuleInit 钩子
|
|
271
284
|
*/
|
|
272
285
|
public async callModuleInitHooks(): Promise<void> {
|
|
273
|
-
const instances = this.
|
|
286
|
+
const instances = this.resolveAllComponentInstances();
|
|
274
287
|
await callOnModuleInit(instances);
|
|
275
288
|
}
|
|
276
289
|
|
|
277
290
|
/**
|
|
278
|
-
*
|
|
291
|
+
* 调用所有组件(providers + controllers)的 onApplicationBootstrap 钩子
|
|
279
292
|
*/
|
|
280
293
|
public async callBootstrapHooks(): Promise<void> {
|
|
281
|
-
const instances = this.
|
|
294
|
+
const instances = this.resolveAllComponentInstances();
|
|
282
295
|
await callOnApplicationBootstrap(instances);
|
|
283
296
|
}
|
|
284
297
|
|
|
285
298
|
/**
|
|
286
|
-
*
|
|
299
|
+
* 调用所有组件(providers + controllers)的 onModuleDestroy 钩子
|
|
287
300
|
*/
|
|
288
301
|
public async callModuleDestroyHooks(): Promise<void> {
|
|
289
|
-
const instances = this.
|
|
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
|
-
*
|
|
309
|
+
* 调用所有组件(providers + controllers)的 onApplicationShutdown 钩子
|
|
295
310
|
*/
|
|
296
311
|
public async callShutdownHooks(signal?: string): Promise<void> {
|
|
297
|
-
const instances = this.
|
|
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(
|