@hile/micro 3.0.0 → 3.0.1

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.
Files changed (3) hide show
  1. package/AI.md +368 -0
  2. package/README.md +56 -478
  3. package/package.json +7 -7
package/AI.md ADDED
@@ -0,0 +1,368 @@
1
+ # AI Guide For @hile/micro
2
+
3
+
4
+
5
+ <!-- Generated by scripts/build-ai-context.mjs from docs/ai. Do not edit by hand. -->
6
+
7
+
8
+
9
+ Purpose: Run registry-backed service discovery, RPC, streaming RPC, and pub/sub between Node services.
10
+
11
+
12
+
13
+ Use this file when an AI agent installs the npm package and needs package-local examples, package selection rules, boundaries, and verification steps.
14
+
15
+
16
+
17
+ ## Package Selection
18
+
19
+
20
+
21
+ | User asks for | Use | Also read |
22
+ |---|---|---|
23
+ | Build service discovery or RPC | `@hile/micro` | `packages/messaging-micro.md`, `recipes/micro-rpc-message-loader.md` |
24
+ | Push runtime config without restarts | `@hile/micro-dynamic-configs` | `packages/messaging-micro.md`, `recipes/runtime-config.md` |
25
+
26
+
27
+
28
+ # Messaging And Microservices
29
+
30
+ Packages: `@hile/message-modem`, `@hile/message-ws`, `@hile/message-ipc`, `@hile/message-worker-thread`, `@hile/message-loader`, `@hile/micro`, `@hile/micro-dynamic-configs`.
31
+
32
+ ## Copy-Paste Example
33
+
34
+ Message handler file:
35
+
36
+ ```ts
37
+ // src/messages/ping.msg.ts
38
+ import { defineMessage } from '@hile/message-loader'
39
+
40
+ export default defineMessage(async ({ data, params }) => {
41
+ return {
42
+ type: 'pong',
43
+ data,
44
+ params,
45
+ timestamp: Date.now(),
46
+ }
47
+ })
48
+ ```
49
+
50
+ Microservice boot file:
51
+
52
+ ```ts
53
+ // src/services/app.boot.ts
54
+ import { defineService } from '@hile/core'
55
+ import { Application } from '@hile/micro'
56
+
57
+ export default defineService('micro.app', async (shutdown) => {
58
+ const app = new Application({
59
+ namespace: process.env.MICRO_NAMESPACE ?? 'example.service',
60
+ registry: {
61
+ host: process.env.REGISTRY_HOST ?? '127.0.0.1',
62
+ port: Number(process.env.REGISTRY_PORT ?? 9876),
63
+ },
64
+ advertiseHost: process.env.HILE_ADVERTISE_HOST ?? '127.0.0.1',
65
+ })
66
+
67
+ await app.load(new URL('../messages', import.meta.url).pathname)
68
+ const stop = await app.listen(Number(process.env.MICRO_PORT ?? 0))
69
+ shutdown(stop)
70
+ return app
71
+ })
72
+ ```
73
+
74
+ Caller:
75
+
76
+ ```ts
77
+ const result = await app.call('example.service', '/ping', { hello: 'world' })
78
+ ```
79
+
80
+ ## More Examples
81
+
82
+ Streaming handler:
83
+
84
+ ```ts
85
+ // src/messages/events.msg.ts
86
+ import { defineMessage } from '@hile/message-loader'
87
+
88
+ export default defineMessage(async function* () {
89
+ for (let i = 0; i < 3; i++) {
90
+ yield { seq: i }
91
+ }
92
+ })
93
+ ```
94
+
95
+ Streaming caller:
96
+
97
+ ```ts
98
+ const stream = await app.stream('example.service', '/events', {})
99
+ for await (const chunk of stream) {
100
+ console.log(chunk)
101
+ }
102
+ ```
103
+
104
+ Custom WebSocket modem:
105
+
106
+ ```ts
107
+ import { MessageWs } from '@hile/message-ws'
108
+ import type WebSocket from 'ws'
109
+
110
+ class RpcWs extends MessageWs {
111
+ constructor(ws: WebSocket, private readonly dispatch: (url: string, data: unknown) => Promise<unknown>) {
112
+ super(ws)
113
+ }
114
+
115
+ protected exec(data: { url: string; data: unknown }) {
116
+ return this.dispatch(data.url, data.data)
117
+ }
118
+
119
+ request<T>(url: string, data: unknown, timeout = 30_000) {
120
+ return this._send<T>({ url, data }, { timeout })
121
+ }
122
+ }
123
+ ```
124
+
125
+ Notice that `request()` returns a `Promise<T>`. Await it directly.
126
+
127
+ ## Use When
128
+
129
+ Use the message packages for request/response messaging over WebSocket, process IPC, worker threads, file-system message handlers, service discovery, streaming RPC, and registry-backed pub/sub.
130
+
131
+ ## Do Not Use When
132
+
133
+ - Do not use `stream()` for normal single-result calls.
134
+ - Do not rely on message IDs for business idempotency. They are transport IDs.
135
+ - Do not bypass `defineMessage()` for file-loaded handlers.
136
+
137
+ ## Install
138
+
139
+ ```bash
140
+ pnpm add @hile/micro @hile/message-loader @hile/message-ws
141
+ ```
142
+
143
+ Use transport-specific packages only when you need to build custom IPC or worker-thread bridges.
144
+
145
+ ## Imports
146
+
147
+ ```ts
148
+ import { defineMessage, MessageLoader } from '@hile/message-loader'
149
+ import { Application, Registry, Server } from '@hile/micro'
150
+ import { MessageWs } from '@hile/message-ws'
151
+ import { MessageIpc } from '@hile/message-ipc'
152
+ import { MessageWorkerThread } from '@hile/message-worker-thread'
153
+ ```
154
+
155
+ ## Compose With
156
+
157
+ - `@hile/context` propagates context in micro message metadata.
158
+ - `@hile/redis-idempotency` protects retryable side effects in message handlers.
159
+ - `@hile/redis-stream-queue` is better for durable background jobs.
160
+
161
+ ## Runtime And Lifecycle Notes
162
+
163
+ - `MessageLoader` maps `*.msg.*` files to routes using `@hile/loader`.
164
+ - `MessageLoader.dispatch(path, data, extras?)` invokes the matched handler.
165
+ - `MessageModem._send()` returns a `Promise`.
166
+ - `MessageModem._stream()` returns a Node `Readable` in object mode.
167
+ - A stream request requires `exec()` to return an async iterable.
168
+ - `Application.call(namespace, url, data, options?)` returns a promise.
169
+ - `Application.stream(namespace, url, data, options?)` returns a readable stream.
170
+ - `Application.publish(topic, payload)` returns an object with `update()` and `unpublish()`.
171
+ - `Application.subscribe(topic, callback)` returns an unsubscribe function.
172
+ - `Registry` stores service addresses and retained config/topic state under `~/.registry`.
173
+
174
+ ## Anti-Patterns
175
+
176
+ - Appending a secondary response getter to `client.request('/x', data)`
177
+ - Returning a plain object from a handler called through `stream()`.
178
+ - Using pub/sub as a durable queue.
179
+ - Forgetting to register `shutdown(await app.listen(...))`.
180
+
181
+ ## Verification Checklist
182
+
183
+ - Message files default-export `defineMessage(...)`.
184
+ - RPC callers use `await app.call(...)`.
185
+ - Streaming handlers are async generators.
186
+ - Registry is started before application nodes need discovery.
187
+ - Micro apps use stable namespaces and advertise reachable hosts.
188
+
189
+
190
+
191
+ # Related Recipes
192
+
193
+
194
+
195
+ # Micro RPC With Message Loader
196
+
197
+ ## Complete Example
198
+
199
+ Provider handler:
200
+
201
+ ```ts
202
+ // src/messages/charge.msg.ts
203
+ import { defineMessage } from '@hile/message-loader'
204
+
205
+ export default defineMessage(async ({ data }) => {
206
+ return { charged: true, input: data }
207
+ })
208
+ ```
209
+
210
+ Provider boot:
211
+
212
+ ```ts
213
+ // src/services/app.boot.ts
214
+ import { defineService } from '@hile/core'
215
+ import { Application } from '@hile/micro'
216
+
217
+ export default defineService('billing.micro', async (shutdown) => {
218
+ const app = new Application({
219
+ namespace: 'billing',
220
+ registry: { host: '127.0.0.1', port: 9876 },
221
+ advertiseHost: '127.0.0.1',
222
+ })
223
+
224
+ await app.load(new URL('../messages', import.meta.url).pathname)
225
+ const stop = await app.listen(9101)
226
+ shutdown(stop)
227
+ return app
228
+ })
229
+ ```
230
+
231
+ Consumer:
232
+
233
+ ```ts
234
+ const result = await app.call('billing', '/charge', {
235
+ tenantId: 't1',
236
+ amount: 100,
237
+ })
238
+ ```
239
+
240
+ ## File Layout
241
+
242
+ ```text
243
+ provider/
244
+ src/messages/charge.msg.ts
245
+ src/services/app.boot.ts
246
+ consumer/
247
+ src/models/payments/pay.model.ts
248
+ ```
249
+
250
+ ## User Intent
251
+
252
+ Use this recipe when services communicate over Hile registry-backed RPC.
253
+
254
+ ## Packages To Use
255
+
256
+ - `@hile/micro`
257
+ - `@hile/message-loader`
258
+ - `@hile/context` when context must cross service boundaries
259
+ - `@hile/redis-idempotency` for retryable side effects
260
+
261
+ ## Implementation Steps
262
+
263
+ 1. Start a Registry with `hile registry`.
264
+ 2. Start providers with stable namespaces.
265
+ 3. Load `*.msg.ts` handlers through `app.load()`.
266
+ 4. Call providers with `await app.call(namespace, url, data)`.
267
+ 5. Use `app.stream()` only for async-generator handlers.
268
+
269
+ ## Failure And Cleanup Behavior
270
+
271
+ - `Application.call()` may retry; side-effecting handlers need idempotency.
272
+ - Registry disconnect triggers reconnect; apps re-declare topics and subscriptions.
273
+ - Circuit breaker excludes failing nodes for cooldown.
274
+
275
+ ## Verification Checklist
276
+
277
+ - Registry is reachable.
278
+ - Provider namespace matches consumer call.
279
+ - Handlers default-export `defineMessage()`.
280
+ - Consumer code awaits `app.call(...)` directly.
281
+
282
+ # Runtime Dynamic Config
283
+
284
+ ## Complete Example
285
+
286
+ ```ts
287
+ import { z } from 'zod'
288
+ import { MicroDynamicConfigsServer } from '@hile/micro-dynamic-configs'
289
+
290
+ const schema = z.object({
291
+ featureCheckout: z.boolean().default(false),
292
+ maxRetries: z.number().int().min(1).max(10).default(3),
293
+ })
294
+
295
+ const configs = new MicroDynamicConfigsServer({
296
+ app,
297
+ redis,
298
+ schema,
299
+ redis_key: 'configs:checkout',
300
+ })
301
+
302
+ const cleanup = await configs.initialize()
303
+ shutdown(cleanup)
304
+
305
+ configs.on('change:featureCheckout', (next, previous) => {
306
+ logger.info({ previous, next }, 'featureCheckout changed')
307
+ })
308
+
309
+ await configs.save({ featureCheckout: true })
310
+ ```
311
+
312
+ ## File Layout
313
+
314
+ ```text
315
+ src/services/configs.boot.ts
316
+ src/services/app.boot.ts
317
+ ```
318
+
319
+ ## User Intent
320
+
321
+ Use this recipe when config changes should persist to Redis and be pushed through the micro registry without restarting services.
322
+
323
+ ## Packages To Use
324
+
325
+ - `@hile/micro-dynamic-configs`
326
+ - `@hile/micro`
327
+ - `@hile/ioredis`
328
+ - `zod`
329
+
330
+ ## Implementation Steps
331
+
332
+ 1. Define a Zod object schema with defaults.
333
+ 2. Create `MicroDynamicConfigsServer`.
334
+ 3. Call `initialize()` after `app` and `redis` are ready.
335
+ 4. Register cleanup with `shutdown()`.
336
+ 5. Use `.save(partial)` for changes.
337
+
338
+ ## Failure And Cleanup Behavior
339
+
340
+ - `save()` validates fields before mutating memory.
341
+ - Redis is written before memory and event emission update.
342
+ - `initialize()` publishes every config field as a topic.
343
+ - Cleanup unpublishes topics and removes listeners.
344
+
345
+ ## Verification Checklist
346
+
347
+ - Schema defaults parse `{}`.
348
+ - `redis_key` is app-specific.
349
+ - Change handlers use `change:key`.
350
+ - Cleanup from `initialize()` is registered.
351
+
352
+
353
+
354
+ # Global Guardrails
355
+
356
+
357
+
358
+ ## Never Generate These Patterns
359
+
360
+ - Do not call `loadService()` at module top level; it starts resources during import.
361
+ - Do not default-export plain functions from `*.boot.*` files; `hile start` expects a Hile service.
362
+ - Do not set `ctx.body` and also return a controller value.
363
+ - Do not assume `@hile/http` Zod validation mutates or coerces `ctx.query`, `ctx.params`, or `ctx.request.body`.
364
+ - Do not put reusable business logic only in controllers, pages, queue workers, or message handlers.
365
+ - Do not use old message examples that append a secondary response getter; current request APIs return promises directly.
366
+ - Do not claim exactly-once delivery or execution from Redis locks, queues, idempotency, or rate limits.
367
+ - Do not use queue `jobId` as the only side-effect idempotency boundary.
368
+ - Do not log the entire async context by default.
package/README.md CHANGED
@@ -1,512 +1,90 @@
1
1
  # @hile/micro
