refactor: scriptWorkspaceStore.ts
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
// ---- 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);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user