216 lines
15 KiB
Markdown
216 lines
15 KiB
Markdown
# Convex / Realtime / Storage 实现偏差审查
|
||||
|
|
|
|||
|
|
## 范围
|
|||
|
|
|
|||
|
|
本报告审查 Convex / storage bridge / realtime / Page Aggregate 数据底座与当前设计主线的一致性,重点对照以下方向:
|
|||
|
|
|
|||
|
|
- Convex 保留为自托管 storage / realtime substrate,不作为树语义 owner。
|
|||
|
|
- Rust kernel / bridge-runtime / mnote-web 持有 tree-first graph、projection、command、Page Aggregate 的语义主导权。
|
|||
|
|
- `/api/page-aggregate/:id` 作为文档页 Rust-first 读取主链。
|
|||
|
|
- `/api/tree/events` 作为 Rust Web tree realtime snapshot / delta / resync 主链。
|
|||
|
|
- 前端只消费稳定 projection、Page Aggregate 与 tree stream,不重新持有第二套对象真相。
|
|||
|
|
|
|||
|
|
重点读取范围:
|
|||
|
|
|
|||
|
|
- `ARCHITECTURE.md`
|
|||
|
|
- `design/01-05-current-priority-overview.md`
|
|||
|
|
- `design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
|
|||
|
|
- `design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
|||
|
|
- `design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
|||
|
|
- `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
|||
|
|
- `design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
|||
|
|
- `infra/convex/README.md`
|
|||
|
|
- `wolai-frontend/convex/`
|
|||
|
|
- `rust/crates/storage-convex-bridge/`
|
|||
|
|
- `rust/crates/mnote-web/src/transport/convex.rs`
|
|||
|
|
- `rust/crates/mnote-web/src/routes/documents.rs`
|
|||
|
|
- `rust/crates/mnote-web/src/routes/tree.rs`
|
|||
|
|
- `rust/crates/mnote-web/src/routes/sse.rs`
|
|||
|
|
- `rust/crates/mnote-web/src/routes/stream_support.rs`
|
|||
|
|
- `wolai-frontend/src/lib/documents/`
|
|||
|
|
- `wolai-frontend/src/lib/tree-stream/`
|
|||
|
|
|
|||
|
|
## 结论
|
|||
|
|
|
|||
|
|
整体方向与设计主线基本一致:Convex 没有被拆掉,仍承担本地自托管存储、文件和实时底座;Rust Web 已注册 `/api/page-aggregate/:document_id` 与 `/api/tree/events`;前端 tree stream 已直接使用 `EventSource("/api/tree/events")`;Next 的 `/api/documents/page` 与 `/api/mnote-web/stream` 已明确作为 410 compat 边界退场。
|
|||
|
|
|
|||
|
|
但当前实现仍是明显过渡态,主要问题集中在三处:
|
|||
|
|
|
|||
|
|
1. Page Aggregate 读链虽然是 Rust endpoint,但实际数据仍由 `documents:getMeta` + `documents:getContent` 分别取回后在 bridge-runtime 中拼 projection,`source` 标识存在“看起来比真实实现更 canonical”的风险。
|
|||
|
|
2. Rust Web 的 `/api/documents/title`、`/api/documents/options` 与 Next route 的 page write adapter 在 bridge artifact 记录上不一致;tree stream 依赖 bridgeLogs 时,Rust page write 路径可能漏掉 command/domain event。
|
|||
|
|
3. `/api/tree/events` 是正式 SSE 入口,但当前 change detection 依赖周期性查询 `bridgeLogs:listWorkspaceOverview`,且 Convex query 内部会按 workspace collect 全量 command/domain rows 后内存过滤排序,实时性和规模风险较高。
|
|||
|
|
|
|||
|
|
## 关键发现表格
|
|||
|
|
|
|||
|
|
| 编号 | 分类 | 严重度 | 发现 | 证据 |
|
|||
|
|
| --- | --- | --- | --- | --- |
|
|||
|
|
| F1 | 实现偏差 | 高 | Rust Page Aggregate endpoint 对外是 `mnote.page_aggregate.v1`,但构建过程仍先分别读取 meta/content,再在 bridge-runtime 中拼 projection;`source=KernelProjection` 容易掩盖底层仍是兼容 join。 | `rust/crates/mnote-web/src/routes/web_shell.rs:2282`、`:2291`、`:2301`;`rust/crates/bridge-runtime/src/lib.rs:8246`、`:8252`、`:5291`、`:5303`、`:5386` |
|
|||
|
|
| F2 | 风险 | 高 | Rust Web 的 title/options 写路由只执行 Convex mutation,不记录 bridge command/domain artifacts;而 tree stream 以 bridgeLogs 为事件来源,主路径 page 写入可能不进入实时流。 | `rust/crates/mnote-web/src/routes/documents.rs:714`、`:802`;`rust/crates/mnote-web/src/routes/command_support.rs:65`;对比 `rust/crates/mnote-web/src/routes/tree.rs:6529` 与 `wolai-frontend/src/lib/documents/page-write-command-adapter.ts:63` |
|
|||
|
|
| F3 | 风险 | 高 | tree realtime 当前是 SSE transport,但后端通过 `sleep(poll_ms)` 周期性轮询 Convex bridgeLogs,而不是直接利用 Convex subscription/push;默认 2s,实时性和负载都需继续验证。 | `rust/crates/mnote-web/src/routes/sse.rs:25`、`:26`、`:51`、`:58`、`:60`;`rust/crates/mnote-web/src/routes/stream_support.rs:610` |
|
|||
|
|
| F4 | 风险 | 中高 | `bridgeLogs:listWorkspaceOverview` 会按 workspace collect 全量 `command_logs` 与 `domain_events` 后在内存中过滤、排序、分页;SSE 轮询叠加后,历史日志增长会放大 Convex 读压力。 | `wolai-frontend/convex/bridgeLogs.ts:283`、`:287`、`:292`、`:301`、`:305` |
|
|||
|
|
| F5 | 方向变化/文档滞后 | 中 | `storage-convex-bridge` 把 `page.aggregate.get` 映射到 `documents:getPageAggregate`,但 Convex `documents.ts` 只发现 `getMeta` / `getContent`,未发现 `getPageAggregate` export;当前真实 Page Aggregate 路线已绕到 Rust adapter。 | `rust/crates/storage-convex-bridge/src/mapping.rs:101`;`wolai-frontend/convex/documents.ts:684`、`:803`;全仓 `rg getPageAggregate` 未发现 Convex 实现 |
|
|||
|
|
| F6 | 已完成/方向一致 | 中 | Next compat 读链和旧 stream alias 已显式退场,前端 tree stream 直接构造 `/api/tree/events`,符合当前主线口径。 | `wolai-frontend/src/app/api/documents/page/route.ts:17`;`wolai-frontend/src/app/api/mnote-web/stream/route.ts:9`;`wolai-frontend/src/lib/tree-stream/protocol.ts:66`、`:72` |
|
|||
|
|
| F7 | 未完成 | 中 | Page Aggregate TS builder 已不在 runtime 主链中被引用,但文件仍保留;当前定位应继续写成 fallback / adapter / 测试材料,不能作为运行时主路径描述。 | `wolai-frontend/src/lib/documents/page-aggregate-builder.ts:51`;全仓非测试引用仅剩定义,runtime loader 只调用 Rust snapshot:`wolai-frontend/src/lib/documents/page-aggregate-loader.ts:225` |
|
|||
|
|
|
|||
|
|
## 证据
|
|||
|
|
|
|||
|
|
### 1. Page Aggregate 读链已收口到 Rust endpoint,但仍由 meta/content join 生成
|
|||
|
|
|
|||
|
|
`mnote-web` 注册了正式 endpoint:
|
|||
|
|
|
|||
|
|
- `rust/crates/mnote-web/src/routes/mod.rs:53` 到 `:56` 注册 `/api/page-aggregate/{document_id}`。
|
|||
|
|
- `rust/crates/mnote-web/src/routes/web_shell.rs:2227` 到 `:2252` 返回 `schema: "mnote.page_aggregate.v1"` 和 `result: aggregate`。
|
|||
|
|
|
|||
|
|
但实际构建流程仍是:
|
|||
|
|
|
|||
|
|
- `rust/crates/mnote-web/src/routes/web_shell.rs:2282` 到 `:2290` 读取 `load_document_meta_result`。
|
|||
|
|
- `rust/crates/mnote-web/src/routes/web_shell.rs:2291` 到 `:2299` 读取 `load_document_content_result`。
|
|||
|
|
- `rust/crates/mnote-web/src/routes/web_shell.rs:2301` 到 `:2315` 把 `meta + content` 作为数据传给 `page.aggregate.get`。
|
|||
|
|
- `rust/crates/bridge-runtime/src/lib.rs:5291` 到 `:5312` 的 `build_page_aggregate_projection_result` 明确从 `data.meta` 和 `data.content` 中拆字段。
|
|||
|
|
|
|||
|
|
这说明“Rust-first 读取主链”成立,但“Page Aggregate 已经由 kernel 原生投影独立产出”仍未成立。该点与 `5-5/5-6` 的过渡态判断一致,但需要在后续文档和汇报中保持精确。
|
|||
|
|
|
|||
|
|
需进一步验证:`PageAggregateSource::KernelProjection` 是否应在这种 `meta/content join` 场景下继续使用,还是应保留 `CompatMetaContentJoin` 以避免 provenance 误导。
|
|||
|
|
|
|||
|
|
### 2. Page write side effects 在 Rust route 与 Next adapter 之间不一致
|
|||
|
|
|
|||
|
|
Next route 侧:
|
|||
|
|
|
|||
|
|
- `wolai-frontend/src/app/api/documents/title/route.ts:58` 调用 `executePageWriteBridgeCommand`。
|
|||
|
|
- `wolai-frontend/src/app/api/documents/options/route.ts:47` 调用 `executePageWriteBridgeCommand`。
|
|||
|
|
- `wolai-frontend/src/app/api/documents/save/route.ts:43` 调用 `executePageWriteBridgeCommand`。
|
|||
|
|
- `wolai-frontend/src/lib/documents/page-write-command-adapter.ts:63` 到 `:69` 成功后调用 `recordRustBridgeCommandArtifacts`。
|
|||
|
|
|
|||
|
|
Rust route 侧:
|
|||
|
|
|
|||
|
|
- `rust/crates/mnote-web/src/routes/documents.rs:579` 的正文保存使用 `execute_runtime_command_via_convex_with_artifacts`。
|
|||
|
|
- `rust/crates/mnote-web/src/routes/documents.rs:714` 的标题更新使用 `execute_runtime_command_via_convex`。
|
|||
|
|
- `rust/crates/mnote-web/src/routes/documents.rs:802` 的页面设置更新使用 `execute_runtime_command_via_convex`。
|
|||
|
|
- `rust/crates/mnote-web/src/routes/command_support.rs:65` 到 `:73` 的 `execute_runtime_command_via_convex` 只执行 Convex command plan,不持久化 artifacts。
|
|||
|
|
- `rust/crates/mnote-web/src/routes/command_support.rs:75` 到 `:93` 的 artifact 版本才会调用 `execute_convex_command_plan_with_artifacts`。
|
|||
|
|
|
|||
|
|
tree command route 是一致的:
|
|||
|
|
|
|||
|
|
- `rust/crates/mnote-web/src/routes/tree.rs:6529` 使用 `execute_runtime_command_via_convex_with_artifacts`。
|
|||
|
|
|
|||
|
|
风险是:如果当前 3000 主入口走 Rust Web `/api/documents/title` 或 `/api/documents/options`,这些 page 写入不会进入 `command_logs/domain_events`,而 `/api/tree/events` 正是通过 bridgeLogs 检测变化。标题类写入尤其可能影响 sidebar/breadcrumb/page tree 的实时一致性。
|
|||
|
|
|
|||
|
|
需进一步验证:当前文档页标题编辑在 3000 主壳中到底走 `/api/documents/title` 还是 `/api/tree/commands`;如果走前者,应补 Rust route artifact 记录或统一到 tree/page command route。
|
|||
|
|
|
|||
|
|
### 3. Tree realtime 是正式 SSE 入口,但实现仍是 polling-backed stream
|
|||
|
|
|
|||
|
|
正式入口成立:
|
|||
|
|
|
|||
|
|
- `rust/crates/mnote-web/src/routes/mod.rs:130` 注册 `/api/tree/events`。
|
|||
|
|
- `rust/crates/mnote-web/src/routes/sse.rs:127` 到 `:146` 给 tree events 加 `x-mnote-tree-stream-owner: rust-web`。
|
|||
|
|
- `wolai-frontend/src/lib/tree-stream/protocol.ts:66` 到 `:77` 构造 `/api/tree/events` URL。
|
|||
|
|
- `wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts:136` 到 `:143` 用 `EventSource` 监听 `snapshot/delta/resync`。
|
|||
|
|
|
|||
|
|
但服务端 change detection 是轮询:
|
|||
|
|
|
|||
|
|
- `rust/crates/mnote-web/src/routes/sse.rs:25` 到 `:26` 读取 `max_polls` 和 `poll_ms`,默认 `2000ms`,最低 `250ms`。
|
|||
|
|
- `rust/crates/mnote-web/src/routes/sse.rs:51` 到 `:58` 循环中 `sleep(Duration::from_millis(poll_ms))`。
|
|||
|
|
- `rust/crates/mnote-web/src/routes/sse.rs:60` 到 `:67` 每轮调用 `load_stream_overview` 再 `resolve_stream_change`。
|
|||
|
|
- `rust/crates/mnote-web/src/routes/stream_support.rs:610` 到 `:625` 的 `load_stream_overview` 通过 `bridge.workspace.overview` 查询 Convex。
|
|||
|
|
|
|||
|
|
这与“Rust Web 提供正式实时 transport”一致,但还不是“基于 Convex realtime subscription 的 push stream”。短期可接受为过渡实现,长期如果继续承载主链,需要明确性能和延迟边界。
|
|||
|
|
|
|||
|
|
### 4. bridgeLogs overview 查询存在规模风险
|
|||
|
|
|
|||
|
|
`wolai-frontend/convex/bridgeLogs.ts` 的 `listWorkspaceOverview`:
|
|||
|
|
|
|||
|
|
- `:283` 到 `:286` 按 workspace 查询并 `collect()` 所有 `command_logs`。
|
|||
|
|
- `:287` 到 `:290` 按 workspace 查询并 `collect()` 所有 `domain_events`。
|
|||
|
|
- `:292` 到 `:301` 在内存中过滤、排序、切片 command logs。
|
|||
|
|
- `:303` 到 `:312` 再用 commandId 集合过滤 domain events。
|
|||
|
|
|
|||
|
|
该实现作为调试/小数据过渡可以工作,但被 `/api/tree/events` 默认每 2 秒调用时,日志增长后会造成:
|
|||
|
|
|
|||
|
|
- Convex query 读放大。
|
|||
|
|
- SSE 连接数量增加时成倍放大。
|
|||
|
|
- cursor 语义依赖内存排序,历史数据规模变大后容易出现延迟或超时。
|
|||
|
|
|
|||
|
|
建议后续至少增加按 `workspace_id + created_at/id` 的索引分页,避免每次 stream poll 全量扫描。
|
|||
|
|
|
|||
|
|
### 5. storage-convex-bridge 中存在疑似过时的 `page.aggregate.get` 映射
|
|||
|
|
|
|||
|
|
`rust/crates/storage-convex-bridge/src/mapping.rs:101`:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
"page.aggregate.get" => "documents:getPageAggregate"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
但当前 `wolai-frontend/convex/documents.ts` 中只发现:
|
|||
|
|
|
|||
|
|
- `getMeta`:`wolai-frontend/convex/documents.ts:684`
|
|||
|
|
- `getContent`:`wolai-frontend/convex/documents.ts:803`
|
|||
|
|
|
|||
|
|
全仓搜索未发现 Convex `documents:getPageAggregate` 实现。当前真实 Page Aggregate 读取是 Rust Web 先读 meta/content,再由 bridge-runtime 生成 projection。因此该映射要么是未来目标,要么已经滞后;如果有代码路径直接通过 storage-convex-bridge 执行 `page.aggregate.get`,会存在运行时函数不存在风险。
|
|||
|
|
|
|||
|
|
需进一步验证:`storage-convex-bridge` 是否仍有生产路径直接执行 `page.aggregate.get -> documents:getPageAggregate`;如果没有,应把映射标注为 future/stale,或改为当前真实读链。
|
|||
|
|
|
|||
|
|
### 6. Convex substrate 定位总体一致
|
|||
|
|
|
|||
|
|
正向证据:
|
|||
|
|
|
|||
|
|
- `infra/convex/README.md` 明确自托管 Convex backend/dashboard、HTTP Actions 和文件存储。
|
|||
|
|
- `wolai-frontend/convex/schema.ts:35` 到 `:91` 的 `documents` 表继续承接页面结构、正文、页面设置、统计字段。
|
|||
|
|
- `wolai-frontend/convex/schema.ts:121` 到 `:162` 的 `media_assets` 与 `_storage` 字段保留文件底座。
|
|||
|
|
- `rust/crates/storage-convex-bridge/README.md:5` 到 `:13` 明确 bridge 只做协议到 Convex 读写请求映射,不复制主事实层、不维护第二数据库。
|
|||
|
|
|
|||
|
|
这与“Convex 不拆,Rust 收口语义”的设计一致。
|
|||
|
|
|
|||
|
|
## 建议优先级
|
|||
|
|
|
|||
|
|
### P0:修正 Rust page write route 的 artifact 一致性
|
|||
|
|
|
|||
|
|
目标:让当前 3000 主入口的 `/api/documents/title`、`/api/documents/options` 至少在 side effect 上与 Next `page-write-command-adapter` 保持一致。
|
|||
|
|
|
|||
|
|
建议:
|
|||
|
|
|
|||
|
|
- 将 `rust/crates/mnote-web/src/routes/documents.rs` 中 title/options 的 `execute_runtime_command_via_convex` 改为 artifact 版本,或统一复用 tree/page command route。
|
|||
|
|
- 对标题更新补最小回归:更新标题后 `bridgeLogs:listWorkspaceOverview` 能看到对应 command/domain event,`/api/tree/events` 能输出 delta 或 resync。
|
|||
|
|
- 对 artifact 写失败的策略重新定级:当前 `rust/crates/mnote-web/src/transport/convex.rs:759` 到 `:770` 是主 mutation 成功、artifact 失败仍返回成功;对 tree-relevant command 至少应打可观测错误并触发保守 resync。
|
|||
|
|
|
|||
|
|
### P1:收口 Page Aggregate provenance
|
|||
|
|
|
|||
|
|
目标:避免把 `meta/content join` 误标为已完成的 kernel-native projection。
|
|||
|
|
|
|||
|
|
建议:
|
|||
|
|
|
|||
|
|
- 明确 `PageAggregateSource::KernelProjection` 与 `CompatMetaContentJoin` 的使用边界。
|
|||
|
|
- 如果 `build_page_aggregate_snapshot` 仍通过 `load_document_meta_result + load_document_content_result` 构建,应在返回 source 或 header 中反映真实来源。
|
|||
|
|
- 如果目标是 kernel-native projection,则补正式 kernel/query 数据路径,避免 route 层长期手工拼装。
|
|||
|
|
|
|||
|
|
### P1:优化 tree stream 的 Convex 查询模型
|
|||
|
|
|
|||
|
|
目标:让 `/api/tree/events` 可以长期承载主链,而不是随着日志增长退化。
|
|||
|
|
|
|||
|
|
建议:
|
|||
|
|
|
|||
|
|
- 为 `command_logs`、`domain_events` 增加按 `workspace_id + created_at/id` 的查询索引和 cursor 查询。
|
|||
|
|
- `listWorkspaceOverview` 不再 `collect()` 全量 workspace 日志后内存分页。
|
|||
|
|
- 明确 polling-backed SSE 是过渡实现,还是长期 realtime transport;若长期使用,应补连接数、日志量、延迟上限的 smoke/bench。
|
|||
|
|
|
|||
|
|
### P2:清理或标注 stale mapping
|
|||
|
|
|
|||
|
|
目标:降低后续 worker 误用 `page.aggregate.get -> documents:getPageAggregate` 的风险。
|
|||
|
|
|
|||
|
|
建议:
|
|||
|
|
|
|||
|
|
- 若 `documents:getPageAggregate` 不计划实现,移除或注释 `storage-convex-bridge` 中的映射。
|
|||
|
|
- 若计划实现,补 Convex query 与最小测试,并让 mnote-web Page Aggregate route 直接消费它或说明为什么不消费。
|
|||
|
|
|
|||
|
|
### P2:保留 Next compat 退场边界,但避免双实现继续发散
|
|||
|
|
|
|||
|
|
目标:Next route 继续作为 legacy/adapter 参考时,不与 Rust Web 主入口形成不同 side effects。
|
|||
|
|
|
|||
|
|
建议:
|
|||
|
|
|
|||
|
|
- 对 `/api/documents/title/options/save` 明确 owner:Rust Web 主路径与 Next legacy 路径只能有一份 canonical side-effect 规则。
|
|||
|
|
- 对 `wolai-frontend/src/lib/documents/page-aggregate-builder.ts` 保留测试/adapter 标签,避免被重新接回 runtime 主链。
|
|||
|
|
|
|||
|
|
## 修改的文件路径
|
|||
|
|
|
|||
|
|
- `design/10-review/03-convex-realtime-storage-review.md`
|