# 使用指南

面向第一次使用的人。能力边界以 [README](../README.md) 与 [边界文档](boundaries.zh-CN.md) 为准；这里只讲怎么用，并把每条用法指向能跑的东西。

## 1. 我想做什么 → 该看哪里

| 我想…… | 命令（仓库根目录；先 `moon build --target js`） | 说明 |
|---|---|---|
| 看一遍完整 API 走位，不起任何服务 | `moon run examples/offline --target js` | 纯 MoonBit：所有服务端帧由示例提供，不是 broker 证据 |
| 看 1.0/1.1/1.2 的 ACK 头差异与边界拒绝 | `moon run examples/versions --target js` | 纯 MoonBit，有逐字节 golden |
| 发布/订阅（topic 扇出、queue 分发） | `node examples/demo/01-pubsub.cjs` | 内置 demo broker |
| 带 receipt 的可靠发送；receipt 丢失意味着什么 | `node examples/demo/02-reliable-send.cjs` | 内置 demo broker |
| 手动 ACK / NACK（Subscription 句柄） | `node examples/demo/03-ack-nack.cjs` | NACK 是否重投由 broker 决定 |
| 事务 BEGIN/COMMIT/ABORT（Transaction 句柄） | `node examples/demo/04-transaction.cjs` | 内置 demo broker |
| 心跳与对端静默检测 | `node examples/demo/05-heartbeat.cjs` | 内置 demo broker |
| 同一套 API 跑 1.0/1.1/1.2 | `node examples/demo/06-versions.cjs` | 内置 demo broker |
| 登录凭据与结构化错误（被拒绝时怎么判断） | `node examples/demo/07-auth-errors.cjs` | 内置 demo broker 开启凭据检查 |
| 命令行发一条/收一条消息 | 见 §4 | `tools/stomp-cli.cjs` |
| 对真实 broker 跑 | `node examples/1-tasks.cjs`（等） | 需要你自己的本地 STOMP broker（Artemis 示例用匿名 broker；认证 broker 用 CLI 的 `--login`），见 §5 |

所有 `examples/demo/*.cjs` 的输出与 `examples/demo/expected/*.txt` 逐字节比对，由 `python3 scripts/usability-test.py` 执行，也是 `scripts/verify.py` 的 `demos-and-cli` 阶段（CI 会跑）。

## 2. 两种用法

**A. 无 I/O 核心（MoonBit）**：`Client::send(frame, now_ms)` 返回要写出去的字节，`Client::receive(bytes, now_ms)` 返回事件，`Client::tick(now_ms)` 返回需要写出的心跳字节。你自己负责读写套接字与调度时钟（单调、毫秒、`Int64`）。可运行的完整走位是 `examples/offline/main.mbt`；公共 API 清单见 `pkg.generated.mbti`。版本相关入口：`connect_versions`、`subscribe_versioned`、`acknowledge_message`、`encode_versioned`、`Decoder::new(version=…)`。

**B. Node TCP 适配器（JS）**：`transport/tcp.cjs` 导出 `TcpClient`，内部调用 MoonBit 编译出的 bridge，因此必须先 `moon build --target js`。

```js
const {TcpClient} = require('./transport/tcp.cjs');
const c = new TcpClient({host: '127.0.0.1', port: 61613});
await c.connect({versions: ['1.2']});   // 需要认证时：connect({versions: ['1.2'], login: 'user', passcode: process.env.PASS})
await c.receipt('SUBSCRIBE', [['id', 'w'], ['destination', '/queue/jobs']]);
const incoming = c.waitFor(e => e.event === 'message');
await c.receipt('SEND', [['destination', '/queue/jobs']], Buffer.from('hello'));
console.log(Buffer.from((await incoming).frame.body).toString());
await c.disconnect();
```

### TcpClient 速查（以 `transport/tcp.cjs` 为准）

