refactor: scriptWorkspaceStore.ts
This commit is contained in:
@@ -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 context(HTTPS / 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();
|
||||
}
|
||||
Reference in New Issue
Block a user