refactor: scriptWorkspaceStore.ts

This commit is contained in:
tao.chen
2026-08-25 11:12:22 +08:00
parent 2ed8d7b2cc
commit baa1e04af6
12 changed files with 2133 additions and 1446 deletions
@@ -0,0 +1,127 @@
// ---- treeSlice ----
//
// 拥有 expandedPaths / loadingChildrenPaths / loadedChildPaths /
// loadedScriptPaths / loadingScriptPaths (5 个 directory-tree 缓存集合)。
// 负责 toggleExpanded 和 loadChildren。
//
// 注意:
// - loadedScriptPaths/loadingScriptPaths 也由 scriptsSlice 写 (loadScripts /
// loadOwnerGroup),但 ownership 在 treeSlice 里 (因为是 cache set,不是数据)
// - scriptsSlice.load 也会写这俩,所以这里只保留这两个 setter (toggleExpanded
// 也要写 expandedPaths)。
import type { StateCreator } from "zustand";
import {
getCurrentUserId,
ownerCacheKey,
pushToast,
requireApi,
} from "./helpers";
import type { TreeSliceActions, TreeSliceState } from "./types";
import type { ScriptWorkspaceStore } from "./useScriptWorkspaceStore";
export const createTreeSlice: StateCreator<
ScriptWorkspaceStore,
[],
[],
TreeSliceState & TreeSliceActions
> = (set, get) => {
const initial: TreeSliceState = {
expandedPaths: new Set<string>(),
loadingChildrenPaths: new Set<string>(),
loadedChildPaths: new Set<string>(),
loadedScriptPaths: new Set<string>(),
loadingScriptPaths: new Set<string>(),
};
return {
...initial,
loadChildren: async (parentPath, ownerUserId) => {
const api = requireApi();
const cacheKey = ownerCacheKey(ownerUserId, parentPath);
if (get().loadedChildPaths.has(cacheKey)) return;
const next = new Set(get().loadingChildrenPaths);
next.add(cacheKey);
set({ loadingChildrenPaths: next });
try {
const children = await api.listWorkspaceDirectories(parentPath, ownerUserId);
set((state) => {
const nextLoaded = new Set(state.loadedChildPaths);
nextLoaded.add(cacheKey);
// 丢弃该 owner 该 parent 下的旧目录行,再并入 fresh(按 (owner,path) 去重)。
const targetOwner = ownerUserId ?? getCurrentUserId() ?? null;
const trimmed = state.directories.filter(
(d) =>
!(d.owner_user_id === targetOwner && d.parent_path === parentPath),
);
const byId = new Map(
trimmed.map((d) => [`${d.owner_user_id}:${d.path}`, d]),
);
for (const c of children) byId.set(`${c.owner_user_id}:${c.path}`, c);
return {
directories: Array.from(byId.values()),
loadedChildPaths: nextLoaded,
loadingChildrenPaths: new Set(
[...state.loadingChildrenPaths].filter((p) => p !== cacheKey),
),
};
});
} catch (error) {
set((state) => ({
loadingChildrenPaths: new Set(
[...state.loadingChildrenPaths].filter((p) => p !== cacheKey),
),
}));
pushToast(
"error",
error instanceof Error ? error.message : "目录加载失败",
);
}
},
toggleExpanded: async (path, loadPath, ownerUserId) => {
const state = get();
const isOpen = state.expandedPaths.has(path);
const next = new Set(state.expandedPaths);
if (isOpen) {
next.delete(path);
} else {
next.add(path);
const me = getCurrentUserId();
// 用 `loadPath === undefined` 区分分组头与真实目录,而不是用
// `path.startsWith("__group__")`:目录的 expandKey 是
// `${groupKey}/${dir.path}` 即 `__group__<owner>/dir`,同样以
// `__group__` 开头,前缀判断会把子目录点击误当成分组头,导致
// 既不调 loadChildren 也不调 loadScripts"子目录点击不触发接口")。
// 分组头 always 传 loadPath=undefined;目录 always 传 loadPath=dir.path。
if (loadPath === undefined) {
// 分组头:他人分组首次展开 → loadOwnerGroup 按需拉取其根级可见
// 脚本+数据+目录(守门去重)。仅翻转 expand;真实子目录的懒加载
// 由目录分支(loadPath !== undefined)负责。
if (
ownerUserId &&
ownerUserId !== me &&
!state.loadedOwnerGroups.has(ownerUserId)
) {
void get().loadOwnerGroup(ownerUserId);
}
} else {
// 真实目录展开:loadChildrenowner 限定的显式目录行)+ loadScripts
// 并行。两者都 idempotent + 缓存;ownerUserId 缺省=我。他人目录同样
// 调 loadChildren(owner) 拉取其目录结构,否则嵌套子目录无法被发现
// list_scripts 非递归,只能看到直接子脚本)。
if (!state.loadedChildPaths.has(ownerCacheKey(ownerUserId, loadPath))) {
void get().loadChildren(loadPath, ownerUserId);
}
if (!state.loadedScriptPaths.has(ownerCacheKey(ownerUserId, loadPath))) {
void get().loadScripts(loadPath, ownerUserId);
}
}
}
set({ expandedPaths: next });
},
};
};