Rx-Rust 的 MoonBit 复刻:基于 Observer 模式的响应式编程库(Observable / Subject / Scheduler / 操作符),零外部依赖。
# 在你的项目里执行(仓库地址替换为实际 git 地址)
moon add git+https://<你的仓库地址>.gitimport {
"vicTop-cw/RX-MBT/rx" @rx,
}moon publish
# 之后任何项目可用:
moon add vicTop-cw/RX-MBT// moon.pkg 中已 import "vicTop-cw/RX-MBT/rx" @rx
fn main {
// 链式调用:map -> filter -> collect
let result = @rx.Observable::from_iter([1, 2, 3, 4, 5])
.map(fn(x) { x * 2 })
.filter(fn(x) { x > 4 })
let _ = result.subscribe_on_next(fn(v) { println(v) })
// Subject 多播
let subject : @rx.PublishSubject[Int, Unit] = @rx.PublishSubject::new()
let _ = subject.subscribe(@rx.Observer::on_next_only(fn(v) { println("收到: \{v}") }))
subject.on_next(42)
subject.on_completed()
}moon check # 类型检查(零警告零错误)
moon test # 运行全部测试完整的 API 签名与语义说明见 DOC.md(按分类整理的全部公开 API)。
| 项目 | 值 |
|---|---|
| MoonBit 工具链 | moon 0.1.20261016 (1af328f 2026-10-16)(moon version 输出) |
| 操作系统 | Windows 11(PowerShell 7 / Windows PowerShell 5.1 均可运行脚本) |
| 编译目标 | wasm(preferred_target = "wasm",见 moon.mod) |
| 测试后端 | moon test 默认 wasm 后端;监控 / rhop 相关测试在 js 与 native 目标 |
| 库版本 | vicTop-cw/RX-MBT@0.4.0(moon.mod 的 version 字段) |
| 测试数量 | 全模块 wasm 612 / js 825 / native 612(2026-10-28 C3 快照,Windows 与 WSL 逐字一致);rx 核心套件见 TEST_REPORT.md,rhop 明细见下方 rhop 章节与矩阵 |
| 基准测试 | test-suit/bench(#skip 默认跳过,moon test -g "bench" 运行) |
工具链版本以 moon version 输出为准。要求 moon >= 0.1.20260724(本项目使用了较新的 moon.mod / moon.pkg 语法与 Feature flags:rr_moon_mod, rr_moon_pkg)。
powershell -ExecutionPolicy Bypass -File scripts/gen_test_report.ps1| 文件 | 说明 |
|---|---|
| TEST_REPORT.md | 测试环境、汇总统计、按包分类的完整测试清单 |
| TEST_REPORT.json | 机器可读报告(供 CI / 对比脚本消费) |
| 目录 | 覆盖 |
|---|---|
| test-suit/create | of / from_iter / empty / never / error / repeat / range / from_callable / start |
| test-suit/transform | map / filter_map / flat_map / scan / concat_map / switch_map / group_by / curry_map |
| test-suit/filter | filter / take / skip / first / last / distinct / skip_until / take_until / contains |
| test-suit/aggregation | count / reduce / collect / sum / average / min / max / mean / median / variance / rolling_* / to_map / to_set |
| test-suit/composite | start_with / concat / merge / zip / combine_latest / with_latest_from / amb / iif / sequence_equal |
| test-suit/math_conditions | drop_none / fill_none / clamp / abs / every / some / all / find / is_empty |
| test-suit/core | run / debug / Subject / Scheduler / cum_* 等基础行为 |
| test-suit/itor | Itor / Node 可控制迭代器:send 插队 / set_pause / resume_iter / restart 重放 / stop / history 策略 |
| test-suit/haskell | Haskell 迭代器 API(Data.List / Data.Foldable 语义):iterate / scanl1 / foldr1 / map_accum_l / inits / tails / union / insert / lines / words 等 |
| test-suit/stress | 高压测试:10 万元素链、多订阅者、Subject 高压、递归重试 |
| test-suit/bench | 基准测试(#skip 默认跳过,moon test -g bench 运行) |
moon test -p test-suit/bench --include-skipped > moon_bench.json| 类型 | 说明 |
|---|---|
| Observable[T, E] | 可观察对象(冷流),包装订阅函数,每次 subscribe 重新执行 |
| Observer[T, E] | 观察者,承载 on_next / on_error / on_completed 三个回调 |
| Subscription | 订阅句柄,dispose() 幂等取消订阅 |
| Clock | 虚拟时钟,时间类操作符基于它实现,advance_time(ms) 推进 |
| Node[T] | 惰性链表节点:持有值 + 懒加载源 + fallback,from_iter 构建 |
| Itor[T] | 可控制迭代器(vools 风格):支持 send 插队 / 暂停 / 重放 / 停止 / 历史策略 |
| TriggerEvent[T] | 触发事件:事件名 + 载荷 + 自增序号 + 发生时刻(虚拟时钟毫秒) |
| Trigger[T] | 触发器:事件名(支持通配)+ 条件 + once + 优先级 + 处理器 |
| TriggerRegistry[T] | 触发器注册表:集中管理 + 通配匹配 + 优先级分发 + once 自动注销 + 幂等去重 |
| API | 说明 |
|---|---|
| Node::new(val, next?) / Node::from_iter(iter) | 构造节点 / 从迭代器构建惰性链表 |
| Node::val() / Node::next() | 取值 / 取下一节点(懒加载展开) |
| Node::to_itor() / Node::to_iter() | 节点链表 → Itor / Iter |
| Itor::new(source) / from_array / from_iter | 构造(数组源可重复迭代,副本独立) |
| Itor::next() | 取下一个值(自动初始化源) |
| Itor::send(node, jump_when?) / send_value(v) / send_values(arr) | 紧急插队 / 条件插队 |
| Itor::set_pause() / resume_iter() / stop() | 暂停 / 恢复 / 终止 |
| Itor::restart() | 回到 Pending,从历史头部重放,重放后继续源 |
| Itor::history_strategy(fn) / set_history_max(n) | 历史保留策略(-1 全保留 / n 条窗口) |
| Itor::state() / is_pending / is_iterring / is_paused / is_stopped | 状态查询 |
| Itor::copy() / call() | 返回独立副本(共享源工厂) |
| Itor::do_iter(f, pre_f?, sub_f?) | 副作用遍历(支持前置 / 后置钩子) |
| API | 说明 |
|---|---|
| TriggerRegistry::new() | 创建空注册表 |
| register(event_name, handler) | 注册简单触发器(无条件、非一次性、优先级 0) |
| register_with(event_name, handler, condition?, once?, priority?) | 注册可配置触发器,返回 trigger_id |
| unregister(trigger_id) | 注销触发器(存在返回 true,否则 false) |
| fire(name, payload) | 分发事件,时刻取默认虚拟时钟当前时间 |
| fire_at(name, payload, at) | 分发事件(指定时刻) |
| fire_with_key(name, payload, key) | 幂等触发:key 已存在则整体跳过 |
| has_trigger(event_name) | 是否存在匹配该事件名(含通配)的触发器 |
| trigger_count() | 当前注册的触发器数量 |
| clear() | 清空所有触发器与幂等键(事件序号继续递增) |
| 模式 | 含义 | 示例 |
|---|---|---|
| 精确 | 逐段相等 | task.done 匹配 task.done |
| * | 单段通配(不跨段边界) | task.* 匹配 task.done,不匹配 task.a.b 或 task |
| ** | 多段通配(匹配零段或多段,可出现在任意位置) | a.**.b 匹配 a.b / a.x.b / a.c.d.b,不匹配 a.x;纯 ** 匹配任意事件名 |
let reg : TriggerRegistry[Int] = TriggerRegistry::new()
let _ = reg.register_with(
"task.*",
fn(ev : TriggerEvent[Int]) { println("收到: \{ev.name}") },
priority=10,
once=true,
)
reg.fire("task.done", 42) // 触发一次后自动注销// 链式调用:map -> filter -> collect
let result = Observable::from_iter([1, 2, 3, 4, 5])
.map(fn(x) { x * 2 })
.filter(fn(x) { x > 4 })
.collect()
let _ = result.subscribe_on_next(fn(v) { println(v) })
// Subject 多播
let subject : PublishSubject[Int, Unit] = PublishSubject::new()
let _ = subject.subscribe_on_next(fn(v) { println("收到: \{v}") })
subject.on_next(42)
subject.on_completed()| 层 | 文件 | 能力 | |
|---|---|---|---|
| 协议 | core.mbt / envelope.mbt | Side(requester/executor)、TaskStatus 六态状态机、Envelope + checksum(sha256: + 规范化 JSON) | |
| 摘要 | digest.mbt / json_canonical.mbt | 纯 MoonBit sha256 / md5 / hmac-sha256 / pbkdf2-hmac-sha256;两套 JSON 规范口径(见下) | |
| 签名 | signer.mbt / integrity.mbt | AbstractSigner(FileHashSigner / NPUSigner)、信封完整性校验 | |
| 密码学 | key.mbt / crypto.mbt / rhop_js.mbt | VerifyResult 三态 reason、Fernet 信封加解密、RHOP-ENC: 秘密串(js);另有既有 Aes256GcmCipher(AES-256-GCM,js-only,读历史文件用) | |
| 传输 | transport_base.mbt / transport_local.mbt / transport_endpoint.mbt / transport_selector.mbt / online.mbt / relay.mbt | SharedPath / OnlineChannel 抽象、本地原子写、file://+http(s):// 端点、在线优先选择、Git/SFTP 探测、自建中转服务 | |
| 会话 | session.mbt / config.mbt / interact.mbt | 通道链(relay > online > shared)、配置构建与脱敏、execute/request/roundtrip 三模式 | |
| 业务 | mirror.mbt / requester.mbt / executor.mbt / daemon.mbt / actions.mbt / sync_folder.mbt | 镜像 SQLite(乐观锁)、需求端/执行端 API、中继守护、动作注册表、目录同步 | |
| 剪贴板 | clipboard.mbt | 本机剪贴板读写(Windows PowerShell / Linux wl-paste | xclip / macOS pbpaste 探测)+ 共享盘 push/pull 交换协议 |
| P2 | rhop_task/ | 通用任务队列(A 提交加密 JSON → B 解密执行,SQLite 管依赖/状态):P2a+P2b 已落地 8 文件;心跳与 vools 互操作(纯文本 <ISO.%fZ> [worker] / closed_token="closed" 哨兵,读端三级兼容;未来时间戳容差 60s fail-closed,与 vools 一致,真 vools 双向互操作 6/6);未实现 Postgres 后端 / 大载荷落文件 / 任务执行明细表,未复刻 CLI·task-daemon·local_runner —— 详见矩阵 §5.9 A/B 节 | |
| 既有 | packet.mbt / cipher.mbt | 本仓库早期的简版 RhopPacket / RhopEnvelope / RhopCipher(不是 vools 对照面) |
| 能力 | wasm | js | native | 说明 |
|---|---|---|---|---|
| 协议 / 摘要 / JSON / 签名 / 完整性 / 配置 / 会话 / 动作 / 剪辑板状态机 | ✅ | ✅ | ✅ | 纯逻辑,无平台依赖 |
| 信封加解密(Fernet)、RHOP-ENC: 秘密串 | — | ✅ | — | 走 node:crypto;非 js 返回可诊断 Err(BAD_TOKEN/"") |
| 本地介质 / 端点 / 目录同步 / 镜像 SQLite / 业务 API / 剪贴板真实读写 | — | ✅ | — | 走 node:fs / node:sqlite / 子进程;非 js 返回可诊断 Err 或 available()=false |
| 文件监控 RhopWatcher | — | ✅ | ✅ | wasm 无 monitor 后端,start() 返回可诊断 Err;可用 process_file() 手动处理 |
| ClipboardFileSync::run_forever 周期轮询 | 首轮 | ✅ | 首轮 | 非 js 无定时器,订阅后只推首轮 |
// 1) 信封 + checksum(三目标)
let env = @rhop.Envelope::new(
"job-001", @rhop.TaskStatus::Pending,
@json.parse("{\"cmd\":\"echo hi\"}") catch { _ => @json.Json::null() },
@rhop.Side::Requester,
)
let raw = env.to_rhop_bytes() // 规范 JSON 字节(含 sha256: 前缀 checksum)
let (ok, reason) = @rhop.IntegrityChecker::verify_bytes(raw)
// 2) 签名(FileHashSigner:preimage 用 ensure_ascii=True 口径)
let signer = @rhop.FileHashSigner::new()
let sig = signer.signature("job-001", @json.parse("{\"ok\":true}") catch { _ => @json.Json::null() })
// 3) 加密信封(仅 js)
let crypto = @rhop.RhopCrypto::from_key("my-secret")
let token = crypto.encrypt_json(@json.parse("{\"a\":1}") catch { _ => @json.Json::null() })
let vr = crypto.decrypt_verify(token) // reason: OK / BAD_TOKEN / MALFORMED / NOT_JSON
// 4) 剪贴板 push/pull(状态机 + 交换文件)
let sync = @rhop.ClipboardFileSync::new("//share/rhop", role="push")
.unwrap_or_else(fn() { abort("role 非法") })
let out = sync.sync_once() // {"changed": true, "text_len": 42} / {"changed": false, "reason": "…"}
let rounds = sync.sync_rounds(3) // {"rounds": 3, "changes": 1}
// 5) 需求端 / 执行端(仅 js;需配置与 data_dir)
let req = @rhop.Requester::from_config_dir(cfg, "./rhop-data").unwrap_or_else(fn() { abort("open") })
let task_id = req.submit("job-002", payload).unwrap_or_else(fn() { abort("submit") })
let result = req.get_result(task_id)完整签名与逐参数语义见 DOC.md 的「rhop」章节;与 vools 的逐符号对照见 fist-mbt/docs/2026-10-27-rhop-vools-parity-matrix.md。
| # | 差异 | 说明 |
|---|---|---|
| 1 | 两套 JSON 口径:envelope checksum 用 ensure_ascii=False,signer preimage 用 ensure_ascii=True | 复刻 vools 事实,分别由 canonical_json / canonical_json_ascii 承担;含中文 result 的签名只在前者口径下才与 vools 一致 |
| 2 | 浮点限制:Python repr(1.0)="1.0",本实现 "1";指数形态也不同形 | checksum/签名语料只用 int/字符串/布尔/null/嵌套;解析侧用 preserve_number_repr=true |
| 3 | 错误表达:vools 抛异常,本项目一律 Result[_, String] | 错误文本保留可诊断短语(checksum mismatch / 缺少必填字段 / 解析失败) |
| 4 | clipboard.get_clipboard_backend 形态:vools 返回 (read_fn, write_fn, name) 三元组 | 本项目返回 &ClipboardBackend? + get_clipboard_backend_name() |
| 5 | run_forever 形态:vools 阻塞线程 + stop_event | 本项目 @rx.Observable(订阅即首轮、dispose() 停止),阻塞等价物 sync_rounds(n) |
| 6 | 平台强依赖项:Fernet、文件系统、SQLite、子进程、剪贴板真实读写 | 仅 js 目标有真实实现;wasm/native API 齐备但显式返回「不可用」 |
| 7 | 不做项:secret.py CLI、SFTP 远端文件操作(put/get/listdir/...)、daemon.main CLI | 属 P1/P2 或 CLI 范畴,已在矩阵中标 ⛔ 并说明原因 |
| 8 | 回执按方向分流(D1):send_package 回执含 role、recv_package 回执含 payload | 矩阵 §4.1 与本表同口径;避免同名字段在收发两个方向语义漂移 |
| 9 | 文件名白名单 + 删除契约(D6/D7,纯增量):safe_filename 拒绝路径穿越/纯点号/Windows 设备名;Session::remove_package 提供一次性 res_ 清理 | vools 无这两项;本项目为安全与闭环清理而加 |
| 10 | 一次性 token 与入参形态(D8/D12):发送前清理陈旧 res_、消费后删除;interact 只接受 RhopConfig(vools _coerce 接受三种形态) | 降低陈旧回执误配、类型更显式 |
moon check # 主包类型检查(rhop 包三目标 0 error)
moon test # 默认 wasm 后端(rhop 相关 156 例,全绿)
moon test --target js # js 后端(本域包合计见下方计数表)
moon test --target native # native 后端(rhop 相关 156 例,全绿)
moon test -p test-suit/rhop_clipboard --target js # 剪贴板 25 例
moon test -p test-suit/rhop_vectors --target js # 互操作产物生产者 2 例
moon test test-suit/rhop_business --target js # 业务层 53 例(wasm/native 10 例)
moon test test-suit/rhop_task --target js # P2 任务队列 101 例(wasm/native 46 例)
moon run demo/rhop_demo --target js -- send <共享目录> # 端到端 demo(发 5 条加密包)
bash scripts/test_all.sh # 跨平台总入口(自动修 PATH,抓三目标汇总行)| 退出码 | 含义 |
|---|---|
| 0 | 全部判绿(check 0 error 且三/四步测试 failed=0、汇总行齐全) |
| 1 | 有步骤真实失败(编译错 / 测试红 / 汇总行缺失) |
| 2 | 用法错误或环境缺失(未知参数、moon/node 不在 PATH 等) |
| 3 | 一步都没执行(--skip 全跳过;不能当 PASS) |
| 4 | 构建锁等待超时(with_moon_lock.sh 并发冲突;非产品红,重试即可) |
⚠️ C2 轮并行 lane(crypto/session/business)仍在持续追加测试,跨 lane 总数会继续变化; 本表数字为上述快照时点实测,最终数字以 bash scripts/test_all.sh(qa 终验)输出为准; 本域(test-suit/rhop / rhop_vectors)数字在本轮修复后自洽。
| 测试包 | 用例数 | 结果 |
|---|---|---|
| test-suit/rhop(wasm / js / native) | 159 / 163 / 159 | 全绿 |
| test-suit/rhop_vectors(js,互操作产物生产者) | 0 / 3 / 0 | 全绿 |
| test-suit/rhop_js(js) | 60 | 全绿 |
| test-suit/rhop_clipboard(js) | 25 | 全绿 |
| test-suit/rhop_interact(js) | 28 | 全绿 |
| test-suit/rhop_business(wasm / js / native) | 10 / 69 / 10 | 全绿 |
| test-suit/rhop_task(wasm / js / native,P2a+P2b) | 46 / 103 / 46 | 全绿 |
| 全模块(项目级) | wasm 615 / js 851 / native 615(C4 终态,双平台一致) | 全绿 |
Install
Download zipRx-Rust 的 MoonBit 复刻:基于 Observer 模式的响应式编程库(Observable / Subject / Scheduler / 操作符),零外部依赖。