// ---- Module-level non-reactive holders ---- // 这些变量不放在 store 里 (避免 React 重渲 + 跨切片共享单例)。 // 每一个切片在 helpers.ts 顶层导入它们,确保所有 slice 看到的同一份。 import type { ActiveEditSession, ScriptItem, WorkspaceBoundApi, } from "~/services/api"; import { toast } from "sonner"; // 缓存的会话类型(多 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(); 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(); 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 { 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 { if (tone === "error") toast.error(message); else if (tone === "info") toast.info(message); else toast.success(message); } export async function sha256Hex( buffer: ArrayBuffer, ): Promise { // `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(); }