Files
mnote/design/03-rust-web/done/3-3-rust-web-tree-realtime-event-stream-v1.md
T
lix-2026 5f97800489 chore: align local-first control plane and editor fixes
- wire SQLite control-plane access/session paths into Rust web local-folder routes

- preserve local Markdown attachment semantics across upload, reload, and secondary-pane resource tabs

- refresh design governance docs, Reasonix task templates, and bug records

- retire root .mcp.json local MCP config
2026-05-23 23:38:42 +08:00

327 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 3-3 [done] Rust Web Tree Realtime Event Stream 方案 v1
> 更新时间:2026-05-18local-first 口径更新)
> 归档时间:2026-05-219.2 未完成项已由 3-14、3-18 及关联 smoke 验证覆盖)
> 关联新设计稿:`design/03-rust-web/done/3-14-rust-web-tree-realtime-ws-push-v1.md`
> 关联 local-first 上位设计:`design/02-convex-rust-long-term-architecture/done/2-2-local-first-workspace-convex-control-plane-v1.md`
>
> 2026-05-18 口径更新:
> - 本文的 Convex realtime substrate 只适用于 `convex_workspace`、同步副本和后续协作场景。
> - 当前早期产品默认 source 是 `local_folder`;本地树变化应优先通过 LocalFS watcher / rescan / Rust projection event 进入同一条前端 projection consumer。
> - `/api/realtime/ws` 与 `/api/tree/events` 的长期职责是统一 transport;本地工作区的数据真相仍是 LocalFS / WorkspaceSource。
>
> 关联文档:
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-2-local-first-workspace-convex-control-plane-v1.md`
> - `/mnt/Data1T/mnote/design/old/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/reference/1-tree-first-graph-kernel-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
## 1. 目标
这份文档用于固定 Stage C-1 的正式实时链路口径。2026-05-18 后,它应按 `WorkspaceSource` 区分底层事件来源:
- `local_folder`LocalFS watcher / rescan / command result 是默认事件来源
- `convex_workspace`Convex 作为 realtime substrate
- Rust 成为 tree-first graph 的 semantic owner
- Rust Web 负责正式页面 transport 与实时事件流
- 前端只消费 projection 与 delta,不再消费实验壳真相
目标效果是接近网页版 wolai / notion 的体验:
- 快速进入页面
- 树结构实时同步
- 局部变化快速响应
- 浏览器端不持有第二套树真相
## 2. 职责划分
### 2.1 Workspace event substrate
不同 `WorkspaceSource` 使用不同 substrate
- `local_folder`:本地文件系统、watcher、rescan、命令执行结果。
- `convex_workspace`:显式 cloud / compat source 的 Convex 持久化、mutation / query、实时订阅、对象存储协作。
Convex 在这里是可选云端 / 协作 source 的 substrate,而不是所有工作区的默认 substrate:
> source-specific 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 页面壳
- **WS push 主链**`/api/realtime/ws`),SSE 降级为 fallback60s 安全网)
- 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 Source-specific 持久化/订阅底座
这里负责:
- 命令落账
- 持久化页面树状态或本地文件状态
- 输出 command / watcher / mutation 后可订阅的数据变化
对于 local-first 默认路径,这层是 LocalFS watcher / rescan / command result;对于云端、同步和协作路径,这层才是 Convex。
### 4.2 Rust kernel 语义 owner
这里负责:
- 把命令结果解释为 domain event
- 把底层 mutation 变化转成树域可消费的语义事件
- 维护 projection rebuild 与 delta 生成规则
### 4.3 Rust Web WS push transport(主)+ SSE fallback
这里负责:
- 暴露正式 `/api/realtime/ws`WebSocket push,主 chain
- `/api/tree/events` 保留为 SSE fallback60s 安全网),不可作为主要 transport 依赖
- 管理 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-17WS push 迁移后更新)
这份方案继续留在 `process/`,因为 page subtree / filetree / preferred snapshot 仍未完全统一到同一条正式 live cache。**SSE transport 已降级为 fallback,主 transport 已切换为 WebSocket push**(详见 `3-14`)。
### 9.1 已完成
- [x] Rust Web 已暴露正式 `/api/realtime/ws`WebSocket push 主链)与 `/api/tree/events`SSE fallback),返回 `x-mnote-web-owner: mnote-web``x-mnote-tree-stream-owner: rust-web`
- [x] `/api/tree/events` 已能输出 workspace / subtree snapshotSSE fallback 保留)。
- [x] `stream_support.rs` 已有 cursor、delta、resync 的基础判定逻辑。
- [x] WS handler 用 `tokio::select!` 订阅 `stream_delta_tx` broadcast channelmutation 后推送 delta 给所有客户端。
- [x] 2026-05-17 commit `57ec8322`SSE pollMs=1000 polling 替换为 WebSocket push + SSE fallback。Convex idle 查询从 17/min 降至 0/min。
- [x] legacy React 侧已有 `useSidebarTreeStream``EventSource` consumer,并有协议 / delta 单测(SSE fallback 仍在用)。
- [x] 当前 `3000` Rust shell 已直接挂载 tree live WebSocket consumer;旧 `convex-command-log-ws` 命名只代表历史 transport 标签,默认 live cache 不再要求 Convex backend,并通过 SSE fallback 的 `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:<documentId>` 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 归档评估(2026-05-21
以下 9.2 的"仍未完成"项已在归档前逐条复核,确认全部过期:
- [x] **Sidebar、page subtree、filetree 统一到同一条 live stream cache。**
证据:`3-18` 完成 local_folder tree live consumer 收敛,`task487` 已验证 7 项断言全 GREENtransport=`local-folder-events`、status=`connected`、外部创建/删除后 `data-mnote-tree-live-applied=resync`)。`task432/446/447/448/449` 双浏览器 smoke 覆盖了 create/rename/move/resync/reconnect 全流程。
- [x] **WS 从骨架成为主实时链路。**
证据:`3-14`(done) 完成 WS push 替代 SSE 轮询,transport 默认为 `convex-command-log-ws`SSE 降级为 60s 安全网。`task446/447/448/449` 均在 WS push 架构上通过。
- [x] **3000 主界面 page subtree / filetree / preferred snapshot 补偿链统一验收。**
证据:`3-18` Worker C 已完成验收,`task487` 验证 local_folder 场景下 `data-mnote-tree-live-applied``resync`FileTree 同步刷新。
### 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`