@hile/micro 2.1.1 → 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.
- package/AI.md +368 -0
- package/README.md +56 -478
- package/dist/client.d.ts +11 -4
- package/dist/client.js +63 -6
- package/package.json +7 -6
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
+
Message handler file:
|
|
136
22
|
|
|
137
|
-
```
|
|
138
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
61
|
+
Caller:
|
|
480
62
|
|
|
481
|
-
```
|
|
482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
86
|
+
## More Context
|
|
511
87
|
|
|
512
|
-
|
|
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/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MessageWs } from "@hile/message-ws";
|
|
2
|
+
import { type ContextData, type ContextInput } from '@hile/context';
|
|
2
3
|
import { Server } from './server.js';
|
|
3
4
|
import { WebSocket } from 'ws';
|
|
4
5
|
import { EventEmitter } from 'node:events';
|
|
@@ -8,6 +9,15 @@ export interface ClientProps {
|
|
|
8
9
|
server: Server;
|
|
9
10
|
ws: WebSocket;
|
|
10
11
|
}
|
|
12
|
+
export type MicroMessageMetadata = {
|
|
13
|
+
context?: ContextInput<ContextData>;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
export type MicroMessage<T = any> = {
|
|
17
|
+
url: string;
|
|
18
|
+
data: T;
|
|
19
|
+
metadata?: MicroMessageMetadata;
|
|
20
|
+
};
|
|
11
21
|
export declare class Client extends MessageWs {
|
|
12
22
|
private readonly server;
|
|
13
23
|
private readonly socket;
|
|
@@ -20,10 +30,7 @@ export declare class Client extends MessageWs {
|
|
|
20
30
|
readonly events: EventEmitter<any>;
|
|
21
31
|
constructor(props: ClientProps);
|
|
22
32
|
private startHeartbeat;
|
|
23
|
-
protected exec(data:
|
|
24
|
-
url: string;
|
|
25
|
-
data: any;
|
|
26
|
-
}): Promise<any>;
|
|
33
|
+
protected exec(data: MicroMessage): Promise<any>;
|
|
27
34
|
request<T = any>(url: string, data: any, options?: {
|
|
28
35
|
timeout?: number;
|
|
29
36
|
signal?: AbortSignal;
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
import { MessageWs } from "@hile/message-ws";
|
|
2
|
+
import { isContextData, runWithContext, snapshotContext, } from '@hile/context';
|
|
2
3
|
import { WebSocket } from 'ws';
|
|
3
4
|
import { EventEmitter } from 'node:events';
|
|
5
|
+
function createEnvelope(url, data) {
|
|
6
|
+
const context = snapshotContext();
|
|
7
|
+
if (Object.keys(context).length === 0)
|
|
8
|
+
return { url, data };
|
|
9
|
+
return {
|
|
10
|
+
url,
|
|
11
|
+
data,
|
|
12
|
+
metadata: {
|
|
13
|
+
context,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function getEnvelopeContext(data) {
|
|
18
|
+
const context = data.metadata?.context;
|
|
19
|
+
if (!isContextData(context))
|
|
20
|
+
return undefined;
|
|
21
|
+
return context;
|
|
22
|
+
}
|
|
23
|
+
function isAsyncIterable(value) {
|
|
24
|
+
return value != null && typeof value[Symbol.asyncIterator] === 'function';
|
|
25
|
+
}
|
|
26
|
+
function bindAsyncIterableToContext(iterable, context) {
|
|
27
|
+
return {
|
|
28
|
+
[Symbol.asyncIterator]() {
|
|
29
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
30
|
+
return {
|
|
31
|
+
next() {
|
|
32
|
+
return Promise.resolve(runWithContext(context, () => iterator.next()));
|
|
33
|
+
},
|
|
34
|
+
return(value) {
|
|
35
|
+
if (!iterator.return) {
|
|
36
|
+
return Promise.resolve({ done: true, value });
|
|
37
|
+
}
|
|
38
|
+
return Promise.resolve(runWithContext(context, () => iterator.return(value)));
|
|
39
|
+
},
|
|
40
|
+
throw(error) {
|
|
41
|
+
if (!iterator.throw) {
|
|
42
|
+
return Promise.reject(error);
|
|
43
|
+
}
|
|
44
|
+
return Promise.resolve(runWithContext(context, () => iterator.throw(error)));
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
4
50
|
export class Client extends MessageWs {
|
|
5
51
|
server;
|
|
6
52
|
socket;
|
|
@@ -48,24 +94,35 @@ export class Client extends MessageWs {
|
|
|
48
94
|
}
|
|
49
95
|
if (!this._online)
|
|
50
96
|
throw new Error('Client is not online');
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
97
|
+
const context = getEnvelopeContext(data);
|
|
98
|
+
const dispatch = async () => {
|
|
99
|
+
const result = await this.server.dispatch(data.url, data.data, {
|
|
100
|
+
client: this,
|
|
101
|
+
metadata: data.metadata,
|
|
102
|
+
});
|
|
103
|
+
if (context && isAsyncIterable(result)) {
|
|
104
|
+
return bindAsyncIterableToContext(result, context);
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
};
|
|
108
|
+
if (context)
|
|
109
|
+
return runWithContext(context, dispatch);
|
|
110
|
+
return dispatch();
|
|
54
111
|
}
|
|
55
112
|
request(url, data, options) {
|
|
56
113
|
if (!this._online)
|
|
57
114
|
throw new Error('Client is not online');
|
|
58
|
-
return this._send(
|
|
115
|
+
return this._send(createEnvelope(url, data), options);
|
|
59
116
|
}
|
|
60
117
|
push(url, data, options) {
|
|
61
118
|
if (!this._online)
|
|
62
119
|
throw new Error('Client is not online');
|
|
63
|
-
return this._push(
|
|
120
|
+
return this._push(createEnvelope(url, data), options);
|
|
64
121
|
}
|
|
65
122
|
stream(url, data, options) {
|
|
66
123
|
if (!this._online)
|
|
67
124
|
throw new Error('Client is not online');
|
|
68
|
-
return this._stream(
|
|
125
|
+
return this._stream(createEnvelope(url, data), options);
|
|
69
126
|
}
|
|
70
127
|
dispose() {
|
|
71
128
|
if (this.heartbeatTimer)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hile/micro",
|
|
3
|
-
"version": "
|
|
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
|
-
"
|
|
14
|
+
"AI.md"
|
|
15
15
|
],
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"publishConfig": {
|
|
@@ -23,12 +23,13 @@
|
|
|
23
23
|
"vitest": "^4.0.18"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@hile/
|
|
27
|
-
"@hile/
|
|
28
|
-
"@hile/message-
|
|
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",
|
|
29
30
|
"internal-ip": "^9.0.0",
|
|
30
31
|
"ws": "^8.21.0",
|
|
31
32
|
"yaml": "^2.9.0"
|
|
32
33
|
},
|
|
33
|
-
"gitHead": "
|
|
34
|
+
"gitHead": "0985b6f8abc1f4de0a36324063585fdc3ac1375b"
|
|
34
35
|
}
|