@hile/micro 2.0.7 → 2.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -108,20 +108,50 @@ const result = await response();
108
108
 
109
109
  ### 熔断器 (Circuit Breaker)
110
110
 
111
- `call()` 自动跟踪每个 namespace 下各 peer 的调用失败。失败的 peer 被临时排除,**30 秒冷卻期**后自动恢复。
111
+ `call()` 和 `stream()` 自动跟踪每个 namespace 下各 peer 的调用结果。熔断状态保存在调用方 `Application` 的本地内存中,不依赖 Redis、数据库或 Registry 共享状态。
112
112
 
113
113
  | 场景 | 行为 |
114
114
  |------|------|
115
- | 调用 peer A 失败 | A 加入排除列表(30s cooldown) |
116
- | 再次调用该 namespace | Registry `/‑/find` 带上 `exclude`,返回其他 peer |
117
- | 所有 peer 都被排除 | 重置排除列表,重新从所有 peer 中选择 |
118
- | 调用成功 | peer 从排除列表中移除 |
115
+ | `closed` 状态调用失败 | 累计连续失败次数 |
116
+ | 连续失败达到阈值 | peer 进入 `open`,选路时通过 Registry `exclude` 临时排除 |
117
+ | 冷却时间到期 | peer 进入 `half-open`,只放行少量探测请求 |
118
+ | `half-open` 探测成功 | 累计成功次数,达到阈值后恢复 `closed` |
119
+ | `half-open` 探测失败 | 重新进入 `open`,冷却时间指数退避 |
120
+ | 所有 `open` peer 都被排除 | 重置该 namespace 的本地熔断状态,重新选择 peer;若 Registry 不可用但已选中的缓存 Client 仍连接,则沿用缓存发起探测,避免完全饿死 |
119
121
 
120
- 排除列表:
121
- - 键:`${host}:${port}`
122
- - 存储:`Map<namespace, Map<peerKey, openedAt>>`
123
- - 冷卻期:`CB_COOLDOWN_MS = 30_000`(30 秒)
124
- - 过期检查:`getActiveExcludes()` 在每次 `call()` 调用时执行
122
+ 如果 peer 正处于 `half-open` 且探测名额已满,本次调用不会占用额外探测名额;有其他 peer 时改选其他 peer,没有可用 peer 时需要在已有探测完成后再尝试。
123
+
124
+ 默认策略:
125
+
126
+ | 配置 | 默认值 | 说明 |
127
+ |------|--------|------|
128
+ | `failureThreshold` | `3` | 连续失败 3 次后打开熔断 |
129
+ | `failureWindowMs` | `60000` | 连续失败统计窗口 |
130
+ | `successThreshold` | `2` | half-open 连续成功 2 次后恢复 |
131
+ | `cooldownMs` | `10000` | 首次打开后的冷却时间 |
132
+ | `maxCooldownMs` | `120000` | 指数退避冷却上限 |
133
+ | `halfOpenMaxProbes` | `1` | half-open 同时放行的探测数 |
134
+
135
+ 可以在构造 `Application` 时覆盖:
136
+
137
+ ```typescript
138
+ const app = new Application({
139
+ namespace: 'checkout',
140
+ registry: { host, port },
141
+ circuitBreaker: {
142
+ failureThreshold: 3,
143
+ failureWindowMs: 60_000,
144
+ successThreshold: 2,
145
+ cooldownMs: 10_000,
146
+ maxCooldownMs: 120_000,
147
+ halfOpenMaxProbes: 1,
148
+ shouldRecordFailure: (err) => true,
149
+ shouldRetry: (err) => true,
150
+ },
151
+ });
152
+ ```
153
+
154
+ `shouldRecordFailure` 返回 `false` 的错误不会计入熔断;`shouldRetry` 返回 `false` 的错误不会自动重试。可用它们区分业务错误和连接、超时等基础设施错误。
125
155
 
126
156
  ### 请求超时
127
157
 
@@ -136,8 +166,8 @@ const app = new Application({
136
166
  });
137
167
 
