227 lines
6.3 KiB
TypeScript
227 lines
6.3 KiB
TypeScript
// ---- 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();
|
||
} |