2
2
 
3
- 基于 `@hile/message-loader` `@hile/message-ws` 的轻量级 **WebSocket 微服务框架**。提供服务注册与发现、心跳保活、熔断、请求超时、自动重试等功能。
3
+ <!-- Generated by scripts/build-ai-context.mjs from docs/ai. Do not edit by hand. -->
4
4
 
5
- ## 架构分层
5
+ Run registry-backed service discovery, RPC, streaming RPC, and pub/sub between Node services.
6
6
 
7
- ```
8
- MessageLoader (路由) + MessageWs (请求/响应传输)
9
- └── Server(WebSocket 服务底层)
10
- ├── Registry(注册中心)
11
- └── Application(应用服务)
12
- ```
7
+ This README is intentionally short and example-first. The complete AI-facing guide ships in `AI.md` in this package.
13
8
 
14
- | 组件 | 职责 |
15
- |------|------|
16
- | **Server** | WebSocket 监听、连接管理、消息路由。不关心注册中心 |
17
- | **Client** | 远端 Server 的代理,提供 `request()` / `push()` 通信接口 |
18
- | **Registry** | 注册中心。维护 namespace → 实例列表,心跳检测剔除死实例 |
19
- | **Application** | 应用服务。集成注册发现、熔断、重试等功能 |
9
+ ## When To Use
20
10
 
