@hile/core 1.0.17 → 1.0.18

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/README.md CHANGED
@@ -120,7 +120,49 @@ export const connectionService = defineService(async (shutdown) => {
120
120
 
121
121
  > 建议始终使用 `async` 服务函数,确保异常路径可正确触发销毁机制。
122
122
 
123
- ### 6) 手动销毁(Graceful Shutdown)
123
+ ### 6) 生命周期、超时与可观测事件
124
+
125
+ 容器支持显式生命周期阶段:`init -> ready -> stopping -> stopped`。
126
+
127
+ 可通过构造参数设置超时:
128
+
129
+ ```typescript
130
+ import { Container } from '@hile/core'
131
+
132
+ const container = new Container({
133
+ startTimeoutMs: 5_000,
134
+ shutdownTimeoutMs: 3_000,
135
+ })
136
+ ```
137
+
138
+ 订阅事件:
139
+
140
+ ```typescript
141
+ const off = container.onEvent((event) => {
142
+ if (event.type === 'service:ready') {
143
+ console.log(`service#${event.id} ready in ${event.durationMs}ms`)
144
+ }
145
+ })
146
+
147
+ // later
148
+ off()
149
+ ```
150
+
151
+ ### 7) 依赖图与启动顺序
152
+
153
+ 容器会在 `resolve` 过程中自动记录依赖关系,并检测循环依赖。
154
+
155
+ ```typescript
156
+ const graph = container.getDependencyGraph()
157
+ // graph.nodes: number[]
158
+ // graph.edges: Array<{ from: number; to: number }>
159
+
160
+ const startupOrder = container.getStartupOrder()
161
+ ```
162
+
163
+ 若出现循环依赖,会抛出 `circular dependency detected` 错误。
164
+
165
+ ### 8) 手动销毁(Graceful Shutdown)
124
166
 
125
167
  ```typescript
126
168
  import container from '@hile/core'
@@ -131,7 +173,7 @@ process.on('SIGTERM', async () => {
131
173
  })
132
174
  ```
133
175
 
134
- ### 7) 服务校验(isService)
176
+ ### 9) 服务校验(isService)
135
177
 
136
178
  ```typescript
137
179
  import { defineService, isService } from '@hile/core'
@@ -172,13 +214,19 @@ const result = await container.resolve(service)
172
214
 
173
215
  | 方法 | 说明 |
174
216
  |------|------|
217
+ | `new Container(options?)` | 创建容器,可配置启动/销毁超时 |
175
218
  | `register(fn)` | 注册服务(同函数引用去重) |
176
219
  | `resolve(props)` | 加载服务(执行、等待或返回缓存) |
220
+ | `shutdown()` | 销毁所有服务并执行清理回调 |
221
+ | `onEvent(listener)` | 订阅容器事件,返回取消订阅函数 |
222
+ | `offEvent(listener)` | 取消订阅 |
223
+ | `getLifecycle(id)` | 获取服务生命周期阶段 |
224
+ | `getDependencyGraph()` | 获取依赖图 `{ nodes, edges }` |
225
+ | `getStartupOrder()` | 获取服务启动顺序(首次启动顺序) |
177
226
  | `hasService(fn)` | 检查函数是否已注册 |
178
227
  | `hasMeta(id)` | 检查服务是否已有运行时元数据 |
179
228
  | `getIdByService(fn)` | 通过函数获取服务 ID |
180
229
  | `getMetaById(id)` | 通过 ID 获取运行时元数据 |
181
- | `shutdown()` | 销毁所有服务并执行清理回调 |
182
230
 
183
231
  ### 服务状态
184
232
 
package/dist/index.d.ts CHANGED
@@ -2,102 +2,100 @@ export type ServiceCutDownFunction = () => unknown | Promise<unknown>;
2
2
  export type ServiceCutDownHandler = (fn: ServiceCutDownFunction) => void;
3
3
  export type ServiceFunction<R> = (fn: ServiceCutDownHandler) => R | Promise<R>;
4
4
  declare const sericeFlag: unique symbol;
5
+ export type ServiceLifecycleStage = 'init' | 'ready' | 'stopping' | 'stopped';
5
6
  export interface ServiceRegisterProps<R> {
6
7
  id: number;
7
8
  fn: ServiceFunction<R>;
8
9
  flag: typeof sericeFlag;
9
10
  }
