Files
mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md
T
lix-2026 f292c6710a feat: EditorRuntimeActor - 三层缓存/delta/事件架构
Phase A — EditorRuntimeActor 内存缓存层
- 新增 editor_actor.rs: EditorBlockDocument 内存态 + apply_command + load_or_init
- block.rs 四个写工具(replace/insert/delete/move)接入 actor 路径
- editor_actor feature flag(MNOTE_WEB_ENABLE_EDITOR_ACTOR=true 默认开启)
- bridge-runtime 三个核心函数公开化
- rust-toolchain: 1.89 → stable(修复 spike WASM 编译阻塞)

Phase B — 编辑器增量 delta channel
- BlockDelta/DeltaOperation 类型 + actor.build_block_delta()
- leptos-tiptap spike: mnote:editor:block-delta CustomEvent 监听 + JSON patch
- DocumentAiAgentPanel: 拦截 blockDelta → window dispatchEvent
- 工具响应含 blockDelta 字段供前端消费

Phase C — 事件 stream delta
- broadcast channel 在 AppState/actor/SSE 三层贯通
- tree_events SSE 端点发 block.delta 事件
- 旧客户端降级兼容

环境修复
- rustc recursion_limit = 1024(修复 Leptos SSR 类型深度溢出)
- run-convex-deploy.js(封装 Convex function 部署到本地后端 3210)

ref: design/07-ai/process/7-13-page-block-editor-runtime-actor-v1.md
2026-05-16 22:03:30 +08:00

13 KiB
Raw Blame History

3-3 [process] Rust Web Tree Realtime Event Stream 方案 v1

更新时间:2026-05-16

关联文档:

  • /mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md
  • /mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md
  • /mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md

1. 目标

这份文档用于固定 Stage C-1 的正式实时链路口径:

  • 保留 Convex 作为 realtime substrate
  • Rust 成为 tree-first graph 的 semantic owner
  • Rust Web 负责正式页面 transport 与实时事件流
  • 前端只消费 projection 与 delta,不再消费实验壳真相

目标效果是接近网页版 wolai / notion 的体验:

  • 快速进入页面
  • 树结构实时同步
  • 局部变化快速响应
  • 浏览器端不持有第二套树真相

2. 职责划分

2.1 Convex substrate

Convex 继续承担:

  • 持久化
  • mutation / query 底座
  • 实时订阅底座
  • 文件 / 对象存储协作

Convex 在这里是:

storage / realtime substrate

而不是页面树语义 owner。

2.2 Rust semantic owner

Rust kernel 负责:

  • page_tree / sidebar_tree / file_tree / subtree 语义
  • node / edge / projection 规则
  • command / query 的语义收口
  • domain event 的统一格式
  • trace / audit / version 口径

Rust 在这里是:

Rust semantic owner

2.3 Rust Web transport

Rust Web 负责:

  • SSR 页面壳
  • SSE / WS 主链
  • workspace / subtree 订阅入口
  • 把 Convex 订阅与 Rust domain event 连接起来
  • 向前端输出稳定的 projection snapshot + delta stream

2.4 前端 projection consumer

前端只负责:

  • 首屏渲染
  • islands 交互
  • optimistic UI
  • 应用 delta 到本地 projection cache

前端不再负责:

  • 重新定义树结构
  • 重新计算页面树语义
  • 维护实验壳 iframe 作为正式实时主链

3. 为什么当前 tree shell 不是正式实时链路

当前 mnote-web tree shell 仍然只是实验壳,原因有三点:

  1. 它依赖 iframe / HTML shell / postMessage 交互。
  2. 它的 command path 和状态边界更接近实验 viewer,而不是正式页面 transport。
  3. 它没有成为首页、sidebar、文档页共享的正式实时订阅主链。

因此当前 tree shell 可以继续保留为:

  • 实验验证壳
  • 独立 tree viewer
  • picker / filetree 的可选增强壳

但不能继续当成正式 realtime 主链。

4. 正式 realtime tree event stream 分层

正式链路建议固定为四层:

4.1 Convex 持久化/订阅底座

这里负责:

  • 命令落账
  • 持久化页面树状态
  • 输出 mutation 后可订阅的数据变化

4.2 Rust kernel 语义 owner

这里负责:

  • 把命令结果解释为 domain event
  • 把底层 mutation 变化转成树域可消费的语义事件
  • 维护 projection rebuild 与 delta 生成规则

4.3 Rust Web SSE/WS transport

这里负责:

  • 暴露正式 /api/tree/events 或等价 stream 入口
  • 管理 workspace / subtree 订阅
  • 发送 snapshot、delta、cursor、ack、resync 信号

推荐策略:

  • 首屏与重连优先使用 snapshot
  • 连续变化优先推送 delta
  • 大规模漂移或 cursor 失配时回退到 resync snapshot

4.4 前端 projection consumer

这里负责:

  • 接收初始 projection snapshot
  • 接收增量事件
  • 合并到 sidebar / filetree / page subtree cache
  • 必要时触发局部重渲染