| 成员 | 行为 |
|---|---|
| `new TcpClient({host='127.0.0.1', port=61613, tickMs=25, maxQueue=1024, maxWaiters=256, maxQueueBytes=4 MiB})` | 参数非法立即抛错；一个实例只连一次 |
| `connect({virtualHost='localhost', versions=['1.2'], heartbeat='1000,1000', login, passcode})` | 提供 1–3 个版本；等 CONNECTED 最多 5 s；`login`/`passcode` 省略时不发送，空字符串会发送，含 CR/LF/NUL 的值在打开 socket 前被拒绝。仅提供 `['1.0']` 时默认 `heartbeat` 为 `'0,0'` |
| `new TcpClient({…, tlsOptions})` | `tlsOptions` 非 null 时用 Node `tls.connect`，必须校验证书（`rejectUnauthorized:false` 与传入已连接 socket 被拒）；自签 CA 用 `{ca}`，IP 以外的 host 自动作 SNI，可设 `servername`。**已在脚本 peer 与真实 Classic 6.3.2 回环 TLS profile 验证；不代表生产网络验收** |
| `await subscribe(destination, {id?, ack='auto', headers=[]})` | SUBSCRIBE receipt 后返回 Subscription：`next(timeout=5000)`、`ack(frame, {transaction?})`、`nack(…)`、`unsubscribe()`；`ack`/`nack` 只接受该句柄 `next` 返回的原 frame。1.0 下同 destination 的多订阅不保证区分 |
| `await beginTransaction({id?})` | BEGIN receipt 后返回 Transaction：`send(destination, Uint8Array, headers)`、`commit()`、`abort()` |
| `send(command, headers, body)` | 发后不管，无回执 |
| `receipt(command, headers, body, id?)` | 自动加 `receipt` 头并等 RECEIPT（5 s）。**超时会关闭本连接**：结果未知，库不会替你重试 |
| `waitFor(predicate, timeoutMs=5000)` | predicate 必须同步返回 Boolean；超时只拒绝该等待，连接保持 |
| `disconnect()` / `stop()` | 带 receipt 的 DISCONNECT 后关闭 / 立即关闭 |
| `state()` / `version()` | `fresh｜connecting｜active｜disconnecting｜closed` / 协商版本字符串 |

错误：协议/参数/broker ERROR/句柄误用抛出带 `name`、`code`、`operation` 的错误（`STOMP_INVALID`、`STOMP_ARGUMENT`、`STOMP_BROKER_ERROR`（保留 `frame`）、`STOMP_HANDLE`）；恢复版增加 `STOMP_EVENT_TIMEOUT`（普通观察等待，保持连接）、`STOMP_CONNECT_TIMEOUT`、`STOMP_RECEIPT_TIMEOUT`、`STOMP_PEER_EOF`、`STOMP_TRUNCATED_STREAM` 与 `STOMP_CONNECTION_CLOSED`。Node I/O 原始 code 保留。receipt 失败另有 `operation`（命令）、`receiptId`、`outcomeUnknown` 和 `cause`（只有开始调用 `socket.write` 后才标记结果未知；本地验证/写缓冲拒绝为 false），broker 的 `frame` 保留。其他参数错误仍可能没有 code。

`new TcpClient({controlTimeoutMs: 5000})` 同时限制 CONNECT 与每个 receipt 等待；必须是 1..2147483647 的整数，构造后只读。`waitFor`/`Subscription.next` 的观察期限独立设置。控制等待失败后会话关闭、未完成的等待被拒绝，不会自动重连或重发。原始 `event timeout` 文本保留，调用方应优先使用 code。

COMMIT receipt 超时后 `Transaction.state === 'failed'` 只说明客户端没有确认结果，**不能解释成已回滚**。业务应使用业务 ID/状态查询等方式对账，再决定是否在新会话重试。STOMP receipt 只确认 broker 对帧的处理，不等于消费端业务处理或 exactly-once。

事件形状：`{event:'message', frame:{command, headers:[[k,v]], body:number[]}}`、`{event:'receipt', id}`、`{event:'heartbeat'}`、`{event:'error', frame}`。`frame.body` 是字节数组，用 `Buffer.from(frame.body)` 还原。