138
168
  // 单次调用覆盖
139
- await app.call('svc', '/api', data, 5_000); // 5s 超时
140
- await app.call('svc', '/api', data, 1_000, 0); // 1s 超时, 不重试
169
+ await app.call('svc', '/api', data, { timeout: 5_000 }); // 5s 超时, 默认重试 1 次
170
+ await app.call('svc', '/api', data, { timeout: 1_000, retries: 0 }); // 1s 超时, 不重试
141
171
  ```
142
172
 
143
173
  超时触发时,底层 MessageModem 会向对端发送 **ABORT** 消息取消远程执行。
@@ -169,12 +199,51 @@ try {
169
199
  `call()` 默认 retries=1,失败后自动换 peer 重试:
170
200
 
171
201
  ```typescript
172
- await app.call('svc', '/api', data); // 默认重试 1 次
173
- await app.call('svc', '/api', data, 5000, 3); // 超时 5s, 重试 3 次
174
- await app.call('svc', '/api', data, 5000, 0); // 超时 5s, 不重试
202
+ await app.call('svc', '/api', data); // 默认重试 1 次
203
+ await app.call('svc', '/api', data, { timeout: 5000, retries: 3 }); // 超时 5s, 重试 3 次
204
+ await app.call('svc', '/api', data, { timeout: 5000, retries: 0 }); // 超时 5s, 不重试
175
205
  ```
176
206
 
177
- 重试策略:失败 → `recordFailure`(peer 被排除)→ 递归 `call(retries-1)` → `getActiveExcludes` 排除已失败的 peer → Registry `/‑/find` 返回其他 peer。
207
+ 重试策略:失败 → `shouldRecordFailure` 更新本地熔断状态 `shouldRetry` 判断是否继续下一次尝试 下次发现服务时用 `getActiveExcludes()` 排除 `open` 或探测名额已满的 `half-open` peer → Registry `/‑/find` 返回其他 peer。
208
+
209
+ ### 流式调用 (Stream)
210
+
211
+ `stream()` 用于**需要持续推送数据**的场景:大数据集、实时事件、LLM token 流、进度上报等。不需要流式传输时优先用 `call()`。
212
+
213
+ **Provider 侧**:消息处理器必须返回 async generator(`async function*`)。
214
+
215
+ ```typescript
216
+ // 通过 register() 注册
217
+ app.register('/events', async function* () {
218
+ for (let i = 0; i < 100; i++) {
219
+ yield { seq: i, time: Date.now() }
220
+ await new Promise(r => setTimeout(r, 100))
221
+ }
222
+ })
223
+
224
+ // 或通过 .msg 文件定义(推荐)
225
+ // src/messages/events.msg.ts
226
+ import { defineMessage } from '@hile/message-loader'
227
+ export default defineMessage(async function* ({ data }) {
228
+ for (const item of await fetchItems(data.query)) {
229
+ yield item
230
+ }
231
+ })
232
+ ```
233
+
234
+ **Consumer 侧**:`app.stream()` 返回 `Readable` stream,可用 `for await` 逐 chunk 消费。
235
+
236
+ ```typescript
237
+ const stream = await app.stream('data-svc', '/events', { query: 'recent' })
238
+ for await (const chunk of stream) {
239
+ console.log(chunk) // { seq: 0, time: 1718000000000 }
240
+ }
241
+ ```
242
+
243
+ **注意事项**:
244
+ - 普通 handler(返回非 async iterable)被 `stream()` 调用时会报错 `"Invalid async iterable"`
245
+ - 不需要流式传输时用 `call()`,不要用 `stream()` 取单次返回值
246
+ - `stream()` 享有与 `call()` 相同的选路与熔断;同步建流失败可按 `retries` 重试,流已返回后的异步错误会计入熔断但不会自动重放流
178
247
 
179
248
  ### 健康检查
180
249
 
@@ -197,7 +266,7 @@ const health = await app.dispatch('/-/health', {});
197
266
 
198
267
  ### 缓存降级
199
268
 
200
- 当 Registry 不可用但本地仍有已缓存的 Client 连接时,`get()` 自动降级使用缓存:
269
+ 当 Registry 不可用但本地仍有已缓存的 Client 连接时,`get()` 自动降级使用缓存。熔断器在“所有 `open` peer 都被排除”并触发本地 reset 时,也会保留已经选中的可用缓存 Client,不会因为 Registry 短暂不可用而丢弃仍可通信的连接。
201
270
 
202
271
  | 场景 | 行为 |
203
272
  |------|------|
@@ -363,10 +432,25 @@ class Application extends Server {
363
432
  namespace: string,
364
433
  url: string,
365
434
  data: any,
366
- timeout?: number, // 请求超时(ms),默认 requestTimeoutMs
367
- retries?: number, // 失败重试次数,默认 1
435
+ options?: {
436
+ timeout?: number, // 请求超时(ms),默认 requestTimeoutMs
437
+ retries?: number, // 失败重试次数,默认 1
438
+ signal?: AbortSignal, // 手动取消
439
+ },
368
440
  ): Promise<T>;
369
441
 
442
+ // 流式调用:get + stream + 熔断;同步建流失败可重试
443
+ // provider handler 必须返回 async generator,consumer 获得 Readable stream
444
+ stream(
445
+ namespace: string,
446
+ url: string,
447
+ data: any,
448
+ options?: {
449
+ signal?: AbortSignal,
450
+ retries?: number, // 失败重试次数,默认 1
451
+ },
452
+ ): Promise<import('stream').Readable>;
453
+
370
454
  // 注册路由(provider 侧)
371
455
  register<T = any>(url: string, handler: (ctx) => Promise<T>): () => void;
372
456
 
@@ -410,6 +494,7 @@ class Server extends MessageLoader {
410
494
  class Client extends MessageWs {
411
495
  request(url: string, data: any, timeout?: number): { abort(): void; response<T>(): Promise<T> };
412
496
  push(url: string, data: any, timeout?: number): void;
497
+ stream(url: string, data: any, options?: { signal?: AbortSignal }): Readable;
413
498
  dispose(): void;
414
499
  }
415
500
  ```
