@hile/core 1.0.21 → 1.1.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/README.md +37 -31
- package/SKILL.md +11 -10
- package/dist/index.d.ts +27 -21
- package/dist/index.js +89 -80
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hile/core
|
|
2
2
|
|
|
3
|
-
轻量级异步服务容器,提供单例管理、并发请求合并与生命周期销毁能力。纯 TypeScript
|
|
3
|
+
轻量级异步服务容器,提供单例管理、并发请求合并与生命周期销毁能力。纯 TypeScript 实现,零运行时依赖。默认容器使用进程级单例 `globalThis.HILE_GLOBAL_CONTAINER`。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ pnpm add @hile/core
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { defineService, loadService } from '@hile/core'
|
|
15
15
|
|
|
16
|
-
const greeterService = defineService(async (shutdown) => {
|
|
16
|
+
const greeterService = defineService('greeter', async (shutdown) => {
|
|
17
17
|
return {
|
|
18
18
|
hello(name: string) {
|
|
19
19
|
return `Hello, ${name}!`
|
|
@@ -27,23 +27,27 @@ greeter.hello('World') // Hello, World!
|
|
|
27
27
|
|
|
28
28
|
## 核心概念
|
|
29
29
|
|
|
30
|
-
### 1)
|
|
30
|
+
### 1) 服务 key
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
每个服务必须指定 **`ServiceKey`(`string` 或 `symbol`)**,用于在容器内唯一标识该服务槽位。同一 key 多次 `defineService`/`register` 指向同一单例;不同 key 彼此独立。集成包常用 `Symbol.for('@scope/package')`,应用内可用 `'http'`、`'database'` 等字符串。
|
|
33
|
+
|
|
34
|
+
### 2) 定义服务
|
|
35
|
+
|
|
36
|
+
通过 `defineService(key, fn)` 注册服务。服务函数接收 `shutdown` 注册器,用于登记资源清理回调。
|
|
33
37
|
|
|
34
38
|
```typescript
|
|
35
39
|
import { defineService } from '@hile/core'
|
|
36
40
|
|
|
37
|
-
export const databaseService = defineService(async (shutdown) => {
|
|
41
|
+
export const databaseService = defineService('database', async (shutdown) => {
|
|
38
42
|
const pool = await createPool('postgres://localhost:5432/app')
|
|
39
43
|
shutdown(() => pool.end())
|
|
40
44
|
return pool
|
|
41
45
|
})
|
|
42
46
|
```
|
|
43
47
|
|
|
44
|
-
###
|
|
48
|
+
### 3) 加载服务
|
|
45
49
|
|
|
46
|
-
通过 `loadService`
|
|
50
|
+
通过 `loadService` 获取服务实例。容器保证同一服务 key 只执行一次工厂函数。
|
|
47
51
|
|
|
48
52
|
```typescript
|
|
49
53
|
import { loadService } from '@hile/core'
|
|
@@ -53,7 +57,7 @@ const db = await loadService(databaseService)
|
|
|
53
57
|
const users = await db.query('SELECT * FROM users')
|
|
54
58
|
```
|
|
55
59
|
|
|
56
|
-
###
|
|
60
|
+
### 4) 并发请求合并
|
|
57
61
|
|
|
58
62
|
并发加载同一服务时,初始化只执行一次,调用方共享结果。
|
|
59
63
|
|
|
@@ -67,7 +71,7 @@ const [r1, r2, r3] = await Promise.all([
|
|
|
67
71
|
// r1 === r2 === r3
|
|
68
72
|
```
|
|
69
73
|
|
|
70
|
-
###
|
|
74
|
+
### 5) 服务间依赖
|
|
71
75
|
|
|
72
76
|
服务内部可通过 `loadService` 继续加载依赖服务。
|
|
73
77
|
|
|
@@ -76,7 +80,7 @@ import { defineService, loadService } from '@hile/core'
|
|
|
76
80
|
import { databaseService } from './database'
|
|
77
81
|
import { cacheService } from './cache'
|
|
78
82
|
|
|
79
|
-
export const userService = defineService(async (shutdown) => {
|
|
83
|
+
export const userService = defineService('user', async (shutdown) => {
|
|
80
84
|
const db = await loadService(databaseService)
|
|
81
85
|
const cache = await loadService(cacheService)
|
|
82
86
|
|
|
@@ -92,12 +96,12 @@ export const userService = defineService(async (shutdown) => {
|
|
|
92
96
|
})
|
|
93
97
|
```
|
|
94
98
|
|
|
95
|
-
###
|
|
99
|
+
### 6) 资源销毁(Shutdown)
|
|
96
100
|
|
|
97
101
|
当服务初始化失败或手动执行全局关闭时,容器会按规则执行已注册的清理回调。`container.shutdown()` 会循环处理队列直到清空,并让出一次事件循环(`setImmediate`),确保在 shutdown 进行中才完成启动并注册的 teardown 也会被执行。
|
|
98
102
|
|
|
99
103
|
```typescript
|
|
100
|
-
export const connectionService = defineService(async (shutdown) => {
|
|
104
|
+
export const connectionService = defineService('connection', async (shutdown) => {
|
|
101
105
|
const primary = await connectPrimary()
|
|
102
106
|
shutdown(() => primary.disconnect())
|
|
103
107
|
|
|
@@ -120,7 +124,7 @@ export const connectionService = defineService(async (shutdown) => {
|
|
|
120
124
|
|
|
121
125
|
> 建议始终使用 `async` 服务函数,确保异常路径可正确触发销毁机制。
|
|
122
126
|
|
|
123
|
-
###
|
|
127
|
+
### 7) 生命周期、超时与可观测事件
|
|
124
128
|
|
|
125
129
|
容器支持显式生命周期阶段:`init -> ready -> stopping -> stopped`。
|
|
126
130
|
|
|
@@ -135,12 +139,14 @@ const container = new Container({
|
|
|
135
139
|
})
|
|
136
140
|
```
|
|
137
141
|
|
|
138
|
-
|
|
142
|
+
订阅事件(服务相关事件携带 `key`):
|
|
139
143
|
|
|
140
144
|
```typescript
|
|
145
|
+
import { formatServiceKey } from '@hile/core'
|
|
146
|
+
|
|
141
147
|
const off = container.on((event) => {
|
|
142
148
|
if (event.type === 'service:ready') {
|
|
143
|
-
console.log(`service
|
|
149
|
+
console.log(`service(${formatServiceKey(event.key)}) ready in ${event.durationMs}ms`)
|
|
144
150
|
}
|
|
145
151
|
})
|
|
146
152
|
|
|
@@ -148,21 +154,21 @@ const off = container.on((event) => {
|
|
|
148
154
|
off()
|
|
149
155
|
```
|
|
150
156
|
|
|
151
|
-
###
|
|
157
|
+
### 8) 依赖图与启动顺序
|
|
152
158
|
|
|
153
159
|
容器会在 `resolve` 过程中自动记录依赖关系,并检测循环依赖。
|
|
154
160
|
|
|
155
161
|
```typescript
|
|
156
162
|
const graph = container.getDependencyGraph()
|
|
157
|
-
// graph.nodes:
|
|
158
|
-
// graph.edges: Array<{ from:
|
|
163
|
+
// graph.nodes: ServiceKey[]
|
|
164
|
+
// graph.edges: Array<{ from: ServiceKey; to: ServiceKey }>
|
|
159
165
|
|
|
160
166
|
const startupOrder = container.getStartupOrder()
|
|
161
167
|
```
|
|
162
168
|
|
|
163
169
|
若出现循环依赖,会抛出 `circular dependency detected` 错误。
|
|
164
170
|
|
|
165
|
-
###
|
|
171
|
+
### 9) 手动销毁(Graceful Shutdown)
|
|
166
172
|
|
|
167
173
|
```typescript
|
|
168
174
|
import container from '@hile/core'
|
|
@@ -175,15 +181,15 @@ process.on('SIGTERM', async () => {
|
|
|
175
181
|
|
|
176
182
|
保证:每个在 defineService 中通过 `shutdown(fn)` 注册的回调都会在 `shutdown()` 时被执行;若某服务在 shutdown 期间才完成启动并调用 `shutdown(fn)`,也会在下一轮事件循环中被关掉。
|
|
177
183
|
|
|
178
|
-
###
|
|
184
|
+
### 10) 服务校验(isService)
|
|
179
185
|
|
|
180
186
|
```typescript
|
|
181
187
|
import { defineService, isService } from '@hile/core'
|
|
182
188
|
|
|
183
|
-
const myService = defineService(async (shutdown) => 'hello')
|
|
189
|
+
const myService = defineService('my', async (shutdown) => 'hello')
|
|
184
190
|
|
|
185
191
|
isService(myService) // true
|
|
186
|
-
isService({
|
|
192
|
+
isService({ key: 'x', fn: () => {} } as any) // false
|
|
187
193
|
```
|
|
188
194
|
|
|
189
195
|
## 隔离容器
|
|
@@ -195,7 +201,7 @@ import { Container } from '@hile/core'
|
|
|
195
201
|
|
|
196
202
|
const container = new Container()
|
|
197
203
|
|
|
198
|
-
const service = container.register(async (shutdown) => {
|
|
204
|
+
const service = container.register('test', async (shutdown) => {
|
|
199
205
|
return { value: 42 }
|
|
200
206
|
})
|
|
201
207
|
|
|
@@ -208,27 +214,27 @@ const result = await container.resolve(service)
|
|
|
208
214
|
|
|
209
215
|
| 函数 | 说明 |
|
|
210
216
|
|------|------|
|
|
211
|
-
| `defineService(fn)` | 注册服务到默认容器 |
|
|
217
|
+
| `defineService(key, fn)` | 注册服务到默认容器 |
|
|
212
218
|
| `loadService(props)` | 从默认容器加载服务 |
|
|
213
219
|
| `isService(props)` | 判断对象是否为合法服务注册信息 |
|
|
220
|
+
| `formatServiceKey(key)` | 将 key 格式化为可读字符串(日志) |
|
|
214
221
|
|
|
215
222
|
### `Container`
|
|
216
223
|
|
|
217
224
|
| 方法 | 说明 |
|
|
218
225
|
|------|------|
|
|
219
226
|
| `new Container(options?)` | 创建容器,可配置启动/销毁超时 |
|
|
220
|
-
| `register(fn)` |
|
|
227
|
+
| `register(key, fn)` | 注册服务(同一 key 共享槽位) |
|
|
221
228
|
| `resolve(props)` | 加载服务(执行、等待或返回缓存) |
|
|
222
229
|
| `shutdown()` | 销毁所有服务并执行清理回调 |
|
|
223
230
|
| `on(listener)` | 订阅容器事件,返回取消订阅函数 |
|
|
224
231
|
| `off(listener)` | 取消订阅 |
|
|
225
|
-
| `getLifecycle(
|
|
226
|
-
| `getDependencyGraph()` | 获取依赖图 `{ nodes, edges }` |
|
|
232
|
+
| `getLifecycle(key)` | 获取服务生命周期阶段 |
|
|
233
|
+
| `getDependencyGraph()` | 获取依赖图 `{ nodes, edges }`(key 为 `ServiceKey`) |
|
|
227
234
|
| `getStartupOrder()` | 获取服务启动顺序(首次启动顺序) |
|
|
228
|
-
| `hasService(
|
|
229
|
-
| `hasMeta(
|
|
230
|
-
| `
|
|
231
|
-
| `getMetaById(id)` | 通过 ID 获取运行时元数据 |
|
|
235
|
+
| `hasService(key)` | 是否已对该 key 注册 |
|
|
236
|
+
| `hasMeta(key)` | 该 key 是否已有运行时元数据 |
|
|
237
|
+
| `getMetaByKey(key)` | 按 key 获取运行时元数据 |
|
|
232
238
|
|
|
233
239
|
### 服务状态
|
|
234
240
|
|
package/SKILL.md
CHANGED
|
@@ -9,14 +9,14 @@ description: "@hile/core 的代码生成与使用规范。适用于定义/加载
|
|
|
9
9
|
|
|
10
10
|
## 1. 强约束(必须遵守)
|
|
11
11
|
|
|
12
|
-
1. 服务必须使用 `async (shutdown)`
|
|
12
|
+
1. 服务必须使用 `defineService(key, async (shutdown) => ...)` 形态:`key` 为 `ServiceKey`(`string | symbol`),在容器内唯一标识该服务槽位;同一 key 共享单例。
|
|
13
13
|
2. 只能通过 `defineService` / `container.register` 产出服务对象。
|
|
14
14
|
3. 只能通过 `loadService` / `container.resolve` 获取服务实例。
|
|
15
15
|
4. 外部资源创建后必须立即注册 `shutdown`。
|
|
16
16
|
5. 禁止在模块顶层缓存 `await loadService(...)` 结果。
|
|
17
17
|
6. 依赖服务必须在服务函数内部加载。
|
|
18
18
|
7. 多个 teardown 默认按 LIFO 顺序执行。
|
|
19
|
-
8. `container.shutdown()` 必须执行所有已通过 `shutdown(fn)` 注册的回调;在 shutdown 进行中才完成启动并调用 `
|
|
19
|
+
8. `container.shutdown()` 必须执行所有已通过 `shutdown(fn)` 注册的回调;在 shutdown 进行中才完成启动并调用 `shutdown` 的服务,也会在让出一次事件循环后被关掉(避免竞态漏关)。
|
|
20
20
|
|
|
21
21
|
## 2. 生命周期与超时约束
|
|
22
22
|
|
|
@@ -34,7 +34,7 @@ description: "@hile/core 的代码生成与使用规范。适用于定义/加载
|
|
|
34
34
|
|
|
35
35
|
允许订阅:`container.on(listener)`。
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
关键事件(服务相关事件携带 `key: ServiceKey`):
|
|
38
38
|
|
|
39
39
|
- `service:init`
|
|
40
40
|
- `service:ready`
|
|
@@ -50,13 +50,14 @@ description: "@hile/core 的代码生成与使用规范。适用于定义/加载
|
|
|
50
50
|
|
|
51
51
|
- 订阅后必须在生命周期结束时取消订阅。
|
|
52
52
|
- 记录错误时保留原始 error 对象。
|
|
53
|
+
- 日志展示 key 时使用 `formatServiceKey(event.key)`。
|
|
53
54
|
|
|
54
55
|
## 4. 依赖图与循环依赖
|
|
55
56
|
|
|
56
57
|
容器会自动记录服务依赖并检测循环依赖:
|
|
57
58
|
|
|
58
|
-
- `getDependencyGraph()`
|
|
59
|
-
- `getStartupOrder()`
|
|
59
|
+
- `getDependencyGraph()`(`nodes` / `edges` 使用 `ServiceKey`)
|
|
60
|
+
- `getStartupOrder()` 返回 `ServiceKey[]`
|
|
60
61
|
|
|
61
62
|
规则:
|
|
62
63
|
|
|
@@ -82,22 +83,22 @@ export async function query(sql: string) {
|
|
|
82
83
|
|
|
83
84
|
```typescript
|
|
84
85
|
// ✗
|
|
85
|
-
const fake = {
|
|
86
|
+
const fake = { key: 'x', fn: async () => 1 }
|
|
86
87
|
|
|
87
88
|
// ✓
|
|
88
|
-
const real = defineService(async () => 1)
|
|
89
|
+
const real = defineService('real', async () => 1)
|
|
89
90
|
```
|
|
90
91
|
|
|
91
92
|
### 5.3 不注册资源清理
|
|
92
93
|
|
|
93
94
|
```typescript
|
|
94
95
|
// ✗
|
|
95
|
-
export const bad = defineService(async () => {
|
|
96
|
+
export const bad = defineService('bad', async () => {
|
|
96
97
|
return await createPool()
|
|
97
98
|
})
|
|
98
99
|
|
|
99
100
|
// ✓
|
|
100
|
-
export const good = defineService(async (shutdown) => {
|
|
101
|
+
export const good = defineService('good', async (shutdown) => {
|
|
101
102
|
const pool = await createPool()
|
|
102
103
|
shutdown(() => pool.end())
|
|
103
104
|
return pool
|
|
@@ -111,4 +112,4 @@ export const good = defineService(async (shutdown) => {
|
|
|
111
112
|
- [ ] teardown 抛错是否不覆盖原始业务错误
|
|
112
113
|
- [ ] 并发 resolve 同一服务是否只初始化一次
|
|
113
114
|
- [ ] shutdown 重复调用是否幂等
|
|
114
|
-
- [ ] shutdown 期间才注册的 teardown 是否在一次 setImmediate
|
|
115
|
+
- [ ] shutdown 期间才注册的 teardown 是否在一次 setImmediate 让出后被正确执行(避免竞态漏关)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
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
|
+
export type ServiceKey = string | symbol;
|
|
4
5
|
declare const sericeFlag: unique symbol;
|
|
6
|
+
declare global {
|
|
7
|
+
var HILE_GLOBAL_CONTAINER: Container;
|
|
8
|
+
}
|
|
5
9
|
export type ServiceLifecycleStage = 'init' | 'ready' | 'stopping' | 'stopped';
|
|
6
10
|
export interface ServiceRegisterProps<R> {
|
|
7
|
-
id: number;
|
|
8
11
|
fn: ServiceFunction<R>;
|
|
9
12
|
flag: typeof sericeFlag;
|
|
13
|
+
key: ServiceKey;
|
|
10
14
|
}
|
|
11
15
|
export interface ContainerOptions {
|
|
12
16
|
startTimeoutMs?: number;
|
|
@@ -14,26 +18,26 @@ export interface ContainerOptions {
|
|
|
14
18
|
}
|
|
15
19
|
export type ContainerEvent = {
|
|
16
20
|
type: 'service:init';
|
|
17
|
-
|
|
21
|
+
key: ServiceKey;
|
|
18
22
|
} | {
|
|
19
23
|
type: 'service:ready';
|
|
20
|
-
|
|
24
|
+
key: ServiceKey;
|
|
21
25
|
durationMs: number;
|
|
22
26
|
} | {
|
|
23
27
|
type: 'service:error';
|
|
24
|
-
|
|
28
|
+
key: ServiceKey;
|
|
25
29
|
error: any;
|
|
26
30
|
durationMs: number;
|
|
27
31
|
} | {
|
|
28
32
|
type: 'service:shutdown:start';
|
|
29
|
-
|
|
33
|
+
key: ServiceKey;
|
|
30
34
|
} | {
|
|
31
35
|
type: 'service:shutdown:done';
|
|
32
|
-
|
|
36
|
+
key: ServiceKey;
|
|
33
37
|
durationMs: number;
|
|
34
38
|
} | {
|
|
35
39
|
type: 'service:shutdown:error';
|
|
36
|
-
|
|
40
|
+
key: ServiceKey;
|
|
37
41
|
error: any;
|
|
38
42
|
} | {
|
|
39
43
|
type: 'container:shutdown:start';
|
|
@@ -56,10 +60,13 @@ interface Paddings<R = any> {
|
|
|
56
60
|
startedAt: number;
|
|
57
61
|
endedAt?: number;
|
|
58
62
|
}
|
|
63
|
+
/** 日志或调试时展示服务 key */
|
|
64
|
+
export declare function formatServiceKey(k: ServiceKey): string;
|
|
59
65
|
export declare class Container {
|
|
60
66
|
private readonly options;
|
|
61
|
-
private
|
|
62
|
-
private readonly
|
|
67
|
+
private keyOrder;
|
|
68
|
+
private readonly keyOrdinal;
|
|
69
|
+
private readonly registeredKeys;
|
|
63
70
|
private readonly paddings;
|
|
64
71
|
private readonly dependencies;
|
|
65
72
|
private readonly dependents;
|
|
@@ -70,32 +77,31 @@ export declare class Container {
|
|
|
70
77
|
private readonly context;
|
|
71
78
|
constructor(options?: ContainerOptions);
|
|
72
79
|
private emit;
|
|
73
|
-
private
|
|
80
|
+
private nextOrdinal;
|
|
74
81
|
private hasPath;
|
|
75
82
|
private trackDependency;
|
|
76
83
|
on(listener: (event: ContainerEvent) => void): () => boolean;
|
|
77
84
|
off(listener: (event: ContainerEvent) => void): void;
|
|
78
|
-
getLifecycle(
|
|
85
|
+
getLifecycle(key: ServiceKey): ServiceLifecycleStage | undefined;
|
|
79
86
|
getDependencyGraph(): {
|
|
80
|
-
nodes:
|
|
87
|
+
nodes: ServiceKey[];
|
|
81
88
|
edges: {
|
|
82
|
-
from:
|
|
83
|
-
to:
|
|
89
|
+
from: ServiceKey;
|
|
90
|
+
to: ServiceKey;
|
|
84
91
|
}[];
|
|
85
92
|
};
|
|
86
|
-
getStartupOrder():
|
|
87
|
-
register<R>(fn: ServiceFunction<R>): ServiceRegisterProps<R>;
|
|
93
|
+
getStartupOrder(): ServiceKey[];
|
|
94
|
+
register<R>(key: ServiceKey, fn: ServiceFunction<R>): ServiceRegisterProps<R>;
|
|
88
95
|
resolve<R>(props: ServiceRegisterProps<R>): Promise<R>;
|
|
89
96
|
private run;
|
|
90
97
|
private shutdownService;
|
|
91
98
|
shutdown(): Promise<void>;
|
|
92
|
-
hasService
|
|
93
|
-
hasMeta(
|
|
94
|
-
|
|
95
|
-
getMetaById(id: number): Paddings<any> | undefined;
|
|
99
|
+
hasService(key: ServiceKey): boolean;
|
|
100
|
+
hasMeta(key: ServiceKey): boolean;
|
|
101
|
+
getMetaByKey(key: ServiceKey): Paddings<any> | undefined;
|
|
96
102
|
}
|
|
97
103
|
export declare const container: Container;
|
|
98
|
-
export declare function defineService<R>(fn: ServiceFunction<R>): ServiceRegisterProps<R>;
|
|
104
|
+
export declare function defineService<R>(key: ServiceKey, fn: ServiceFunction<R>): ServiceRegisterProps<R>;
|
|
99
105
|
export declare function loadService<R>(props: ServiceRegisterProps<R>): Promise<R>;
|
|
100
106
|
export declare function isService<R>(props: ServiceRegisterProps<R>): boolean;
|
|
101
107
|
export default container;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
-
const sericeFlag = Symbol('service');
|
|
2
|
+
const sericeFlag = Symbol.for('service');
|
|
3
3
|
async function withTimeout(promise, timeoutMs, message) {
|
|
4
4
|
if (!timeoutMs || timeoutMs <= 0)
|
|
5
5
|
return promise;
|
|
@@ -15,10 +15,15 @@ async function withTimeout(promise, timeoutMs, message) {
|
|
|
15
15
|
clearTimeout(timer);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
/** 日志或调试时展示服务 key */
|
|
19
|
+
export function formatServiceKey(k) {
|
|
20
|
+
return typeof k === 'string' ? k : String(k);
|
|
21
|
+
}
|
|
18
22
|
export class Container {
|
|
19
23
|
options;
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
keyOrder = 0;
|
|
25
|
+
keyOrdinal = new Map();
|
|
26
|
+
registeredKeys = new Set();
|
|
22
27
|
paddings = new Map();
|
|
23
28
|
dependencies = new Map();
|
|
24
29
|
dependents = new Map();
|
|
@@ -40,12 +45,12 @@ export class Container {
|
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
|
-
|
|
44
|
-
let
|
|
45
|
-
if (
|
|
46
|
-
|
|
48
|
+
nextOrdinal() {
|
|
49
|
+
let o = ++this.keyOrder;
|
|
50
|
+
if (o >= Number.MAX_SAFE_INTEGER) {
|
|
51
|
+
o = this.keyOrder = 1;
|
|
47
52
|
}
|
|
48
|
-
return
|
|
53
|
+
return o;
|
|
49
54
|
}
|
|
50
55
|
hasPath(from, to, visited = new Set()) {
|
|
51
56
|
if (from === to)
|
|
@@ -62,25 +67,25 @@ export class Container {
|
|
|
62
67
|
}
|
|
63
68
|
return false;
|
|
64
69
|
}
|
|
65
|
-
trackDependency(
|
|
66
|
-
if (
|
|
67
|
-
throw new Error(`circular dependency detected: ${
|
|
70
|
+
trackDependency(parentKey, childKey) {
|
|
71
|
+
if (parentKey === childKey) {
|
|
72
|
+
throw new Error(`circular dependency detected: ${formatServiceKey(parentKey)} -> ${formatServiceKey(childKey)}`);
|
|
68
73
|
}
|
|
69
|
-
if (!this.dependencies.has(
|
|
70
|
-
this.dependencies.set(
|
|
74
|
+
if (!this.dependencies.has(parentKey)) {
|
|
75
|
+
this.dependencies.set(parentKey, new Set());
|
|
71
76
|
}
|
|
72
|
-
if (!this.dependents.has(
|
|
73
|
-
this.dependents.set(
|
|
77
|
+
if (!this.dependents.has(childKey)) {
|
|
78
|
+
this.dependents.set(childKey, new Set());
|
|
74
79
|
}
|
|
75
|
-
const parentDeps = this.dependencies.get(
|
|
76
|
-
if (!parentDeps.has(
|
|
77
|
-
if (this.hasPath(
|
|
78
|
-
const error = new Error(`circular dependency detected: ${
|
|
80
|
+
const parentDeps = this.dependencies.get(parentKey);
|
|
81
|
+
if (!parentDeps.has(childKey)) {
|
|
82
|
+
if (this.hasPath(childKey, parentKey)) {
|
|
83
|
+
const error = new Error(`circular dependency detected: ${formatServiceKey(parentKey)} -> ${formatServiceKey(childKey)}`);
|
|
79
84
|
this.emit({ type: 'container:error', error });
|
|
80
85
|
throw error;
|
|
81
86
|
}
|
|
82
|
-
parentDeps.add(
|
|
83
|
-
this.dependents.get(
|
|
87
|
+
parentDeps.add(childKey);
|
|
88
|
+
this.dependents.get(childKey).add(parentKey);
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
on(listener) {
|
|
@@ -90,15 +95,15 @@ export class Container {
|
|
|
90
95
|
off(listener) {
|
|
91
96
|
this.listeners.delete(listener);
|
|
92
97
|
}
|
|
93
|
-
getLifecycle(
|
|
94
|
-
return this.paddings.get(
|
|
98
|
+
getLifecycle(key) {
|
|
99
|
+
return this.paddings.get(key)?.lifecycle;
|
|
95
100
|
}
|
|
96
101
|
getDependencyGraph() {
|
|
97
|
-
const nodes = Array.from(this.
|
|
102
|
+
const nodes = Array.from(this.registeredKeys).sort((a, b) => (this.keyOrdinal.get(a) - this.keyOrdinal.get(b)));
|
|
98
103
|
const edges = [];
|
|
99
|
-
for (const [
|
|
100
|
-
for (const
|
|
101
|
-
edges.push({ from:
|
|
104
|
+
for (const [fromKey, deps] of this.dependencies.entries()) {
|
|
105
|
+
for (const toKey of deps) {
|
|
106
|
+
edges.push({ from: fromKey, to: toKey });
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
109
|
return { nodes, edges };
|
|
@@ -106,24 +111,23 @@ export class Container {
|
|
|
106
111
|
getStartupOrder() {
|
|
107
112
|
return [...this.startupOrder];
|
|
108
113
|
}
|
|
109
|
-
register(fn) {
|
|
110
|
-
if (this.
|
|
111
|
-
|
|
114
|
+
register(key, fn) {
|
|
115
|
+
if (!this.keyOrdinal.has(key)) {
|
|
116
|
+
this.keyOrdinal.set(key, this.nextOrdinal());
|
|
112
117
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return { id, fn, flag: sericeFlag };
|
|
118
|
+
this.registeredKeys.add(key);
|
|
119
|
+
return { key, fn, flag: sericeFlag };
|
|
116
120
|
}
|
|
117
121
|
resolve(props) {
|
|
118
|
-
const {
|
|
122
|
+
const { key, fn } = props;
|
|
119
123
|
const stack = this.context.getStore() || [];
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
this.trackDependency(
|
|
124
|
+
const parentKey = stack.length ? stack[stack.length - 1] : undefined;
|
|
125
|
+
if (parentKey !== undefined) {
|
|
126
|
+
this.trackDependency(parentKey, key);
|
|
123
127
|
}
|
|
124
128
|
return new Promise((resolve, reject) => {
|
|
125
|
-
if (!this.paddings.has(
|
|
126
|
-
return this.run(
|
|
129
|
+
if (!this.paddings.has(key)) {
|
|
130
|
+
return this.run(key, fn, (e, v) => {
|
|
127
131
|
if (e) {
|
|
128
132
|
reject(e);
|
|
129
133
|
}
|
|
@@ -132,7 +136,7 @@ export class Container {
|
|
|
132
136
|
}
|
|
133
137
|
});
|
|
134
138
|
}
|
|
135
|
-
const state = this.paddings.get(
|
|
139
|
+
const state = this.paddings.get(key);
|
|
136
140
|
switch (state.status) {
|
|
137
141
|
case 0:
|
|
138
142
|
state.queue.add({ resolve, reject });
|
|
@@ -146,7 +150,7 @@ export class Container {
|
|
|
146
150
|
}
|
|
147
151
|
});
|
|
148
152
|
}
|
|
149
|
-
run(
|
|
153
|
+
run(key, fn, callback) {
|
|
150
154
|
const state = {
|
|
151
155
|
status: 0,
|
|
152
156
|
lifecycle: 'init',
|
|
@@ -154,32 +158,32 @@ export class Container {
|
|
|
154
158
|
queue: new Set(),
|
|
155
159
|
startedAt: Date.now(),
|
|
156
160
|
};
|
|
157
|
-
this.paddings.set(
|
|
158
|
-
if (!this.startupOrder.includes(
|
|
159
|
-
this.startupOrder.push(
|
|
161
|
+
this.paddings.set(key, state);
|
|
162
|
+
if (!this.startupOrder.includes(key)) {
|
|
163
|
+
this.startupOrder.push(key);
|
|
160
164
|
}
|
|
161
|
-
this.emit({ type: 'service:init',
|
|
165
|
+
this.emit({ type: 'service:init', key });
|
|
162
166
|
const curDown = (cutDownFn) => {
|
|
163
|
-
if (!this.shutdownQueues.includes(
|
|
164
|
-
this.shutdownQueues.push(
|
|
167
|
+
if (!this.shutdownQueues.includes(key)) {
|
|
168
|
+
this.shutdownQueues.push(key);
|
|
165
169
|
}
|
|
166
|
-
if (!this.shutdownFunctions.has(
|
|
167
|
-
this.shutdownFunctions.set(
|
|
170
|
+
if (!this.shutdownFunctions.has(key)) {
|
|
171
|
+
this.shutdownFunctions.set(key, []);
|
|
168
172
|
}
|
|
169
|
-
const pools = this.shutdownFunctions.get(
|
|
173
|
+
const pools = this.shutdownFunctions.get(key);
|
|
170
174
|
if (!pools.includes(cutDownFn)) {
|
|
171
175
|
pools.push(cutDownFn);
|
|
172
176
|
}
|
|
173
177
|
};
|
|
174
178
|
const parentStack = this.context.getStore() || [];
|
|
175
|
-
const startupPromise = this.context.run([...parentStack,
|
|
176
|
-
withTimeout(startupPromise, this.options.startTimeoutMs, `service startup timeout: ${
|
|
179
|
+
const startupPromise = this.context.run([...parentStack, key], () => Promise.resolve(fn(curDown)));
|
|
180
|
+
withTimeout(startupPromise, this.options.startTimeoutMs, `service startup timeout: ${formatServiceKey(key)} exceeded ${this.options.startTimeoutMs}ms`).then((value) => {
|
|
177
181
|
state.status = 1;
|
|
178
182
|
state.lifecycle = 'ready';
|
|
179
183
|
state.value = value;
|
|
180
184
|
state.endedAt = Date.now();
|
|
181
185
|
const durationMs = state.endedAt - state.startedAt;
|
|
182
|
-
this.emit({ type: 'service:ready',
|
|
186
|
+
this.emit({ type: 'service:ready', key, durationMs });
|
|
183
187
|
for (const queue of state.queue) {
|
|
184
188
|
queue.resolve(value);
|
|
185
189
|
}
|
|
@@ -191,7 +195,7 @@ export class Container {
|
|
|
191
195
|
state.error = e;
|
|
192
196
|
state.endedAt = Date.now();
|
|
193
197
|
const durationMs = state.endedAt - state.startedAt;
|
|
194
|
-
this.emit({ type: 'service:error',
|
|
198
|
+
this.emit({ type: 'service:error', key, error: e, durationMs });
|
|
195
199
|
const clear = () => {
|
|
196
200
|
state.lifecycle = 'stopped';
|
|
197
201
|
for (const queue of state.queue) {
|
|
@@ -200,39 +204,39 @@ export class Container {
|
|
|
200
204
|
state.queue.clear();
|
|
201
205
|
callback(e);
|
|
202
206
|
};
|
|
203
|
-
this.shutdownService(
|
|
207
|
+
this.shutdownService(key)
|
|
204
208
|
.then(clear)
|
|
205
209
|
.catch((shutdownError) => {
|
|
206
|
-
this.emit({ type: 'service:shutdown:error',
|
|
210
|
+
this.emit({ type: 'service:shutdown:error', key, error: shutdownError });
|
|
207
211
|
clear();
|
|
208
212
|
});
|
|
209
213
|
});
|
|
210
214
|
}
|
|
211
|
-
async shutdownService(
|
|
212
|
-
if (this.shutdownQueues.includes(
|
|
213
|
-
const meta = this.paddings.get(
|
|
215
|
+
async shutdownService(key) {
|
|
216
|
+
if (this.shutdownQueues.includes(key)) {
|
|
217
|
+
const meta = this.paddings.get(key);
|
|
214
218
|
if (meta) {
|
|
215
219
|
meta.lifecycle = 'stopping';
|
|
216
220
|
}
|
|
217
|
-
this.emit({ type: 'service:shutdown:start',
|
|
221
|
+
this.emit({ type: 'service:shutdown:start', key });
|
|
218
222
|
const startedAt = Date.now();
|
|
219
|
-
const pools = this.shutdownFunctions.get(
|
|
223
|
+
const pools = this.shutdownFunctions.get(key);
|
|
220
224
|
let i = pools.length;
|
|
221
225
|
while (i--) {
|
|
222
226
|
const teardown = pools[i];
|
|
223
227
|
try {
|
|
224
|
-
await withTimeout(Promise.resolve(teardown()), this.options.shutdownTimeoutMs, `service shutdown timeout: ${
|
|
228
|
+
await withTimeout(Promise.resolve(teardown()), this.options.shutdownTimeoutMs, `service shutdown timeout: ${formatServiceKey(key)} exceeded ${this.options.shutdownTimeoutMs}ms`);
|
|
225
229
|
}
|
|
226
230
|
catch (error) {
|
|
227
|
-
this.emit({ type: 'service:shutdown:error',
|
|
231
|
+
this.emit({ type: 'service:shutdown:error', key, error });
|
|
228
232
|
}
|
|
229
233
|
}
|
|
230
|
-
this.shutdownFunctions.delete(
|
|
231
|
-
this.shutdownQueues.splice(this.shutdownQueues.indexOf(
|
|
234
|
+
this.shutdownFunctions.delete(key);
|
|
235
|
+
this.shutdownQueues.splice(this.shutdownQueues.indexOf(key), 1);
|
|
232
236
|
if (meta) {
|
|
233
237
|
meta.lifecycle = 'stopped';
|
|
234
238
|
}
|
|
235
|
-
this.emit({ type: 'service:shutdown:done',
|
|
239
|
+
this.emit({ type: 'service:shutdown:done', key, durationMs: Date.now() - startedAt });
|
|
236
240
|
}
|
|
237
241
|
}
|
|
238
242
|
async shutdown() {
|
|
@@ -241,8 +245,8 @@ export class Container {
|
|
|
241
245
|
// 循环直到队列清空;再让出一次事件循环,处理「shutdown 期间才调用 curDown」的晚注册 teardown
|
|
242
246
|
while (true) {
|
|
243
247
|
while (this.shutdownQueues.length > 0) {
|
|
244
|
-
const
|
|
245
|
-
await this.shutdownService(
|
|
248
|
+
const key = this.shutdownQueues[this.shutdownQueues.length - 1];
|
|
249
|
+
await this.shutdownService(key);
|
|
246
250
|
}
|
|
247
251
|
await new Promise(r => setImmediate(r));
|
|
248
252
|
if (this.shutdownQueues.length === 0)
|
|
@@ -252,27 +256,32 @@ export class Container {
|
|
|
252
256
|
this.shutdownQueues.length = 0;
|
|
253
257
|
this.emit({ type: 'container:shutdown:done', durationMs: Date.now() - startedAt });
|
|
254
258
|
}
|
|
255
|
-
hasService(
|
|
256
|
-
return this.
|
|
259
|
+
hasService(key) {
|
|
260
|
+
return this.registeredKeys.has(key);
|
|
257
261
|
}
|
|
258
|
-
hasMeta(
|
|
259
|
-
return this.paddings.has(
|
|
262
|
+
hasMeta(key) {
|
|
263
|
+
return this.paddings.has(key);
|
|
260
264
|
}
|
|
261
|
-
|
|
262
|
-
return this.
|
|
265
|
+
getMetaByKey(key) {
|
|
266
|
+
return this.paddings.get(key);
|
|
263
267
|
}
|
|
264
|
-
|
|
265
|
-
|
|
268
|
+
}
|
|
269
|
+
function getGlobalContainer() {
|
|
270
|
+
if (!globalThis.HILE_GLOBAL_CONTAINER) {
|
|
271
|
+
globalThis.HILE_GLOBAL_CONTAINER = new Container();
|
|
266
272
|
}
|
|
273
|
+
return globalThis.HILE_GLOBAL_CONTAINER;
|
|
267
274
|
}
|
|
268
|
-
export const container =
|
|
269
|
-
export function defineService(fn) {
|
|
270
|
-
return container.register(fn);
|
|
275
|
+
export const container = getGlobalContainer();
|
|
276
|
+
export function defineService(key, fn) {
|
|
277
|
+
return container.register(key, fn);
|
|
271
278
|
}
|
|
272
279
|
export function loadService(props) {
|
|
273
280
|
return container.resolve(props);
|
|
274
281
|
}
|
|
275
282
|
export function isService(props) {
|
|
276
|
-
return props.flag === sericeFlag
|
|
283
|
+
return (props.flag === sericeFlag
|
|
284
|
+
&& typeof props.fn === 'function'
|
|
285
|
+
&& (typeof props.key === 'string' || typeof props.key === 'symbol'));
|
|
277
286
|
}
|
|
278
287
|
export default container;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hile/core",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Hile core - lightweight async service container",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -24,5 +24,5 @@
|
|
|
24
24
|
"fix-esm-import-path": "^1.10.3",
|
|
25
25
|
"vitest": "^4.0.18"
|
|
26
26
|
},
|
|
27
|
-
"gitHead": "
|
|
27
|
+
"gitHead": "ec49eb8979b6c9c3ce99cb5311f1684bcde1bf2a"
|
|
28
28
|
}
|