@hile/micro 1.0.3 → 1.0.5

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/SKILL.md CHANGED
@@ -1,159 +1,520 @@
1
1
  ---
2
2
  name: micro
3
- description: Code generation and contribution rules for @hile/micro. Use when editing this package or when the user asks about @hile/micro patterns or API.
3
+ description: Code generation and contribution rules for @hile/micro. Use when editing this package or when the user asks about @hile/micro API, types, patterns, or features.
4
4
  ---
5
5
 
6
- # @hile/micro
6
+ # @hile/micro — AI Skill Reference
7
7
 
8
- 本文档面向 **AI 编码模型** 与 **贡献者**:在修改或生成 `@hile/micro` 相关代码前必读,保证与现有 WebSocket 路由、注册中心与 `MessageLoader` 约定一致。
8
+ 本文档面向 **AI 编码模型**。在生成或修改 `@hile/micro` 代码前必读,保证与现有架构、API 约定、状态机、测试模式一致。
9
9
 
10
10
  ---
11
11
 
12
12
  ## 1. 架构总览
13
13
 
14
- `@hile/micro` 在 `@hile/message-loader` + `@hile/message-ws` 之上提供 **轻量级进程间服务发现与会话**:
15
-
16
- - **`Server`**:基于 `ws` 的 `WebSocketServer`,用 **URL 路径** 携带对端身份与命名空间;对内用 `MessageLoader.register` / `dispatch` 处理消息路由。
17
- - **`Client`**:连接到远端 `Server`,`request(url, data)` / `push(url, data)` 将负载交给本端 `Server.dispatch`;`dispose()` 会移除监听并 **关闭底层 WebSocket**,避免 `listen` 关闭阶段挂住。
18
- - **`Registry`**:固定 `namespace` 为 `'registry'`,维护「逻辑 namespace → 一组 `host:port`」;`/-/find` 在集合中 **随机** 返回一条地址(见 `selectRandomRegistryAddress`)。
19
- - **`Application`**:`Server` 的子类;`listen(port)` 后自动 `connect` 到注册中心;`get(targetNamespace)` 向注册中心查询并 **缓存** 到目标服务的 `Client`,目标断连后删除缓存以便下次重新发现。
20
-
21
- 依赖链:
14
+ ### 依赖链
22
15
 
23
16
  ```
24
- MessageLoader (路由) + MessageWs (请求/响应传输)
25
- └── Server / Client
26
- ├── Registry
27
- └── Application
17
+ @hile/message-loader (路由: register/dispatch)
18
+ └── @hile/message-ws (WebSocket 请求/响应, MessageModem timeout/abort)
19
+ └── packages/micro
20
+ ├── Server — WebSocket 监听 + 连接管理 + Client 生命周期
21
+ ├── Client — 远端代理, request/push/dispose
22
+ ├── Registry — 注册中心 (extends Server)
23
+ └── Application — 应用服务 (extends Server)
28
24
  ```
29
25
 
30
- ---
26
+ ### 分层职责
31
27
 
32
- ## 2. 路由与 URL 约定(强约束)
28
+ | | 文件 | 职责 | 关键约束 |
29
+ |----|------|------|---------|
30
+ | `Server` | `server.ts` | WebSocketServer 生命周期, 出入站连接, Client Map | 不感知 Registry |
31
+ | `Client` | `client.ts` | 远端 Server 的 WebSocket 会话代理 | `dispose()` 必须关闭底层 socket |
32
+ | `Registry` | `registry.ts` | namespace → Set\<host:port\>, 心跳检测, /-/find 随机返回 | `namespace` 固定 `'registry'` |
33
+ | `Application` | `application.ts` | 注册发现 + 熔断 + 重试 + 追踪 + 心跳 | `listen()` 后自动连 Registry |
33
34
 
34
- ### 2.1 入站连接路径
35
+ ### 应用模型
35
36
 
36
- 对端发起连接时的 URL 必须为:
37
+ 一个 `Application` 实例同时是 provider 和 consumer。不要创建两个类来区分角色。
37
38
 
