@hile/core 1.0.17 → 1.0.19
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 +51 -3
- package/SKILL.md +63 -131
- package/dist/index.d.ts +61 -63
- package/dist/index.js +149 -73
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -120,7 +120,49 @@ export const connectionService = defineService(async (shutdown) => {
|
|
|
120
120
|
|
|
121
121
|
> 建议始终使用 `async` 服务函数,确保异常路径可正确触发销毁机制。
|
|
122
122
|
|
|
123
|
-
### 6)
|
|
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.on((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
|
-
###
|
|
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
|
+
| `on(listener)` | 订阅容器事件,返回取消订阅函数 |
|
|
222
|
+
| `off(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/SKILL.md
CHANGED
|
@@ -1,180 +1,112 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hile-core
|
|
3
|
-
description: @hile/core 的代码生成与使用规范。适用于定义/加载 Hile
|
|
3
|
+
description: "@hile/core 的代码生成与使用规范。适用于定义/加载 Hile 服务、生命周期编排、依赖图与容器事件相关场景。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# @hile/core SKILL
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
本文档面向代码生成器与维护者,目标是确保生成代码严格符合容器语义。
|
|
9
9
|
|
|
10
|
-
## 1.
|
|
10
|
+
## 1. 强约束(必须遵守)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
1. 服务必须使用 `async (shutdown)` 形态定义。
|
|
13
|
+
2. 只能通过 `defineService` / `container.register` 产出服务对象。
|
|
14
|
+
3. 只能通过 `loadService` / `container.resolve` 获取服务实例。
|
|
15
|
+
4. 外部资源创建后必须立即注册 `shutdown`。
|
|
16
|
+
5. 禁止在模块顶层缓存 `await loadService(...)` 结果。
|
|
17
|
+
6. 依赖服务必须在服务函数内部加载。
|
|
18
|
+
7. 多个 teardown 默认按 LIFO 顺序执行。
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
- 并发合并:并发加载同一服务时共享同一初始化过程
|
|
16
|
-
- 失败回收:初始化失败时自动执行已注册的清理回调
|
|
20
|
+
## 2. 生命周期与超时约束
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
容器生命周期:`init -> ready -> stopping -> stopped`。
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
- 启动超时:`new Container({ startTimeoutMs })`
|
|
25
|
+
- 销毁超时:`new Container({ shutdownTimeoutMs })`
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
type ServiceCutDownFunction = () => unknown | Promise<unknown>;
|
|
24
|
-
type ServiceCutDownHandler = (fn: ServiceCutDownFunction) => void;
|
|
25
|
-
type ServiceFunction<R> = (shutdown: ServiceCutDownHandler) => R | Promise<R>;
|
|
26
|
-
|
|
27
|
-
const sericeFlag = Symbol('service');
|
|
27
|
+
生成代码时:
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
fn: ServiceFunction<R>;
|
|
32
|
-
flag: typeof sericeFlag;
|
|
33
|
-
}
|
|
34
|
-
```
|
|
29
|
+
- 不要吞掉启动超时错误。
|
|
30
|
+
- 不要假设 teardown 一定成功;应允许 `service:shutdown:error` 事件出现。
|
|
35
31
|
|
|
36
|
-
## 3.
|
|
32
|
+
## 3. 可观测事件约束
|
|
37
33
|
|
|
38
|
-
|
|
34
|
+
允许订阅:`container.on(listener)`。
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
import { defineService } from '@hile/core'
|
|
36
|
+
关键事件:
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
- `service:init`
|
|
39
|
+
- `service:ready`
|
|
40
|
+
- `service:error`
|
|
41
|
+
- `service:shutdown:start`
|
|
42
|
+
- `service:shutdown:done`
|
|
43
|
+
- `service:shutdown:error`
|
|
44
|
+
- `container:shutdown:start`
|
|
45
|
+
- `container:shutdown:done`
|
|
46
|
+
- `container:error`
|
|
49
47
|
|
|
50
48
|
规则:
|
|
51
49
|
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
- `defineService` 结果需使用模块级 `export const` 暴露
|
|
55
|
-
- 命名建议以 `Service` 结尾
|
|
50
|
+
- 订阅后必须在生命周期结束时取消订阅。
|
|
51
|
+
- 记录错误时保留原始 error 对象。
|
|
56
52
|
|
|
57
|
-
|
|
53
|
+
## 4. 依赖图与循环依赖
|
|
58
54
|
|
|
59
|
-
|
|
60
|
-
import { loadService } from '@hile/core'
|
|
61
|
-
import { databaseService } from './services/database'
|
|
55
|
+
容器会自动记录服务依赖并检测循环依赖:
|
|
62
56
|
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
- `getDependencyGraph()`
|
|
58
|
+
- `getStartupOrder()`
|
|
65
59
|
|
|
66
60
|
规则:
|
|
67
61
|
|
|
68
|
-
-
|
|
69
|
-
- `
|
|
62
|
+
- 不要绕开容器手动构建“隐式全局单例依赖”。
|
|
63
|
+
- 出现 `circular dependency detected` 时应通过拆分服务职责或引入中间层服务解决。
|
|
70
64
|
|
|
71
|
-
|
|
65
|
+
## 5. 反模式(禁止)
|
|
72
66
|
|
|
73
|
-
|
|
74
|
-
import { defineService, loadService } from '@hile/core'
|
|
75
|
-
import { databaseService } from './database'
|
|
76
|
-
|
|
77
|
-
export const userService = defineService(async (shutdown) => {
|
|
78
|
-
const db = await loadService(databaseService)
|
|
79
|
-
return new UserRepository(db)
|
|
80
|
-
})
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
规则:
|
|
84
|
-
|
|
85
|
-
- 在服务函数内部加载依赖
|
|
86
|
-
- 不在模块顶层缓存 `loadService` 结果
|
|
87
|
-
|
|
88
|
-
### 3.4 注册清理回调
|
|
67
|
+
### 5.1 顶层缓存实例
|
|
89
68
|
|
|
90
69
|
```typescript
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
shutdown(() => a.close())
|
|
94
|
-
|
|
95
|
-
const b = await connectB()
|
|
96
|
-
shutdown(() => b.close())
|
|
70
|
+
// ✗
|
|
71
|
+
const db = await loadService(dbService)
|
|
97
72
|
|
|
98
|
-
|
|
99
|
-
|
|
73
|
+
// ✓
|
|
74
|
+
export async function query(sql: string) {
|
|
75
|
+
const db = await loadService(dbService)
|
|
76
|
+
return db.query(sql)
|
|
77
|
+
}
|
|
100
78
|
```
|
|
101
79
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
- 资源创建后立即注册清理
|
|
105
|
-
- 回调按 LIFO 执行
|
|
106
|
-
- 支持异步回调
|
|
107
|
-
|
|
108
|
-
### 3.5 全局优雅关闭
|
|
80
|
+
### 5.2 手动伪造服务对象
|
|
109
81
|
|
|
110
82
|
```typescript
|
|
111
|
-
|
|
83
|
+
// ✗
|
|
84
|
+
const fake = { id: 1, fn: async () => 1 }
|
|
112
85
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
process.exit(0)
|
|
116
|
-
})
|
|
86
|
+
// ✓
|
|
87
|
+
const real = defineService(async () => 1)
|
|
117
88
|
```
|
|
118
89
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
1. 服务函数必须使用 `async`,避免同步 `throw` 破坏销毁机制。
|
|
122
|
-
2. 不要手动构造 `ServiceRegisterProps`。
|
|
123
|
-
3. 不要在工厂函数里动态调用 `defineService` 生成新引用。
|
|
124
|
-
4. 不要在模块顶层 `await loadService(...)`。
|
|
125
|
-
5. 每个外部资源都应对应一次 `shutdown` 注册。
|
|
126
|
-
|
|
127
|
-
## 5. 常见反模式
|
|
128
|
-
|
|
129
|
-
### 同步 throw
|
|
90
|
+
### 5.3 不注册资源清理
|
|
130
91
|
|
|
131
92
|
```typescript
|
|
132
93
|
// ✗
|
|
133
|
-
export const bad = defineService((
|
|
134
|
-
|
|
135
|
-
shutdown(() => r.close())
|
|
136
|
-
throw new Error('boom')
|
|
94
|
+
export const bad = defineService(async () => {
|
|
95
|
+
return await createPool()
|
|
137
96
|
})
|
|
138
97
|
|
|
139
98
|
// ✓
|
|
140
99
|
export const good = defineService(async (shutdown) => {
|
|
141
|
-
const
|
|
142
|
-
shutdown(() =>
|
|
143
|
-
|
|
100
|
+
const pool = await createPool()
|
|
101
|
+
shutdown(() => pool.end())
|
|
102
|
+
return pool
|
|
144
103
|
})
|
|
145
104
|
```
|
|
146
105
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
```typescript
|
|
150
|
-
// ✗
|
|
151
|
-
const db = await loadService(databaseService)
|
|
152
|
-
|
|
153
|
-
// ✓
|
|
154
|
-
export async function query(sql: string) {
|
|
155
|
-
const db = await loadService(databaseService)
|
|
156
|
-
return db.query(sql)
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
## 6. API 速查
|
|
161
|
-
|
|
162
|
-
### 便捷函数
|
|
163
|
-
|
|
164
|
-
| 函数 | 说明 |
|
|
165
|
-
|---|---|
|
|
166
|
-
| `defineService(fn)` | 注册服务到默认容器 |
|
|
167
|
-
| `loadService(props)` | 加载服务实例 |
|
|
168
|
-
| `isService(props)` | 判断是否为合法服务注册对象 |
|
|
169
|
-
|
|
170
|
-
### Container
|
|
106
|
+
## 6. 边界条件清单
|
|
171
107
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
| `hasMeta(id)` | 检查运行时元数据 |
|
|
178
|
-
| `getIdByService(fn)` | 通过函数获取 ID |
|
|
179
|
-
| `getMetaById(id)` | 通过 ID 获取元数据 |
|
|
180
|
-
| `shutdown()` | 销毁所有服务 |
|
|
108
|
+
- [ ] 服务同步抛错路径是否可观测
|
|
109
|
+
- [ ] 异步 reject 路径是否会触发 teardown
|
|
110
|
+
- [ ] teardown 抛错是否不覆盖原始业务错误
|
|
111
|
+
- [ ] 并发 resolve 同一服务是否只初始化一次
|
|
112
|
+
- [ ] shutdown 重复调用是否幂等
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
74
|
+
private hasPath;
|
|
75
|
+
private trackDependency;
|
|
76
|
+
on(listener: (event: ContainerEvent) => void): () => boolean;
|
|
77
|
+
off(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
|
-
|
|
18
|
-
|
|
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
|
+
on(listener) {
|
|
87
|
+
this.listeners.add(listener);
|
|
88
|
+
return () => this.listeners.delete(listener);
|
|
89
|
+
}
|
|
90
|
+
off(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 = {
|
|
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
|
-
|
|
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(
|
|
89
|
-
pools.push(
|
|
170
|
+
if (!pools.includes(cutDownFn)) {
|
|
171
|
+
pools.push(cutDownFn);
|
|
90
172
|
}
|
|
91
173
|
};
|
|
92
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.0.19",
|
|
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": "
|
|
26
|
+
"gitHead": "4bb9d38b309e72c720f2cba579ce5c498d27e044"
|
|
27
27
|
}
|