11
+ export interface ContainerOptions {
12
+ startTimeoutMs?: number;
13
+ shutdownTimeoutMs?: number;
14
+ }
15
+ export type ContainerEvent = {
16
+ type: 'service:init';
17
+ id: number;
18
+ } | {
19
+ type: 'service:ready';
20
+ id: number;
21
+ durationMs: number;
22
+ } | {
23
+ type: 'service:error';
24
+ id: number;
25
+ error: any;
26
+ durationMs: number;
27
+ } | {
28
+ type: 'service:shutdown:start';
29
+ id: number;
30
+ } | {
31
+ type: 'service:shutdown:done';
32
+ id: number;
33
+ durationMs: number;
34
+ } | {
35
+ type: 'service:shutdown:error';
36
+ id: number;
37
+ error: any;
38
+ } | {
39
+ type: 'container:shutdown:start';
40
+ } | {
41
+ type: 'container:shutdown:done';
42
+ durationMs: number;
43
+ } | {
44
+ type: 'container:error';
45
+ error: any;
46
+ };
10
47
  interface Paddings<R = any> {
11
48
  status: -1 | 0 | 1;
49
+ lifecycle: ServiceLifecycleStage;
12
50
  value: R;
13
51
  error?: any;
14
52
  queue: Set<{
15
53
  resolve: (value: R) => void;
16
54
  reject: (error: any) => void;
17
55
  }>;
56
+ startedAt: number;
57
+ endedAt?: number;
18
58
  }
19
59
  export declare class Container {
60
+ private readonly options;
20
61
  private id;
21
62
  private readonly packages;
22
63
  private readonly paddings;
64
+ private readonly dependencies;
65
+ private readonly dependents;
23
66
  private readonly shutdownFunctions;
24
67
  private readonly shutdownQueues;
68
+ private readonly startupOrder;
69
+ private readonly listeners;
70
+ private readonly context;
71
+ constructor(options?: ContainerOptions);
72
+ private emit;
25
73
  private getId;
26
- /**
27
- * 注册服务到容器
28
- * @param fn - 服务函数
29
- * @returns - 服务注册信息
30
- */
74
+ private hasPath;
75
+ private trackDependency;
76
+ onEvent(listener: (event: ContainerEvent) => void): () => boolean;
77
+ offEvent(listener: (event: ContainerEvent) => void): void;
78
+ getLifecycle(id: number): ServiceLifecycleStage | undefined;
79
+ getDependencyGraph(): {
80
+ nodes: number[];
81
+ edges: {
82
+ from: number;
83
+ to: number;
84
+ }[];
85
+ };
86
+ getStartupOrder(): number[];
31
87
  register<R>(fn: ServiceFunction<R>): ServiceRegisterProps<R>;
32
- /**
33
- * 从容器中解决服务
34
- * 当服务未注册时,会自动注册并运行服务
35
- * 当服务已注册时,会返回服务实例
36
- * 当服务运行中时,会等待服务运行完成并返回服务实例
37
- * 当服务运行完成时,会返回服务实例
38
- * 当服务运行失败时,会返回错误
39
- * 多次调用正在运行中的服务时,不会重复运行同一服务,而是将等待状态(Promise)加入到等待队列,
40
- * 直到服务运行完毕被 resolve 或者 reject
41
- * @param props - 服务注册信息
42
- * @returns - 服务实例
43
- */
44
88
  resolve<R>(props: ServiceRegisterProps<R>): Promise<R>;
45
- /**
46
- * 运行服务
47
- * 注意:运行服务过程中将自动按顺序注册销毁函数,
48
- * 如果服务启动失败,则立即执行销毁函数,并返回错误
49
- * 销毁函数执行都是逆向执行的
50
- * 先加入的后执行,后加入的先执行
51
- * @param id - 服务ID
52
- * @param fn - 服务函数
53
- * @param callback - 回调函数
54
- */
55
89
  private run;
56
- /**
57
- * 销毁服务
58
- * @param id - 服务ID
59
- * @returns - 销毁结果
60
- */
61
90
  private shutdownService;
62
- /**
63
- * 销毁所有服务
64
- * 销毁过程都是逆向销毁的,
65
- * 先注册的后销毁,后注册的先销毁
66
- * @returns - 销毁结果
67
- */
68
91
  shutdown(): Promise<void>;
69
- /**
70
- * 检查服务是否已注册
71
- * @param fn - 服务函数
72
- * @returns - 是否已注册
73
- */
74
92
  hasService<R>(fn: ServiceFunction<R>): boolean;
75
- /**
76
- * 检查服务是否已运行
77
- * @param id - 服务ID
78
- * @returns - 是否已运行
79
- */
80
93
  hasMeta(id: number): boolean;
81
- /**
82
- * 获取服务ID
83
- * @param fn - 服务函数
84
- * @returns - 服务ID
85
- */
86
94
  getIdByService<R>(fn: ServiceFunction<R>): number | undefined;
87
- /**
88
- * 获取服务元数据
89
- * @param id - 服务ID
90
- * @returns - 服务元数据
91
- */
92
95
  getMetaById(id: number): Paddings<any> | undefined;
93
96
  }
