184 lines
13 KiB
Markdown
184 lines
13 KiB
Markdown
# 4-25 [done][bug] 树命令新建/删除后强制刷新导致卡顿 v1
|
||||
|
|
|
|||
|
|
> 更新时间:2026-05-14
|
|||
|
|
>
|
|||
|
|
> 分类归属:
|
|||
|
|
> - `04-tree-domain/done`
|
|||
|
|
> - 关联缺陷:`bugs/05-editor-mainline/done/5-11-mindmap-ghost-assets-and-tree-command-latency-v1.md`
|
|||
|
|
>
|
|||
|
|
> 用户反馈:
|
|||
|
|
> - “删除页面和新建页面都很慢,不知道为什么这么卡。”
|
|||
|
|
|
|||
|
|
## 1. 问题定义
|
|||
|
|
|
|||
|
|
当前页面树/文件树的新建页面、删除页面等命令,在服务端命令成功后仍会触发整页刷新或完整树刷新。用户感知是:新建和删除并不是局部更新,而是卡一下、等整套页面壳或树壳重新加载。
|
|||
|
|
|
|||
|
|
这属于 `tree command -> projection update -> UI apply` 链路问题,应归到 `04-tree-domain`,而不是只作为编辑器 UI 问题处理。
|
|||
|
|
|
|||
|
|
## 2. 真实现象
|
|||
|
|
|
|||
|
|
用户反馈:
|
|||
|
|
|
|||
|
|
1. 新建页面慢。
|
|||
|
|
2. 删除页面慢。
|
|||
|
|
3. 卡顿与同轮 mindmap 文件树重复增长一起出现,怀疑树刷新链路被频繁触发。
|
|||
|
|
|
|||
|
|
期望结果:
|
|||
|
|
|
|||
|
|
1. `tree.node.create` 成功后,树 UI 局部插入新节点并进入重命名/导航状态。
|
|||
|
|
2. `tree.node.archive` 成功后,树 UI 局部移除节点或移动到回收站 projection。
|
|||
|
|
3. 只有 delta 无法应用、projection 缺失、SSE 断线或数据不一致时,才进入 resync/reload fallback。
|
|||
|
|
4. 普通新建/删除不应默认整页 reload。
|
|||
|
|
|
|||
|
|
## 3. 证据
|
|||
|
|
|
|||
|
|
### 3.1 Rust tree shell 有硬刷新路径
|
|||
|
|
|
|||
|
|
`rust/crates/mnote-web/src/routes/tree.rs:2976` 到 `:2986`:
|
|||
|
|
|
|||
|
|
- `scheduleRefresh()` 在 80ms 后执行 `window.location.reload()`。
|
|||
|
|
- 如果带 `renameRowId`,则通过 `window.location.assign(url.toString())` 重新加载。
|
|||
|
|
|
|||
|
|
调用点:
|
|||
|
|
|
|||
|
|
- 文件树删除:`rust/crates/mnote-web/src/routes/tree.rs:2791` 到 `:2824`
|
|||
|
|
- 新建页面:`rust/crates/mnote-web/src/routes/tree.rs:4309` 到 `:4340`
|
|||
|
|
- 重命名:`rust/crates/mnote-web/src/routes/tree.rs:4373` 到 `:4406`
|
|||
|
|
- 移动:`rust/crates/mnote-web/src/routes/tree.rs:4448` 到 `:4502`
|
|||
|
|
|
|||
|
|
这条路径会让 tree shell 命令体验明显慢于纯 delta / reducer 更新。
|
|||
|
|
|
|||
|
|
### 3.2 主页面已有 tree live controller,但命令后仍刷新
|
|||
|
|
|
|||
|
|
主页面已经监听 tree live:
|
|||
|
|
|
|||
|
|
- `rust/crates/mnote-web/src/ssr/pages/layout.rs:3881` 到 `:3917` 处理 `tree:snapshot` / `tree:delta` / `tree:resync`
|
|||
|
|
- `rust/crates/mnote-web/src/ssr/pages/layout.rs:3939` 到 `:4050` 启动 `/api/tree/events` EventSource
|
|||
|
|
|
|||
|
|
也就是说,命令完成后理论上可以由 delta/resync 更新 projection;当前硬 reload 与 live projection 机制重复。
|
|||
|
|
|
|||
|
|
### 3.3 命令本身已有 delta hint
|
|||
|
|
|
|||
|
|
Bridge runtime 对树命令生成 stream delta hint:
|
|||
|
|
|
|||
|
|
- `tree.node.create`:`rust/crates/bridge-runtime/src/lib.rs:9545` 到 `:9592`
|
|||
|
|
- `tree.node.archive`:`rust/crates/bridge-runtime/src/lib.rs:9655` 到 `:9702`
|
|||
|
|
|
|||
|
|
当前前端没有把这些结果作为默认局部更新来源,而是在 tree shell 中继续 reload。
|
|||
|
|
|
|||
|
|
### 3.4 React Sidebar 路径也存在重刷链
|
|||
|
|
|
|||
|
|
子代理只读调查发现 React Sidebar 路径也存在重刷:
|
|||
|
|
|
|||
|
|
- 新建页面:`wolai-frontend/src/components/sidebar/sidebar.tsx:1799`、`:1847` 附近在 command 后 `await refreshTree()` 再跳转。
|
|||
|
|
- 删除页面:`wolai-frontend/src/components/sidebar/sidebar.tsx:2148` 附近删除后等待 command、`refreshTree()`、广播文档变化/跳转。
|
|||
|
|
- Next API 的 `api/tree/commands` create/delete 还会执行 workspace/default scaffold、bridge mutation、artifact 记录等多段流程。
|
|||
|
|
|
|||
|
|
这说明卡顿不只来自单个 reload,而是命令后刷新链路偏重。
|
|||
|
|
|
|||
|
|
### 3.5 tree stream overview 查询曾经全量扫描 workspace 日志
|
|||
|
|
|
|||
|
|
`/api/tree/events` 的 live 轮询依赖 `bridgeLogs:listWorkspaceOverview` 读取最新 command/domain event 窗口。2026-05-13 复核发现,该查询原先对 `command_logs` 和 `domain_events` 都是按 workspace 全量 `.collect()` 后再在内存中过滤、排序和分页;日志量增长后,这会让每轮 SSE 轮询成本随历史日志线性增长。
|
|||
|
|
|
|||
|
|
本轮已完成代码侧收口:
|
|||
|
|
|
|||
|
|
- `command_logs` 增加 `workspace_id + created_at + id` 及 status/page/block 常用过滤索引。
|
|||
|
|
- `domain_events` 增加 `workspace_id + created_at + id` 及 status/aggregate 常用过滤索引。
|
|||
|
|
- `listWorkspaceOverview` 改为按索引读取有界窗口,不再全量 collect 后分页。
|
|||
|
|
|
|||
|
|
这只能降低 tree live polling 的背景成本,并不等于新建/删除的强刷新问题已经修完。React Sidebar 的 `refreshTree()` 和 Rust tree shell 的 `scheduleRefresh()` 仍需单独收口。
|
|||
|
|
|
|||
|
|
### 3.6 2026-05-14 tree live 局部 apply 进展
|
|||
|
|
|
|||
|
|
本轮已完成主页面 Rust 3000 下的部分局部 apply 收口:
|
|||
|
|
|
|||
|
|
- `rust/crates/mnote-web/src/transport/convex.rs`:tree create / rename / archive / restore / move 及 compat documents mutation 发往 legacy Convex mutation 前剥离 artifact-only 字段,修复 `documents:softDelete` 因 `streamDeltaHint` / `domainEventPlan` 等额外字段触发 validator 502 的问题。
|
|||
|
|
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`:`tree:delta` 的 `move_document` 会局部更新页面树与 File Tree 行的 `data-parent-id`;`remove_document` 会局部移除页面树与 File Tree 行。
|
|||
|
|
- 已通过:`cargo test -p mnote-web convex_command_args_strips -- --nocapture`。
|
|||
|
|
- 已通过:`cargo test -p mnote-web sidebar_tree_runtime -- --nocapture`。
|
|||
|
|
- 已通过:`node scripts/task177-tree-move-archive-live-smoke.js`,覆盖 move 后页面树 / File Tree 行父节点更新、archive 后两棵树局部移除,以及文档页头保持稳定。
|
|||
|
|
|
|||
|
|
这说明 move / archive 的 Rust 3000 主页面 live reducer 已有可验证进展;当时普通新建/删除、React Sidebar `refreshTree()` 与耗时阈值仍待后续章节继续收口。后续 3.7-3.9 已补齐这些流转条件。
|
|||
|
|
|
|||
|
|
### 3.7 2026-05-14 Rust tree shell create / delete no-reload 验收
|
|||
|
|
|
|||
|
|
本轮已完成 Rust `/tree` debug shell 的普通新建 / 删除强刷新收口:
|
|||
|
|
|
|||
|
|
- `rust/crates/mnote-web/src/routes/tree.rs`:新增 `addTreeItemLocally()` / `applyCreatedDocumentLocally()`,`convex_workspace` create 成功后直接插入本地树项并进入 inline rename;只有本地 apply 失败时才 fallback 到 `scheduleRefresh()`。
|
|||
|
|
- `rust/crates/mnote-web/src/routes/tree.rs`:新增 `removeTreeItemEverywhere()` / `applyRemovedDocumentLocally()`,`convex_workspace` delete 成功后从本地 projection 移除对应页面及子项;只有删除后仍能在本地 row map 中看到目标行时才 fallback `scheduleRefresh()`。
|
|||
|
|
- `scripts/task179-tree-create-delete-no-reload-smoke.js`:真实浏览器覆盖 `/tree?mode=filetree` 的 create + delete,以及 `/tree?mode=page` 的 create;断言操作后 URL 不变、对应 mode 内无新增顶层 navigation。
|
|||
|
|
- 已通过:`cargo test -p mnote-web tree_shell_returns_interactive_html_document -- --nocapture`。
|
|||
|
|
- 已通过:`cargo test -p mnote-web tree_shell -- --nocapture`。
|
|||
|
|
- 已通过:`node scripts/task179-tree-create-delete-no-reload-smoke.js`,最新结果 `ok=true`;`filetree.createMs=120`、`filetree.deleteMs=178`、`page.createMs=120`、两个 mode 的 `navigationEvents.length=0`。
|
|||
|
|
|
|||
|
|
### 3.8 2026-05-14 React Sidebar 直接 create / delete 收口进展
|
|||
|
|
|
|||
|
|
React Sidebar 的直接新建 / 删除路径已完成一层去阻塞:
|
|||
|
|
|
|||
|
|
- `wolai-frontend/src/components/sidebar/sidebar.tsx`:`handleCreate()` 继续使用 `createDocumentCommand()` 结果本地 `setTree()` 插入节点并导航,但不再同步 `await refreshTree()`。
|
|||
|
|
- `wolai-frontend/src/components/sidebar/sidebar.tsx`:新增 `removeDocumentsFromTree()`,`handleDelete()` 与 `handleDeleteResourceSelection()` 在 delete command 成功后本地移除页面节点,不再同步等待整树 refetch;附件删除仍保留 asset 专用刷新链。
|
|||
|
|
- `wolai-frontend/src/components/sidebar/sidebar-delete-preflight-source.test.ts`:新增源级回归断言,防止页面 create/delete 成功路径重新出现同步 `await refreshTree()`。
|
|||
|
|
- 已按 TDD 验证:新增测试先失败,修改后通过。
|
|||
|
|
- 已通过:`pnpm test -- src/components/sidebar/sidebar-delete-preflight-source.test.ts`(Vitest 实际执行 112 个测试文件、459 个测试)。
|
|||
|
|
- 已通过:`pnpm exec eslint src/components/sidebar/sidebar.tsx src/components/sidebar/sidebar-delete-preflight-source.test.ts`,无 error;`sidebar.tsx` 保留既有 warning。
|
|||
|
|
|
|||
|
|
### 3.9 2026-05-14 Rust-family React shell mutation 本地 apply 收口
|
|||
|
|
|
|||
|
|
Rust-family React shell 的 host mutation 回调已从“命令成功后默认整树刷新”改为可局部 apply:
|
|||
|
|
|
|||
|
|
- `wolai-frontend/src/components/sidebar/tree-shell-dom-host.tsx`:`tree.node.created` / `tree.node.renamed` / `tree.subtree.moved` 成功后把 command result 中的 `documentId`、`parentId`、`title`、`sortOrder`、`workspaceId`、`execution` 传回 Sidebar。
|
|||
|
|
- `wolai-frontend/src/components/sidebar/tree-shell-iframe-host.tsx`:legacy iframe bridge 同步转发 mutation payload 中的局部 apply 字段,保持兼容。
|
|||
|
|
- `wolai-frontend/src/components/sidebar/sidebar.tsx`:`handleTreeShellMutation` 对 create / rename / move 分别本地插入、重命名、移动页面树;只有未知事件或缺少必要字段时保留 `refreshTree()` 兜底。
|
|||
|
|
- `wolai-frontend/src/components/sidebar/sidebar-delete-preflight-source.test.ts`:新增 RED -> GREEN 源级回归断言,防止 `onTreeMutation -> refreshTree()` 重新退化成默认整树 refetch。
|
|||
|
|
- 已通过:`pnpm test -- src/components/sidebar/sidebar-delete-preflight-source.test.ts`(Vitest 实际执行 112 个测试文件、460 个测试)。
|
|||
|
|
- 已通过:`pnpm exec eslint src/components/sidebar/sidebar.tsx src/components/sidebar/sidebar-delete-preflight-source.test.ts src/components/sidebar/tree-shell-host.tsx src/components/sidebar/tree-shell-surface.tsx src/components/sidebar/tree-shell-dom-host.tsx src/components/sidebar/tree-shell-iframe-host.tsx`,无 error;保留既有 warning。
|
|||
|
|
|
|||
|
|
## 4. 当前判断
|
|||
|
|
|
|||
|
|
当前慢的核心不是“Convex 一定慢”,而是树命令执行后缺少轻量本地 apply:
|
|||
|
|
|
|||
|
|
1. 命令 result / delta hint 已经具备局部更新信息。
|
|||
|
|
2. UI 仍经常走 `refreshTree()`、`window.location.reload()` 或 full snapshot。
|
|||
|
|
3. `resync_required` 类事件会触发完整 workspace snapshot,结构性变化越多,越容易造成卡顿。
|
|||
|
|
4. tree stream overview 全量日志扫描已在代码侧改为有界窗口查询,并已有 `task123` / `task120` / `task177` 证明 Rust 3000 下可输出并消费相关 live 事件。
|
|||
|
|
5. move / archive 已具备局部 DOM reducer 证据。
|
|||
|
|
6. Rust `/tree` debug shell 的 `convex_workspace` 普通新建 / 删除已经通过无 reload 与耗时 smoke。
|
|||
|
|
7. React Sidebar 直接 create/delete 成功路径已改成本地更新,不再同步等待 `refreshTree()`。
|
|||
|
|
8. Rust-family React shell 的 create / rename / move host mutation 回调已改成本地 apply,不再默认触发整树 refetch;未知事件或缺字段才保留刷新兜底。
|
|||
|
|
|
|||
|
|
## 5. 建议验证
|
|||
|
|
|
|||
|
|
修复前先补一条计时 smoke:
|
|||
|
|
|
|||
|
|
1. 登录真实测试账号。
|
|||
|
|
2. 新建 `TEST-TREE-LATENCY-<timestamp>` 页面。
|
|||
|
|
3. 记录 `POST /api/tree/commands` 响应耗时。
|
|||
|
|
4. 记录下一次 `tree:delta` / `tree:resync` 到达耗时。
|
|||
|
|
5. 断言过程中是否发生 `window.location.reload()` 或顶层 navigation。
|
|||
|
|
6. 删除该测试页面,重复同样计时。
|
|||
|
|
7. 输出新建/删除从点击到 DOM 稳定的总耗时。
|
|||
|
|
|
|||
|
|
## 6. 建议修复方向
|
|||
|
|
|
|||
|
|
1. `tree.node.create` 成功后直接用 command result 插入本地节点,并只在后台等待 live delta 校准。
|
|||
|
|
2. `tree.node.archive` 成功后直接从当前 projection 移除节点,并只在后台等待 live delta 校准。
|
|||
|
|
3. `scheduleRefresh()` 改成显式 fallback,只有 reducer 无法应用时才调用。
|
|||
|
|
4. React Sidebar 的 `refreshTree()` 应去重、节流,并避免与 SSE/full snapshot 同时触发。
|
|||
|
|
5. smoke 增加无 reload 断言和耗时阈值。
|
|||
|
|
6. tree stream 查询模型已完成第一步优化;后续若 polling-backed SSE 继续作为长期实现,还需要补连接数、日志量、延迟边界测试。
|
|||
|
|
|
|||
|
|
## 7. 流转条件
|
|||
|
|
|
|||
|
|
当前状态:`done`
|
|||
|
|
|
|||
|
|
Rust tree shell 的 create / delete 强刷新子项已关闭;React Sidebar 直接 create/delete 同步刷新等待已关闭;Rust-family React shell 的 create / rename / move host mutation 回调已改为本地 apply,不再默认整树 refetch。
|
|||
|
|
|
|||
|
|
只有满足以下条件后才能移动到 `bugs/04-tree-domain/done/`:
|
|||
|
|
|
|||
|
|
1. [x] 新建页面普通路径不再默认整页 reload。Rust `/tree` `convex_workspace` 的 File Tree 与 Page Tree create 已由 `task179` 验证。
|
|||
|
|
2. [x] 删除页面普通路径不再默认整页 reload。Rust `/tree` `convex_workspace` 的 File Tree delete 已由 `task179` 验证。
|
|||
|
|
3. [x] command result / tree delta 能局部更新页面树与文件树 projection。create 走 `applyCreatedDocumentLocally()`;delete 走 `applyRemovedDocumentLocally()`;move/archive 由 `task177` 验证主页面与 File Tree DOM reducer。
|
|||
|
|
4. [x] fallback reload 只在明确异常条件下触发,并有可观测标记。当前 create/delete 仅在本地 apply 失败或目标行未移除时 fallback `scheduleRefresh()`。
|
|||
|
|
5. [x] smoke 记录新建/删除耗时,并通过无 reload 断言。`task179` 最新记录 `filetree.createMs=120`、`filetree.deleteMs=178`、`page.createMs=120`,对应 mode 内 navigation 为 0。
|
|||
|
|
6. [x] Rust-family React shell 的 `onTreeMutation -> refreshTree()` 不再在 create / rename / move 成功后默认整树 refetch。`handleTreeShellMutation` 已按 create / rename / move 本地 apply,未知事件或缺字段才刷新兜底,并由 `sidebar-delete-preflight-source.test.ts` 防回归。
|