Files
model-platform/frontend/app/features/platform/state/helpers.ts
T

229 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ---- 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<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 {
if (tone === "error") toast.error(message);
else if (tone === "info") toast.info(message);
else toast.success(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();
}