38
- ```text
39
- ws://{listenHost}:{listenPort}/{callerHost}/{callerPort}/{...extras}
39
+ ```
40
+ Application:
41
+ └─ register(url, handler) → provider 侧
42
+ └─ get(namespace) → consumer 侧, 返回 Client
43
+ └─ call(namespace, url, data) → consumer 侧, get+request+熔断+重试 一站式
40
44
  ```
41
45
 
42
- 解析规则见 `Server.onConnected`:
46
+ ---
43
47
 
44
- - 至少 **三段** 路径:`callerHost`、`callerPort` 以及后续 `extras`(可为空)。
45
- - `extras` 以 `/` 分段,在 `events.emit('connect', client, extras)` 中交给业务;**Registry** 用 `extras.join('/')` 作为 **服务逻辑 namespace**。
48
+ ## 2. 类型定义(代码生成时必须一致)
46
49
 
47
- 生成侧:`Server.connect()` 使用:
50
+ ### Server (`server.ts`)
48
51
 
49
- ```text
50
- ws://{host}:{port}/${this.ipv4}/${this.port}/${this.namespace}
52
+ ```typescript
53
+ export type MicroServerProps = MessageLoaderProps & {
54
+ advertiseHost?: string; // 缺省 getLocalIPv4(), 皆无则构造抛错
55
+ };
56
+
57
+ export class Server extends MessageLoader {
58
+ protected readonly clients = new Map<string, Client>(); // key: "host:port"
59
+ public readonly events = new EventEmitter(); // connect/disconnect
60
+
61
+ constructor(namespace: string, props?: MicroServerProps);
62
+ public listen(port: number): Promise<() => Promise<void>>; // 返回 teardown
63
+ public setPort(port: number): this;
64
+ protected connect(host: string, port: number, timeout?: number): Promise<Client>;
65
+ // 继承自 MessageLoader:
66
+ // register<T, E>(url, handler): () => void;
67
+ // dispatch(url, data, extras): Promise<any>;
68
+ }
51
69
  ```
52
70
 
53
- 其中 `this.ipv4` 来自 `getLocalIPv4()`(见 `utils.ts`),`this.port` 为当前 `listen` 端口,`this.namespace` 为构造传入的字符串(如 `provider`、`consumer`、`registry`)。
54
-
55
- ### 2.2 MessageLoader 路由
71
+ ### Client (`client.ts`)
56
72
 
57
- - 对内消息体为 `{ url, data }`,与 `Client.exec` 一致。
58
- - `Registry` 注册:`register<RegistryFindData>('/-/find', handler)`,请求体 `{ namespace: string }`。
59
-
60
- ---
73
+ ```typescript
74
+ export class Client extends MessageWs {
75
+ public readonly host: string;
76
+ public readonly port: number;
77
+ public readonly events = new EventEmitter(); // connect/disconnect
78
+
79
+ request(url: string, data: any, timeout?: number): {
80
+ abort(): void;
81
+ response<T = any>(): Promise<T>; // 超时或 abort 时 reject
82
+ };
83
+ push(url: string, data: any, timeout?: number): void;
84
+ dispose(): void; // 关闭底层 WebSocket
85
+ }
86
+ ```
61
87
 
62
- ## 3. 类型与关键 API(生成代码须一致)
88
+ ### Registry (`registry.ts`)
63
89
 
64
90
  ```typescript
65
- // registry.ts
66
91
  export interface RegistryFindData {
67
92
  namespace: string;
93
+ exclude?: string[];
68
94
  }
69
95
 
70
- export interface RegistryAddress {
71
- host: string;
72
- port: number;
73
- }
96
+ export function parseAddressKey(key: string): RegistryAddress | undefined;
97
+ export function selectRandomRegistryAddress(keys: Iterable<string>): RegistryAddress | undefined;
74
98
 
75
- export function selectRandomRegistryAddress(
76
- keys: Iterable<string>,
77
- ): RegistryAddress | undefined;
99
+ export class Registry extends Server {
100
+ // heartbeat 常量:
101
+ // HEARTBEAT_INTERVAL = 1000 (1s 轮询)
102
+ // HEARTBEAT_TIMEOUT = 20000 (20s 未收到心跳则剔除)
103
+ constructor(props?: MicroServerProps);
104
+ listen(port: number): Promise<() => Promise<void>>;
105
+ onFind(): void; // 幂等,可重复调用
106
+ // 内部路由:
107
+ // /-/find — 按 namespace 随机返回地址 (支持 exclude)
108
+ // /-/heartbeat — 更新实例心跳时间戳
109
+ }
110
+ ```
78
111
 
