Files
mnote/bugs/04-tree-domain/done/4-26-sidebar-filetree-projection-refresh-stalled-after-tree-asset-mutations-v1.md
T

240 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.
# 4-26 [done][bug] Sidebar File Tree projection 在树/资产变更后不刷新 v1
> 更新时间:2026-05-14
>
> 分类归属:
> - `04-tree-domain/done`
> - 关联缺陷:`bugs/04-tree-domain/done/4-25-tree-command-create-delete-reload-latency-v1.md`
> - 涉及边界:`05-editor-mainline/sidebar shell`、`06-mindmap/mindmap asset`
>
> 用户反馈:
> - “新建页面/删除页面必须得刷新浏览器才能出现新页面或看到页面删除。”
> - “包括新建思维导图,也是得刷新才能看到。”
> - “有点像是全局事件被禁用了或类似的。”
## 1. 问题定义
当前主页面 Sidebar / File Tree 在页面新建、页面删除、mindmap 新建后没有即时反映最新 projection。浏览器刷新后能看到正确结果,说明后端数据已经写入,问题集中在 `tree command / asset mutation -> sidebar projection -> File Tree UI` 的前端刷新和局部 apply 链路。
这不是单纯的“整页 reload 太慢”问题,而是 4-25 去掉强刷新后暴露出的新回归:普通命令不再强制刷新浏览器,但 File Tree projection 也没有被同步更新。
## 2. 真实现象
已确认现象:
1. 新建页面后,当前页面壳里的 File Tree / Sidebar 不能立即看到新页面。
2. 删除页面后,当前页面壳里的 File Tree / Sidebar 仍可能保留旧页面行。
3. 新建 mindmap 后,当前页面壳里的 File Tree 不能立即看到 `mindmap-<id>.json` 资源行。
4. 手动刷新浏览器后,页面或 mindmap 资源会按后端最新数据出现/消失。
期望结果:
1. `tree.node.create` 成功后,Page Tree 与 File Tree 都能在当前页面壳中即时出现新页面。
2. `tree.node.archive` / 删除成功后,Page Tree 与 File Tree 都能在当前页面壳中即时移除目标页面及子树。
3. `mindmaps.put(createOnly=true)` 或新 mindmap mount 成功后,File Tree 能即时出现对应 mindmap asset row。
4. 不依赖浏览器刷新作为普通成功路径;刷新只能作为 projection 无法局部 apply 时的显式 fallback。
## 3. 确认证据
### 3.1 主 Sidebar 的数据源仍来自 preferred snapshot
`wolai-frontend/src/components/sidebar/sidebar.tsx:201``:209`
- Sidebar 通过 `usePreferredSidebarSnapshot()``initialData``sidebarQuery.data``treeStream.data` 之间选择数据源。
- 后续 File Tree projection 仍读取这个 `sidebarData`
### 3.2 Page Tree 有本地 setTree,但 File Tree projection 没有同步局部更新
`wolai-frontend/src/components/sidebar/sidebar.tsx:305``:316`
- `sidebarData.kernelSidebarTree` 变化时会同步到本地 `tree` state。
`wolai-frontend/src/components/sidebar/sidebar.tsx:1900``:1957`
- `handleCreate()``createDocumentCommand()` 成功后只对页面树做本地 `setTree()`,然后 `router.push()`
- 该路径没有同步更新 `sidebarData.kernelFileTreeProjection.items`,也没有广播 `emitDocumentsChanged()`
`wolai-frontend/src/components/sidebar/sidebar.tsx:2239``:2249`
- `handleDelete()` 成功后调用 `removeDocumentsFromTree()``emitDocumentsChanged()`
- 该本地删除只影响 Page Tree 的 `tree` state,不会同步移除 File Tree projection items。
### 3.3 File Tree shell 直接依赖 stale projection items
`wolai-frontend/src/components/sidebar/sidebar.tsx:678``:687`
- 非搜索状态下 `effectiveResourceTreeShellItems` 直接等于 `sidebarData.kernelFileTreeProjection.items`
- 如果 preferred snapshot 没有变化,File Tree 行模型就不会包含新页面、删除结果或新 mindmap asset。
`wolai-frontend/src/components/sidebar/sidebar.tsx:695``:702`
- `buildFileTreeShellRowById()` 基于 `effectiveResourceTreeShellItems` 构建 row map。
- 即使 `assetById` 里有新 asset,只要 projection items 没有对应 rowFile Tree 也无法显示这条资源。
### 3.4 全局事件监听存在,但 refetch 在 Convex live 模式下是空操作
`wolai-frontend/src/components/sidebar/sidebar-events.ts:17``:28`
- `wolai:documents-changed` / `wolai:assets-changed` 会触发 `sidebarRefetch()`
- 这说明全局事件监听并非完全禁用。
`wolai-frontend/src/hooks/use-sidebar-data.ts:93``:101`
- `sidebarQuery.refetch()` 在 Convex live subscription 模式下转调 `convexSidebar.refetch()`
`wolai-frontend/src/hooks/use-convex-sidebar-data.ts:90``:96`
- `hasLiveSubscription = shouldFetch`
- `refetch` 明确是空操作,注释为“Convex 会自动同步数据”。
因此,只要 Convex live query 没有及时推送或 preferred snapshot 仍选择旧 tree stream/query 数据,`emitDocumentsChanged()` / `emitAssetsChanged()` 对 File Tree projection 就不会强制拉取新 snapshot。
### 3.5 mindmap 新建只乐观补 asset 列表,不补 File Tree projection row
`wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx:1527``:1534``:1591``:1621``:1636``:1647`
- mindmap 保存、初始同步、实例就绪都会调用 `emitAssetsChanged(docId, asset)`
`wolai-frontend/src/components/sidebar/sidebar.tsx:506``:528`
- Sidebar 收到带 `asset` 的事件后会把 mindmap 加入 `mindmapAssets` 本地 state。
- 但 File Tree 的 row 输入仍是 `sidebarData.kernelFileTreeProjection.items`,没有把新 mindmap asset materialize 成 projection item。
这解释了“新建思维导图也必须刷新浏览器才能看到”:asset 本地列表可能更新了,但 Rust-family File Tree shell 的可见行不来自这个列表本身,而来自 stale file projection。
## 4. 当前判断
该问题已通过代码路径确认,属于事件/刷新链路和 projection 局部 apply 缺口:
1. 事件监听存在,不是全局事件完全被禁用。
2. 文档变更事件在 create 路径缺失,在 delete 路径存在但只触发空 refetch。
3. asset 变更事件能把 mindmap 加进本地 asset state,但不能生成 File Tree projection row。
4. File Tree shell 以 `kernelFileTreeProjection.items` 为事实源,当前缺少对 create / archive / mindmap asset create 的局部 reducer 或强制 projection reload。
5. 浏览器刷新后能看到正确结果,进一步说明后端写入成功,前端 projection 没有及时刷新。
## 5. 建议修复方向
1.`tree.node.create` / `tree.node.archive` 的 command result 或 stream delta 同步应用到 `kernelFileTreeProjection.items`,不要只更新 Page Tree state。
2. `emitDocumentsChanged()` 应能触发一个真实的 projection refresh:在 Convex live 模式下不能继续是空操作,至少需要 invalidate/fetch `/api/sidebar``/api/tree/projections/file`
3. `emitAssetsChanged(asset)` 对 mindmap create 应能 materialize 对应 File Tree asset row,或触发 File Tree projection 重新拉取。
4. `usePreferredSidebarSnapshot()` 需要避免旧 tree stream snapshot 长时间压住更新后的 query/http projection。
5. 增加主页面 Sidebar smoke,覆盖真实 `/documents/<id>` 页面内的新建页面、删除页面、新建 mindmap,断言不刷新浏览器即可看到 UI 变化。
## 6. 修复记录
2026-05-14 已完成修复:
1. `wolai-frontend/src/components/sidebar/sidebar-local-projection.ts`
- 新增 Sidebar 本地 documents/file-tree projection helper。
- `tree.node.create` 成功后可将新页面 upsert 到本地 documents,并重建 File Tree projection。
- `tree.node.archive` / 删除成功后可从本地 documents 中移除目标页面及子树,并重建 File Tree projection。
- mindmap asset 进入本地 `mindmapAssets` 后,可 materialize 成 File Tree asset row。
2. `wolai-frontend/src/components/sidebar/sidebar.tsx`
- 新增本地 `documents` state,并随后端 sidebar snapshot 校准。
- File Tree 非搜索状态改为消费 `localFileTreeProjection.items`,不再只依赖 stale `sidebarData.kernelFileTreeProjection.items`
- `handleCreate()` 成功后同步 upsert documents、广播 `emitDocumentsChanged(nextNode.id)`Page Tree 与 File Tree 都可即时反映。
- `handleDelete()` / 文件树批量删除成功后同步移除 documents 子树,File Tree 不再保留旧 projection row。
- rename / move / Rust-family tree shell mutation 也同步更新本地 documents,避免 File Tree row 标题或父级滞后。
3. `wolai-frontend/src/hooks/use-sidebar-data.ts`
- Convex live 模式下的显式 `refetch()` 不再只是空操作;现在会拉取 `/api/sidebar?workspaceId=<id>` 作为手动 snapshot 补偿。
- 手动 snapshot 绑定 workspaceId,避免跨工作区污染。
4. `wolai-frontend/src/components/sidebar/sidebar-sync.ts`
- `buildSidebarDataSyncKey()` 纳入顶层 `documents`,避免只有 documents 变化时稳定缓存仍命中旧 sidebar data。
5. `rust/crates/mnote-web/src/ssr/pages/layout.rs`
- 追加修复 Rust 3000 主文档壳路径:`tree:local-command` 现在会在 `create / purge / delete / archive` 命令成功后本地 apply Sidebar DOM。
- `create` 成功后立即插入 Page Tree 行,以及 File Tree 的 `doc:<id>` / `index:<id>` 行。
- `purge/delete/archive` 成功后立即从 Page Tree 与 File Tree 移除目标页面行。
- 命令成功后清理按钮 pending 状态,避免无整页刷新时按钮残留禁用。
6. `rust/crates/mnote-web/src/ssr/pages/layout.rs`
- 追加修复 Rust 3000 页面内 mindmap 创建路径:主文档壳现在同时监听 `wolai:assets-changed`,并观察同页 `/api/mindmap/<doc>/<mindmap>` POST 成功请求。
- mindmap 创建成功后立即把 `mindmap-<id>.json` 作为 File Tree asset row 插入当前页面下,避免等刷新后 projection 才出现。
- 这覆盖 Leptos/Rust island 未触发 React `emitAssetsChanged()` 的路径。
2026-05-14 回归复现与补修:
- 用户复测发现 3000 主入口仍需刷新。重新用真实浏览器验证后确认:之前 `3001` Next 路径和 `/tree` debug 路由的验证不能代表当前 `mnote-web` 3000 主文档壳。
- 复现证据:旧 3000 进程为 `/mnt/Data1T/mnote/rust/target/debug/mnote-web (deleted)`;点击“新建页面”后接口返回成功并跳转到 `/documents/tree_1778714360642_10?...`,但 5 秒后 `#sidebar-tree-root` 仍不包含该 document id。
- 当前源码 3002 在补修前也复现同类问题:新建后跳转到 `/documents/tree_1778714400752_12?...`,但 Page Tree DOM 不包含当前 document id。
- 补修后已重启 3000,并在 3000 主入口完成新建/删除无刷新 smoke。
- 用户继续复测确认页面新建/删除已恢复,但页面内新建思维导图仍需刷新。真实 smoke 复核后确认:3000 页面内导图创建会走 `/api/mindmap/<doc>/<mindmap>` 写入链路,主文档壳原先没有把该成功写入同步 materialize 到 File Tree asset row。
- 补修后已在 3000 主入口通过 slash 菜单真实新建思维导图,并确认 File Tree 无刷新出现 `asset:<mindmapId>` 行。
## 7. 验收标准
只有满足以下条件后才能移动到 `bugs/04-tree-domain/done/`
1. [x] 在 `http://127.0.0.1:3000/documents/<id>` 主页面壳中新建页面后,不刷新浏览器即可在 Page Tree 与 File Tree 中看到新页面。代码证据:React Sidebar 路径由 `handleCreate()` 本地 upsert documentsRust 3000 主文档壳由 `tree:local-command` 本地插入 Page Tree 行与 File Tree `doc:<id>` / `index:<id>` 行;单测覆盖:`sidebar-local-projection.test.ts`Rust runtime 覆盖:`cargo test -p mnote-web sidebar_tree_runtime -- --nocapture`
2. [x] 在主页面壳中删除页面后,不刷新浏览器即可在 Page Tree 与 File Tree 中看到目标页面消失。代码证据:React Sidebar 路径由 `handleDelete()` / `handleDeleteResourceSelection()` 同步 `removeSidebarDocumentRecords()`Rust 3000 主文档壳由 `tree:local-command``purge/delete/archive` 成功后调用 remove delta 本地移除;单测覆盖:`sidebar-local-projection.test.ts``sidebar-delete-preflight-source.test.ts`Rust runtime 覆盖:`cargo test -p mnote-web sidebar_tree_runtime -- --nocapture`
3. [x] 在主页面壳中新建 mindmap 后,不刷新浏览器即可在 File Tree 中看到对应 `mindmap-<id>.json` 行。代码证据:React Sidebar 路径由 `emitAssetsChanged(asset)` 进入本地 `mindmapAssets` 并重建 asset rowRust 3000 主文档壳由 `wolai:assets-changed` listener 和 `/api/mindmap/<doc>/<mindmap>` POST observer 本地插入 File Tree asset row;单测覆盖:`sidebar-local-projection.test.ts`Rust runtime 覆盖:`cargo test -p mnote-web sidebar_tree_runtime -- --nocapture`
4. [x] 上述三条路径均不得依赖 `window.location.reload()` 或顶层 navigation。代码证据:本轮没有新增 reloadcreate/delete 以本地 state/projection apply 为主,显式 refetch 仅作 snapshot 校准。
5. [x] smoke 需要记录事件或本地 apply 标记、最终 DOM 状态与相关网络请求。React Sidebar 路径保留 `tmp/task426-sidebar-main-no-reload-smoke/result.json` 作为 Next/3001 与 mindmap asset 证据;Rust 3000 主文档壳新增 `tmp/task426-mnote-web-main-no-reload-smoke/result.json`,覆盖真实 3000 页面新建/删除无刷新 DOM 结果。代码级测试同时覆盖:`sidebar-delete-preflight-source.test.ts` 锁定 create/delete 事件与本地 documents apply`use-sidebar-data.test.tsx` 锁定 Convex live 显式 refetch 会真实拉 `/api/sidebar``cargo test -p mnote-web sidebar_tree_runtime -- --nocapture` 锁定 Rust 主文档壳 runtime 基础行为。
6. [x] 修复后补测试,防止 `sidebarQuery.refetch()` 在 Convex live 模式下继续作为空操作吞掉显式刷新请求。测试覆盖:`use-sidebar-data.test.tsx`
## 8. 验证记录
已通过:
```bash
cd /mnt/Data1T/mnote/wolai-frontend
pnpm test -- src/components/sidebar/sidebar-delete-preflight-source.test.ts src/components/sidebar/sidebar-local-projection.test.ts src/hooks/use-sidebar-data.test.tsx src/components/sidebar/sidebar-sync.test.ts
```
结果:`113 passed``465 passed`
```bash
cd /mnt/Data1T/mnote/wolai-frontend
pnpm exec eslint src/components/sidebar/sidebar-sync.ts src/components/sidebar/sidebar-sync.test.ts src/components/sidebar/sidebar-local-projection.ts src/components/sidebar/sidebar-local-projection.test.ts src/hooks/use-sidebar-data.ts src/hooks/use-sidebar-data.test.tsx src/components/sidebar/sidebar.tsx
```
结果:无 error`sidebar.tsx` 保留既有 warning。
真实浏览器 smoke 已通过:
```bash
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task426-mnote-web-main-no-reload-smoke.js
```
结果文件:`tmp/task426-mnote-web-main-no-reload-smoke/result.json`
关键结果:
- `ok=true`
- `baseUrl=http://127.0.0.1:3000`
- `createdDocumentId=tree_1778720530823_2`
- `mindmapId=mindmap_1778720531308`
- 新建页面后:`localApplied=create``pageRows=1``fileRows=["doc:tree_1778720530823_2","index:tree_1778720530823_2"]`
- 新建思维导图后:`localApplied=true``rows=[{"rowId":"asset:mindmap_1778720531308","docId":"tree_1778720530823_2","title":"mindmap-mindmap_1778720531308.json…","kind":"mindmap"}]`
- 删除后:`localApplied=remove``pageRows=0``fileRows=[]`
- `requests` 包含两次 `POST /api/tree/commands`、一次 mindmap projection GET、两次 `/api/mindmap/<doc>/<mindmap>` POST;无浏览器刷新依赖
- 结果文件:`tmp/task426-mnote-web-main-no-reload-smoke/result.json`
补充对比:
```bash
cd /mnt/Data1T/mnote
MNOTE_UI_BASE_URL=http://127.0.0.1:3002 node scripts/task426-mnote-web-main-no-reload-smoke.js
```
结果:`ok=true`;新建后 `createdDocumentId=tree_1778714773920_2`Page Tree 与 File Tree 立即出现;删除后两处立即消失。
历史说明:
- 旧记录中的 `MNOTE_UI_BASE_URL=http://127.0.0.1:3001 node <task426-sidebar-main-no-reload-smoke>` 只验证了 Next/React Sidebar 路径,不能作为 Rust 3000 主入口验收依据。
- `/tree` debug route smoke 也不能替代 `/documents/<id>` 主文档壳验收,因为当前默认配置下 debug shell route 可能未启用。
已尝试:
```bash
cd /mnt/Data1T/mnote/wolai-frontend
pnpm exec tsc --noEmit --pretty false
```
结果:失败,但失败集中在既有 Convex / AI / OnlyOffice / 测试类型问题,不是本轮新增文件或本轮修改路径的专属错误。
## 9. 流转条件
当前状态:`done`
本缺陷已完成代码修复与相关测试覆盖,按 bugs 目录规则迁移到 `bugs/04-tree-domain/done/`