227 lines
6.8 KiB
TypeScript
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);
|
|
}
|
|
},
|
|
};
|
|
}; |