79
- export class Registry extends Server { /* ... */ }
112
+ ### Application (`application.ts`)
80
113
 
81
- // application.ts
114
+ ```typescript
82
115
  export type ApplicationProps = {
83
116
  namespace: string;
84
117
  registry: RegistryAddress;
85
- } & MessageLoaderProps;
118
+ registryLookupTimeoutMs?: number; // /-/find 超时, 默认 10_000
119
+ requestTimeoutMs?: number; // 单次请求超时, 默认 30_000
120
+ } & MicroServerProps;
86
121
 
87
122
  export class Application extends Server {
88
- listen(port: number): Promise<() => Promise<void>>;
89
- get(namespace: string): Promise<Client>;
123
+ // 内部常量:
124
+ // HEARTBEAT_INTERVAL = 10000 (10s 向 Registry 推送心跳)
125
+ // CB_COOLDOWN_MS = 30000 (熔断冷卻期 30s)
126
+ // 内部状态:
127
+ // namespaces: Map<ns, { host, port, status: IDLE|PENDING|READY, handlers }>
128
+ // circuitBreakers: Map<ns, Map<peerKey, openedAt>>
129
+
130
+ constructor(props: ApplicationProps);
131
+ listen(port: number): Promise<() => Promise<void>>; // 自动连 Registry + 启心跳
132
+
133
+ get(namespace: string, exclude?: string[]): Promise<Client>;
134
+ // call() = get + request + response + correlationId + 熔断 + 重试 + 超时
135
+ call<T = any>(
136
+ namespace: string,
137
+ url: string,
138
+ data: any,
139
+ timeout?: number, // 单次超时, 默认 requestTimeoutMs
140
+ retries?: number, // 重试次数, 默认 1
141
+ ): Promise<T>;
142
+
143
+ // 继承自 Server/MessageLoader:
144
+ // register<T, E>(url, handler): () => void;
145
+ // dispatch(url, data, extras): Promise<any>;
90
146
  }
147
+ ```
91
148
 
92
- // server.ts — connect 第三参仅用于测试或内部,默认超时 5s
93
- protected async connect(host: string, port: number, timeout?: number): Promise<Client>;
149
+ ---
94
150
 
95
- // client.ts
96
- export class Client extends MessageWs {
97
- request(url: string, data: any, timeout?: number): ReturnType<MessageWs['_send']>;
98
- push(url: string, data: any, timeout?: number): void;
99
- dispose(): void;
100
- }
151
+ ## 3. 功能详解(含状态机与逻辑流)
152
+
153
+ ### 3.1 服务发现 `get(namespace, exclude?)`
154
+
155
+ ```
156
+ get(ns, exclude?)
157
+ ├─ namespaces 无此 ns → 创建 IDLE 条目
158
+ ├─ status=READY + (client 已断连 or peer 被 exclude)
159
+ │ └─ 重置为 IDLE, 清空 host/port
160
+ ├─ status=READY + client 有效 + 未被 exclude
161
+ │ └─ 直接返回缓存 Client (快路径)
162
+ └─ 否则:
163
+ └─ 新建 Promise, handler 入队
164
+ └─ status=IDLE → PENDING → findFromRegistry(ns, exclude)
165
+ ├─ Registry 返回地址 → connect → status=READY
166
+ │ └─ 注册 disconnect → 清理 namespace 缓存
167
+ │ └─ resolve 所有 handler
168
+ └─ Registry 失败 or 无数据 →
169
+ └─ catch: 检查旧缓存 (cachedHost/cachedPort)
170
+ ├─ 缓存有效 → 降级: restore status=READY, resolve
171
+ └─ 无缓存 → delete namespace, reject 所有 handler
172
+ └─ finally: handlers.clear()
101
173
  ```
102
174
 
103
- `Application` 内部缓存查找状态为 `IDLE` → `PENDING` → `READY`:首次 `get` 必须从 `IDLE` 触发 `findFromRegistry`,禁止再出现「初始状态与触发条件不匹配」导致永久挂起。
175
+ **关键点:**
176
+ - `cachedHost` / `cachedPort` 在缓存失效前保存,用于降级路径
177
+ - 降级时恢复 `stack.host/port/status=READY`,使后续调用走快路径
178
+ - 多并发 `get()` 共享同一个 Promise,handler 入队后统一 resolve/reject
104
179
 