21
- 一个 `Application` 实例 **同时** 扮演 provider(`register` 暴露接口)和 consumer(`get` / `call` 调用其它服务)。
11
+ Use the message packages for request/response messaging over WebSocket, process IPC, worker threads, file-system message handlers, service discovery, streaming RPC, and registry-backed pub/sub.
22
12
 
23
- ## 安装
13
+ ## Install
24
14
 
25
15
  ```bash
26
16
  pnpm add @hile/micro
27
17
  ```
28
18
 
29
- 依赖:`@hile/message-loader`、`@hile/message-ws`、`ws`。
30
-
31
- ---
32
-
33
- ## 快速开始
34
-
35
- ### 1. 启动 Registry
36
-
37
- ```typescript
38
- import { Registry } from '@hile/micro';
39
-
40
- const registry = new Registry({ advertiseHost: '127.0.0.1' });
41
- await registry.listen(9000);
42
- ```
43
-
44
- ### 2. 启动 Provider(服务 A)
45
-
46
- 以下 `provider` 和 `consumer` 是两个不同进程(不同 namespace),**每个进程只需要一个 `Application` 实例**,同时扮演 provider 和 consumer:
47
-
48
- ```typescript
49
- import { Application } from '@hile/micro';
50
-
51
- const provider = new Application({
52
- namespace: 'payments',
53
- registry: { host: '127.0.0.1', port: 9000 },
54
- advertiseHost: '127.0.0.1',
55
- });
56
-
57
- await provider.listen(9100);
58
-
59
- provider.register('/charge', async ({ data }) => {
60
- return { charged: true, amount: data.amount };
61
- });
62
- ```
63
-
64
- ### 3. 启动 Consumer 调用(服务 B)
65
-
66
- ```typescript
67
- import { Application } from '@hile/micro';
68
-
69
- const consumer = new Application({
70
- namespace: 'checkout',
71
- registry: { host: '127.0.0.1', port: 9000 },
72
- advertiseHost: '127.0.0.1',
73
- });
74
-
75
- await consumer.listen(9200);
76
-
77
- // call() = get() + request() + response() + 熔断 + 重试
78
- const result = await consumer.call('payments', '/charge', { amount: 100 });
79
- console.log(result); // { charged: true, amount: 100 }
80
- ```
81
-
82
- ### 4. 关闭
83
-
84
- `listen()` 返回的 teardown 函数关闭 WebSocketServer 并断开所有连接:
85
-
86
- ```typescript
87
- const stop = await provider.listen(9100);
88
- await stop();
89
- ```
90
-
91
- ---
92
-
93
- ## 核心功能
94
-
95
- ### 服务发现 (`get`)
96
-
97
- 按 namespace 从 Registry 获取一个远端 Client 并缓存:
98
-
99
- ```typescript
100
- const client = await consumer.get('payments');
101
- const { response } = client.request('/charge', { amount: 100 });
102
- const result = await response();
103
- ```
104
-
105
- - 首次查询通过 Registry `/-/find` 获取地址
106
- - 结果**缓存**在内存中(namespace → Client)
107
- - 当 Client 断连时自动清理缓存,下次 `get` 重新发现
108
-
109
- ### 熔断器 (Circuit Breaker)
110
-
111
- `call()` 和 `stream()` 自动跟踪每个 namespace 下各 peer 的调用结果。熔断状态保存在调用方 `Application` 的本地内存中,不依赖 Redis、数据库或 Registry 共享状态。
112
-
113
- | 场景 | 行为 |
114
- |------|------|
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 仍连接,则沿用缓存发起探测,避免完全饿死 |
121
-
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 同时放行的探测数 |
19
+ ## Copy-Paste Example
134
20
 
