fix: loadChildren + loadScripts + loadDataResources

This commit is contained in:
tao.chen
2026-09-01 11:29:07 +08:00
parent 0deb2d205b
commit 64d33fb1d0
4 changed files with 56 additions and 17 deletions
@@ -283,28 +283,41 @@ export const createScriptsSlice: StateCreator<
loadDataResources: async (parentPath = "", ownerUserId) => {
const api = requireApi();
const cacheKey = ownerCacheKey(ownerUserId, parentPath);
// 命中缓存:listResources 是非递归的,同一 (owner, parent_path) 拉过的
// 内容不会自己变化;省去 toggleExpanded 重复展开同一目录时的网络往返。
if (get().loadedDataResourcePaths.has(cacheKey)) return;
set({ dataResourcesLoading: true });
try {
const list = await api.listResources(parentPath, { ownerUserId });
const fresh = Array.isArray(list) ? list : [];
set((state) => {
// 按 owner 范围合并:丢该 owner 的旧资源再并入 fresh(fresh 覆盖
// 同 id)。owner 缺省=我,故根加载/刷新我的数据时替换我的一级资源
// 按 (owner, parent_path) 局部替换:丢该 owner 在 parentPath 下的
// 旧条目,保留该 owner 在其它路径下的条目,再并入 fresh
// 这样 toggleExpanded 在子目录展开时按需拉取不会把根已加载的数据
// 资源擦掉(修"根加载后子目录展开丢数据 / 子目录数据本来不显示")。
const targetOwner = ownerUserId ?? getCurrentUserId() ?? null;
const kept = state.dataResources.filter(
(r) => r.owner_user_id !== targetOwner,
);
const kept = state.dataResources.filter((r) => {
if (r.owner_user_id !== targetOwner) return true;
return parentPathOf(r.jupyter_accessible_path) !== parentPath;
});
const byId = new Map(kept.map((r) => [r.resource_id, r]));
for (const item of fresh) byId.set(item.resource_id, item);
return { dataResources: Array.from(byId.values()) };
const nextLoaded = new Set(state.loadedDataResourcePaths);
nextLoaded.add(cacheKey);
return {
dataResources: Array.from(byId.values()),
loadedDataResourcePaths: nextLoaded,
};
});
} catch {
set((state) => {
const targetOwner = ownerUserId ?? getCurrentUserId() ?? null;
return {
dataResources: state.dataResources.filter(
(r) => r.owner_user_id !== targetOwner,
),
dataResources: state.dataResources.filter((r) => {
if (r.owner_user_id !== targetOwner) return true;
return parentPathOf(r.jupyter_accessible_path) !== parentPath;
}),
};
});
} finally {
@@ -336,4 +349,13 @@ export const createScriptsSlice: StateCreator<
}
},
};
};
};
// 提取 jupyter-accessible 路径的父目录;用于 `loadDataResources` 局部替换时
// 判断一条缓存资源是否落在目标 parent_path 下(list-resources 按 parent_path
// 精确匹配,非递归)。
function parentPathOf(path: string): string {
const parts = path.split("/");
parts.pop();
return parts.join("/");
}
@@ -1,14 +1,16 @@
// ---- treeSlice ----
//
// 拥有 expandedPaths / loadingChildrenPaths / loadedChildPaths /
// loadedScriptPaths / loadingScriptPaths (5 个 directory-tree 缓存集合)。
// 负责 toggleExpanded 和 loadChildren。
// loadedScriptPaths / loadingScriptPaths / loadedDataResourcePaths
// (6 个 directory-tree 缓存集合)。负责 toggleExpanded 和 loadChildren。
//
// 注意:
// - loadedScriptPaths/loadingScriptPaths 也由 scriptsSlice 写 (loadScripts /
// loadOwnerGroup),但 ownership 在 treeSlice 里 (因为是 cache set,不是数据)
// - scriptsSlice.load 也会写这俩,所以这里只保留这两个 setter (toggleExpanded
// 也要写 expandedPaths)
// - loadedDataResourcePaths 由 scriptsSlice.loadDataResources 写(同样的 cache
// 不放数据原则),toggleExpanded 在真实目录分支按需触发
// - scriptsSlice.load 也会写 cached script paths,所以这里只保留 toggleExpanded
// (写 expandedPaths) 和 loadChildren (写目录缓存)。
import type { StateCreator } from "zustand";
@@ -34,6 +36,7 @@ export const createTreeSlice: StateCreator<
loadedChildPaths: new Set<string>(),
loadedScriptPaths: new Set<string>(),
loadingScriptPaths: new Set<string>(),
loadedDataResourcePaths: new Set<string>(),
};
return {
@@ -110,15 +113,24 @@ export const createTreeSlice: StateCreator<
}
} else {
// 真实目录展开:loadChildrenowner 限定的显式目录行)+ loadScripts
// 并行。者都 idempotent + 缓存;ownerUserId 缺省=我。他人目录同样
// 调 loadChildren(owner) 拉取其目录结构,否则嵌套子目录无法被发现
// list_scripts 非递归,只能看到直接子脚本)。
// + loadDataResources 并行。者都 idempotent + 缓存;ownerUserId
// 缺省=我。他人目录同样调 loadChildren(owner) 拉取其目录结构,否则
// 嵌套子目录无法被发现list_scripts 非递归,只能看到直接子脚本)。
// 数据资源也是非递归的——不按需拉取,子目录里的 csv/xlsx/json 等
// 都不会出现(修"目录树子目录里的数据文件不显示")。
if (!state.loadedChildPaths.has(ownerCacheKey(ownerUserId, loadPath))) {
void get().loadChildren(loadPath, ownerUserId);
}
if (!state.loadedScriptPaths.has(ownerCacheKey(ownerUserId, loadPath))) {
void get().loadScripts(loadPath, ownerUserId);
}
if (
!state.loadedDataResourcePaths.has(
ownerCacheKey(ownerUserId, loadPath),
)
) {
void get().loadDataResources(loadPath, ownerUserId);
}
}
}
set({ expandedPaths: next });
@@ -62,6 +62,10 @@ export type TreeSliceState = {
// `${owner_user_id}:${parent_path}` (see ownerCacheKey).
loadedScriptPaths: Set<string>;
loadingScriptPaths: Set<string>;
// 与 loadedScriptPaths 同形:按 (owner, parent_path) 缓存已拉取的数据资源,
// 让 toggleExpanded 在子目录展开时也按需请求 listDataResources,而不是
// 只在根加载一次(修"子目录下的数据文件不展示")。
loadedDataResourcePaths: Set<string>;
};
export type TreeSliceActions = {
@@ -112,6 +112,7 @@ const INITIAL: ScriptWorkspaceState = {
loadedChildPaths: new Set<string>(),
loadedScriptPaths: new Set<string>(),
loadingScriptPaths: new Set<string>(),
loadedDataResourcePaths: new Set<string>(),
// selectionSlice
selectedId: null,
openTabIds: [],