同步封面和标题更新后的画布版本
自动封面和标题更新会推进画布版本,但页面未同步,导致后续节点保存持续冲突。元数据写入返回前一版本,客户端仅在与本地版本衔接时推进版本,并与画布保存共用队列;处理结果在合并成功后才更新本地版本。视频画布导入也按节点保存、标题更新顺序执行。 验证:17 项画布能力与同步测试通过,包含封面更新后的保存、不覆盖未同步的远端改动、结果合并冲突和切换画布;TypeScript 检查通过。
This commit is contained in:
parent
6b7b24a86e
commit
6053271352
@ -207,7 +207,11 @@ export async function PUT(
|
||||
),
|
||||
)
|
||||
.returning();
|
||||
return NextResponse.json({ success: true, data: saved });
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: saved,
|
||||
previousCanvasRevision: existingProject?.updatedAt.toISOString() ?? null,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("更新/创建画布失败:", error);
|
||||
|
||||
@ -3947,20 +3947,18 @@ export default function CanvasEditorLegacyPage({
|
||||
loadCanvas(normalized.canvasData);
|
||||
setProjectTitle(nextTitle);
|
||||
|
||||
const [saveResult, updateResult] = await Promise.all([
|
||||
canvasService.saveCanvasState(
|
||||
const saveResult = await canvasService.saveCanvasState(
|
||||
id,
|
||||
normalized.canvasData,
|
||||
activeCanvasWorkspaceType
|
||||
),
|
||||
canvasService.updateProject(
|
||||
);
|
||||
const updateResult = await canvasService.updateProject(
|
||||
id,
|
||||
{
|
||||
title: nextTitle,
|
||||
},
|
||||
activeCanvasWorkspaceType
|
||||
),
|
||||
]);
|
||||
);
|
||||
|
||||
if (!saveResult.success) {
|
||||
throw new Error(saveResult.error || "导入后保存失败");
|
||||
@ -5350,11 +5348,11 @@ export default function CanvasEditorLegacyPage({
|
||||
|
||||
try {
|
||||
if (autoThumbnailSyncedRef.current !== autoTitleKey) {
|
||||
await canvasService.updateProject(
|
||||
await canvasSaveQueue.current!.enqueue(() => canvasService.updateProject(
|
||||
id,
|
||||
{ thumbnail: imageSrc },
|
||||
activeCanvasWorkspaceType
|
||||
);
|
||||
));
|
||||
autoThumbnailSyncedRef.current = autoTitleKey;
|
||||
}
|
||||
|
||||
@ -5370,11 +5368,11 @@ export default function CanvasEditorLegacyPage({
|
||||
const data = await res.json();
|
||||
if (data.title) {
|
||||
setProjectTitle(data.title);
|
||||
await canvasService.updateProject(
|
||||
await canvasSaveQueue.current!.enqueue(() => canvasService.updateProject(
|
||||
id,
|
||||
{ title: data.title },
|
||||
activeCanvasWorkspaceType
|
||||
);
|
||||
));
|
||||
toast.success(`${workspaceLabel}已自动命名为:${data.title}`);
|
||||
}
|
||||
} catch (error) {
|
||||
@ -5425,11 +5423,11 @@ export default function CanvasEditorLegacyPage({
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await canvasService.updateProject(
|
||||
const result = await canvasSaveQueue.current!.enqueue(() => canvasService.updateProject(
|
||||
id,
|
||||
{ title: editTitle.trim() },
|
||||
activeCanvasWorkspaceType
|
||||
);
|
||||
));
|
||||
if (result.success) {
|
||||
setProjectTitle(editTitle.trim());
|
||||
} else {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { CanvasCapabilityClientCommand } from "@/lib/canvas/capabilities/client-command";
|
||||
import { mergeCanvasSnapshots } from "@/lib/video-canvas/merge-snapshot";
|
||||
import { canvasService } from "@/services/canvas";
|
||||
import { useCanvasStore } from "@/store/canvas";
|
||||
|
||||
/** Runs after the editor has saved and while its save queue is reserved. */
|
||||
@ -21,8 +20,9 @@ export async function executeCanvasToolbarCapability(
|
||||
throw new Error(payload?.error || "图片处理失败");
|
||||
}
|
||||
|
||||
const result = await canvasService.getProject(projectId);
|
||||
if (!result.success || !result.data?.canvasData) {
|
||||
const projectResponse = await fetch(`/api/canvas/${projectId}`);
|
||||
const result = await projectResponse.json();
|
||||
if (!projectResponse.ok || !result.success || !result.data?.canvasData) {
|
||||
throw new Error(result.error || "结果已生成,但画布同步失败");
|
||||
}
|
||||
const current = useCanvasStore.getState();
|
||||
|
||||
@ -248,6 +248,17 @@ export const canvasService = {
|
||||
),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data?.success && data?.previousCanvasRevision && data?.data?.updatedAt) {
|
||||
const current = useCanvasStore.getState();
|
||||
// Metadata-only writes advance the same revision used by canvas saves.
|
||||
// Adopt it only when no unseen canvas revision lies between them.
|
||||
if (
|
||||
current.projectId === id &&
|
||||
current.canvasRevision === data.previousCanvasRevision
|
||||
) {
|
||||
current.setCanvasRevision(String(data.data.updatedAt), String(data.data.updatedAt));
|
||||
}
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
return { success: false, error: '更新画布失败' };
|
||||
|
||||
@ -3,6 +3,7 @@ import { createCanvasSaveQueue } from "@/lib/client/canvas-save-queue";
|
||||
import { executeCanvasToolbarCapability } from "@/lib/client/canvas-toolbar-capability";
|
||||
import { createCanvasCapabilityClientCommand } from "@/lib/canvas/capabilities/client-command";
|
||||
import { useCanvasStore } from "@/store/canvas";
|
||||
import { canvasService } from "@/services/canvas";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const node = (id: string) => ({ id, type: "image", position: { x: 0, y: 0 },
|
||||
@ -79,3 +80,37 @@ test("switching projects during processing does not load the old result into the
|
||||
expect(useCanvasStore.getState().nodes.map(n => n.id)).toEqual(["other"]);
|
||||
expect(useCanvasStore.getState().canvasRevision).toBe("r1");
|
||||
});
|
||||
|
||||
test("cover updates advance the local revision before the next queued save", async () => {
|
||||
const queue = createCanvasSaveQueue();
|
||||
globalThis.fetch = (async () => Response.json({
|
||||
success: true, previousCanvasRevision: "r1", data: { updatedAt: "r2" },
|
||||
})) as typeof fetch;
|
||||
const cover = queue.enqueue(() => canvasService.updateProject("project-a", { thumbnail: "https://example.com/source.png" }));
|
||||
const nextSave = queue.enqueue(async () => useCanvasStore.getState().canvasRevision);
|
||||
await cover;
|
||||
expect(await nextSave).toBe("r2");
|
||||
});
|
||||
|
||||
test("metadata changes never adopt an unseen remote canvas revision", async () => {
|
||||
globalThis.fetch = (async () => Response.json({
|
||||
success: true, previousCanvasRevision: "external-edit", data: { updatedAt: "r3" },
|
||||
})) as typeof fetch;
|
||||
await canvasService.updateProject("project-a", { title: "Renamed" });
|
||||
expect(useCanvasStore.getState().canvasRevision).toBe("r1");
|
||||
});
|
||||
|
||||
test("a result merge conflict retains both the local edit and its original revision", async () => {
|
||||
globalThis.fetch = (async (url) => {
|
||||
if (String(url).endsWith("/capabilities")) {
|
||||
useCanvasStore.getState().moveNode("source", { x: 50, y: 80 });
|
||||
return Response.json({ success: true });
|
||||
}
|
||||
return Response.json({ success: true, data: { updatedAt: "r2", canvasData: {
|
||||
nodes: [{ ...node("source"), position: { x: 200, y: 80 } }, node("result")], edges: [],
|
||||
} } });
|
||||
}) as typeof fetch;
|
||||
await expect(executeCanvasToolbarCapability(command())).rejects.toThrow("画布有冲突");
|
||||
expect(useCanvasStore.getState().canvasRevision).toBe("r1");
|
||||
expect(useCanvasStore.getState().nodes[0].position).toEqual({ x: 50, y: 80 });
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user