@@ -15,6 +15,28 @@ type EnvRequestResult<T extends Record<string, Record<string, any>>, R> = R exte
15
15
  } ? {
16
16
  [K in N]: EnvFieldsForRequest<T, N, F>;
17
17
  } : never;
18
+ export type CircuitBreakerStatus = 'closed' | 'open' | 'half-open';
19
+ export type CircuitBreakerOptions = {
20
+ /** 连续失败达到阈值后打开熔断器,默认 3 */
21
+ failureThreshold?: number;
22
+ /** 连续失败统计窗口(毫秒),默认 60000 */
23
+ failureWindowMs?: number;
24
+ /** half-open 探测连续成功达到阈值后恢复,默认 2 */
25
+ successThreshold?: number;
26
+ /** 首次打开后的冷却时间(毫秒),默认 10000 */
27
+ cooldownMs?: number;
28
+ /** 指数退避冷却时间上限(毫秒),默认 120000 */
29
+ maxCooldownMs?: number;
30
+ /** half-open 状态下同时放行的探测请求数,默认 1 */
31
+ halfOpenMaxProbes?: number;
32
+ /** 返回 false 的错误不会计入熔断,默认全部计入 */
33
+ shouldRecordFailure?: (err: unknown) => boolean;
34
+ /** 返回 false 的错误不会自动重试,默认全部允许重试 */
35
+ shouldRetry?: (err: unknown) => boolean;
36
+ };
37
+ type RegistryLookupOptions = {
38
+ allowExcludedCachedFallback?: boolean;
39
+ };
18
40
  export type GetEnvVariablesResult<T extends Record<string, Record<string, any>>, Requests extends readonly EnvRequest<T>[]> = UnionToIntersection<EnvRequestResult<T, Requests[number]>>;
