Files
model-platform/frontend/app/features/platform/hooks/useEditSessionLifecycle.ts
T
tao.chen b600c6810b fix: P0-5 — upload-status rollback, streaming copy (LOCAL only), user re-verify, honest lock
B1: `_mark_upload_failed_and_raise` now commits on a separate session
  - Helper takes `request + upload_id`, opens a fresh session from
    `request.app.state.session_factory` and commits there before raising.
  - Closes the named-lock connection-pool leak Codex flagged: the old
    "commit-on-the-same-session" implementation could return the
    GET_LOCK connection to the pool before the enclosing
    `finally: release_named_lock` ran, leaking `mp:<hash>` for up to
    `pool_recycle` and re-opening the same-key upload race.
  - Same helper now used by `create_server_object_payload`'s put-failure
    branch — two failure paths have identical semantics.

B2: streaming copy for soft-delete + restore (`get_stream() + put()`)
  - LOCAL backend: zero-copy (aiofiles stream write). OOM fixed.
  - S3 backend: still OOMs on multi-GB objects — `put()` materializes
    the async iter via `b"".join(chunks)`. Multipart `put` is a
    follow-up; do NOT claim "OOM fixed on production" since production
    defaults to S3.

C1: worker re-verifies `Users.status='active' AND is_deleted=0`
  - `_assert_user_active` called from `_execution_context` after
    resolving `triggered_by`; skips `SYSTEM_CRON_USER_ID`.
  - `USER_DISABLED` error_code goes into the `NODE_FINISHED_EVENT`
    outbox payload — `schedule_node_runs` has no `error_code` column,
    the row only carries the `message` text. Docstrings corrected to
    say so explicitly (previous docstring falsely promised row-level
    observability).

F1: honest browser-local file lock
  - `api.ts` `acquireFileLock/heartbeatFileLock/releaseFileLock/
    releaseFileLockOnUnload` are now no-ops with comments stating they
    never call the network.
  - `scriptWorkspaceStore` dropped `tickHeartbeats`; `tickCleanup`
    simplified to just clear cache.
  - `useEditSessionLifecycle` dropped its 15s heartbeat `setInterval`.
  - `ScriptWorkspace.tsx` renders `.local-lock-banner` info bar when
    `isEditing`. Two tabs may still silently last-write — banner is the
    only guard (acceptable disclosure-only tradeoff).

Dead code: deleted the duplicate `upload_bytes_to_session` in
`backend/src/backend/storage_api.py`. The `services.storage` import
is now the only source of the function; `create_upload_record`'s
docstring updated to point at `backend.resources`.
2026-08-20 12:07:52 +08:00

56 lines
1.8 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 { useEffect } from "react";
import {
editSessionHandle,
useScriptWorkspaceStore,
} from "../state/scriptWorkspaceStore";
type ActivePage = "home" | "scripts" | "schedules" | "system";
/**
* 必须在 layout 层挂载,不能放在 ScriptsPage。
* 原因:cleanup / beforeunload 需要在用户切到 /schedules 时仍运行,否则跨页
* 浏览 10 分钟以上再回来时,sessionCache 里塞的全是陈旧的本地锁。
* 心跳已删除——本地锁没有过期概念,存不存在 15s 定时器都一样。
*/
export function useEditSessionLifecycle({
activePage,
}: {
activePage: ActivePage;
}) {
// 1) 切到非 scripts 页时,如果当前有 active session,结束编辑
useEffect(() => {
if (activePage === "scripts") return;
const editBusy = useScriptWorkspaceStore.getState().editBusy;
if (editBusy) return;
if (!editSessionHandle.current) return;
void useScriptWorkspaceStore.getState().endEditing(true, false);
}, [activePage]);
// 2) 本地缓存回收(60s 检查一次,10 分钟无活动的本地锁清掉)
useEffect(() => {
let cleanupRunning = false;
const cleanupTimer = window.setInterval(() => {
if (cleanupRunning) return;
cleanupRunning = true;
try {
useScriptWorkspaceStore.getState().tickCleanup();
} finally {
cleanupRunning = false;
}
}, 60 * 1000);
return () => {
window.clearInterval(cleanupTimer);
};
}, []);
// 3) 卸载前清理本地锁引用(详见 store.releaseActiveOnUnload
useEffect(() => {
const handleUnload = () => {
useScriptWorkspaceStore.getState().releaseActiveOnUnload();
};
window.addEventListener("beforeunload", handleUnload);
return () => window.removeEventListener("beforeunload", handleUnload);
}, []);
}