@hile/message-modem 2.0.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/AI.md +295 -0
  2. package/README.md +63 -188
  3. package/package.json +3 -3
package/AI.md ADDED
@@ -0,0 +1,295 @@
1
+ # AI Guide For @hile/message-modem
2
+
3
+
4
+
5
+ <!-- Generated by scripts/build-ai-context.mjs from docs/ai. Do not edit by hand. -->
6
+
7
+
8
+
9
+ Purpose: Implement request/response, push, abort, and stream protocol behavior independent of transport.
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
+ Use `@hile/message-modem` for: Implement request/response, push, abort, and stream protocol behavior independent of transport.
22
+
23
+
24
+
25
+ # Messaging And Microservices
26
+
27
+ Packages: `@hile/message-modem`, `@hile/message-ws`, `@hile/message-ipc`, `@hile/message-worker-thread`, `@hile/message-loader`, `@hile/micro`, `@hile/micro-dynamic-configs`.
28
+
29
+ ## Copy-Paste Example
30
+
31
+ Message handler file:
32
+
33
+ ```ts
34
+ // src/messages/ping.msg.ts
35
+ import { defineMessage } from '@hile/message-loader'
36
+
37
+ export default defineMessage(async ({ data, params }) => {
38
+ return {
39
+ type: 'pong',
40
+ data,
41
+ params,
42
+ timestamp: Date.now(),
43
+ }
44
+ })
45
+ ```
46
+
47
+ Microservice boot file:
48
+
49
+ ```ts
50
+ // src/services/app.boot.ts
51
+ import { defineService } from '@hile/core'
52
+ import { Application } from '@hile/micro'
53
+
54
+ export default defineService('micro.app', async (shutdown) => {
55
+ const app = new Application({
56
+ namespace: process.env.MICRO_NAMESPACE ?? 'example.service',
57
+ registry: {
58
+ host: process.env.REGISTRY_HOST ?? '127.0.0.1',
59
+ port: Number(process.env.REGISTRY_PORT ?? 9876),
60
+ },
61
+ advertiseHost: process.env.HILE_ADVERTISE_HOST ?? '127.0.0.1',
62
+ })
63
+
64
+ await app.load(new URL('../messages', import.meta.url).pathname)
65
+ const stop = await app.listen(Number(process.env.MICRO_PORT ?? 0))
66
+ shutdown(stop)
67
+ return app
68
+ })
69
+ ```
70
+
71
+ Caller:
72
+
73
+ ```ts
74
+ const result = await app.call('example.service', '/ping', { hello: 'world' })
75
+ ```
76
+
77
+ ## More Examples
78
+
79
+ Streaming handler:
80
+
81
+ ```ts
82
+ // src/messages/events.msg.ts
83
+ import { defineMessage } from '@hile/message-loader'
84
+
85
+ export default defineMessage(async function* () {
86
+ for (let i = 0; i < 3; i++) {
87
+ yield { seq: i }
88
+ }
89
+ })
90
+ ```
91
+
92
+ Streaming caller:
93
+
94
+ ```ts
95
+ const stream = await app.stream('example.service', '/events', {})
96
+ for await (const chunk of stream) {
97
+ console.log(chunk)
98
+ }
99
+ ```
100
+
101
+ Custom WebSocket modem:
102
+
103
+ ```ts
104
+ import { MessageWs } from '@hile/message-ws'
105
+ import type WebSocket from 'ws'
106
+
107
+ class RpcWs extends MessageWs {
108
+ constructor(ws: WebSocket, private readonly dispatch: (url: string, data: unknown) => Promise<unknown>) {
109
+ super(ws)
110
+ }
111
+
112
+ protected exec(data: { url: string; data: unknown }) {
113
+ return this.dispatch(data.url, data.data)
114
+ }
115
+
116
+ request<T>(url: string, data: unknown, timeout = 30_000) {
117
+ return this._send<T>({ url, data }, { timeout })
118
+ }
119
+ }
120
+ ```
121
+
122
+ Notice that `request()` returns a `Promise<T>`. Await it directly.
123
+
124
+ ## Use When
125
+
126
+ 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.
127
+
128
+ ## Do Not Use When
129
+
130
+ - Do not use `stream()` for normal single-result calls.
131
+ - Do not rely on message IDs for business idempotency. They are transport IDs.
132
+ - Do not bypass `defineMessage()` for file-loaded handlers.
133
+
134
+ ## Install
135
+
136
+ ```bash
137
+ pnpm add @hile/micro @hile/message-loader @hile/message-ws
138
+ ```
139
+
140
+ Use transport-specific packages only when you need to build custom IPC or worker-thread bridges.
141
+
142
+ ## Imports
143
+
144
+ ```ts
145
+ import { defineMessage, MessageLoader } from '@hile/message-loader'
146
+ import { Application, Registry, Server } from '@hile/micro'
147
+ import { MessageWs } from '@hile/message-ws'
148
+ import { MessageIpc } from '@hile/message-ipc'
149
+ import { MessageWorkerThread } from '@hile/message-worker-thread'
150
+ ```
151
+
152
+ ## Compose With
153
+
154
+ - `@hile/context` propagates context in micro message metadata.
155
+ - `@hile/redis-idempotency` protects retryable side effects in message handlers.
156
+ - `@hile/redis-stream-queue` is better for durable background jobs.
157
+
158
+ ## Runtime And Lifecycle Notes
159
+
160
+ - `MessageLoader` maps `*.msg.*` files to routes using `@hile/loader`.
161
+ - `MessageLoader.dispatch(path, data, extras?)` invokes the matched handler.
162
+ - `MessageModem._send()` returns a `Promise`.
163
+ - `MessageModem._stream()` returns a Node `Readable` in object mode.
164
+ - A stream request requires `exec()` to return an async iterable.
165
+ - `Application.call(namespace, url, data, options?)` returns a promise.
166
+ - `Application.stream(namespace, url, data, options?)` returns a readable stream.
167
+ - `Application.publish(topic, payload)` returns an object with `update()` and `unpublish()`.
168
+ - `Application.subscribe(topic, callback)` returns an unsubscribe function.
169
+ - `Registry` stores service addresses and retained config/topic state under `~/.registry`.
170
+
171
+ ## Anti-Patterns
172
+
173
+ - Appending a secondary response getter to `client.request('/x', data)`
174
+ - Returning a plain object from a handler called through `stream()`.
175
+ - Using pub/sub as a durable queue.
176
+ - Forgetting to register `shutdown(await app.listen(...))`.
177
+
178
+ ## Verification Checklist
179
+
180
+ - Message files default-export `defineMessage(...)`.
181
+ - RPC callers use `await app.call(...)`.
182
+ - Streaming handlers are async generators.
183
+ - Registry is started before application nodes need discovery.
184
+ - Micro apps use stable namespaces and advertise reachable hosts.
185
+
186
+
187
+
188
+ # Related Recipes
189
+
190
+
191
+
192
+ # Micro RPC With Message Loader
193
+
194
+ ## Complete Example
195
+
196
+ Provider handler:
197
+
198
+ ```ts
199
+ // src/messages/charge.msg.ts
200
+ import { defineMessage } from '@hile/message-loader'
201
+
202
+ export default defineMessage(async ({ data }) => {
203
+ return { charged: true, input: data }
204
+ })
205
+ ```
206
+
207
+ Provider boot:
208
+
209
+ ```ts
210
+ // src/services/app.boot.ts
211
+ import { defineService } from '@hile/core'
212
+ import { Application } from '@hile/micro'
213
+
214
+ export default defineService('billing.micro', async (shutdown) => {
215
+ const app = new Application({
216
+ namespace: 'billing',
217
+ registry: { host: '127.0.0.1', port: 9876 },
218
+ advertiseHost: '127.0.0.1',
219
+ })
220
+
221
+ await app.load(new URL('../messages', import.meta.url).pathname)
222
+ const stop = await app.listen(9101)
223
+ shutdown(stop)
224
+ return app
225
+ })
226
+ ```
227
+
228
+ Consumer:
229
+
230
+ ```ts
231
+ const result = await app.call('billing', '/charge', {
232
+ tenantId: 't1',
233
+ amount: 100,
234
+ })
235
+ ```
236
+
237
+ ## File Layout
238
+
239
+ ```text
240
+ provider/
241
+ src/messages/charge.msg.ts
242
+ src/services/app.boot.ts
243
+ consumer/
244
+ src/models/payments/pay.model.ts
245
+ ```
246
+
247
+ ## User Intent
248
+
249
+ Use this recipe when services communicate over Hile registry-backed RPC.
250
+
251
+ ## Packages To Use
252
+
253
+ - `@hile/micro`
254
+ - `@hile/message-loader`
255
+ - `@hile/context` when context must cross service boundaries
256
+ - `@hile/redis-idempotency` for retryable side effects
257
+
258
+ ## Implementation Steps
259
+
260
+ 1. Start a Registry with `hile registry`.
261
+ 2. Start providers with stable namespaces.
262
+ 3. Load `*.msg.ts` handlers through `app.load()`.
263
+ 4. Call providers with `await app.call(namespace, url, data)`.
264
+ 5. Use `app.stream()` only for async-generator handlers.
265
+
266
+ ## Failure And Cleanup Behavior
267
+
268
+ - `Application.call()` may retry; side-effecting handlers need idempotency.
269
+ - Registry disconnect triggers reconnect; apps re-declare topics and subscriptions.
270
+ - Circuit breaker excludes failing nodes for cooldown.
271
+
272
+ ## Verification Checklist
273
+
274
+ - Registry is reachable.
275
+ - Provider namespace matches consumer call.
276
+ - Handlers default-export `defineMessage()`.
277
+ - Consumer code awaits `app.call(...)` directly.
278
+
279
+
280
+
281
+ # Global Guardrails
282
+
283
+
284
+
285
+ ## Never Generate These Patterns
286
+
287
+ - Do not call `loadService()` at module top level; it starts resources during import.
288
+ - Do not default-export plain functions from `*.boot.*` files; `hile start` expects a Hile service.
289
+ - Do not set `ctx.body` and also return a controller value.
290
+ - Do not assume `@hile/http` Zod validation mutates or coerces `ctx.query`, `ctx.params`, or `ctx.request.body`.
291
+ - Do not put reusable business logic only in controllers, pages, queue workers, or message handlers.
292
+ - Do not use old message examples that append a secondary response getter; current request APIs return promises directly.
293
+ - Do not claim exactly-once delivery or execution from Redis locks, queues, idempotency, or rate limits.
294
+ - Do not use queue `jobId` as the only side-effect idempotency boundary.
295
+ - Do not log the entire async context by default.
package/README.md CHANGED
@@ -1,215 +1,90 @@
1
1
  # @hile/message-modem
