# 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 用于: - 当前工作区根树更新 - 垃圾桶、模板、共享页等全局区域变化 建议形状: ```json { "stream": "workspace", "workspaceId": "ws_123", "cursor": "evt_1001", "kind": "snapshot|delta|resync", "projection": "sidebar_tree", "data": {} } ``` ### 5.2 Subtree stream 用于: - 当前文档子树 - file tree 局部展开区域 - picker 只关注的局部节点集合 建议形状: ```json { "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` 参考形状: ```json { "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 已完成 - [x] Rust Web 已暴露正式 `/api/tree/events`,并返回 `x-mnote-web-owner: mnote-web` 与 `x-mnote-tree-stream-owner: rust-web`。 - [x] `/api/tree/events` 已能输出 workspace / subtree snapshot。 - [x] `stream_support.rs` 已有 cursor、delta、resync 的基础判定逻辑。 - [x] `/api/stream/events` 与 WebSocket snapshot / resync 骨架已存在。 - [x] legacy React 侧已有 `useSidebarTreeStream` 与 `EventSource` consumer,并有协议 / delta 单测。 - [x] 当前 `3000` Rust shell 已直接挂载 tree live `EventSource` consumer,并通过 `data-mnote-tree-live-applied` 应用 delta / resync。 - [x] `task112` / `task120` / `task123` 已覆盖 `/api/tree/events` snapshot、delta / resync 与 stream owner 可用性。 - [x] `task165` 已验证双 pane 不重复建立第二条 tree live stream。 - [x] 2026-05-16 复核确认 workspace snapshot 同时携带 `data.dataset.kernel_sidebar_projection` 与 `data.dataset.kernel_file_tree_projection`,`task123` 已断言临时页 `doc:` file tree row 出现在 `/api/tree/events` snapshot 中。 - [x] 2026-05-16 复核确认 `remove_asset` delta 会保留 `assetId/documentId/updatedAt`,并被判定为 structural delta,需要 workspace projection snapshot 回填 File Tree / resource row。 - [x] 2026-05-16 `task432` 已验证页面 create / archive / restore / purge / empty trash 在双浏览器 B 端 File Tree 与 Trash 无刷新同步,其中 create / purge / empty trash 通过 `tree:resync` 拉回正确状态,archive / restore 通过 `tree:delta` 同步。 - [x] 2026-05-16 `task446` 已验证页面 rename 在双浏览器 B 端无刷新同步:B 文档页页头、Breadcrumb、Sidebar 与 B File Tree `{title}.md` 均通过 `tree:delta upsert_document` 更新,rename 后 `navigationEvents=[]`。 - [x] 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/B`,move 后 `navigationEvents=[]`。 - [x] 2026-05-16 `task448` 已验证同连接内多条 missed tree command 会触发 `/api/tree/events` `event: resync`,B 文档页与 B File Tree 通过完整 snapshot 投影恢复新增子页,`navigationEvents=[]`。 - [x] 2026-05-16 `task449` 已验证 SSE 断线恢复:B 端离线期间错过两条 create,恢复在线后 EventSource 收到 snapshot/resync 类完整投影,Page Tree / File Tree 拉回最新,`liveStatus=connected`、`liveError=""`、`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 -- --nocapture`:18 passed,覆盖 cursor、delta、resync、`upsert_assets`、`remove_asset` 与 structural snapshot 判定。 - `cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_events -- --nocapture`:1 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.tsx`:10 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 -- --nocapture`:1 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 -- --nocapture`:1 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.png` 与 `tmp/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 -- --nocapture`:1 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 -- --nocapture`:1 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 -- --nocapture`:1 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.png` 与 `tmp/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.png` 与 `tmp/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.png` 与 `tmp/tree-live-cache-smoke/20260516-task449-reconnect/b-filetree-after-reconnect.png`。