fix: 消除保存并发竞态——同步锁防重复保存,冲突判定先比对内容
This commit is contained in:
parent
6cb10db375
commit
a5d9f0bfee
@ -129,14 +129,23 @@ function registerIpc(): void {
|
||||
});
|
||||
ipcMain.handle("flows:get", (_event, id: string) => getFlow(id));
|
||||
ipcMain.handle("flows:save", (_event, flow: FlowDefinition, opts?: FlowSaveOptions): FlowSaveResult => {
|
||||
if (!opts?.force && opts?.expectedUpdatedAt) {
|
||||
const disk = getFlow(flow.id);
|
||||
if (disk && disk.meta?.updatedAt && disk.meta.updatedAt !== opts.expectedUpdatedAt) {
|
||||
return { conflict: true, diskFlow: disk };
|
||||
if (!opts?.force && opts?.expectedUpdatedAt) {
|
||||
let disk: FlowDefinition | null = null;
|
||||
try {
|
||||
disk = getFlow(flow.id);
|
||||
} catch {
|
||||
disk = null; // 磁盘文件不存在或损坏时视为全新保存
|
||||
}
|
||||
if (disk && disk.meta?.updatedAt && disk.meta.updatedAt !== opts.expectedUpdatedAt) {
|
||||
// 内容一致(仅 updatedAt 差异,如并发/重复保存)不视为冲突
|
||||
const normalize = (item: FlowDefinition) => JSON.stringify({ ...item, meta: undefined });
|
||||
if (normalize(disk) !== normalize(flow)) {
|
||||
return { conflict: true, diskFlow: disk };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { conflict: false, flow: saveFlow(flow) };
|
||||
});
|
||||
return { conflict: false, flow: saveFlow(flow) };
|
||||
});
|
||||
ipcMain.handle("flows:delete", (_event, id: string) => {
|
||||
deleteFlow(id);
|
||||
return true;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ArrowLeft, Check, CircleAlert, CirclePlay, Lightbulb, Loader2, Redo2, Save, Square, Undo2, X } from "lucide-react";
|
||||
import type { Catalog } from "@shared/types";
|
||||
import { useEditorStore } from "../store/editorStore";
|
||||
@ -34,6 +34,9 @@ export function EditorPage({ catalog, onToast, onBack }: EditorPageProps) {
|
||||
const startRun = useRunStore((state) => state.start);
|
||||
const stopRun = useRunStore((state) => state.stop);
|
||||
const [saving, setSaving] = useState(false);
|
||||
// 同步锁:防止自动保存定时器与按钮点击(或双击)在同一帧内触发两次并发保存,
|
||||
// 并发保存会让后到者误判磁盘版本冲突(saving state 异步更新拦不住同一帧的第二次调用)
|
||||
const savingRef = useRef(false);
|
||||
const [saveConflict, setSaveConflict] = useState(false);
|
||||
const [savedAt, setSavedAt] = useState<string | null>(null);
|
||||
const [validationIssues, setValidationIssues] = useState<FlowValidationIssue[] | null>(null);
|
||||
@ -57,7 +60,8 @@ export function EditorPage({ catalog, onToast, onBack }: EditorPageProps) {
|
||||
|
||||
const save = useCallback(async (opts?: { interactive?: boolean }) => {
|
||||
const current = useEditorStore.getState().flow;
|
||||
if (!current || saving) return null;
|
||||
if (!current || savingRef.current) return null;
|
||||
savingRef.current = true;
|
||||
setSaving(true);
|
||||
try {
|
||||
const result = await window.rpa.flows.save(current, {
|
||||
@ -99,9 +103,10 @@ export function EditorPage({ catalog, onToast, onBack }: EditorPageProps) {
|
||||
onToast(error instanceof Error ? error.message : String(error), "error");
|
||||
return null;
|
||||
} finally {
|
||||
savingRef.current = false;
|
||||
setSaving(false);
|
||||
}
|
||||
}, [loadFlow, markSaved, onToast, saving, syncSavedFlow]);
|
||||
}, [loadFlow, markSaved, onToast, syncSavedFlow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dirty || saveConflict) return;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user