2
2
 
3
- 传输无关的请求/响应消息通信抽象层。将底层传输机制(WebSocket、postMessage、IPC 等)与业务逻辑解耦,提供统一的 _send/receive 语义。
3
+ <!-- Generated by scripts/build-ai-context.mjs from docs/ai. Do not edit by hand. -->
4
4
 
5
- ## 安装
5
+ Implement request/response, push, abort, and stream protocol behavior independent of transport.
6
6
 
7
- ```bash
8
- pnpm add @hile/message-modem
9
- ```
10
-
11
- ## 核心特性
7
+ This README is intentionally short and example-first. The complete AI-facing guide ships in `AI.md` in this package.
12
8
 
13
- - **传输无关** 子类只需实现 `post`(如何发送)和 `exec`(如何处理),即可运行于任何通信通道
14
- - **双向请求/响应** — `_send` 发送请求并等待对端响应(`twoway: true`)
15
- - **单向推送** — `_push` 发送消息无需对端响应(`twoway: false`)
16
- - **流式传输** — `_stream` 发送流式请求,对端返回 async generator 时分块回传,发送方获得 `Readable` stream
17
- - **请求/响应配对** — 自增 ID + Promise 栈,自动配对请求与响应
18
- - **超时控制** — 默认 30 秒,可按请求自定义
19
- - **主动中止** — 发送方可 abort 等待,接收方可取消正在执行的任务
20
- - **错误传播** — `Exception` 携带 status 码透传;普通 Error 映射为 500
9
+ ## When To Use
21
10
 
