refactor: scriptWorkspaceStore.ts

This commit is contained in:
tao.chen
2026-09-02 10:10:41 +08:00
committed by tao.chen
parent 8278cdd8b0
commit e9d9bcfc97
12 changed files with 2133 additions and 1446 deletions
@@ -0,0 +1,227 @@
// ---- Module-level non-reactive holders ----
// 这些变量不放在 store 里 (避免 React 重渲 + 跨切片共享单例)。
// 每一个切片在 helpers.ts 顶层导入它们,确保所有 slice 看到的同一份。
import type {
ActiveEditSession,
ScriptItem,
WorkspaceBoundApi,
} from "~/services/api";
import { useUiStore } from "./uiStore";
// 缓存的会话类型(多 iframe 共存方案)
type CachedSession = {
session: ActiveEditSession;
jupyterUrl: string;
lastActiveTime: number;
};
export type PythonEditorBuffer = {
initialContent: string | null;
content: string | null;
dirty: boolean;
saving: boolean;
initial: boolean;
loadError: string | null;
};
// 模块级可变 holder(非响应式,避免 React 重渲)
export const sessionCache = new Map<string, CachedSession>();
let _selectedId: string | null = null;
let _editSession: ActiveEditSession | null = null;
let _editorOpening = false;
let _editorOpenRequest = 0;
let _api: WorkspaceBoundApi | null = null;
let _previewController: AbortController | null = null;
let _previewRequest = 0;
let _pythonEditorOpeningIds = new Set<string>();
let _scriptCountSeq = 0;
// 当前登录用户 id —— 与 `_api` 一样由 layout 在 render body 绑定。
// 用于:(1) `loadScripts`/`loadOwnerGroup` 区分"我"与他人;
// (2) `toggleExpanded` 判定展开真实目录时是否需要 loadChildren(他人的
// 目录全靠脚本路径推断,不调 listWorkspaceDirectories)。
let _currentUserId: string | null = null;
// 当前工作区 id —— listWorkspaceMembers 不像 listScripts 那样把 workspaceId
// 烤进 bound api,需要显式传入,故由 layout 绑定。
let _workspaceId: string | null = null;
export const bindScriptWorkspaceApi = (api: WorkspaceBoundApi | null) => {
_api = api;
};
export const bindScriptWorkspaceUser = (userId: string | null) => {
_currentUserId = userId;
};
export const bindScriptWorkspaceId = (workspaceId: string | null) => {
_workspaceId = workspaceId;
};
// `loadedScriptPaths` / `loadedChildPaths` 的 key 命名空间:
// `${owner_user_id}:${parent_path}`,让"我"与他人的同名子目录缓存互不串扰。
// owner 缺省时回退当前用户,再回退字面量 "me"(仅作占位 key,不会发到后端)。
export function ownerCacheKey(
ownerUserId: string | undefined,
path: string,
): string {
const owner = ownerUserId ?? _currentUserId ?? "me";
return `${owner}:${path}`;
}
export const getSessionCache = () => sessionCache;
export const clearSessionCache = () => {
sessionCache.clear();
};
// handle ref 给 Sidebar 用,避免订阅 store
export const editSessionHandle: { current: ActiveEditSession | null } = {
current: null,
};
// ---- 跨切片共享的内部 getters (供切片实现使用) ----
export function getApi(): WorkspaceBoundApi | null {
return _api;
}
export function getCurrentUserId(): string | null {
return _currentUserId;
}
export function getWorkspaceId(): string | null {
return _workspaceId;
}
export function getSelectedId(): string | null {
return _selectedId;
}
export function setSelectedId(value: string | null): void {
_selectedId = value;
}
export function getEditSession(): ActiveEditSession | null {
return _editSession;
}
export function getEditorOpening(): boolean {
return _editorOpening;
}
export function setEditorOpening(value: boolean): void {
_editorOpening = value;
}
export function getEditorOpenRequest(): number {
return _editorOpenRequest;
}
export function bumpEditorOpenRequest(): number {
_editorOpenRequest += 1;
return _editorOpenRequest;
}
export function getPreviewController(): AbortController | null {
return _previewController;
}
export function setPreviewController(value: AbortController | null): void {
_previewController = value;
}
export function bumpPreviewRequest(): number {
_previewRequest += 1;
return _previewRequest;
}
export function getPreviewRequest(): number {
return _previewRequest;
}
export function getPythonEditorOpeningIds(): Set<string> {
return _pythonEditorOpeningIds;
}
export function bumpScriptCountSeq(): number {
_scriptCountSeq += 1;
return _scriptCountSeq;
}
export function getScriptCountSeq(): number {
return _scriptCountSeq;
}
// ---- 工具函数 (原模块级 helper, 不依赖 set/get) ----
export function requireApi(): WorkspaceBoundApi {
if (!_api) {
throw new Error("script workspace API 未绑定");
}
return _api;
}
export function ownedScriptPath(item: ScriptItem) {
return item.relative_path
.replaceAll("\\", "/")
.split("/")
.slice(2)
.join("/");
}
export function pushToast(
tone: "success" | "error" | "info",
message: string,
): void {
useUiStore.getState().pushToast({ tone, message });
}
export async function sha256Hex(
buffer: ArrayBuffer,
): Promise<string | null> {
// `crypto.subtle` 仅在 secure contextHTTPS / localhost)下可用;不可用时
// 跳过 hash 计算,让后端走非校验路径。
if (typeof crypto === "undefined" || !crypto.subtle?.digest) {
return null;
}
const digest = await crypto.subtle.digest("SHA-256", buffer);
return Array.from(new Uint8Array(digest))
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
// ---- setEditSessionState (跨切片 helper, 原 store 闭包内 function) ----
//
// 必须在每个使用它的 slice 里手动传 `set`,因为 store 不再是单 closure。
// 内部更新模块级 `_editSession` + `editSessionHandle.current`,再 `set` 切片 state。
export function applyEditSessionState(
set: (partial: {
editSession: ActiveEditSession | null;
embeddedJupyterUrl: string | null;
}) => void,
next: ActiveEditSession | null,
nextJupyterUrl: string | null,
): void {
_editSession = next;
editSessionHandle.current = next;
set({
editSession: next,
embeddedJupyterUrl: next ? nextJupyterUrl : null,
});
}
// ---- 模块级 reset (供 useScriptWorkspaceStore.reset 调用) ----
//
// 同步模块级可变状态 + abort preview controller;不写响应式 state(state 由根 reset 处理)。
export function resetModuleState(): void {
if (_previewController) {
_previewController.abort();
_previewController = null;
}
_previewRequest += 1;
_selectedId = null;
_editSession = null;
editSessionHandle.current = null;
sessionCache.clear();
_pythonEditorOpeningIds.clear();
}