@hile/core 1.0.16 → 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 +92 -52
- package/SKILL.md +75 -351
- package/dist/index.d.ts +61 -63
- package/dist/index.js +149 -73
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hile/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
轻量级异步服务容器,提供单例管理、并发请求合并与生命周期销毁能力。纯 TypeScript 实现,零运行时依赖。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
@@ -13,7 +13,6 @@ pnpm add @hile/core
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { defineService, loadService } from '@hile/core'
|
|
15
15
|
|
|
16
|
-
// 定义服务
|
|
17
16
|
const greeterService = defineService(async (shutdown) => {
|
|
18
17
|
return {
|
|
19
18
|
hello(name: string) {
|
|
@@ -22,16 +21,15 @@ const greeterService = defineService(async (shutdown) => {
|
|
|
22
21
|
}
|
|
23
22
|
})
|
|
24
23
|
|
|
25
|
-
// 加载并使用
|
|
26
24
|
const greeter = await loadService(greeterService)
|
|
27
|
-
greeter.hello('World') //
|
|
25
|
+
greeter.hello('World') // Hello, World!
|
|
28
26
|
```
|
|
29
27
|
|
|
30
28
|
## 核心概念
|
|
31
29
|
|
|
32
|
-
### 定义服务
|
|
30
|
+
### 1) 定义服务
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
通过 `defineService` 注册服务。服务函数接收 `shutdown` 注册器,用于登记资源清理回调。
|
|
35
33
|
|
|
36
34
|
```typescript
|
|
37
35
|
import { defineService } from '@hile/core'
|
|
@@ -43,9 +41,9 @@ export const databaseService = defineService(async (shutdown) => {
|
|
|
43
41
|
})
|
|
44
42
|
```
|
|
45
43
|
|
|
46
|
-
### 加载服务
|
|
44
|
+
### 2) 加载服务
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
通过 `loadService` 获取服务实例。容器保证同一服务函数只执行一次。
|
|
49
47
|
|
|
50
48
|
```typescript
|
|
51
49
|
import { loadService } from '@hile/core'
|
|
@@ -55,23 +53,23 @@ const db = await loadService(databaseService)
|
|
|
55
53
|
const users = await db.query('SELECT * FROM users')
|
|
56
54
|
```
|
|
57
55
|
|
|
58
|
-
### 并发请求合并
|
|
56
|
+
### 3) 并发请求合并
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
并发加载同一服务时,初始化只执行一次,调用方共享结果。
|
|
61
59
|
|
|
62
60
|
```typescript
|
|
63
|
-
// 三个并发调用,服务函数只执行一次
|
|
64
61
|
const [r1, r2, r3] = await Promise.all([
|
|
65
62
|
loadService(heavyService),
|
|
66
63
|
loadService(heavyService),
|
|
67
64
|
loadService(heavyService),
|
|
68
65
|
])
|
|
66
|
+
|
|
69
67
|
// r1 === r2 === r3
|
|
70
68
|
```
|
|
71
69
|
|
|
72
|
-
### 服务间依赖
|
|
70
|
+
### 4) 服务间依赖
|
|
73
71
|
|
|
74
|
-
|
|
72
|
+
服务内部可通过 `loadService` 继续加载依赖服务。
|
|
75
73
|
|
|
76
74
|
```typescript
|
|
77
75
|
import { defineService, loadService } from '@hile/core'
|
|
@@ -94,37 +92,77 @@ export const userService = defineService(async (shutdown) => {
|
|
|
94
92
|
})
|
|
95
93
|
```
|
|
96
94
|
|
|
97
|
-
### 资源销毁(Shutdown)
|
|
95
|
+
### 5) 资源销毁(Shutdown)
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
当服务初始化失败或手动执行全局关闭时,容器会按规则执行已注册的清理回调。
|
|
100
98
|
|
|
101
99
|
```typescript
|
|
102
100
|
export const connectionService = defineService(async (shutdown) => {
|
|
103
101
|
const primary = await connectPrimary()
|
|
104
|
-
shutdown(() => primary.disconnect())
|
|
102
|
+
shutdown(() => primary.disconnect())
|
|
105
103
|
|
|
106
104
|
const replica = await connectReplica()
|
|
107
|
-
shutdown(() => replica.disconnect())
|
|
105
|
+
shutdown(() => replica.disconnect())
|
|
108
106
|
|
|
109
107
|
const cache = await initCache()
|
|
110
|
-
shutdown(() => cache.flush())
|
|
108
|
+
shutdown(() => cache.flush())
|
|
111
109
|
|
|
112
110
|
return { primary, replica, cache }
|
|
113
111
|
})
|
|
114
112
|
```
|
|
115
113
|
|
|
116
|
-
|
|
114
|
+
特性:
|
|
117
115
|
|
|
118
|
-
-
|
|
116
|
+
- 清理回调按逆序(LIFO)执行
|
|
119
117
|
- 支持异步清理函数
|
|
120
|
-
-
|
|
121
|
-
-
|
|
118
|
+
- 同一函数引用重复注册只执行一次
|
|
119
|
+
- 清理函数错误不会覆盖原始业务错误
|
|
120
|
+
|
|
121
|
+
> 建议始终使用 `async` 服务函数,确保异常路径可正确触发销毁机制。
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
### 6) 生命周期、超时与可观测事件
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
容器支持显式生命周期阶段:`init -> ready -> stopping -> stopped`。
|
|
126
126
|
|
|
127
|
-
|
|
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)
|
|
128
166
|
|
|
129
167
|
```typescript
|
|
130
168
|
import container from '@hile/core'
|
|
@@ -135,24 +173,20 @@ process.on('SIGTERM', async () => {
|
|
|
135
173
|
})
|
|
136
174
|
```
|
|
137
175
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
### 服务校验(isService)
|
|
141
|
-
|
|
142
|
-
使用 `isService` 判断一个对象是否为合法的服务注册信息。内部通过不可伪造的 Symbol 标识校验,确保只有通过 `defineService` / `container.register` 创建的对象才会返回 `true`:
|
|
176
|
+
### 9) 服务校验(isService)
|
|
143
177
|
|
|
144
178
|
```typescript
|
|
145
179
|
import { defineService, isService } from '@hile/core'
|
|
146
180
|
|
|
147
181
|
const myService = defineService(async (shutdown) => 'hello')
|
|
148
182
|
|
|
149
|
-
isService(myService)
|
|
150
|
-
isService({ id: 1, fn: () => {} } as any)
|
|
183
|
+
isService(myService) // true
|
|
184
|
+
isService({ id: 1, fn: () => {} } as any) // false
|
|
151
185
|
```
|
|
152
186
|
|
|
153
187
|
## 隔离容器
|
|
154
188
|
|
|
155
|
-
|
|
189
|
+
除了默认容器,也可以手动创建独立容器以实现作用域隔离。
|
|
156
190
|
|
|
157
191
|
```typescript
|
|
158
192
|
import { Container } from '@hile/core'
|
|
@@ -168,31 +202,37 @@ const result = await container.resolve(service)
|
|
|
168
202
|
|
|
169
203
|
## API
|
|
170
204
|
|
|
171
|
-
###
|
|
205
|
+
### 顶层函数
|
|
172
206
|
|
|
173
207
|
| 函数 | 说明 |
|
|
174
208
|
|------|------|
|
|
175
|
-
| `defineService(fn)` |
|
|
176
|
-
| `loadService(props)` |
|
|
177
|
-
| `isService(props)` |
|
|
209
|
+
| `defineService(fn)` | 注册服务到默认容器 |
|
|
210
|
+
| `loadService(props)` | 从默认容器加载服务 |
|
|
211
|
+
| `isService(props)` | 判断对象是否为合法服务注册信息 |
|
|
178
212
|
|
|
179
|
-
### Container
|
|
213
|
+
### `Container`
|
|
180
214
|
|
|
181
215
|
| 方法 | 说明 |
|
|
182
216
|
|------|------|
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
217
|
+
| `new Container(options?)` | 创建容器,可配置启动/销毁超时 |
|
|
218
|
+
| `register(fn)` | 注册服务(同函数引用去重) |
|
|
219
|
+
| `resolve(props)` | 加载服务(执行、等待或返回缓存) |
|
|
220
|
+
| `shutdown()` | 销毁所有服务并执行清理回调 |
|
|
221
|
+
| `onEvent(listener)` | 订阅容器事件,返回取消订阅函数 |
|
|
222
|
+
| `offEvent(listener)` | 取消订阅 |
|
|
223
|
+
| `getLifecycle(id)` | 获取服务生命周期阶段 |
|
|
224
|
+
| `getDependencyGraph()` | 获取依赖图 `{ nodes, edges }` |
|
|
225
|
+
| `getStartupOrder()` | 获取服务启动顺序(首次启动顺序) |
|
|
226
|
+
| `hasService(fn)` | 检查函数是否已注册 |
|
|
227
|
+
| `hasMeta(id)` | 检查服务是否已有运行时元数据 |
|
|
228
|
+
| `getIdByService(fn)` | 通过函数获取服务 ID |
|
|
229
|
+
| `getMetaById(id)` | 通过 ID 获取运行时元数据 |
|
|
190
230
|
|
|
191
231
|
### 服务状态
|
|
192
232
|
|
|
193
|
-
| 状态 | 值 | `resolve`
|
|
194
|
-
|
|
195
|
-
|
|
|
233
|
+
| 状态 | 值 | `resolve` 行为 |
|
|
234
|
+
|------|---|---------------|
|
|
235
|
+
| 未运行 | — | 执行服务函数 |
|
|
196
236
|
| 运行中 | `0` | 加入等待队列 |
|
|
197
237
|
| 已成功 | `1` | 返回缓存值 |
|
|
198
238
|
| 已失败 | `-1` | 返回缓存错误 |
|
|
@@ -201,9 +241,9 @@ const result = await container.resolve(service)
|
|
|
201
241
|
|
|
202
242
|
```bash
|
|
203
243
|
pnpm install
|
|
204
|
-
pnpm build
|
|
205
|
-
pnpm dev
|
|
206
|
-
pnpm test
|
|
244
|
+
pnpm build
|
|
245
|
+
pnpm dev
|
|
246
|
+
pnpm test
|
|
207
247
|
```
|
|
208
248
|
|
|
209
249
|
## License
|
package/SKILL.md
CHANGED
|
@@ -1,44 +1,31 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hile-core
|
|
3
|
-
description:
|
|
3
|
+
description: @hile/core 的代码生成与使用规范。适用于定义/加载 Hile 服务、生命周期 shutdown 编排、或涉及 defineService、loadService、Container 等话题。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# @hile/core SKILL
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
本文档用于约束 AI 与开发者在使用 `@hile/core` 时的代码生成方式,目标是保证服务定义、依赖加载与资源销毁行为一致且可维护。
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## 1. 架构总览
|
|
13
|
-
|
|
14
|
-
Hile 的核心是 `Container`(服务容器)。所有服务都必须经过 **定义 → 加载** 两个步骤才能使用。容器保证:
|
|
15
|
-
|
|
16
|
-
- 每个服务函数只执行一次(**单例**)
|
|
17
|
-
- 并发请求同一服务时自动合并(**不重复执行**)
|
|
18
|
-
- 服务启动失败时自动执行已注册的清理回调(**销毁机制**)
|
|
10
|
+
## 1. 架构概览
|
|
19
11
|
|
|
20
|
-
|
|
12
|
+
Hile 以 `Container` 为核心,服务遵循“定义 → 加载”两阶段:
|
|
21
13
|
|
|
22
|
-
|
|
14
|
+
- 单例:同一服务函数仅初始化一次
|
|
15
|
+
- 并发合并:并发加载同一服务时共享同一初始化过程
|
|
16
|
+
- 失败回收:初始化失败时自动执行已注册的清理回调
|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
模块默认导出全局容器,并提供 `defineService` / `loadService` 便捷函数。
|
|
25
19
|
|
|
26
|
-
|
|
20
|
+
## 2. 关键类型
|
|
27
21
|
|
|
28
22
|
```typescript
|
|
29
|
-
// 销毁回调:无参数,可返回 Promise
|
|
30
23
|
type ServiceCutDownFunction = () => unknown | Promise<unknown>;
|
|
31
|
-
|
|
32
|
-
// 销毁注册器:在服务函数内部调用,将清理回调注册到容器
|
|
33
24
|
type ServiceCutDownHandler = (fn: ServiceCutDownFunction) => void;
|
|
34
|
-
|
|
35
|
-
// 服务函数:第一个参数固定为销毁注册器,返回值为同步值或 Promise
|
|
36
25
|
type ServiceFunction<R> = (shutdown: ServiceCutDownHandler) => R | Promise<R>;
|
|
37
26
|
|
|
38
|
-
// 内部服务标识(Symbol,不可外部构造)
|
|
39
27
|
const sericeFlag = Symbol('service');
|
|
40
28
|
|
|
41
|
-
// 服务注册信息:由 defineService/register 返回,作为 loadService/resolve 的入参
|
|
42
29
|
interface ServiceRegisterProps<R> {
|
|
43
30
|
id: number;
|
|
44
31
|
fn: ServiceFunction<R>;
|
|
@@ -46,39 +33,28 @@ interface ServiceRegisterProps<R> {
|
|
|
46
33
|
}
|
|
47
34
|
```
|
|
48
35
|
|
|
49
|
-
|
|
36
|
+
## 3. 标准模板
|
|
50
37
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
### 3.1 定义服务(必须遵循的模式)
|
|
54
|
-
|
|
55
|
-
**模板:**
|
|
38
|
+
### 3.1 定义服务
|
|
56
39
|
|
|
57
40
|
```typescript
|
|
58
41
|
import { defineService } from '@hile/core'
|
|
59
42
|
|
|
60
43
|
export const xxxService = defineService(async (shutdown) => {
|
|
61
|
-
// 1. 初始化资源
|
|
62
44
|
const resource = await createResource()
|
|
63
|
-
|
|
64
|
-
// 2. 注册销毁回调(每创建一个资源就注册一个对应的清理)
|
|
65
45
|
shutdown(() => resource.close())
|
|
66
|
-
|
|
67
|
-
// 3. 返回服务实例
|
|
68
46
|
return resource
|
|
69
47
|
})
|
|
70
48
|
```
|
|
71
49
|
|
|
72
|
-
|
|
73
|
-
- 服务函数的第一个参数 **必须** 命名为 `shutdown`,类型为 `ServiceCutDownHandler`
|
|
74
|
-
- 服务函数 **应当** 使用 `async` 声明(确保销毁机制在失败时正确触发)
|
|
75
|
-
- `defineService` 的返回值 **必须** 赋值给一个模块级常量并 `export`
|
|
76
|
-
- 常量命名 **必须** 以 `Service` 结尾(如 `databaseService`、`cacheService`)
|
|
77
|
-
- 每个服务定义 **应当** 放在独立的文件中
|
|
50
|
+
规则:
|
|
78
51
|
|
|
79
|
-
|
|
52
|
+
- 服务函数第一个参数固定为 `shutdown`
|
|
53
|
+
- 推荐必须使用 `async`
|
|
54
|
+
- `defineService` 结果需使用模块级 `export const` 暴露
|
|
55
|
+
- 命名建议以 `Service` 结尾
|
|
80
56
|
|
|
81
|
-
|
|
57
|
+
### 3.2 加载服务
|
|
82
58
|
|
|
83
59
|
```typescript
|
|
84
60
|
import { loadService } from '@hile/core'
|
|
@@ -87,68 +63,49 @@ import { databaseService } from './services/database'
|
|
|
87
63
|
const db = await loadService(databaseService)
|
|
88
64
|
```
|
|
89
65
|
|
|
90
|
-
|
|
91
|
-
- **永远** 使用 `loadService()` 获取服务实例,**不要** 直接调用服务函数
|
|
92
|
-
- `loadService` 返回 `Promise`,**必须** `await`
|
|
93
|
-
- 可以在任何地方多次调用 `loadService(同一个服务)`,容器保证只执行一次
|
|
66
|
+
规则:
|
|
94
67
|
|
|
95
|
-
|
|
68
|
+
- 始终通过 `loadService` 获取实例
|
|
69
|
+
- `loadService` 返回 Promise,必须 `await`
|
|
96
70
|
|
|
97
|
-
|
|
71
|
+
### 3.3 服务依赖服务
|
|
98
72
|
|
|
99
73
|
```typescript
|
|
100
74
|
import { defineService, loadService } from '@hile/core'
|
|
101
75
|
import { databaseService } from './database'
|
|
102
|
-
import { configService } from './config'
|
|
103
76
|
|
|
104
77
|
export const userService = defineService(async (shutdown) => {
|
|
105
|
-
// 加载依赖的服务(若已完成则直接返回缓存,否则等待)
|
|
106
|
-
const config = await loadService(configService)
|
|
107
78
|
const db = await loadService(databaseService)
|
|
108
|
-
|
|
109
|
-
const repo = new UserRepository(db, config)
|
|
110
|
-
shutdown(() => repo.dispose())
|
|
111
|
-
|
|
112
|
-
return repo
|
|
79
|
+
return new UserRepository(db)
|
|
113
80
|
})
|
|
114
81
|
```
|
|
115
82
|
|
|
116
|
-
|
|
117
|
-
- 依赖的服务通过 `import` 引入其 `ServiceRegisterProps`,然后在函数体内 `loadService` 加载
|
|
118
|
-
- **不要** 将 `loadService` 的结果缓存到模块作用域变量中
|
|
119
|
-
- **不要** 在服务函数外部调用 `loadService` 来获取另一个服务并传入——应在服务函数内部加载
|
|
83
|
+
规则:
|
|
120
84
|
|
|
121
|
-
|
|
85
|
+
- 在服务函数内部加载依赖
|
|
86
|
+
- 不在模块顶层缓存 `loadService` 结果
|
|
122
87
|
|
|
123
|
-
|
|
88
|
+
### 3.4 注册清理回调
|
|
124
89
|
|
|
125
90
|
```typescript
|
|
126
91
|
export const connectionService = defineService(async (shutdown) => {
|
|
127
|
-
const
|
|
128
|
-
shutdown(() =>
|
|
129
|
-
|
|
130
|
-
const replica = await connectReplica()
|
|
131
|
-
shutdown(() => replica.disconnect()) // 注册第 2 个
|
|
92
|
+
const a = await connectA()
|
|
93
|
+
shutdown(() => a.close())
|
|
132
94
|
|
|
133
|
-
const
|
|
134
|
-
shutdown(() =>
|
|
95
|
+
const b = await connectB()
|
|
96
|
+
shutdown(() => b.close())
|
|
135
97
|
|
|
136
|
-
return {
|
|
98
|
+
return { a, b }
|
|
137
99
|
})
|
|
138
100
|
```
|
|
139
101
|
|
|
140
|
-
|
|
141
|
-
- 每初始化一个需要清理的资源后,**立即** 调用 `shutdown()` 注册对应的清理函数
|
|
142
|
-
- 不要把所有清理逻辑放在一个 shutdown 里,**每个资源对应一个 shutdown 调用**
|
|
143
|
-
- 销毁函数按 **逆序(LIFO)** 执行:后注册的先执行,先注册的后执行
|
|
144
|
-
- 销毁函数可以是 `async`,容器会依次 `await`
|
|
145
|
-
- 同一个函数引用多次传给 `shutdown()` 只会注册一次
|
|
146
|
-
|
|
147
|
-
### 3.5 手动销毁所有服务
|
|
102
|
+
规则:
|
|
148
103
|
|
|
149
|
-
|
|
104
|
+
- 资源创建后立即注册清理
|
|
105
|
+
- 回调按 LIFO 执行
|
|
106
|
+
- 支持异步回调
|
|
150
107
|
|
|
151
|
-
|
|
108
|
+
### 3.5 全局优雅关闭
|
|
152
109
|
|
|
153
110
|
```typescript
|
|
154
111
|
import container from '@hile/core'
|
|
@@ -159,298 +116,65 @@ process.on('SIGTERM', async () => {
|
|
|
159
116
|
})
|
|
160
117
|
```
|
|
161
118
|
|
|
162
|
-
|
|
163
|
-
- `shutdown()` 返回 `Promise`,**必须** `await`
|
|
164
|
-
- 销毁按 **服务注册逆序** 执行:后注册的服务先销毁
|
|
165
|
-
- 每个服务内部的销毁回调同样按 **逆序(LIFO)** 执行
|
|
166
|
-
- `shutdown()` 执行完毕后,再次调用不会重复执行(销毁队列已被清空)
|
|
119
|
+
## 4. 强制规则
|
|
167
120
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
121
|
+
1. 服务函数必须使用 `async`,避免同步 `throw` 破坏销毁机制。
|
|
122
|
+
2. 不要手动构造 `ServiceRegisterProps`。
|
|
123
|
+
3. 不要在工厂函数里动态调用 `defineService` 生成新引用。
|
|
124
|
+
4. 不要在模块顶层 `await loadService(...)`。
|
|
125
|
+
5. 每个外部资源都应对应一次 `shutdown` 注册。
|
|
171
126
|
|
|
172
|
-
|
|
173
|
-
|---|------|------|
|
|
174
|
-
| 1 | 服务函数**必须**使用 `async` 声明 | 同步 `throw` 不会触发销毁机制,只有异步 reject 才会触发 |
|
|
175
|
-
| 2 | 服务函数第一个参数**必须**是 `shutdown` | 这是容器注入的销毁注册器,即使不使用也要声明 |
|
|
176
|
-
| 3 | `defineService` 的结果**必须**赋给模块级 `export const` | 服务基于函数引用去重,引用必须稳定 |
|
|
177
|
-
| 4 | **不要**在 `defineService` 内直接写匿名函数再传给另一个函数 | 每次调用会创建新引用,导致重复注册 |
|
|
178
|
-
| 5 | **不要**手动构造 `ServiceRegisterProps` 对象 | 必须通过 `defineService` 或 `container.register` 获取,内部 `flag` 为不可伪造的 Symbol |
|
|
179
|
-
| 6 | **不要**缓存 `loadService` 的结果到模块顶层变量 | 服务可能尚未初始化,应在需要时 `await loadService()` |
|
|
180
|
-
| 7 | 每个外部资源初始化后**立即**注册 `shutdown` | 确保初始化中途失败时已创建的资源能被正确清理 |
|
|
181
|
-
| 8 | 一个文件只定义一个服务 | 保持服务职责单一、依赖清晰 |
|
|
182
|
-
|
|
183
|
-
---
|
|
127
|
+
## 5. 常见反模式
|
|
184
128
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
```
|
|
188
|
-
src/
|
|
189
|
-
├── services/
|
|
190
|
-
│ ├── config.ts # 配置服务
|
|
191
|
-
│ ├── database.ts # 数据库服务(依赖 config)
|
|
192
|
-
│ ├── cache.ts # 缓存服务(依赖 config)
|
|
193
|
-
│ └── user.ts # 用户服务(依赖 database, cache)
|
|
194
|
-
└── main.ts # 入口
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
### services/config.ts
|
|
129
|
+
### 同步 throw
|
|
198
130
|
|
|
199
131
|
```typescript
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export const configService = defineService(async (shutdown) => {
|
|
208
|
-
const config: AppConfig = {
|
|
209
|
-
dbUrl: process.env.DB_URL ?? 'postgres://localhost:5432/app',
|
|
210
|
-
cacheHost: process.env.CACHE_HOST ?? 'localhost',
|
|
211
|
-
}
|
|
212
|
-
return config
|
|
132
|
+
// ✗
|
|
133
|
+
export const bad = defineService((shutdown) => {
|
|
134
|
+
const r = createSync()
|
|
135
|
+
shutdown(() => r.close())
|
|
136
|
+
throw new Error('boom')
|
|
213
137
|
})
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
### services/database.ts
|
|
217
|
-
|
|
218
|
-
```typescript
|
|
219
|
-
import { defineService, loadService } from '@hile/core'
|
|
220
|
-
import { configService } from './config'
|
|
221
138
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
shutdown(() =>
|
|
226
|
-
|
|
139
|
+
// ✓
|
|
140
|
+
export const good = defineService(async (shutdown) => {
|
|
141
|
+
const r = await createAsync()
|
|
142
|
+
shutdown(() => r.close())
|
|
143
|
+
throw new Error('boom')
|
|
227
144
|
})
|
|
228
145
|
```
|
|
229
146
|
|
|
230
|
-
###
|
|
147
|
+
### 顶层缓存服务实例
|
|
231
148
|
|
|
232
149
|
```typescript
|
|
233
|
-
|
|
234
|
-
import { configService } from './config'
|
|
235
|
-
|
|
236
|
-
export const cacheService = defineService(async (shutdown) => {
|
|
237
|
-
const config = await loadService(configService)
|
|
238
|
-
const client = await createRedisClient(config.cacheHost)
|
|
239
|
-
shutdown(() => client.quit())
|
|
240
|
-
return client
|
|
241
|
-
})
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
### services/user.ts
|
|
245
|
-
|
|
246
|
-
```typescript
|
|
247
|
-
import { defineService, loadService } from '@hile/core'
|
|
248
|
-
import { databaseService } from './database'
|
|
249
|
-
import { cacheService } from './cache'
|
|
250
|
-
|
|
251
|
-
interface User {
|
|
252
|
-
id: number
|
|
253
|
-
name: string
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export const userService = defineService(async (shutdown) => {
|
|
257
|
-
const db = await loadService(databaseService)
|
|
258
|
-
const cache = await loadService(cacheService)
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
async getById(id: number): Promise<User> {
|
|
262
|
-
const cached = await cache.get(`user:${id}`)
|
|
263
|
-
if (cached) return JSON.parse(cached)
|
|
264
|
-
const user = await db.query('SELECT * FROM users WHERE id = $1', [id])
|
|
265
|
-
await cache.set(`user:${id}`, JSON.stringify(user))
|
|
266
|
-
return user
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
})
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### main.ts
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
import { loadService } from '@hile/core'
|
|
276
|
-
import { userService } from './services/user'
|
|
277
|
-
|
|
278
|
-
async function main() {
|
|
279
|
-
const users = await loadService(userService)
|
|
280
|
-
const user = await users.getById(1)
|
|
281
|
-
console.log(user)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
main()
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
---
|
|
288
|
-
|
|
289
|
-
## 6. 反模式(生成代码时必须避免)
|
|
290
|
-
|
|
291
|
-
### 6.1 不要使用同步 throw
|
|
292
|
-
|
|
293
|
-
```typescript
|
|
294
|
-
// ✗ 错误:同步 throw 不会触发 shutdown 销毁机制
|
|
295
|
-
export const badService = defineService((shutdown) => {
|
|
296
|
-
const res = createResourceSync()
|
|
297
|
-
shutdown(() => res.close())
|
|
298
|
-
if (!res.isValid()) throw new Error('invalid')
|
|
299
|
-
return res
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
// ✓ 正确:使用 async 函数
|
|
303
|
-
export const goodService = defineService(async (shutdown) => {
|
|
304
|
-
const res = await createResource()
|
|
305
|
-
shutdown(() => res.close())
|
|
306
|
-
if (!res.isValid()) throw new Error('invalid')
|
|
307
|
-
return res
|
|
308
|
-
})
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
### 6.2 不要在模块顶层缓存服务结果
|
|
312
|
-
|
|
313
|
-
```typescript
|
|
314
|
-
// ✗ 错误:模块加载时服务可能尚未就绪
|
|
150
|
+
// ✗
|
|
315
151
|
const db = await loadService(databaseService)
|
|
316
|
-
export function query(sql: string) {
|
|
317
|
-
return db.query(sql)
|
|
318
|
-
}
|
|
319
152
|
|
|
320
|
-
// ✓
|
|
153
|
+
// ✓
|
|
321
154
|
export async function query(sql: string) {
|
|
322
155
|
const db = await loadService(databaseService)
|
|
323
156
|
return db.query(sql)
|
|
324
157
|
}
|
|
325
158
|
```
|
|
326
159
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
```typescript
|
|
330
|
-
// ✗ 错误:每次调用 getService() 都创建新函数引用,无法去重
|
|
331
|
-
function getService() {
|
|
332
|
-
return defineService(async (shutdown) => { ... })
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// ✓ 正确:模块级常量
|
|
336
|
-
export const myService = defineService(async (shutdown) => { ... })
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
### 6.4 不要延迟注册 shutdown
|
|
340
|
-
|
|
341
|
-
```typescript
|
|
342
|
-
// ✗ 错误:如果 doSomething 抛错,resourceA 不会被清理
|
|
343
|
-
export const badService = defineService(async (shutdown) => {
|
|
344
|
-
const a = await createResourceA()
|
|
345
|
-
const b = await doSomething(a)
|
|
346
|
-
shutdown(() => a.close()) // 太晚了!
|
|
347
|
-
shutdown(() => b.close())
|
|
348
|
-
return b
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
// ✓ 正确:创建后立即注册
|
|
352
|
-
export const goodService = defineService(async (shutdown) => {
|
|
353
|
-
const a = await createResourceA()
|
|
354
|
-
shutdown(() => a.close()) // 立即注册
|
|
355
|
-
const b = await doSomething(a)
|
|
356
|
-
shutdown(() => b.close())
|
|
357
|
-
return b
|
|
358
|
-
})
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
---
|
|
362
|
-
|
|
363
|
-
## 7. API 速查
|
|
364
|
-
|
|
365
|
-
### 便捷函数(操作默认容器)
|
|
366
|
-
|
|
367
|
-
| 函数 | 签名 | 说明 |
|
|
368
|
-
|------|------|------|
|
|
369
|
-
| `defineService` | `<R>(fn: ServiceFunction<R>) => ServiceRegisterProps<R>` | 注册服务,返回注册信息 |
|
|
370
|
-
| `loadService` | `<R>(props: ServiceRegisterProps<R>) => Promise<R>` | 加载服务,返回服务实例 |
|
|
371
|
-
| `isService` | `<R>(props: ServiceRegisterProps<R>) => boolean` | 判断对象是否为合法的服务注册信息(通过内部 Symbol 校验) |
|
|
372
|
-
|
|
373
|
-
### Container 类(用于创建隔离容器)
|
|
374
|
-
|
|
375
|
-
| 方法 | 签名 | 说明 |
|
|
376
|
-
|------|------|------|
|
|
377
|
-
| `register` | `<R>(fn: ServiceFunction<R>) => ServiceRegisterProps<R>` | 注册服务。同一函数引用只注册一次 |
|
|
378
|
-
| `resolve` | `<R>(props: ServiceRegisterProps<R>) => Promise<R>` | 解析服务(状态机见下方) |
|
|
379
|
-
| `hasService` | `<R>(fn: ServiceFunction<R>) => boolean` | 检查服务是否已注册 |
|
|
380
|
-
| `hasMeta` | `(id: number) => boolean` | 检查服务是否已运行过 |
|
|
381
|
-
| `getIdByService` | `<R>(fn: ServiceFunction<R>) => number \| undefined` | 根据函数引用查 ID |
|
|
382
|
-
| `getMetaById` | `(id: number) => Paddings \| undefined` | 根据 ID 查运行时元数据 |
|
|
383
|
-
| `shutdown` | `() => Promise<void>` | 手动销毁所有服务,按服务注册逆序执行所有销毁回调 |
|
|
384
|
-
|
|
385
|
-
### resolve 状态机
|
|
386
|
-
|
|
387
|
-
```
|
|
388
|
-
resolve(props)
|
|
389
|
-
│
|
|
390
|
-
├─ paddings 中无记录 → 首次运行
|
|
391
|
-
│ → run(id, fn, callback)
|
|
392
|
-
│ → 创建 Paddings { status: 0 }
|
|
393
|
-
│ ├─ fn 成功 → status = 1, value = 返回值, 通知 queue 所有等待者
|
|
394
|
-
│ └─ fn 失败 → status = -1, error = 错误
|
|
395
|
-
│ → 先逆序执行 shutdown 回调
|
|
396
|
-
│ → 再通知 queue 所有等待者
|
|
397
|
-
│
|
|
398
|
-
├─ status = 0 (运行中) → 加入 queue 等待
|
|
399
|
-
├─ status = 1 (已成功) → 直接 resolve(缓存值)
|
|
400
|
-
└─ status = -1 (已失败) → 直接 reject(缓存错误)
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
---
|
|
404
|
-
|
|
405
|
-
## 8. 内部机制(供理解,不供直接调用)
|
|
160
|
+
## 6. API 速查
|
|
406
161
|
|
|
407
|
-
###
|
|
408
|
-
|
|
409
|
-
| 字段 | 类型 | 说明 |
|
|
410
|
-
|------|------|------|
|
|
411
|
-
| `packages` | `Map<Function, number>` | 函数引用 → 服务 ID |
|
|
412
|
-
| `paddings` | `Map<number, Paddings>` | 服务 ID → 运行时状态 |
|
|
413
|
-
| `shutdownFunctions` | `Map<number, ServiceCutDownFunction[]>` | 服务 ID → 销毁回调数组 |
|
|
414
|
-
| `shutdownQueues` | `number[]` | 注册了销毁回调的服务 ID 队列(有序) |
|
|
415
|
-
|
|
416
|
-
### Paddings 结构
|
|
417
|
-
|
|
418
|
-
| 字段 | 类型 | 说明 |
|
|
419
|
-
|------|------|------|
|
|
420
|
-
| `status` | `-1 \| 0 \| 1` | -1 失败 / 0 运行中 / 1 成功 |
|
|
421
|
-
| `value` | `R` | 成功时的返回值 |
|
|
422
|
-
| `error` | `any` | 失败时的错误 |
|
|
423
|
-
| `queue` | `Set<{ resolve, reject }>` | 等待中的 Promise 回调 |
|
|
424
|
-
|
|
425
|
-
### 销毁执行顺序
|
|
426
|
-
|
|
427
|
-
- **单个服务内**:销毁回调按注册的逆序(LIFO)依次 `await` 执行
|
|
428
|
-
- **全局销毁**:按服务注册顺序的逆序依次销毁
|
|
429
|
-
- **触发时机**:
|
|
430
|
-
- 服务函数异步失败(reject)时自动触发该服务的销毁回调
|
|
431
|
-
- 手动调用 `container.shutdown()` 时触发所有已注册服务的销毁回调(按服务注册逆序)
|
|
432
|
-
|
|
433
|
-
### 服务标识机制(sericeFlag)
|
|
434
|
-
|
|
435
|
-
容器内部使用 `const sericeFlag = Symbol('service')` 作为服务注册信息的标识。`register` 返回的对象会携带 `flag: sericeFlag` 字段。`isService()` 通过检查 `flag === sericeFlag` 以及 `id` 和 `fn` 的类型来判断一个对象是否为合法的服务注册信息。由于 Symbol 不可伪造,外部无法手动构造通过 `isService` 校验的对象。
|
|
436
|
-
|
|
437
|
-
### 函数去重机制
|
|
438
|
-
|
|
439
|
-
容器通过 `===` 比较函数引用。两个函数即使代码完全相同,只要引用不同就会被视为不同服务。因此服务必须定义为模块级常量。
|
|
440
|
-
|
|
441
|
-
---
|
|
442
|
-
|
|
443
|
-
## 9. 开发
|
|
444
|
-
|
|
445
|
-
```bash
|
|
446
|
-
pnpm install
|
|
447
|
-
pnpm build # 编译
|
|
448
|
-
pnpm dev # 监听模式
|
|
449
|
-
pnpm test # 运行测试
|
|
450
|
-
```
|
|
162
|
+
### 便捷函数
|
|
451
163
|
|
|
452
|
-
|
|
164
|
+
| 函数 | 说明 |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `defineService(fn)` | 注册服务到默认容器 |
|
|
167
|
+
| `loadService(props)` | 加载服务实例 |
|
|
168
|
+
| `isService(props)` | 判断是否为合法服务注册对象 |
|
|
453
169
|
|
|
454
|
-
|
|
170
|
+
### Container
|
|
455
171
|
|
|
456
|
-
|
|
172
|
+
| 方法 | 说明 |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `register(fn)` | 注册服务 |
|
|
175
|
+
| `resolve(props)` | 解析服务 |
|
|
176
|
+
| `hasService(fn)` | 检查函数是否已注册 |
|
|
177
|
+
| `hasMeta(id)` | 检查运行时元数据 |
|
|
178
|
+
| `getIdByService(fn)` | 通过函数获取 ID |
|
|
179
|
+
| `getMetaById(id)` | 通过 ID 获取元数据 |
|
|
180
|
+
| `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
|
+
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
|
-
|
|
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
|
+
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 = {
|
|
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.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": "
|
|
26
|
+
"gitHead": "81347b9de460b693ed82af46c0f4a287d4527323"
|
|
27
27
|
}
|