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

227 lines
6.8 KiB
TypeScript

// ---- editSessionSlice ----
//
// 拥有 editSession / embeddedJupyterUrl / editBusy / editorOpenError。
// Actions: openScriptEditor / endEditing / tickCleanup / releaseActiveOnUnload。
// tickCleanup/releaseActiveOnUnload 放在这里因为它们都围绕 editSession 的生命周期。
//
// 模块级状态 (_editSession / sessionCache / _editorOpening /
// _editorOpenRequest / _api) 通过 helpers 访问。
import type { StateCreator } from "zustand";
import type { ActiveEditSession, ScriptItem } from "~/services/api";
import {
applyEditSessionState,
bumpEditorOpenRequest,
getApi,
getEditorOpening,
getEditorOpenRequest,
getSelectedId,
pushToast,
requireApi,
sessionCache,
setEditorOpening,
} from "./helpers";
import type {
EditSessionSliceActions,
EditSessionSliceState,
} from "./types";
import type { ScriptWorkspaceStore } from "./useScriptWorkspaceStore";
export const createEditSessionSlice: StateCreator<
ScriptWorkspaceStore,
[],
[],
EditSessionSliceState & EditSessionSliceActions
> = (set, get) => {
const initial: EditSessionSliceState = {
editSession: null,
embeddedJupyterUrl: null,
editBusy: false,
editorOpenError: null,
};
return {
...initial,
openScriptEditor: async (script: ScriptItem, showToast = true) => {
if (!script) return;
if (getEditorOpening()) return;
setEditorOpening(true);
const api = requireApi();
const requestId = getEditorOpenRequest() + 1;
// 记录本次请求的 id (供 requestIsCurrent 校验)。
// 原版用模块级 _editorOpenRequest 配合闭包变量;现在 requestIsCurrent
// 通过 helpers 读取最新的模块级值 + getSelectedId()。
const requestIsCurrent = () =>
getEditorOpenRequest() === requestId &&
getSelectedId() === script.script_id;
const clearSessionIfActive = (session: ActiveEditSession) => {
const active = get().editSession;
if (active?.edit_session_id === session.edit_session_id) {
applyEditSessionState((p) => set(p), null, null);
}
};
set({ editBusy: true });
set((state) => ({
editorOpenError:
state.editorOpenError?.scriptId === script.script_id
? null
: state.editorOpenError,
}));
try {
const cached = sessionCache.get(script.script_id);
if (cached) {
if (!requestIsCurrent()) return;
applyEditSessionState(
(p) => set(p),
cached.session,
cached.jupyterUrl,
);
if (showToast) {
pushToast(
"success",
`${script.script_name} 已打开,Jupyter 已嵌入当前工作区`,
);
}
return;
}
let active = get().editSession;
let newlyAcquired = false;
if (active && active.script_id !== script.script_id) {
await api.releaseFileLock(active);
applyEditSessionState((p) => set(p), null, null);
active = null;
}
if (!requestIsCurrent()) return;
if (!active) {
active = await api.acquireFileLock(script);
newlyAcquired = true;
}
if (!requestIsCurrent()) {
if (active) {
await api.releaseFileLock(active);
clearSessionIfActive(active);
}
return;
}
const ticket = await api.createJupyterAccessTicket(active);
if (!requestIsCurrent()) {
if (newlyAcquired) {
await api.releaseFileLock(active);
clearSessionIfActive(active);
}
return;
}
const readySession = {
...active,
ticket_expires_at: ticket.expires_at,
};
applyEditSessionState(
(p) => set(p),
readySession,
ticket.jupyter_url,
);
sessionCache.set(script.script_id, {
session: readySession,
jupyterUrl: ticket.jupyter_url,
lastActiveTime: Date.now(),
});
if (showToast) {
pushToast(
"success",
`${script.script_name} 已打开,Jupyter 已嵌入当前工作区`,
);
}
} catch (error) {
applyEditSessionState((p) => set(p), null, null);
if (requestIsCurrent()) {
const message =
error instanceof Error ? error.message : "打开编辑器失败";
set({ editorOpenError: { scriptId: script.script_id, message } });
if (showToast) {
pushToast("error", message);
}
}
} finally {
setEditorOpening(false);
set({ editBusy: false });
}
},
endEditing: async (closeTabFlag = true, showToast = true) => {
const api = requireApi();
// bump editorOpenRequest 让未完成的 openScriptEditor 失效
bumpEditorOpenRequest();
set({ editorOpenError: null });
const active = get().editSession;
const scriptId = active?.script_id;
if (!active) {
if (closeTabFlag && scriptId) {
await get().closeTab(scriptId);
}
return;
}
set({ editBusy: true });
try {
await api.releaseFileLock(active);
applyEditSessionState((p) => set(p), null, null);
if (scriptId) sessionCache.delete(scriptId);
if (closeTabFlag && scriptId) {
await get().closeTab(scriptId);
}
if (showToast) {
pushToast("success", `${active.script_name} 的编辑锁已释放`);
}
} catch (error) {
pushToast(
"error",
error instanceof Error ? error.message : "释放编辑锁失败",
);
} finally {
set({ editBusy: false });
}
},
tickCleanup: () => {
// 本地锁:清缓存即可,不要发 releaseFileLock 请求。
// 接口是异步的,但在本地实现里等价于无操作,promise 没人 await。
const TEN_MINUTES = 10 * 60 * 1000;
const now = Date.now();
const toCleanup: string[] = [];
const selectedId = getSelectedId();
for (const [scriptId, cached] of sessionCache.entries()) {
if (scriptId !== selectedId) {
const inactiveTime = now - cached.lastActiveTime;
if (inactiveTime > TEN_MINUTES) {
toCleanup.push(scriptId);
}
}
}
if (toCleanup.length === 0) return;
for (const scriptId of toCleanup) {
sessionCache.delete(scriptId);
}
pushToast(
"info",
`已清理 ${toCleanup.length} 个长时间未活动的本地编辑会话`,
);
},
releaseActiveOnUnload: () => {
const api = getApi();
if (!api) return;
const current = get().editSession;
if (current) {
api.releaseFileLockOnUnload(current);
}
},
};
};