5. 建议事件形状

正式事件建议至少覆盖三类 scope

  • workspace
  • subtree
  • tree delta

5.1 Workspace stream

用于:

  • 当前工作区根树更新
  • 垃圾桶、模板、共享页等全局区域变化

建议形状:

{
  "stream": "workspace",
  "workspaceId": "ws_123",
  "cursor": "evt_1001",
  "kind": "snapshot|delta|resync",
  "projection": "sidebar_tree",
  "data": {}
}

5.2 Subtree stream

用于:

  • 当前文档子树
  • file tree 局部展开区域
  • picker 只关注的局部节点集合

建议形状:

{
  "stream": "subtree",
  "workspaceId": "ws_123",
  "rootNodeId": "page_1",
  "cursor": "evt_1002",
  "kind": "snapshot|delta|resync",
  "projection": "page_tree",
  "data": {}
}

5.3 Tree delta payload

建议包含:

  • eventId
  • commandId
  • traceId
  • aggregateType
  • aggregateId
  • op
  • node
  • parentNodeId
  • position
  • removedNodeIds
  • changedProjectionKeys

参考形状:

{
  "eventId": "evt_1003",
  "commandId": "cmd_2001",
  "traceId": "trace_3001",
  "aggregateType": "page",
  "aggregateId": "page_9",
  "op": "create|rename|move|delete|restore|purge|embed",
  "node": {
    "id": "page_9",
    "title": "新页面"
  },
  "parentNodeId": "page_root",
  "position": 3,
  "removedNodeIds": [],
  "changedProjectionKeys": ["sidebar_tree", "file_tree"]
}

6. 与首页和首屏的关系

首页与 /api/sidebar 的 P0 原则必须保持不变:

  • 首页不能再依赖 3104 实验壳
  • 3000 主入口必须可稳定进入 /auth 或文档页
  • 首屏仍要有稳定 SSR snapshot,但不再通过第二条 runtime projection fallback 偷偷补拉 live 语义

也就是说:

  • 首屏可用性优先于实时增强
  • realtime stream 是正式增强层,不是新的首屏阻塞点

7. 实施顺序

推荐顺序如下:

  1. 先固定首页、sidebar、picker 不再依赖 tree shell 实验壳。
  2. 再固定 Rust kernel 的 tree command / projection owner 身份。
  3. 然后补正式 Rust Web SSE/WS tree stream。
  4. 最后把 sidebar、page subtree、filetree 逐步切到统一 stream。

8. 最终固定口径

长期口径固定为:

  • Convex substrate 不拆
  • Rust semantic owner 收口语义
  • Rust Web 提供正式实时 transport
  • 前端只消费 projection snapshot 与 tree delta
  • iframe/postMessage tree shell 不是正式 realtime 主链