135
- 可以在构造 `Application` 时覆盖:
21
+ Message handler file:
136
22
 
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` 的错误不会自动重试。可用它们区分业务错误和连接、超时等基础设施错误。
155
-
156
- ### 请求超时
157
-
158
- 每个请求都有超时控制:
159
-
160
- ```typescript
161
- // 构造时设置全局默认超时
162
- const app = new Application({
163
- namespace: 'svc',
164
- registry: { host, port },
165
- requestTimeoutMs: 10_000, // 默认 30000ms
166
- });
167
-
168
- // 单次调用覆盖
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 超时, 不重试
171
- ```
172
-
173
- 超时触发时,底层 MessageModem 会向对端发送 **ABORT** 消息取消远程执行。
174
-
175
- ### 手动取消请求
176
-
177
- 使用 `get()` 拿到 Client 后,`request()` 返回的 `abort()` 可主动取消正在等待响应的请求:
178
-
179
- ```typescript
180
- const client = await consumer.get('payments');
181
- const { response, abort } = client.request('/charge', { amount: 100 });
182
-
183
- // 例如 5 秒后主动取消
184
- setTimeout(() => abort(), 5000);
185
-
186
- try {
187
- const result = await response();
188
- } catch (err) {
189
- // 超时或手动 abort 都会 reject
190
- }
191
- ```
192
-
193
- - `abort()` 向对端发送 **ABORT 消息**,让远程 handler 提前终止
194
- - 超时到期内部也会调用 `controller.abort()`,机制相同
195
- - 适用场景:用户取消、页面卸载、竞态淘汰
196
-
197
- ### 自动重试
198
-
199
- `call()` 默认 retries=1,失败后自动换 peer 重试:
200
-
201
- ```typescript
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, 不重试
205
- ```
206
-
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
23
+ ```ts
24
+ // src/messages/ping.msg.ts
226
25
  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