22
- ## 快速开始
23
-
24
- ### 第一步:继承并实现抽象方法
25
-
26
- ```typescript
27
- import { MessageModem, type MessageTransferFormat } from '@hile/message-modem';
28
-
29
- class WebSocketModem extends MessageModem {
30
- constructor(private ws: WebSocket) {
31
- super();
32
- ws.addEventListener('message', (e) => {
33
- this.receive(JSON.parse(e.data));
34
- });
35
- }
36
-
37
- protected post<T>(data: MessageTransferFormat<T>): void {
38
- this.ws.send(JSON.stringify(data));
39
- }
40
-
41
- protected async exec(data: any): Promise<any> {
42
- // 处理远端请求
43
- return handleRequest(data);
44
- }
45
-
46
- // 暴露发送方法
47
- public request<T>(data: T, timeout?: number) {
48
- return this._send(data, timeout);
49
- }
50
- }
51
- ```
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.
52
12
 
53
- ### 第二步:发送请求
13
+ ## Install
54
14
 
55
- ```typescript
56
- const modem = new WebSocketModem(ws);
57
-
58
- const { abort, response } = modem.request({ action: 'getUser', id: 1 });
59
-
60
- // 等待响应
61
- const user = await response();
62
- console.log(user);
63
-
64
- // 或中止请求
65
- abort();
15
+ ```bash
16
+ pnpm add @hile/message-modem
66
17
  ```
67
18
 
68
- ### 第三步:处理超时与异常
19
+ ## Copy-Paste Example
69
20
 
70
- 所有异常类均从主入口导出,无需单独引入:
21
+ Message handler file:
71
22
 
72
- ```typescript
73
- import { AbortException, Exception } from '@hile/message-modem';
23
+ ```ts
24
+ // src/messages/ping.msg.ts
25
+ import { defineMessage } from '@hile/message-loader'
74
26
 
75
- try {
76
- // 5 秒超时
77
- const result = await modem.request(data, 5000).response();
78
- } catch (e) {
79
- if (e instanceof AbortException) {
80
- console.log('请求超时或被中止');
81
- } else if (e instanceof Exception) {
82
- console.log(`远端错误 [${e.status}]: ${e.message}`);
27
+ export default defineMessage(async ({ data, params }) => {
28
+ return {
29
+ type: 'pong',
30
+ data,
31
+ params,
32
+ timestamp: Date.now(),
83
33
  }
84
- }
85
- ```
86
-
87
- ## API
88
-
89
- ### `MessageModem`(抽象类)
90
-
91
- | 方法 | 可见性 | 说明 |
92
- |------|--------|------|
93
- | `post(data)` | `protected abstract` | 子类实现:如何将消息发送到远端 |
94
- | `exec(data)` | `protected abstract` | 子类实现:如何处理收到的请求,返回 Promise |
95
- | `_send(data, opts?)` | `protected` | 发送双向请求(`twoway: true`),返回 `{ abort, response }` |
96
- | `_push(data, opts?)` | `protected` | 发送单向推送(`twoway: false`),无返回值,接收方不回复 RESPONSE |
97
- | `_stream(data, opts?)` | `protected` | 发送流式请求,返回 `Readable` stream。对端 `exec()` 须返回 async generator |
98
- | `receive(msg)` | `public` | 接收消息入口,根据 mode 分发处理 |
99
-
100
- ### `_send` 返回值
101
-
102
- | 属性 | 类型 | 说明 |
103
- |------|------|------|
104
- | `abort` | `() => void` | 中止本次请求 |
105
- | `response` | `<U>() => Promise<U>` | 等待远端响应 |
106
-
107
- ### 消息类型
108
-
109
- | 枚举值 | 说明 |
110
- |--------|------|
111
- | `MESSAGE_MODEM_TYPE.REQUEST` | 请求消息 |
112
- | `MESSAGE_MODEM_TYPE.RESPONSE` | 响应消息 |
113
- | `MESSAGE_MODEM_TYPE.ABORT` | 中止消息 |
114
-
115
- ### 异常类
116
-
117
- | 类 | status | 默认 message | 说明 |
118
- |------|--------|------|------|
119
- | `Exception` | 自定义 | 自定义 | 基础异常,携带 status |
120
- | `TimeoutException` | `ETIMEDOUT` | `Timeout` | 超时异常 |
121
- | `AbortException` | `ECONNABORTED` | `Abort` | 中止异常 |
122
-
123
- ### 消息格式
124
-
125
- ```typescript
126
- // 传输格式
127
- interface MessageTransferFormat<T = any> {
128
- id: number;
129
- mode: MESSAGE_MODEM_TYPE;
130
- twoway: boolean;
131
- stream?: boolean; // true → 流式模式
132
- data?: T;
133
- }
134
-
135
- // 响应数据格式
136
- interface MessageReturnFormat<T = any> {
137
- status: string | number;
138
- data: T;
139
- message: string;
140
- }
141
-
142
- // 流式分块格式(stream=true 时 RESPONSE 携带)
143
- interface MessageStreamChunk<T = any> {
144
- status: string | number;
145
- seq: number; // 块序号,从 0 递增
146
- payload: T; // 块数据
147
- final: boolean; // true → 最后一块
148
- }
34
+ })
149
35
  ```
