Files
mnote/design/10-review/done/03-convex-realtime-storage-review.md
T
lix-2026 d1d19f755c docs(design): 归档 10-review 到 done
将 10-review 的 01-06 文档统一迁入 done/,根目录 README 收缩为索引。

补充各历史审查稿的归档说明,并把 06 的活跃引用改到 done/ 下,避免后续重复把已完成审查当成当前待办。
2026-05-14 16:30:13 +08:00

16 KiB
Raw Blame History

Convex / Realtime / Storage 实现偏差审查

执行状态:已归档到 design/10-review/done/。本文保留为历史审查快照,当前活跃口径以 ./06-execution-checklist-and-acceptance.md 为准。

范围

本报告审查 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 中拼 projectionsource 标识存在“看起来比真实实现更 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 中拼 projectionsource=KernelProjection 容易掩盖底层仍是兼容 join。 rust/crates/mnote-web/src/routes/web_shell.rs:2282:2291:2301rust/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:802rust/crates/mnote-web/src/routes/command_support.rs:65;对比 rust/crates/mnote-web/src/routes/tree.rs:6529wolai-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:60rust/crates/mnote-web/src/routes/stream_support.rs:610
F4 风险 中高 bridgeLogs:listWorkspaceOverview 会按 workspace collect 全量 command_logsdomain_events 后在内存中过滤、排序、分页;SSE 轮询叠加后,历史日志增长会放大 Convex 读压力。 wolai-frontend/convex/bridgeLogs.ts:283:287:292:301:305
F5 方向变化/文档滞后 storage-convex-bridgepage.aggregate.get 映射到 documents:getPageAggregate,但 Convex documents.ts 只发现 getMeta / getContent,未发现 getPageAggregate export;当前真实 Page Aggregate 路线已绕到 Rust adapter。 rust/crates/storage-convex-bridge/src/mapping.rs:101wolai-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:17wolai-frontend/src/app/api/mnote-web/stream/route.ts:9wolai-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 snapshotwolai-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:2315meta + content 作为数据传给 page.aggregate.get
  • rust/crates/bridge-runtime/src/lib.rs:5291:5312build_page_aggregate_projection_result 明确从 data.metadata.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:73execute_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:143EventSource 监听 snapshot/delta/resync

但服务端 change detection 是轮询:

  • rust/crates/mnote-web/src/routes/sse.rs:25:26 读取 max_pollspoll_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_overviewresolve_stream_change
  • rust/crates/mnote-web/src/routes/stream_support.rs:610:625load_stream_overview 通过 bridge.workspace.overview 查询 Convex。

这与“Rust Web 提供正式实时 transport”一致,但还不是“基于 Convex realtime subscription 的 push stream”。短期可接受为过渡实现,长期如果继续承载主链,需要明确性能和延迟边界。

4. bridgeLogs overview 查询存在规模风险

wolai-frontend/convex/bridgeLogs.tslistWorkspaceOverview

  • :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

"page.aggregate.get" => "documents:getPageAggregate"

但当前 wolai-frontend/convex/documents.ts 中只发现:

  • getMetawolai-frontend/convex/documents.ts:684
  • getContentwolai-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:91documents 表继续承接页面结构、正文、页面设置、统计字段。
  • wolai-frontend/convex/schema.ts:121:162media_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::KernelProjectionCompatMetaContentJoin 的使用边界。
  • 如果 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_logsdomain_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 明确 ownerRust Web 主路径与 Next legacy 路径只能有一份 canonical side-effect 规则。
  • wolai-frontend/src/lib/documents/page-aggregate-builder.ts 保留测试/adapter 标签,避免被重新接回 runtime 主链。

修改的文件路径

  • design/10-review/done/03-convex-realtime-storage-review.md