26
+
27
+ export default defineMessage(async ({ data, params }) => {
28
+ return {
29
+ type: 'pong',
30
+ data,
31
+ params,
32
+ timestamp: Date.now(),
230
33
  }
231
34
  })
232
35
  ```
233
36
 
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` 重试,流已返回后的异步错误会计入熔断但不会自动重放流
247
-
248
- ### 健康检查
249
-
250
- 每个 `Application` 自动注册 `/-/health` 端点:
251
-
252
- ```typescript
253
- // 通过 dispatch 调用(同进程内)
254
- const health = await app.dispatch('/-/health', {});
255
- // { status: 'ok', registry: true, uptime: 123.45, namespaces: ['payments'] }
256
- ```
257
-
258
- 返回字段:
259
-
260
- | 字段 | 类型 | 说明 |
261
- |------|------|------|
262
- | `status` | `'ok'` | 固定值 |
263
- | `registry` | `boolean` | 是否已连接 Registry |
264
- | `uptime` | `number` | 进程启动时长(秒) |
265
- | `namespaces` | `string[]` | 本地已缓存的 namespace 列表 |
37
+ Microservice boot file:
266
38
 
267
- ### 缓存降级
39
+ ```ts
40
+ // src/services/app.boot.ts
41
+ import { defineService } from '@hile/core'
42
+ import { Application } from '@hile/micro'
268
43
 
269
- Registry 不可用但本地仍有已缓存的 Client 连接时,`get()` 自动降级使用缓存。熔断器在“所有 `open` peer 都被排除”并触发本地 reset 时,也会保留已经选中的可用缓存 Client,不会因为 Registry 短暂不可用而丢弃仍可通信的连接。
270
-
271
- | 场景 | 行为 |
272
- |------|------|
273
- | Registry 宕机 + 缓存有效 | 返回缓存 Client,继续服务 |
274
- | Registry 宕机 + 缓存已过期 | 报错 |
275
- | 全新 namespace + Registry 宕机 | 报错 |
276
- | Registry 恢复 | 恢复正常查询 |
277
-
278
- ### Registry 心跳保活
279
-
280
- - **Application** 每 10 秒向 Registry 推送 `/-/heartbeat`
281
- - **Registry** 每 1 秒检查所有实例的心跳时间戳
282
- - 超过 20 秒未收到心跳的实例被自动剔除并断开连接
283
-
284
- ### Registry 重连
285
-
286
- 当 Application 与 Registry 的连接断开时:
287
- 1. 立即尝试重连(`reconnectToRegistry`)
288
- 2. 若失败,3 秒后重试(`scheduleRegistryRetry`)
289
- 3. 若 `listen()` 返回的 teardown 已触发(`stopped = true`),停止重连
290
-
291
- ---
292
-
293
- ## 配置管理
294
-
295
- ### Registry 工作目录
296
-
297
- Registry 启动时自动创建 `~/.registry/` 工作目录。YAML 配置文件存放在 `~/.registry/configs/` 下,按 namespace 分文件管理:
298
-
299
- ```
300
- ~/.registry/
301
- └── configs/
302
- ├── service-a.config.yaml
303
- ├── service-b.config.yaml
304
- └── global.config.yaml
305
- ```
306
-
307
- ### 配置文件热加载
308
-
309
- Regsitry 监听 `configs/` 目录的文件变化,新增或修改 `*.config.yaml` 文件时自动加载(兼容 vim 原子写入),无需重启:
310
-
311
- ```bash
312
- # 创建或修改 ~/.registry/configs/my-service.config.yaml
313
- # Registry 自动检测变化并更新内存中的配置
314
- ```
315
-
316
- ### 远程读取配置
317
-
318
- 通过 `/-/env/variables` 端点,已连服务可按 namespace 和字段从 Registry 远程读取配置:
319
-
320
- ```typescript
321
- // Application 侧
322
- const result = await app.getEnvVariables(
323
- { namespace: 'service-a', fields: ['db.host', 'db.port'] },
324
- { namespace: 'global' },
325
- );
326
- // result = {
327
- // 'service-a': { 'db.host': '10.0.0.2', 'db.port': 3306 },
328
- // 'global': { featureFlag: true },
329
- // }
330
- ```
331
-
332
- ---
333
-
334
- ## CLI
335
-
336
- ### `hile registry`
337
-
338
- 启动注册中心:
339
-
340
- ```bash
341
- # 使用默认配置
342
- hile registry
343
-
344
- # 指定端口
345
- hile registry --port 8888
346
-
347
- # 指定宣告地址
348
- hile registry --host 10.0.0.1
349
- ```
350
-
351
- ### `hile registry configs`
352
-
353
- 管理 `~/.registry/configs/` 下的 YAML 配置文件:
354
-
355
- ```bash
356
- # 列出所有 namespace
357
- hile registry configs
358
-
359
- # 查看配置(YAML 输出)
360
- hile registry configs get my-service
361
-
362
- # 查看配置(JSON 输出)
363
- hile registry configs get my-service --json
364
-
365
- # 设置配置项
366
- hile registry configs set my-service port=8080
367
- hile registry configs set my-service debug=true
368
-
369
- # 删除整个 namespace
370
- hile registry configs del my-service
371
-
372
- # 删除某个字段(需确认)
373
- hile registry configs del my-service port
374
-
375
- # 跳过确认删除
376
- hile registry configs del my-service -y
377
- ```
378
-
379
- ## 连接协议
380
-
381
- ### 连接 URL 格式
382
-
383
- 出站 WebSocket URL:
384
-
385
- ```
386
- ws://{targetHost}:{targetPort}/{announceHost}/{listenPort}/{namespace}
387
- ```
388
-
389
- - `announceHost`:构造时传入 `advertiseHost` 或自动获取的 IPv4
390
- - `listenPort`:`listen(port)` 设置的端口
391
- - `namespace`:构造时传入的 namespace 字符串
392
-
393
- ### 入站路径解析
394
-
395
- `Server.onConnected` 将 URL 路径按 `/` 分割为:
396
-
397
- ```
398
- [callerHost, callerPort, ...extras]
399
- ```
400
-
401
- - `extras` 以 `/` 分段,Registry 用 `extras.join('/')` 作为 namespace
402
-
403
- ---
404
-
405
- ## API 参考
406
-
407
- ### ApplicationProps
408
-
409
- ```typescript
410
- type ApplicationProps = {
411
- namespace: string; // 本服务 namespace
412
- registry: { host: string; port: number }; // Registry 地址
413
- registryLookupTimeoutMs?: number; // /-/find 超时,默认 10000
414
- requestTimeoutMs?: number; // 单次请求超时,默认 30000
415
- } & { advertiseHost?: string }; // 出站宣告地址
416
- ```
417
-
418
- ### Application
419
-
420
- ```typescript
421
- class Application extends Server {
422
- constructor(props: ApplicationProps);
423
-
424
- // 启动监听,自动连接 Registry,启动心跳
425
- listen(port: number): Promise<() => Promise<void>>;
426
-
427
- // 获取 namespace 对应的远端 Client(缓存 + 自动发现)
428
- get(namespace: string, exclude?: string[]): Promise<Client>;
429
-
430
- // 一站式调用:get + request + response + 熔断 + 重试
431
- call<T = any>(
432
- namespace: string,
433
- url: string,
434
- data: any,
435
- options?: {
436
- timeout?: number, // 请求超时(ms),默认 requestTimeoutMs
437
- retries?: number, // 失败重试次数,默认 1
438
- signal?: AbortSignal, // 手动取消
439
- },
440
- ): Promise<T>;
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
44
+ export default defineService('micro.app', async (shutdown) => {
45
+ const app = new Application({
46
+ namespace: process.env.MICRO_NAMESPACE ?? 'example.service',
47
+ registry: {
48
+ host: process.env.REGISTRY_HOST ?? '127.0.0.1',
49
+ port: Number(process.env.REGISTRY_PORT ?? 9876),
451
50
  },
452
- ): Promise<import('stream').Readable>;
453
-
454
- // 注册路由(provider 侧)
455
- register<T = any>(url: string, handler: (ctx) => Promise<T>): () => void;
456
-
457
- // 同进程调用路由
458
- dispatch(url: string, data: any): Promise<any>;
459
-
460
- // 远程读取 Registry 的配置(强类型)
461
- getEnvVariables<
462
- T extends Record<string, Record<string, any>>,
463
- const Requests extends readonly EnvRequest<T>[],
464
- >(...data: Requests): Promise<GetEnvVariablesResult<T, Requests>>;
465
- }
466
- ```
51
+ advertiseHost: process.env.HILE_ADVERTISE_HOST ?? '127.0.0.1',
52
+ })
467
53
 
468
- ### Registry
469
-
470
- ```typescript
471
- class Registry extends Server {
472
- constructor(props?: MicroServerProps);
473
- listen(port: number): Promise<() => Promise<void>>;
474
- onFind(): void; // 幂等地挂载 /-/find 路由
475
- watchEnvFile(): fs.FSWatcher | undefined; // 监听 ~/.registry/configs/ 目录内的 *.config.yaml 文件变化
476
- }
54
+ await app.load(new URL('../messages', import.meta.url).pathname)
55
+ const stop = await app.listen(Number(process.env.MICRO_PORT ?? 0))
56
+ shutdown(stop)
57
+ return app
58
+ })
477
59
  ```
478
60
 
479
- ### Server
61
+ Caller:
480
62
 
481
- ```typescript
482
- class Server extends MessageLoader {
483
- constructor(namespace: string, props?: MicroServerProps);
484
- listen(port: number): Promise<() => Promise<void>>;
485
- setPort(port: number): this;
486
- // 以下方法受保护:
487
- protected connect(host: string, port: number, timeout?: number): Promise<Client>;
488
- }
63
+ ```ts
64
+ const result = await app.call('example.service', '/ping', { hello: 'world' })
489
65
  ```
490
66
 
491
- ### Client
492
-
493
- ```typescript
494
- class Client extends MessageWs {
495
- request(url: string, data: any, timeout?: number): { abort(): void; response<T>(): Promise<T> };
496
- push(url: string, data: any, timeout?: number): void;
497
- stream(url: string, data: any, options?: { signal?: AbortSignal }): Readable;
498
- dispose(): void;
499
- }
500
- ```
67
+ ## Boundaries
501
68
 
502
- ---
69
+ - Do not use `stream()` for normal single-result calls.
70
+ - Do not rely on message IDs for business idempotency. They are transport IDs.
71
+ - Do not bypass `defineMessage()` for file-loaded handlers.
503
72
 
504
- ## Hile core 的关系
73
+ - Appending a secondary response getter to `client.request('/x', data)`
74
+ - Returning a plain object from a handler called through `stream()`.
75
+ - Using pub/sub as a durable queue.
76
+ - Forgetting to register `shutdown(await app.listen(...))`.
505
77
 
506
- `@hile/micro` 不依赖 `@hile/core`,可与任意 Node.js 进程或在未来由 `defineService` 包装后接入 Hile 容器。
78
+ ## Verify
507
79
 
508
- ---
80
+ - Message files default-export `defineMessage(...)`.
81
+ - RPC callers use `await app.call(...)`.
82
+ - Streaming handlers are async generators.
83
+ - Registry is started before application nodes need discovery.
84
+ - Micro apps use stable namespaces and advertise reachable hosts.
509
85
 
510
- ## License
86
+ ## More Context
511
87
 
512
- MIT
88
+ - `AI.md` in this package: full package-local AI guide.
89
+ - Root `llms-full.txt`: full monorepo AI context.
90
+ - Root `references/`: source files copied from `docs/ai`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hile/micro",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  "dist",
13
13
  "README.md",
14
- "SKILL.md"
14
+ "AI.md"
15
15
  ],
16
16
  "license": "MIT",
17
17
  "publishConfig": {
@@ -23,13 +23,13 @@
23
23
  "vitest": "^4.0.18"
24
24
  },
25
25
  "dependencies": {
26
- "@hile/context": "^3.0.1",
27
- "@hile/logger": "^2.1.1",
28
- "@hile/message-loader": "^2.1.1",
29
- "@hile/message-ws": "^2.1.1",
26
+ "@hile/context": "^3.0.2",
27
+ "@hile/logger": "^3.0.0",
28
+ "@hile/message-loader": "^3.0.0",
29
+ "@hile/message-ws": "^3.0.0",
30
30
  "internal-ip": "^9.0.0",
31
31
  "ws": "^8.21.0",
32
32
  "yaml": "^2.9.0"
33
33
  },
34
- "gitHead": "14b55afba0e9af80a782eaa84f6f48c1e66861a1"
34
+ "gitHead": "0985b6f8abc1f4de0a36324063585fdc3ac1375b"
35
35
  }