150
36
 
151
- ## 消息流转
152
-
153
- ### 双向模式(`_send`)
154
-
155
- ```
156
- 发送方 接收方
157
- │ │
158
- │ _send(data)
159
- │──── REQUEST (twoway) ──────►│
160
- │ │ exec(data)
161
- │ │
162
- │◄──── RESPONSE ──────────────│
163
- │ resolve(data)
164
- │ │
165
- │ abort() │
166
- │──── ABORT ─────────────────►│
167
- │ │ 取消 exec
37
+ Microservice boot file:
38
+
39
+ ```ts
40
+ // src/services/app.boot.ts
41
+ import { defineService } from '@hile/core'
42
+ import { Application } from '@hile/micro'
43
+
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),
50
+ },
51
+ advertiseHost: process.env.HILE_ADVERTISE_HOST ?? '127.0.0.1',
52
+ })
53
+
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
+ })
168
59
  ```
169
60
 
170
- ### 单向模式(`_push`)
61
+ Caller:
171
62
 
172
- ```
173
- 发送方 接收方
174
- │ │
175
- │ _push(data) │
176
- │──── REQUEST (!twoway) ─────►│
177
- │ │ exec(data)
178
- │ │ (不回复 RESPONSE)
63
+ ```ts
64
+ const result = await app.call('example.service', '/ping', { hello: 'world' })
179
65
  ```