19
41
  export type ApplicationProps = {
20
42
  namespace: string;
@@ -23,30 +45,68 @@ export type ApplicationProps = {
23
45
  registryLookupTimeoutMs?: number;
24
46
  /** 单次 request() 等待响应的上限(毫秒),默认 `30000` */
25
47
  requestTimeoutMs?: number;
48
+ /** 本地内存熔断策略配置 */
49
+ circuitBreaker?: CircuitBreakerOptions;
26
50
  } & MicroServerProps;
27
51
  export declare class Application extends Server {
28
52
  private registry?;
29
53
  private reconnectTimeout?;
30
54
  private registryReconnectPromise;
55
+ private registryReconnectGeneration;
31
56
  /** 为 true 时不再向 Registry 重连(listen 返回的 teardown 已触发) */
32
57
  private stopped;
58
+ private listenGeneration;
33
59
  private readonly _registry_address;
34
60
  private readonly _registryLookupTimeoutMs;
35
61
  private readonly _requestTimeoutMs;
62
+ private readonly _circuitBreaker;
36
63
  private readonly namespaces;
37
- private static readonly CB_COOLDOWN_MS;
38
64
  private readonly circuitBreakers;
39
- private readonly fallbacks;
40
65
  private readonly topics;
66
+ private readonly publishedTopics;
67
+ private readonly publishedTopicRevisions;
68
+ private readonly publishedTopicDirty;
69
+ private readonly publishedTopicVersions;
70
+ private readonly publishedTopicSignatures;
71
+ private readonly topicSyncs;
72
+ private readonly topicUpdateVersions;
73
+ private publishIntentVersion;
41
74
  constructor(props: ApplicationProps);
75
+ private dispatchTopicUpdate;
42
76
  listen(port?: number): Promise<() => Promise<void>>;
43
77
  private scheduleRegistryRetry;
78
+ private canUsePubSub;
79
+ private assertCanUsePubSub;
80
+ private ensureRegistryReconnectScheduled;
81
+ private handleRegistrySyncFailure;
82
+ private registryRequestOptions;
83
+ private recordPublishedTopic;
84
+ private enqueueTopicSync;
85
+ private syncPublishedTopic;
86
+ private syncUnpublishedTopic;
87
+ private syncUnsubscribedTopic;
88
+ private syncRestoredSubscription;
89
+ private restoreSubscription;
90
+ private rollbackLocalSubscription;
44
91
  private reconnectToRegistry;
92
+ private peerKey;
93
+ private getPeerStates;
94
+ private getOrCreatePeerState;
95
+ private deletePeerState;
96
+ private openCircuit;
97
+ private acquireCircuitProbe;
98
+ private shouldRecordCircuitFailure;
99
+ private shouldRetryCircuitFailure;
45
100
  private recordSuccess;
46
101
  private recordFailure;
102
+ private getActiveCircuitExcludes;
47
103
  private getActiveExcludes;
104
+ private trackCircuitStream;
105
+ private selectCircuitClient;
106
+ private selectCircuitProbe;
48
107
  private findFromRegistry;
49
108
  get(namespace: string, exclude?: string[]): Promise<Client>;
109
+ protected resolveClient(namespace: string, exclude?: string[], options?: RegistryLookupOptions): Promise<Client>;
50
110
  call<T = any>(namespace: string, url: string, data: any, options?: {
51
111
  timeout?: number;
52
112
  retries?: number;
@@ -60,7 +120,10 @@ export declare class Application extends Server {
60
120
  update: (payload: T) => Promise</*elided*/ any>;
61
121
  unpublish: () => Promise</*elided*/ any>;
62
122
  }>;
63
- /** 对同一 topic 重复 subscribe 是幂等的:第二次调用只返回 unsubscribe 函数,不会注册第二个 callback */
123
+ /**
124
+ * 对同一 topic 可多次 subscribe,各自独立回调。
125
+ * 传入同一个 callback 引用第二次调用时幂等返回 unsubscribe,不重复注册。
126
+ */
64
127
  subscribe<T = any>(topic: string, callback: (data: T) => any, isReconnect?: boolean): Promise<() => Promise<void>>;
65
128
  }
66
129
  export {};