9. 当前实现复核(2026-05-09

这份方案继续留在 process/,因为 Rust Web transport 与当前 3000 主壳 live consumer 已落地,但 page subtree / filetree / preferred snapshot 仍未完全统一到同一条正式 live cache。

9.1 已完成

  • Rust Web 已暴露正式 /api/tree/events,并返回 x-mnote-web-owner: mnote-webx-mnote-tree-stream-owner: rust-web
  • /api/tree/events 已能输出 workspace / subtree snapshot。
  • stream_support.rs 已有 cursor、delta、resync 的基础判定逻辑。
  • /api/stream/events 与 WebSocket snapshot / resync 骨架已存在。
  • legacy React 侧已有 useSidebarTreeStreamEventSource consumer,并有协议 / delta 单测。
  • 当前 3000 Rust shell 已直接挂载 tree live EventSource consumer,并通过 data-mnote-tree-live-applied 应用 delta / resync。
  • task112 / task120 / task123 已覆盖 /api/tree/events snapshot、delta / resync 与 stream owner 可用性。
  • task165 已验证双 pane 不重复建立第二条 tree live stream。
  • 2026-05-16 复核确认 workspace snapshot 同时携带 data.dataset.kernel_sidebar_projectiondata.dataset.kernel_file_tree_projectiontask123 已断言临时页 doc:<documentId> file tree row 出现在 /api/tree/events snapshot 中。
  • 2026-05-16 复核确认 remove_asset delta 会保留 assetId/documentId/updatedAt,并被判定为 structural delta,需要 workspace projection snapshot 回填 File Tree / resource row。
  • 2026-05-16 task432 已验证页面 create / archive / restore / purge / empty trash 在双浏览器 B 端 File Tree 与 Trash 无刷新同步,其中 create / purge / empty trash 通过 tree:resync 拉回正确状态,archive / restore 通过 tree:delta 同步。
  • 2026-05-16 task446 已验证页面 rename 在双浏览器 B 端无刷新同步:B 文档页页头、Breadcrumb、Sidebar 与 B File Tree {title}.md 均通过 tree:delta upsert_document 更新,rename 后 navigationEvents=[]
  • 2026-05-16 task447 已验证页面 move order 在双浏览器 B 端无刷新同步:同父级 A/B/C 执行 move C sortOrder=1 后,B 文档页与 B File Tree 的 Page Tree / File Tree direct child order 均通过 tree:delta move_document 更新为 A/C/Bmove 后 navigationEvents=[]
  • 2026-05-16 task448 已验证同连接内多条 missed tree command 会触发 /api/tree/events event: resyncB 文档页与 B File Tree 通过完整 snapshot 投影恢复新增子页,navigationEvents=[]
  • 2026-05-16 task449 已验证 SSE 断线恢复:B 端离线期间错过两条 create,恢复在线后 EventSource 收到 snapshot/resync 类完整投影,Page Tree / File Tree 拉回最新,liveStatus=connectedliveError=""navigationEvents=[]

9.2 仍未完成

  • Sidebar、page subtree、filetree 还没有全部统一到同一条 live stream cache。
  • WS 目前只证明 snapshot/resync 骨架,尚未成为主实时链路。
  • 不能把本稿移动到 done/,直到 3000 当前主界面的 page subtree / filetree / preferred snapshot 补偿链也完成统一验收。

9.3 2026-05-16 验证记录

  • cargo test --manifest-path rust/Cargo.toml -p mnote-web routes::stream_support -- --nocapture18 passed,覆盖 cursor、delta、resync、upsert_assetsremove_asset 与 structural snapshot 判定。
  • cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_events -- --nocapture1 passed,确认 /api/tree/events owner、event id 与 revision。
  • cd wolai-frontend && pnpm test src/components/sidebar/use-preferred-sidebar-snapshot.test.tsx src/lib/tree-stream/use-sidebar-tree-stream.test.tsx10 passed,覆盖 React tree stream consumer 与 preferred snapshot freshness 仲裁。
  • MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task123-rust-web-tree-live-stream-consumer-smoke.js:通过,证据 tmp/tree-live-cache-smoke/20260516-task123/task123.stdout.json
  • cargo test --manifest-path rust/Cargo.toml -p mnote-web convex_command_args_strips_tree_purge_artifacts_for_legacy_mutation -- --nocapture1 passed,确认 tree.node.purge / documents.purge 发往 Convex legacy mutation 前会剥离 commandProtocol
  • MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task432-filetree-trash-page-dual-browser-no-refresh-smoke.js:通过,证据 tmp/tree-live-cache-smoke/20260516-task432/result.json
  • cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_renders_context_menu_and_scoped_title_updates -- --nocapture1 passed,确认 Rust SSR tree live title update 同步当前文档页 title input 与 Breadcrumb selector。
  • MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task446-tree-rename-dual-browser-live-smoke.js:通过,证据 tmp/tree-live-cache-smoke/20260516-task446-rename/result.json,截图 tmp/tree-live-cache-smoke/20260516-task446-rename/b-document-after-rename.pngtmp/tree-live-cache-smoke/20260516-task446-rename/b-filetree-after-rename.png
  • node --check scripts/task447-tree-move-order-dual-browser-live-smoke.js:通过,确认 move order smoke 语法有效。
  • cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_handles_navigation_drag_and_filetree_actions -- --nocapture1 passed,确认 Rust SSR tree local/live move apply 消费 sortOrder 并传入排序插入逻辑。
  • cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_move_returns_structured_payload -- --nocapture1 passed,确认 /api/tree/commands move response 保留 sortOrder=1
  • cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_response_includes_rust_artifact_plan_for_domain_event -- --nocapture1 passed,确认 domain event 与 command log 的 streamDelta.sortOrder=1
  • MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task447-tree-move-order-dual-browser-live-smoke.js:通过,证据 tmp/tree-live-cache-smoke/20260516-task447-move-order/result.json,截图 tmp/tree-live-cache-smoke/20260516-task447-move-order/b-document-after-move.pngtmp/tree-live-cache-smoke/20260516-task447-move-order/b-filetree-after-move.png
  • node --check scripts/task448-tree-resync-recovery-dual-browser-smoke.js:通过,确认 resync smoke 语法有效。
  • MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task448-tree-resync-recovery-dual-browser-smoke.js:通过,证据 tmp/tree-live-cache-smoke/20260516-task448-resync/result.json,截图 tmp/tree-live-cache-smoke/20260516-task448-resync/b-document-after-resync.pngtmp/tree-live-cache-smoke/20260516-task448-resync/b-filetree-after-resync.png
  • node --check scripts/task449-tree-sse-reconnect-snapshot-recovery-smoke.js:通过,确认 SSE reconnect smoke 语法有效。
  • MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task449-tree-sse-reconnect-snapshot-recovery-smoke.js:通过,证据 tmp/tree-live-cache-smoke/20260516-task449-reconnect/result.json,截图 tmp/tree-live-cache-smoke/20260516-task449-reconnect/b-document-after-reconnect.pngtmp/tree-live-cache-smoke/20260516-task449-reconnect/b-filetree-after-reconnect.png