## 3. 版本相关的 ACK 头

| 协商版本 | ACK/NACK 头 |
|---|---|
| 1.2 | `id` = MESSAGE 的 `ack` 头 |
| 1.1 | `subscription` + `message-id` |
| 1.0 | `message-id`（无 NACK；无 `client-individual`） |

`examples/demo/06-versions.cjs` 与命令行 `--versions` 展示了三者；核心会拒绝不合版本的组合（见 `examples/versions`）。

## 4. 命令行 `tools/stomp-cli.cjs`

两个终端（或把 broker 放后台）：

```sh
node tools/demo-broker.cjs --port 61613                      # 终端 1：内置 demo broker
node tools/stomp-cli.cjs subscribe --destination /queue/jobs --ack client-individual   # 终端 2：等一条
node tools/stomp-cli.cjs send --destination /queue/jobs --body 'hello 你好'              # 终端 3
```

| 子命令 | 常用选项 |
|---|---|
| `send` | `--body TEXT｜--body-file F｜--body-hex HEX｜--stdin`、`--header k=v`（可重复）、`--no-receipt`、`--tx commit｜abort` |
| `subscribe` | `--count N`、`--ack auto｜client｜client-individual`、`--nack-if SUBSTR`、`--timeout MS`、`--format text｜json` |
| 通用 | `--host`（默认 127.0.0.1）、`--port`（默认 61613）、`--versions 1.0,1.1,1.2`、`--heartbeat cx,cy`、`--vhost NAME`、`--allow-remote` |
| 认证与 TLS | `--login USER` 配 `--passcode-env VAR`（推荐）或 `--passcode PASS`（命令行对本机其他用户可见）；`--tls`、`--tls-ca FILE`、`--tls-servername NAME`（证书校验始终开启）；带登录的非回环连接必须 `--tls` |

退出码：`0` 成功；`1` 运行失败（连接被拒、超时、broker ERROR、核心拒绝）；`2` 用法错误。非回环主机需 `--allow-remote`（带登录时还需 `--tls`），本项目只在回环上测过。认证失败时 stderr 显示错误类别和 broker 的原因（引号括起、限长），不回显口令。`--format json` 输出 `bodyHex`，二进制正文可无损取回。

## 5. 对真实 broker

`examples/1-tasks.cjs`、`2-reliable.cjs`、`3-transactions.cjs` 与 CLI 都可指向你自己的本地 STOMP broker（默认 `127.0.0.1:61613`，环境变量 `STOMP_PORT` 可改端口，命令行用 `--port`）。本项目验证过的真实 broker 有两个：Apache Artemis 2.57.0 的无认证、无持久化 profile，以及 Apache ActiveMQ Classic 6.3.2 的认证 profile（见 [broker-interop](broker-interop.zh-CN.md)）；可复现的启动与验证命令见 [README](../README.md#真实-broker-复现) 与 [broker-plan](broker-plan.zh-CN.md)。

用 Docker 起一个 Artemis 只是常见做法，**本项目没有验证过任何 Docker 镜像**，所以不把它写成已验证路径；已验证路径是 `scripts/verify.py … --broker-archive` 那条（官方发行包 + 已有 Java 17，仅回环）。

## 6. demo broker 是什么、不是什么

`tools/demo-broker.cjs` 是几百行 JS 的内存 STOMP 测试对端：支持 1.0/1.1/1.2 协商、queue（轮转分发）、topic（扇出）、三种 ACK 模式、事务、receipt、心跳。它**不是**一致性参照，不是本项目的产品功能，也**不能**证明与真实 broker 互通（那由 Artemis + go-stomp 的记录负责）。它的 NACK 策略默认“重新入队”（`--nack discard` 可改），而已记录的 Artemis 2.57.0 profile 是“丢弃”，所以 `03-ack-nack` 同时展示了两种。解析器刻意独立于被测的 MoonBit 编解码。