105
- ---
180
+ ### 3.2 熔断器 (Circuit Breaker)
181
+
182
+ **数据结构:** `Map<namespace, Map<peerKey, openedAt>>`
183
+
184
+ ```
185
+ call() 失败:
186
+ recordFailure(ns, host, port)
187
+ → excludes.set("host:port", Date.now())
188
+
189
+ call() 成功:
190
+ recordSuccess(ns, host, port)
191
+ → excludes.delete("host:port")
192
+
193
+ getActiveExcludes(ns):
194
+ → 遍历 excludes, 移除 Date.now() - openedAt >= 30000 的过期条目
195
+ → 返回活跃的 exclude key 数组
196
+ ```
106
197
 
107
- ## 4. 代码生成模板与规则
198
+ **生命周期:**
108
199
 
109
- ### 4.1 Registry 服务端
200
+ ```
201
+ peer 首次失败
202
+ → circuitBreakers: { svc: { "127.0.0.1:8080": now } }
203
+ → getActiveExcludes("svc") → ["127.0.0.1:8080"]
204
+ → next call() 的 get() 带上 exclude, 排除该 peer
205
+ → Registry 返回其他 peer (有则) 或 undefined (无则)
206
+ → 如果所有 peer 都被排除, catch 块 delete circuitBreakers, 无 exclude 重试
207
+ => "全排除 → 重置" 策略: 当 Registry 找不到未被排除的 peer, 熔断器清空, 从不安全全量重试
208
+ ```
209
+
210
+ **冷卻期:** `CB_COOLDOWN_MS = 30000` (30 秒)。到期后 `getActiveExcludes` 自动清除旧条目。
211
+
212
+ ### 3.3 Correlation ID
213
+
214
+ `call()` 自动处理 `_correlationId`:
110
215
 
111
216
  ```typescript
112
- const registry = new Registry();
113
- const dispose = await registry.listen(registryPort);
114
- // shutdown: await dispose();
217
+ // 非对象/假值/数组 包装
218
+ !data || typeof data !== 'object' || Array.isArray(data)
219
+ data = { _correlationId: randomUUID(), data }
220
+
221
+ // 对象无 _correlationId → 浅拷贝注入
222
+ else if (!data._correlationId)
223
+ → data = { ...data, _correlationId: randomUUID() }
224
+
225
+ // 对象已有 _correlationId → 保留(透传)
115
226
  ```
116
227
 