94
97
  export declare const container: Container;
95
98
  export declare function defineService<R>(fn: ServiceFunction<R>): ServiceRegisterProps<R>;
96
99
  export declare function loadService<R>(props: ServiceRegisterProps<R>): Promise<R>;
97
- /**
98
- * 判断是否为服务
99
- * @param props - 服务注册信息
100
- * @returns - 是否为服务
101
- */
102
100
  export declare function isService<R>(props: ServiceRegisterProps<R>): boolean;
103
101
  export default container;
package/dist/index.js CHANGED
@@ -1,10 +1,45 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
1
2
  const sericeFlag = Symbol('service');
3
+ async function withTimeout(promise, timeoutMs, message) {
4
+ if (!timeoutMs || timeoutMs <= 0)
5
+ return promise;
6
+ let timer;
7
+ const timeoutPromise = new Promise((_, reject) => {
8
+ timer = setTimeout(() => reject(new Error(message || `Operation timeout after ${timeoutMs}ms`)), timeoutMs);
9
+ });
10
+ try {
11
+ return await Promise.race([promise, timeoutPromise]);
12
+ }
13
+ finally {
14
+ if (timer)
15
+ clearTimeout(timer);
16
+ }
17
+ }
2
18
  export class Container {
19
+ options;
3
20
  id = 1;
4
21
  packages = new Map();
5
22
  paddings = new Map();
23
+ dependencies = new Map();
24
+ dependents = new Map();
6
25
  shutdownFunctions = new Map();
7
26
  shutdownQueues = [];
27
+ startupOrder = [];
28
+ listeners = new Set();
29
+ context = new AsyncLocalStorage();
30
+ constructor(options = {}) {
31
+ this.options = options;
32
+ }
33
+ emit(event) {
34
+ for (const listener of this.listeners) {
35
+ try {
36
+ listener(event);
37
+ }
38
+ catch {
39
+ // ignore listener errors
40
+ }
41
+ }
42
+ }
8
43
  getId() {
9
44
  let i = this.id++;
10
45
  if (i >= Number.MAX_SAFE_INTEGER) {
@@ -12,11 +47,65 @@ export class Container {
12
47
  }
13
48
  return i;
14
49
  }
15
- /**
16
- * 注册服务到容器
17
- * @param fn - 服务函数
18
- * @returns - 服务注册信息
19
- */
50
+ hasPath(from, to, visited = new Set()) {
51
+ if (from === to)
52
+ return true;
53
+ if (visited.has(from))
54
+ return false;
55
+ visited.add(from);
56
+ const deps = this.dependencies.get(from);
57
+ if (!deps)
58
+ return false;
59
+ for (const next of deps) {
60
+ if (this.hasPath(next, to, visited))
61
+ return true;
62
+ }
63
+ return false;
64
+ }
65
+ trackDependency(parentId, childId) {
66
+ if (parentId === childId) {
67
+ throw new Error(`circular dependency detected: ${parentId} -> ${childId}`);
68
+ }
69
+ if (!this.dependencies.has(parentId)) {
70
+ this.dependencies.set(parentId, new Set());
71
+ }
72
+ if (!this.dependents.has(childId)) {
73
+ this.dependents.set(childId, new Set());
74
+ }
75
+ const parentDeps = this.dependencies.get(parentId);
76
+ if (!parentDeps.has(childId)) {
77
+ if (this.hasPath(childId, parentId)) {
78
+ const error = new Error(`circular dependency detected: ${parentId} -> ${childId}`);
79
+ this.emit({ type: 'container:error', error });
80
+ throw error;
81
+ }
82
+ parentDeps.add(childId);
83
+ this.dependents.get(childId).add(parentId);
84
+ }
85
+ }
86
+ onEvent(listener) {
87
+ this.listeners.add(listener);
88
+ return () => this.listeners.delete(listener);
89
+ }
90
+ offEvent(listener) {
91
+ this.listeners.delete(listener);
92
+ }
93
+ getLifecycle(id) {
94
+ return this.paddings.get(id)?.lifecycle;
95
+ }
96
+ getDependencyGraph() {
97
+ const nodes = Array.from(this.packages.values()).sort((a, b) => a - b);
98
+ const edges = [];
99
+ for (const [id, deps] of this.dependencies.entries()) {
100
+ for (const dep of deps) {
101
+ edges.push({ from: id, to: dep });
102
+ }
103
+ }
104
+ return { nodes, edges };
105
+ }
106
+ getStartupOrder() {
107
+ return [...this.startupOrder];
108
+ }
20
109
  register(fn) {
21
110
  if (this.packages.has(fn)) {
22
111
  return { id: this.packages.get(fn), fn, flag: sericeFlag };
@@ -25,20 +114,13 @@ export class Container {
25
114
  this.packages.set(fn, id);
26
115
  return { id, fn, flag: sericeFlag };
27
116
  }
28
- /**
29
- * 从容器中解决服务
30
- * 当服务未注册时,会自动注册并运行服务
31
- * 当服务已注册时,会返回服务实例
32
- * 当服务运行中时,会等待服务运行完成并返回服务实例
33
- * 当服务运行完成时,会返回服务实例
34
- * 当服务运行失败时,会返回错误
35
- * 多次调用正在运行中的服务时,不会重复运行同一服务,而是将等待状态(Promise)加入到等待队列,
36
- * 直到服务运行完毕被 resolve 或者 reject
37
- * @param props - 服务注册信息
38
- * @returns - 服务实例
39
- */
40
117
  resolve(props) {
41
118
  const { id, fn } = props;
119
+ const stack = this.context.getStore() || [];
120
+ const parentId = stack.length ? stack[stack.length - 1] : undefined;
121
+ if (parentId !== undefined) {
122
+ this.trackDependency(parentId, id);
123
+ }
42
124
  return new Promise((resolve, reject) => {
43
125
  if (!this.paddings.has(id)) {
44
126
  return this.run(id, fn, (e, v) => {
@@ -64,20 +146,20 @@ export class Container {
64
146
  }
65
147
  });
66
148
  }
67
- /**
68
- * 运行服务
69
- * 注意:运行服务过程中将自动按顺序注册销毁函数,
70
- * 如果服务启动失败,则立即执行销毁函数,并返回错误
71
- * 销毁函数执行都是逆向执行的
72
- * 先加入的后执行,后加入的先执行
73
- * @param id - 服务ID
74
- * @param fn - 服务函数
75
- * @param callback - 回调函数
76
- */
77
149
  run(id, fn, callback) {
78
- const state = { status: 0, value: undefined, queue: new Set() };
150
+ const state = {
151
+ status: 0,
152
+ lifecycle: 'init',
153
+ value: undefined,
154
+ queue: new Set(),
155
+ startedAt: Date.now(),
156
+ };
79
157
  this.paddings.set(id, state);
80
- const curDown = (fn) => {
158
+ if (!this.startupOrder.includes(id)) {
159
+ this.startupOrder.push(id);
160
+ }
161
+ this.emit({ type: 'service:init', id });
162
+ const curDown = (cutDownFn) => {
81
163
  if (!this.shutdownQueues.includes(id)) {
82
164
  this.shutdownQueues.push(id);
83
165
  }
@@ -85,13 +167,19 @@ export class Container {
85
167
  this.shutdownFunctions.set(id, []);
86
168
  }
87
169
  const pools = this.shutdownFunctions.get(id);
88
- if (!pools.includes(fn)) {
89
- pools.push(fn);
170
+ if (!pools.includes(cutDownFn)) {
171
+ pools.push(cutDownFn);
90
172
  }
91
173
  };
92
- Promise.resolve(fn(curDown)).then((value) => {
174
+ const parentStack = this.context.getStore() || [];
175
+ const startupPromise = this.context.run([...parentStack, id], () => Promise.resolve(fn(curDown)));
176
+ withTimeout(startupPromise, this.options.startTimeoutMs, `service startup timeout: ${id} exceeded ${this.options.startTimeoutMs}ms`).then((value) => {
93
177
  state.status = 1;
178
+ state.lifecycle = 'ready';
94
179
  state.value = value;
180
+ state.endedAt = Date.now();
181
+ const durationMs = state.endedAt - state.startedAt;
182
+ this.emit({ type: 'service:ready', id, durationMs });
95
183
  for (const queue of state.queue) {
96
184
  queue.resolve(value);
97
185
  }
@@ -99,81 +187,74 @@ export class Container {
99
187
  callback(null, value);
100
188
  }).catch(e => {
101
189
  state.status = -1;
190
+ state.lifecycle = 'stopping';
102
191
  state.error = e;
103
- // 通知所有等待的任务结果是失败的,并清空等待队列
192
+ state.endedAt = Date.now();
193
+ const durationMs = state.endedAt - state.startedAt;
194
+ this.emit({ type: 'service:error', id, error: e, durationMs });
104
195
  const clear = () => {
196
+ state.lifecycle = 'stopped';
105
197
  for (const queue of state.queue) {
106
198
  queue.reject(e);
107
199
  }
108
200
  state.queue.clear();
109
201
  callback(e);
110
202
  };
111
- // 已运行的销毁函数立即执行,
112
- // 无论成功失败都通知所有等待的任务结果是失败的,并清空等待队列
113
203
  this.shutdownService(id)
114
204
  .then(clear)
115
- .catch(clear);
205
+ .catch((shutdownError) => {
206
+ this.emit({ type: 'service:shutdown:error', id, error: shutdownError });
207
+ clear();
208
+ });
116
209
  });
117
210
  }
118
- /**
119
- * 销毁服务
120
- * @param id - 服务ID
121
- * @returns - 销毁结果
122
- */
123
211
  async shutdownService(id) {
124
212
  if (this.shutdownQueues.includes(id)) {
213
+ const meta = this.paddings.get(id);
214
+ if (meta) {
215
+ meta.lifecycle = 'stopping';
216
+ }
217
+ this.emit({ type: 'service:shutdown:start', id });
218
+ const startedAt = Date.now();
125
219
  const pools = this.shutdownFunctions.get(id);
126
220
  let i = pools.length;
127
221
  while (i--) {
128
- await Promise.resolve(pools[i]());
222
+ const teardown = pools[i];
223
+ try {
224
+ await withTimeout(Promise.resolve(teardown()), this.options.shutdownTimeoutMs, `service shutdown timeout: ${id} exceeded ${this.options.shutdownTimeoutMs}ms`);
225
+ }
226
+ catch (error) {
227
+ this.emit({ type: 'service:shutdown:error', id, error });
228
+ }
129
229
  }
130
230
  this.shutdownFunctions.delete(id);
131
231
  this.shutdownQueues.splice(this.shutdownQueues.indexOf(id), 1);
232
+ if (meta) {
233
+ meta.lifecycle = 'stopped';
234
+ }
235
+ this.emit({ type: 'service:shutdown:done', id, durationMs: Date.now() - startedAt });
132
236
  }
133
237
  }
134
- /**
135
- * 销毁所有服务
136
- * 销毁过程都是逆向销毁的,
137
- * 先注册的后销毁,后注册的先销毁
138
- * @returns - 销毁结果
139
- */
140
238
  async shutdown() {
239
+ const startedAt = Date.now();
240
+ this.emit({ type: 'container:shutdown:start' });
141
241
  let i = this.shutdownQueues.length;
142
242
  while (i--) {
143
243
  await this.shutdownService(this.shutdownQueues[i]);
144
244
  }
145
245
  this.shutdownFunctions.clear();
146
246
  this.shutdownQueues.length = 0;
247
+ this.emit({ type: 'container:shutdown:done', durationMs: Date.now() - startedAt });
147
248
  }
148
- /**
149
- * 检查服务是否已注册
150
- * @param fn - 服务函数
151
- * @returns - 是否已注册
152
- */
153
249
  hasService(fn) {
154
250
  return this.packages.has(fn);
155
251
  }
156
- /**
157
- * 检查服务是否已运行
158
- * @param id - 服务ID
159
- * @returns - 是否已运行
160
- */
161
252
  hasMeta(id) {
162
253
  return this.paddings.has(id);
163
254
  }
164
- /**
165
- * 获取服务ID
166
- * @param fn - 服务函数
167
- * @returns - 服务ID
168
- */
169
255
  getIdByService(fn) {
170
256
  return this.packages.get(fn);
171
257
  }
172
- /**
173
- * 获取服务元数据
174
- * @param id - 服务ID
175
- * @returns - 服务元数据
176
- */
177
258
  getMetaById(id) {
178
259
  return this.paddings.get(id);
179
260
  }
@@ -185,11 +266,6 @@ export function defineService(fn) {
185
266
  export function loadService(props) {
186
267
  return container.resolve(props);
187
268
  }
188
- /**
189
- * 判断是否为服务
190
- * @param props - 服务注册信息
191
- * @returns - 是否为服务
192
- */
193
269
  export function isService(props) {
194
270
  return props.flag === sericeFlag && typeof props.id === 'number' && typeof props.fn === 'function';
195
271
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hile/core",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "Hile core - lightweight async service container",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,5 +23,5 @@
23
23
  "@types/node": "^25.3.1",
24
24
  "vitest": "^4.0.18"
25
25
  },
26
- "gitHead": "6672fc4cdc551c4265912cc85ee2e96fc44bc4c9"
26
+ "gitHead": "81347b9de460b693ed82af46c0f4a287d4527323"
27
27
  }