180
66
 
181
- ### 流式模式(`_stream`)
182
-
183
- ```
184
- 发送方 接收方
185
- │ │
186
- │ _stream(data) │
187
- │──── REQUEST (stream) ──────►│
188
- │ │ exec(data) → async generator
189
- │ │
190
- │◄─── RESPONSE (seq:0) ───────│ for await (chunk of gen)
191
- │◄─── RESPONSE (seq:1) ───────│
192
- │◄─── RESPONSE (seq:2) ───────│
193
- │ ... │
194
- │◄─── RESPONSE (final) ───────│ 迭代结束
195
- │ │
196
- │ abort() │
197
- │──── ABORT ─────────────────►│ 取消迭代
198
- ```
67
+ ## Boundaries
199
68
 
200
- 对端 `exec()` 返回 `AsyncIterable` 时,`MessageModem` 自动检测并进入分块传输模式。发送方通过 `for await` 消费 `Readable` stream。
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.
201
72
 
202
- **何时用**:大数据集、实时事件流、LLM token 输出、进度上报等需要持续推送的场景。
203
- **何时不用**:单次请求/响应 —— `_send()` 即可,无需 `_stream()`。
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(...))`.
204
77
 
205
- ## 适用场景
78
+ ## Verify
206
79
 
207
- - **iframe 通信** 父子页面 postMessage
208
- - **WebSocket** 客户端与服务端双向通信
209
- - **Web Worker** 主线程与 Worker 通信
210
- - **Electron IPC** 主进程与渲染进程通信
211
- - **Node.js child_process** 父子进程通信
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.
212
85
 
213
- ## License
86
+ ## More Context
214
87
 
215
- 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/message-modem",
3
- "version": "2.0.3",
3
+ "version": "3.0.0",
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": {
@@ -21,5 +21,5 @@
21
21
  "fix-esm-import-path": "^1.10.3",
22
22
  "vitest": "^4.0.18"
23
23
  },
24
- "gitHead": "a7615000fcb87e6bc0e573af760c814ec935bab2"
24
+ "gitHead": "0985b6f8abc1f4de0a36324063585fdc3ac1375b"
25
25
  }