// ---- 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); } }, }; };