Skip to content

Commit 66965a0

Browse files
committed
fix(capture): prevent concurrent canvas overwrite
1 parent 908011d commit 66965a0

File tree

2 files changed

+53
-0
lines changed

2 files changed

+53
-0
lines changed

src/engine/canvasCapture.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,50 @@ describe("canvasCapture", () => {
227227
expect(updated.nodes[0].text).toBe("Updated");
228228
});
229229

230+
it("aborts configured text-node write when canvas changed concurrently", async () => {
231+
const canvasFile = {
232+
path: "Boards/Plan.canvas",
233+
basename: "Plan",
234+
extension: "canvas",
235+
};
236+
let modifyCalls = 0;
237+
let readCalls = 0;
238+
const initial = JSON.stringify({
239+
nodes: [{ id: "t1", type: "text", text: "Current" }],
240+
});
241+
const changed = JSON.stringify({
242+
nodes: [{ id: "t1", type: "text", text: "Changed elsewhere" }],
243+
});
244+
245+
const app = createApp({
246+
vault: {
247+
getAbstractFileByPath: (path: string) =>
248+
path === "Boards/Plan.canvas" ? (canvasFile as any) : null,
249+
read: async () => {
250+
readCalls += 1;
251+
return readCalls === 1 ? initial : changed;
252+
},
253+
modify: async () => {
254+
modifyCalls += 1;
255+
},
256+
},
257+
});
258+
259+
const target = await resolveConfiguredCanvasCaptureTarget(
260+
app,
261+
"Boards/Plan.canvas",
262+
"t1",
263+
"append",
264+
);
265+
expect(target.kind).toBe("text");
266+
if (target.kind !== "text") return;
267+
268+
await expect(
269+
setCanvasTextCaptureContent(app, target, "Updated"),
270+
).rejects.toThrow("Canvas target changed while capture was running");
271+
expect(modifyCalls).toBe(0);
272+
});
273+
230274
it("fails configured canvas capture when node id does not exist", async () => {
231275
const canvasFile = {
232276
path: "Boards/Plan.canvas",

src/engine/canvasCapture.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export type ConfiguredCanvasCaptureTarget =
7676
kind: "text";
7777
source: "configured";
7878
canvasFile: TFile;
79+
rawCanvas: string;
7980
canvasData: StoredCanvasData;
8081
nodeData: CanvasDataNodeLike;
8182
nodeIndex: number;
@@ -350,6 +351,7 @@ export async function resolveConfiguredCanvasCaptureTarget(
350351
kind: "text",
351352
source: "configured",
352353
canvasFile: abstractCanvasFile,
354+
rawCanvas,
353355
canvasData,
354356
nodeData,
355357
nodeIndex,
@@ -404,6 +406,13 @@ export async function setCanvasTextCaptureContent(
404406
return;
405407
}
406408

409+
const latestRawCanvas = await app.vault.read(target.canvasFile);
410+
if (latestRawCanvas !== target.rawCanvas) {
411+
throw new ChoiceAbortError(
412+
"Canvas target changed while capture was running. Re-run capture to avoid overwriting newer Canvas edits.",
413+
);
414+
}
415+
407416
target.nodeData.text = nextText;
408417
target.canvasData.nodes[target.nodeIndex] = target.nodeData;
409418
await app.vault.modify(

0 commit comments

Comments
 (0)