117
- ### 4.2 可被发现的服务(Application
228
+ - 使用 `import { randomUUID } from 'node:crypto'`(Node >= 14.17
229
+ - 永远不修改原始 data 对象(浅拷贝 `{ ...data }`)
230
+ - retry 递归时 data 已包含 `_correlationId`,自动透传
231
+
232
+ ### 3.4 请求超时
233
+
234
+ **配置链:**
235
+
236
+ ```
237
+ ApplicationProps.requestTimeoutMs (default 30_000)
238
+ → call(timeout?) // 可选 override
239
+ → client.request(url, data, timeout ?? this._requestTimeoutMs)
240
+ → MessageModem._send({ url, data }, timeout)
241
+ → MessageModem._write(data, timeout, twoway=true)
242
+ → setTimeout(reject, timeout)
243
+ → 超时: 发送 ABORT 消息 → 对端取消执行
244
+ ```
245
+
246
+ - 超时默认 30 秒(MessageModem 默认值)
247
+ - 超时触发时向对端发送 **ABORT** 消息(不是仅仅本地 reject)
248
+ - 支持每个调用单独覆盖
249
+
250
+ ### 3.5 手动取消(abort)
251
+
252
+ `client.request()` 返回的 `abort()` 函数可主动取消请求,**与超时共享同一套 ABORT 机制**:
118
253
 
119
254
  ```typescript
120
- const app = new Application({
121
- namespace: 'my-service',
255
+ const client = await app.get('svc');
256
+ const { response, abort } = client.request('/api', data);
257
+
258
+ // 主动取消
259
+ abort();
260
+ await response(); // → reject (AbortException)
261
+
262
+ // 典型场景:竞态淘汰
263
+ const req = client.request('/slow', data);
264
+ // 如果其他条件满足,提前取消
265
+ if (cached) abort();
266
+ ```
267
+
268
+ **实现原理:**
269
+ - `request()` 内部通过 `MessageModem._write()` 创建 `AbortController`
270
+ - `abort()` 调用 `controller.abort()` 触发 ABORT 消息发送
271
+ - 超时到期也调用同一个 `controller.abort()`,机制完全相同
272
+ - 区别仅在于触发源头:手动调用 vs 定时器到期
273
+
274
+ **适用场景:** 用户取消操作、页面/组件卸载、竞态条件(先发请求后发先至时取消旧请求)
275
+
276
+ ```typescript
277
+ call(ns, url, data, timeout, retries = 1):
278
+ get(ns, exclude) → client
279
+ try:
280
+ client.request(url, data, timeout) → response() → result
281
+ recordSuccess(ns, host, port)
282
+ return result
283
+ catch err:
284
+ recordFailure(ns, host, port)
285
+ if retries > 0:
286
+ return call(ns, url, data, timeout, retries - 1) // 递归
287
+ throw err
288
+ ```
289
+
290
+ **重试语义:**
291
+ - 首次失败 → peer 被排除
292
+ - 递归 `call(retries-1)` → `getActiveExcludes` 包含已失败的 peer
293
+ - `get()` 带上 exclude → Registry 返回其他 peer
294
+ - 所有 peer 都失败 → 熔断全排除 → catch 块重置 → 无 exclude 重试
295
+ - 每个 retry 共享同一个 timeout 值(不会延长总时间)
296
+
297
+ ### 3.6 健康检查
298
+
299
+ 在 `Application` 构造函数中注册:
300
+
301
+ ```typescript
302
+ this.register('/-/health', async () => ({
303
+ status: 'ok' as const,
304
+ registry: !!this.registry, // 是否连上 Registry
305
+ uptime: process.uptime(), // 进程运行秒数
306
+ namespaces: [...this.namespaces.keys()], // 已缓存的 namespace
307
+ }));
308
+ ```
309
+
310
+ 只在 Application 上注册,Server 和 Registry 没有。
311
+
312
+ ### 3.7 Registry 心跳
313
+
314
+ ```
315
+ Application (10s 间隔):
316
+ startHeartbeat():
317
+ setInterval(10000):
318
+ registry.push('/-/heartbeat', {})
319
+
320
+ Registry (1s 间隔检查, 20s 超时):
321
+ 构造函数:
322
+ events.on('connect', ...) → heartbeats.set(key, Date.now())
323
+ register('/-/heartbeat', ...) → heartbeats.set(key, Date.now())
324
+ listen():
325
+ setInterval(1000):
326
+ for each heartbeat entry:
327
+ if now - lastTime >= 20000:
328
+ clients.get(key).dispose() // 剔除死实例
329
+ ```
330
+
331
+ ### 3.8 Registry 重连
332
+
333
+ ```
334
+ listen():
335
+ reconnectToRegistry() → connect + events.on('disconnect')
336
+
337
+ disconnect 触发:
338
+ registry = undefined
339
+ reconnectToRegistry():
340
+ ├─ 成功 → 恢复正常
341
+ └─ 失败 → scheduleRegistryRetry():
342
+ └─ setTimeout(3000) → reconnectToRegistry()
343
+ └─ 失败 → scheduleRegistryRetry() (循环)
344
+
345
+ listen() teardown 触发:
346
+ stopped = true → 停止所有重连尝试
347
+ ```
348
+
349
+ ### 3.9 缓存降级
350
+
351
+ **触发条件:** `get()` 的 registry lookup 失败,但 `this.clients` 中仍然有之前缓存的 Client(WebSocket 未断开)。
352
+
353
+ **处理流程:**
354
+ 1. `cachedHost` / `cachedPort` 在缓存失效前保存
355
+ 2. Registry lookup 失败 → catch 块
356
+ 3. `cachedHost && this.clients.has(cachedKey)` → 有效缓存
357
+ 4. 恢复 `stack.host/port/status=READY`,resolve 所有 handler
358
+ 5. 后续 `get()` 命中快路径,直接返回该 Client
359
+
360
+ **不处理的情况:** 全新 namespace(无缓存)、缓存 Client 已断连。
361
+
362
+ ---
363
+
364
+ ## 4. 代码生成模板
365
+
366
+ ### 4.1 三节点测试拓扑(所有 test 必须使用)
367
+
368
+ ```typescript
369
+ const registryPort = await getAvailablePort();
370
+ const providerPort = await getAvailablePort();
371
+ const consumerPort = await getAvailablePort();
372
+
373
+ const registry = new Registry(testAdvertise);
374
+ const provider = new Application({
375
+ namespace: 'svc',
122
376
  registry: { host: '127.0.0.1', port: registryPort },
377
+ ...testAdvertise,
123
378
  });
124
- const dispose = await app.listen(appPort);
379
+ const consumer = new Application({
380
+ namespace: 'consumer',
381
+ registry: { host: '127.0.0.1', port: registryPort },
382
+ ...testAdvertise,
383
+ });
384
+
385
+ const disposeRegistry = await registry.listen(registryPort);
386
+ const disposeProvider = await provider.listen(providerPort);
387
+ const disposeConsumer = await consumer.listen(consumerPort);
388
+ const unregister = provider.register('/echo', async ({ data }) => {
389
+ return { value: data.value };
390
+ });
391
+
392
+ try {
393
+ // ... test logic ...
394
+ } finally {
395
+ unregister();
396
+ await disposeConsumer();
397
+ await disposeProvider();
398
+ await disposeRegistry();
399
+ }
400
+ ```
125
401
 
126
- app.register('/hello', async ({ data }) => {
127
- return { ok: true, data };
402
+ ### 4.2 call() timeout + retries 参数顺序
403
+
404
+ ```typescript
405
+ // signature: call(ns, url, data, timeout?, retries?)
406
+ await app.call('svc', '/api', data); // 默认超时 + 1 次重试
407
+ await app.call('svc', '/api', data, 5000); // 5s 超时 + 1 次重试
408
+ await app.call('svc', '/api', data, 5000, 0); // 5s 超时 + 不重试
409
+ await app.call('svc', '/api', data, undefined, 0); // 默认超时 + 不重试
410
+ ```
411
+
412
+ ### 4.3 超时测试 Handler
413
+
414
+ ```typescript
415
+ // handler 延迟 500ms, call 超时 50ms → 应 reject
416
+ provider.register('/slow', async () => {
417
+ await new Promise(resolve => setTimeout(resolve, 500));
418
+ return { value: 'too-late' };
128
419
  });
420
+ await expect(consumer.call('svc', '/slow', {}, 50, 0)).rejects.toThrow('Abort');
421
+ ```
129
422
 
130
- const peer = await otherApp.get('my-service');
131
- const { response } = peer.request('/hello', { x: 1 });
132
- const result = await response();
423
+ ### 4.4 熔断测试模板
424
+
425
+ ```typescript
426
+ // 两个 provider 同 namespace, 一个失败, 排除后应选另一个
427
+ // 见 circuit breaker test "excludes a failing peer and selects a different one"
428
+
429
+ // 单个 peer 全排除后应重置并重试该 peer
430
+ // 见 circuit breaker test "resets breaker when all peers are excluded"
431
+ ```
432
+
433
+ ### 4.5 缓存降级测试模板
434
+
435
+ ```typescript
436
+ // 1. 首次 call → 建立缓存
437
+ // 2. 切换为失败 handler → call 失败 → peer 被排除
438
+ // 3. 切回成功 handler → call 带 exclude → Registry find 返回 undefined
439
+ // 4. catch 块降级 → 返回缓存 Client → 请求成功
133
440
  ```
134
441
 
135
- ### 4.2 连接超时
442
+ ---
443
+
444
+ ## 5. 测试门禁(修改时必须遵守)
136
445
 
137
- `Server.connect` 默认 **5 秒** 内未完成握手则 `reject(new Error('Connection timeout'))`。自定义第三参仅限受保护方法与测试辅助类,不要在公开 API 上强制调用方传入。
446
+ ### 5.1 运行命令
447
+
448
+ ```bash
449
+ pnpm --filter @hile/micro build # 必须通过
450
+ pnpm --filter @hile/micro test # 必须全部通过
451
+ ```
452
+
453
+ ### 5.2 测试覆盖要求
454
+
455
+ 修改行为时至少覆盖:
456
+
457
+ | 场景 | 测试位置 |
458
+ |------|----------|
459
+ | 服务发现端到端 | `application discovery > resolves a provider through the registry` |
460
+ | listen teardown 后可重新 listen | `application discovery > allows listen again after teardown` |
461
+ | Registry 不可达时 listen 回滚 | `application discovery > rolls back listen when registry is unreachable` |
462
+ | 心跳保活 | `heartbeat > keeps client alive when heartbeats arrive on time` |
463
+ | 心跳超时剔除 | `heartbeat > disconnects client that stops sending heartbeats` |
464
+ | call() 基本调用 | `circuit breaker > call() returns data on success` |
465
+ | 熔断排除 | `circuit breaker > excludes a failing peer and selects a different one` |
466
+ | 全排除重置 | `circuit breaker > resets breaker when all peers are excluded` |
467
+ | Correlation ID 注入 | `correlation ID > injects _correlationId into call() data` |
468
+ | Correlation ID 透传 | `correlation ID > preserves existing _correlationId` |
469
+ | 健康检查 | `health endpoint > /-/health returns status and registry state` |
470
+ | 超时 reject | `request timeout > rejects when request exceeds the timeout` |
471
+ | 超时充足则成功 | `request timeout > succeeds when timeout is long enough` |
472
+ | 缓存降级 | `cache degradation > uses cached client when registry lookup fails due to exclusion` |
473
+
474
+ ### 5.3 测试规范(必须遵守)
475
+
476
+ - 端口必须使用 `getAvailablePort()` 获取
477
+ - 所有清理必须放在 `finally` 块中
478
+ - 清理顺序:`unregister()` → `disposeConsumer` → `disposeProvider` → `disposeRegistry`
479
+ - 禁止使用真实定时等待代替事件驱动(心跳测试是唯一例外,因其依赖实时时钟)
480
+ - 禁止 mock `Application`、`Registry`、`Server`、`Client` 的内部方法
481
+ - 禁止共享可变状态(每个测试独立端口)
482
+
483
+ ---
138
484
 
139
- ### 4.3 随机选择
485
+ ## 6. 反模式(禁止)
140
486
 
141
- `/-/find` 返回的地址必须来自 Registry 内存集合内的 `host:port` 键;新增选择策略时需保持 **Registry 端无额外状态机**(尽量无 cursor),除非你同时补充设计与测试。
487
+ 1. **不要修改 WebSocket URL 三段式约定**不同时更新 `Server.onConnected` `Server.connect` 的拼接格式
488
+ 2. **不要在 Registry 中按 Set 迭代顺序固定返回第一个**(破坏负载分散)
489
+ 3. **不要在 `Client.dispose()` 中删除 `socket.close()`**(会导致 WebSocketServer.close 长时间等待)
490
+ 4. **不要假设 `host:port` 可无损表达 IPv6** — 使用 `[IPv6]:port` 格式,`parseAddressKey` 按最后一个 `:` 切分
491
+ 5. **不要传错 Registry 端口** — 丢失 Registry 连接时依赖 `reconnectToRegistry`,不要在外部缓存 registry Client
492
+ 6. **不要在 call() 中修改原始 data 对象** — 必须使用浅拷贝 `{ ...data, _correlationId }`
493
+ 7. **不要在其他文件中重复 `selectRandomRegistryAddress` 或 `parseAddressKey`** — 这些是 Registry 的内部 helper
494
+ 8. **不要给 call() 增加非可选参数** — `timeout` 和 `retries` 都在尾部且保持可选,不影响现有调用
142
495
 
143
496
  ---
144
497
 
145
- ## 5. 反模式(禁止)
498
+ ## 7. 文件改动范围
146
499
 
147
- - 修改 WebSocket URL 三段式约定却不同时更新 **`Server.onConnected`** 与 **`Server.connect`** 的拼接格式。
148
- - 在 `Registry` 中按 `Set` **迭代顺序** 固定返回「第一个」实例(破坏负载分散);除非你明确改需求并改写测试。
149
- - `Client`/`Server` **`dispose`** 后不关闭 **`ws`**(会导致 **`WebSocketServer.close`** 长时间等待);本包已在 `Client.dispose` 内关闭socket,不要随意删除。
150
- - 假设 `host:port` 串可无损表达 **IPv6**(当前实现按 **首个 `:` 分割**,仅适合 IPv4 或不含冒号的 host)。
151
- - `Application.props.registry` **不要传错端口**;丢失与注册中心的连接时依赖 `reconnectToRegistry`,不要在外部缓存 `registry` Client 绕过重连语义。
500
+ | 文件 | 可修改 | 说明 |
501
+ |------|--------|------|
502
+ | `packages/micro/src/application.ts` | | 核心业务逻辑 |
503
+ | `packages/micro/src/index.test.ts` | | 测试 |
504
+ | `packages/micro/src/server.ts` | | 底层协议,不动 |
505
+ | `packages/micro/src/client.ts` | ❌ | 底层协议,不动 |
506
+ | `packages/micro/src/registry.ts` | ❌ | 注册中心,不动 |
507
+ | `packages/micro/src/utils.ts` | ❌ | 工具函数,不动 |
508
+ | `packages/micro/README.md` | ✅ | 用户文档 |
509
+ | `packages/micro/SKILL.md` | ✅ | AI 参考文档 |
152
510
 
153
511
  ---
154
512
 
155
- ## 6. 测试与改动范围
513
+ ## 8. 参考文件
156
514
 
157
- - 包内测试:`packages/micro/src/index.test.ts`(Vitest)。
158
- - 修改行为时至少覆盖:**随机选择 helper**、`Application` 端到端发现、**连接超时**。
159
- - 发布前在项目根或通过 filter 运行:`pnpm --filter @hile/micro test` `pnpm --filter @hile/micro build`。
515
+ | 文件 | 用途 |
516
+ |------|------|
517
+ | `packages/micro/src/application.ts` | 完整实现(共 ~310 行) |
518
+ | `packages/micro/src/index.test.ts` | 28 个测试用例 |
519
+ | `docs/superpowers/specs/2026-05-14-micro-improvements-design.md` | 四种改进的设计规约 |
520
+ | `docs/superpowers/plans/2026-05-14-micro-improvements.md` | 实施计划 |
@@ -1,19 +1,38 @@
1
1
  import { Client } from './client.js';
2
- import { Server } from './server.js';
3
- import { MessageLoaderProps } from '@hile/message-loader';
2
+ import { Server, type MicroServerProps } from './server.js';
4
3
  import { RegistryAddress } from './registry.js';
5
4
  export type ApplicationProps = {
6
5
  namespace: string;
7
6
  registry: RegistryAddress;
8
- } & MessageLoaderProps;
7
+ /** `/-/find` 等待响应的上限(毫秒),默认 `10000` */
8
+ registryLookupTimeoutMs?: number;
9
+ /** 单次 request() 等待响应的上限(毫秒),默认 `30000` */
10
+ requestTimeoutMs?: number;
11
+ } & MicroServerProps;
9
12
  export declare class Application extends Server {
10
13
  private registry?;
11
14
  private reconnectTimeout?;
15
+ private registryReconnectPromise;
16
+ /** 为 true 时不再向 Registry 重连(listen 返回的 teardown 已触发) */
17
+ private stopped;
12
18
  private readonly _registry_address;
19
+ private readonly _registryLookupTimeoutMs;
20
+ private readonly _requestTimeoutMs;
13
21
  private readonly namespaces;
22
+ private static readonly HEARTBEAT_INTERVAL;
23
+ private heartbeatTimer?;
24
+ private static readonly CB_COOLDOWN_MS;
25
+ private readonly circuitBreakers;
14
26
  constructor(props: ApplicationProps);
15
27
  listen(port?: number): Promise<() => Promise<void>>;
28
+ private scheduleRegistryRetry;
16
29
  private reconnectToRegistry;
30
+ private startHeartbeat;
31
+ private stopHeartbeat;
32
+ private recordSuccess;
33
+ private recordFailure;
34
+ private getActiveExcludes;
17
35
  private findFromRegistry;
18
- get(namespace: string): Promise<Client>;
36
+ get(namespace: string, exclude?: string[]): Promise<Client>;
37
+ call<T = any>(namespace: string, url: string, data: any, timeout?: number, retries?: number): Promise<T>;
19
38
  }