Files
mnote/design/05-editor-mainline/done/5-33-filetree-viewstate-generation-followup-v1.md
T
lix-2026 1882db7681 收口 MNote P0 P1 P2 审查尾项
- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目
- 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线
- 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径

验证:
- cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
- cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1
- git diff --check
- git diff --cached --check
- codegraph index . --force && codegraph status .
- codegraph sync . && codegraph status .
2026-06-01 09:29:12 +08:00

213 lines
10 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.
# 5-33 FileTree ViewState 与请求代际收口 v1
> 状态:done
> Owner05-editor-mainline / 03-rust-web
> 前置:`design/05-editor-mainline/done/5-32-filetree-lazy-loading-sidex-alignment-v1.md`
> 背景:5-32 已把 FileTree lazy children、cache 和局部 refresh 推到可用层,但 Sidex / VSCode 对照显示,当前仍缺少统一 view-state、request generation、collapsed stale 和 reveal command。继续只做“展开恢复”会掩盖慢请求、重复请求和过期结果覆盖的问题。
## 1. 结论
FileTree 下一步不应继续围绕 DOM 还原做补丁,而应收口为一个显式状态机:
1. `FileTreeViewState` 负责展开、选择、焦点、滚动、loaded/stale parent。
2. `FileTreeRequestTracker` 负责同一 parent 的 in-flight 去重、request generation 和过期结果丢弃。
3. `FileTreeRevealCommand` 负责从 active resource / command result 精确展开父链、选中目标、聚焦目标。
这三层只管理浏览器 view model,不改变 local-first 文件系统事实源,也不新增第二套树真相。数据真相仍来自 Rust projection / kernel command result。
## 2. Sidex / VSCode 对照
### 2.1 可借鉴的行为
- `AsyncDataTree.refreshNode` 会复用相交 subtree refresh promise,避免同一节点重复 refresh。
- collapsed node 被刷新时只标记 `stale`,不立即解析 children。
- `doGetChildren` 对同一 node 的 children promise 去重。
- 慢请求超过阈值后进入 slow/loading state,完成后清理。
- Explorer `selectResource` 负责展开 parent chain、reveal、focus、selection,而不是依赖 DOM 模糊匹配。
### 2.2 不照搬的内容
- 不引入 VSCode workbench service container。
- 不照搬完整 AsyncDataTree 实现。
- 不把前端 view state 升级成系统事实源。
- 不把 PageTree / ResourceTree 强行塞进同一个前端树组件。
## 3. MNote 当前缺口
### 3.1 状态分散
当前 FileTree 展开状态至少分散在:
- DOM `aria-expanded` / `data-filetree-children-loaded`
- `fileTreeExpandedRelativePaths`
- `sessionStorage`
- `fileTreeState.loadedParents/loadingParents/dirtyParents`
- 后端 projection 的 expanded/default scope
这会导致 refresh 替换 DOM 后,逻辑展开状态与实际 children DOM 不一致。
### 3.2 缺少 request generation
当前已能用 `loadingParents` 复用 in-flight promise,但缺少 generation
- A 请求慢,B 请求快时,A 完成后仍可能 patch 旧 rows。
- scope/rootUri 切换后,旧请求返回仍可能误写当前 DOM。
- watcher refresh 与用户展开请求并发时,无法判定谁是最新。
### 3.3 collapsed stale 语义不清
当前未展开且未 loaded 的 parent 在局部 patch 时直接跳过,但没有显式 `stale` 标记。结果是:
- 用户展开时不知道是否应该强制拉最新 children。
- watch/command 只知道 parent dirty,不知道 dirty 是“已展开需后台更新”还是“折叠后下次展开更新”。
### 3.4 reveal/focus 仍靠 DOM 匹配
resource tab 激活、命令执行后,FileTree 的 active row/reveal 仍依赖 DOM 查询、path includes 或当前可见节点。lazy children 下,目标父链可能还没加载,必须由 tree runtime command 负责展开父链。
## 4. 目标
- 同一 `rootUri + scope + parentRelativePath` 在任一时刻最多一个 active children 请求。
- 任一 fetch/render 结果必须带 generation;过期结果不得 patch DOM 或覆盖 cache。
- collapsed parent 收到 refresh 时只标记 stale,不立即加载 children。
- expanded parent 收到 refresh 时保留当前 children,后台刷新并 patch。
- active resource / command result 可通过 canonical row id 或 relative path 精确 reveal。
- render/patch 后 selection/focus 基于 logical id 复投影,不依赖“当前 DOM 还在”。
## 5. 非目标
- 不实现虚拟滚动;大目录 viewport virtualization 另行设计。
- 不重写 Rust projection 协议。
- 不改变 FileTree 排序、DND、右键命令语义。
- 不修搜索索引 rebuild 问题;该问题独立记录在 `bugs/04-tree-domain/process/2026-05-27-local-search-query-rebuilds-index.md`
## 6. 设计
### 6.1 FileTreeViewState
建议在 `sidebar-tree-live-apply-runtime.js` 中把 FileTree 状态收敛为:
```js
const fileTreeViewState = {
rootUri: '',
scope: '',
expandedParents: new Set(),
selectedRowIds: new Set(),
focusedRowId: '',
activeRowId: '',
scrollTop: 0,
loadedParents: new Set(),
staleParents: new Set(),
dirtyParents: new Set(),
rowsByParent: new Map(),
revisionByParent: new Map(),
};
```
约束:
- key 必须包含 `rootUri`,避免多个 workspace/local root 串状态。
- `selectedRowIds/focusedRowId` 存 logical row id,不存 DOM element。
- `expandedParents` 只表达用户视图意图;children 是否已加载由 `loadedParents/staleParents` 表达。
- scope/rootUri 变化时必须 reset 或 hydrate 对应 namespace 的状态。
### 6.2 FileTreeRequestTracker
建议新增轻量 request tracker
```js
const fileTreeRequests = {
generation: 0,
loadingByParent: new Map(),
latestGenerationByParent: new Map(),
};
```
请求流程:
1. `beginFileTreeRequest(parent)` 递增 generation,并记录 parent 最新 generation。
2. 如果 parent 已有 in-flight promise,直接返回该 promise。
3. fetch 结果回来后检查 `isLatestFileTreeRequest(parent, generation)`
4. 若不是最新,返回 `{ stale: true }`,不得写 cache,不得 patch DOM。
5. scope/rootUri 切换时递增全局 generation,旧请求全部自然失效。
### 6.3 stale parent 规则
- collapsed + dirty`staleParents.add(parent)`,不拉 children。
- expanded + dirty:保留旧 rows,后台 refresh parent。
- expand parent
- 若 loaded 且不 stale,直接渲染 cache。
- 若 stale 或未 loaded,进入 loading state 并 fetch children。
- fetch 失败:
- 不清旧 rows。
- parent 标记 `stale`
- row 展示可恢复错误状态,允许重试。
### 6.4 reveal command
新增内部 command
```js
revealFileTreeResource({
rootUri,
relativePath,
rowId,
select: true,
focus: true,
scroll: true,
})
```
行为:
1. 校验 rootUri 是否当前 root。
2. 计算 parent chain,例如 `design/03-rust-web/process`
3. 逐层调用 `getFileTreeChildren(parent)`,确保父链已加载。
4. 渲染每层 children 后设置 expanded。
5. 使用 rowId 或 relativePath 精确选中目标。
6. 同步 `selectedRowIds/focusedRowId/activeRowId`
## 7. Checklist
- [x] 审查 `sidebar-tree-live-apply-runtime.js` 中所有读写展开、选择、focus 的入口,列出迁移点。
- [x] 新增 `fileTreeViewState`,把 `fileTreeExpandedRelativePaths` 迁入 view-state namespace。
- [x] 新增 request generation,覆盖 `getFileTreeChildren` 的 children 请求结果写入。
- [x] 让 scope/rootUri 切换使旧 generation 失效。
- [x] 把 collapsed + dirty 改成 stale parent,不立即加载 children。
- [x] render/patch 后按 logical state 复投影 selection/focus。
- [x] 新增 `revealFileTreeResource` 内部 command,替代 path includes DOM 匹配。
- [x] 补 JS/Rust 字符串契约测试,检查 generation/stale 关键函数存在。
- [x] 扩展 `scripts/task494-filetree-lazy-loading-dedup-smoke.js`,覆盖慢请求后切 scope、快速展开/收起、watch refresh 后不折叠。
## 8. 验收
- 快速连续点击同一目录 5 次,只产生 1 个 in-flight children 请求。
- 人为延迟第一个 children 请求,再触发 refresh,旧请求返回后不会覆盖新 rows。
- collapsed parent 收到 watcher/command dirty 后不发 children 请求;下一次展开才请求。
- active resource tab 能 reveal 未加载的深层 filetree row。
- 创建/重命名/删除后 selection/focus 不因局部 patch 丢失。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree -- --test-threads=1` 通过。
- `node scripts/task494-filetree-lazy-loading-dedup-smoke.js` 通过。
## 9. 风险
- 如果把 view-state 写成新的事实源,会和 Rust projection 冲突。必须限定为浏览器视图状态。
- 如果 generation 只绑定 parent,不绑定 rootUri/scope,仍会出现跨 workspace 旧请求污染。
- 如果 reveal command 隐式加载过多 parent chain,深层大目录仍可能慢;需要只加载 path chain,不加载 siblings 的 children。
## 10. 执行记录
- 2026-05-27:在 `sidebar-tree-live-apply-runtime.js` 中新增 `staleParents``requestGeneration``latestGenerationByParent``beginFileTreeRequest(...)``isLatestFileTreeRequest(...)`
- 2026-05-27`getFileTreeChildren(...)` 写入 cache/DOM 前检查 generationscope/rootUri 切换时递增 generation 并清理 latest generation map,使旧请求自然失效。
- 2026-05-27`refreshFileTreeParent(...)` 对 collapsed non-root parent 只标记 stale,不立即加载 children;下一次展开绕过旧 cache 拉取最新 children。
- 2026-05-27:新增 `sidebar_filetree_runtime_discards_stale_generation_results` 契约测试。
- 2026-05-27:审查 FileTree 展开/selection/focus 入口后,确认 `fileTreeViewState` 已承接 expanded/loaded/stale/dirty/selection/focus/active`fileTreeExpandedRelativePaths` 保留为 `fileTreeViewState.expandedParents` 的兼容 alias。
- 2026-05-27`renderFileProjection(...)``patchFileTreeParentChildren(...)` 以及 lazy children render 后按 logical view-state 复投影 selection/focus/active,避免局部 patch 后丢失选中态。
- 2026-05-27`revealFileTreeResource(...)` 接入 `selectSidebarFileTreeDocument(...)` 的 DOM miss fallbackactive document 变更可按 local markdown documentId 解出 relative path,逐层加载未展开父链并选中目标 row。
- 2026-05-27:扩展 `task494-filetree-lazy-loading-dedup-smoke.js`,覆盖 watch batch 局部刷新不折叠、未加载深层 resource reveal、opened Markdown rename 后 BufferStore rekey。
- 验证:
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_runtime -- --nocapture`5 passed。
- `node scripts/task494-filetree-lazy-loading-dedup-smoke.js`,通过,`targetChildrenRequests=1``cachedExpandMs=33`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_filetree_runtime -- --nocapture --test-threads=1`10 passed。
- `node scripts/task494-filetree-lazy-loading-dedup-smoke.js`,通过,`targetChildrenRequests=1``staleScopeChildrenRequests=1``cachedExpandMs=32`