Files
model-platform/frontend/app/services/api/fileLocks.ts
T

140 lines
4.3 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.
import { ApiRequestError, createUuid } from "./_shared";
import type { ScriptItem } from "./scripts";
export type FileLockSession = {
edit_session_id: string;
workspace_id: string;
storage_object_id: string;
user_id: string;
session_status: "active" | "closed" | "expired";
lease_seconds: number;
heartbeat_interval_seconds: number;
expires_at: string;
runtime_id: string;
jupyter_session_id: string;
jupyter_url?: string;
relative_path?: string;
lock_token?: string;
};
export type ActiveEditSession = FileLockSession & {
script_id: string;
script_name: string;
jupyter_path: string;
lock_token: string;
ticket_expires_at?: string;
};
export type JupyterAccessTicket = {
edit_session_id: string;
jupyter_url: string;
expires_at: string;
};
// 本地浏览器级“编辑锁”——后端没有 acquire/heartbeat/release/edit-session 表。
// 这里的四个函数全部是占位:返回结构是为了让上层 store 的
// _editSession / sessionCache 继续按“session”接口工作,但锁的实际作用域
// 仅限当前 tab。关闭 tab、刷新页面、用隐身模式打开、或换浏览器,锁即失效。
// 不要把这些函数当作鉴权或并发控制用——它们什么都不查、什么都不写。
// 真实并发控制需要后端 edit_sessions 表 + Nginx auth_request 联动,是后续工单。
export async function acquireFileLock(
workspaceId: string,
script: ScriptItem,
): Promise<ActiveEditSession> {
const now = Date.now();
return {
edit_session_id: createUuid().replaceAll("-", ""),
workspace_id: workspaceId,
storage_object_id: script.current_object_id,
user_id: script.owner_user_id,
session_status: "active",
lease_seconds: 3600,
heartbeat_interval_seconds: 300,
expires_at: new Date(now + 3600_000).toISOString(),
runtime_id: workspaceId,
jupyter_session_id: "local",
relative_path: script.relative_path,
lock_token: "local",
script_id: script.script_id,
script_name: script.script_name,
jupyter_path: script.jupyter_path,
};
}
export async function heartbeatFileLock(
_workspaceId: string,
session: ActiveEditSession,
): Promise<FileLockSession> {
// 本地锁不存在过期概念;只是把 expires_at 推后让 UI 看着还活着。
// 该字段当前没有任何消费者,保留只是为了不破坏契约。
return session;
}
export async function releaseFileLock(
_workspaceId: string,
session: ActiveEditSession,
): Promise<FileLockSession> {
return { ...session, session_status: "closed" };
}
export function releaseFileLockOnUnload(
_workspaceId: string,
_session: ActiveEditSession,
): void {
// 本地锁随 tab 生命周期结束。beforeunload 调到这里只是让 store 端
// 清理模块级引用,避免下一个 tab 复用时看到陈旧 _editSession。
}
async function waitForJupyterReady(jupyterUrl: string): Promise<void> {
const retryableStatuses = new Set([502, 503, 504]);
let lastStatus = 0;
for (let attempt = 0; attempt < 10; attempt += 1) {
const response = await fetch(jupyterUrl, {
credentials: "same-origin",
cache: "no-store",
});
if (response.ok) return;
lastStatus = response.status;
if (!retryableStatuses.has(response.status)) {
throw new ApiRequestError(
`Jupyter 打开失败(HTTP ${response.status}`,
response.status,
);
}
await new Promise((resolve) => window.setTimeout(resolve, 400));
}
throw new ApiRequestError(
`Jupyter 服务启动超时${lastStatus ? `HTTP ${lastStatus}` : ""}`,
lastStatus || 504,
);
}
export async function createJupyterAccessTicket(
workspaceId: string,
session: ActiveEditSession,
): Promise<JupyterAccessTicket> {
const editorRoute = session.script_name.toLowerCase().endsWith(".ipynb")
? "notebooks"
: "edit";
const encodedPath = session.jupyter_path
.split("/")
.filter(Boolean)
.map(encodeURIComponent)
.join("/");
if (!encodedPath) {
throw new Error("脚本缺少 Jupyter 存储路径");
}
const jupyterUrl = `/jupyter/${encodeURIComponent(workspaceId)}/${editorRoute}/${encodedPath}`;
await waitForJupyterReady(jupyterUrl);
return {
edit_session_id: session.edit_session_id,
jupyter_url: jupyterUrl,
expires_at: new Date(Date.now() + 3600_000).toISOString(),
};
}