Skip to content

Commit 9bfedc8

Browse files
committed
fix(studio): a failed save never blocks undo, and a nudge burst always ends its pending edit
1 parent f631398 commit 9bfedc8

5 files changed

Lines changed: 70 additions & 24 deletions

File tree

‎packages/studio/src/App.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
256256
showToast,
257257
syncHistoryPreviewAfterApply: previewPersistence.syncHistoryPreviewAfterApply,
258258
showHistoryRestoreNow: previewPersistence.showHistoryRestoreNow,
259-
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
259+
waitForPendingDomEditSaves: previewPersistence.settlePendingEdits,
260260
handleCopy,
261261
handlePaste,
262262
handleCut,

‎packages/studio/src/components/editor/useDomEditNudge.test.tsx‎

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const REST_RECT: OverlayRect = {
2828
// Stable across renders on purpose: the test targets the `selection` identity
2929
// key specifically, so `groupSelections` must not itself be a source of churn.
3030
const EMPTY_GROUP_SELECTIONS: DomEditSelection[] = [];
31+
let flushNudge = () => {};
3132

3233
function Harness({
3334
selection,
@@ -36,7 +37,7 @@ function Harness({
3637
selection: DomEditSelection | null;
3738
onPathOffsetCommit: UseDomEditNudgeParams["onPathOffsetCommitRef"]["current"];
3839
}) {
39-
useDomEditNudge({
40+
flushNudge = useDomEditNudge({
4041
selection,
4142
groupSelections: EMPTY_GROUP_SELECTIONS,
4243
allowCanvasMovement: true,
@@ -50,7 +51,7 @@ function Harness({
5051
onBlockedMoveRef: makeRef(() => {}),
5152
onPathOffsetCommitRef: makeRef(onPathOffsetCommit),
5253
onGroupPathOffsetCommitRef: makeRef(async () => {}),
53-
});
54+
}).flushNudge;
5455
return null;
5556
}
5657

@@ -265,6 +266,7 @@ describe("useDomEditNudge carries the route its press chose", () => {
265266
describe("useDomEditNudge — undo right after a burst", () => {
266267
it("undo's drain commits a burst still inside its debounce and waits for its save", async () => {
267268
__resetForTests();
269+
vi.useFakeTimers();
268270
const root = createRoot(document.body.appendChild(document.createElement("div")));
269271
const element = document.body.appendChild(document.createElement("div"));
270272
element.id = "dot-undo";
@@ -282,7 +284,9 @@ describe("useDomEditNudge — undo right after a burst", () => {
282284

283285
let drained = false;
284286
const drain = flushStudioPendingEdits().then(() => (drained = true));
285-
await vi.waitFor(() => expect(commit).toHaveBeenCalledTimes(1));
287+
expect(commit).toHaveBeenCalledTimes(1);
288+
vi.useRealTimers();
289+
await new Promise((resolve) => setTimeout(resolve, 20));
286290
expect(drained).toBe(false);
287291
saved();
288292
await drain;
@@ -318,3 +322,28 @@ describe("useDomEditNudge — undo right after a burst", () => {
318322
act(() => root.unmount());
319323
});
320324
});
325+
326+
describe("useDomEditNudge — a commit that throws", () => {
327+
it("still ends the burst's pending edit, so undo and export never wait on it", async () => {
328+
__resetForTests();
329+
const root = createRoot(document.body.appendChild(document.createElement("div")));
330+
const element = document.body.appendChild(document.createElement("div"));
331+
element.id = "dot-throws";
332+
const commit = vi.fn(() => {
333+
throw new Error("The commit threw.");
334+
});
335+
act(() => {
336+
root.render(
337+
React.createElement(Harness, {
338+
selection: makeSelection("Dot", element),
339+
onPathOffsetCommit: commit,
340+
}),
341+
);
342+
});
343+
act(() => dispatchArrowRight());
344+
expect(hasStudioPendingEdits()).toBe(true);
345+
expect(() => flushNudge()).toThrow("The commit threw.");
346+
await vi.waitFor(() => expect(hasStudioPendingEdits()).toBe(false));
347+
act(() => root.unmount());
348+
});
349+
});

‎packages/studio/src/components/editor/useDomEditNudge.ts‎

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -159,22 +159,26 @@ export function useDomEditNudge(params: UseDomEditNudgeParams): { flushNudge: ()
159159
plainTranslate: member.plainTranslate,
160160
}));
161161
const p = paramsRef.current;
162-
const commit = session.isGroup
163-
? p.onGroupPathOffsetCommitRef.current(updates)
164-
: p.onPathOffsetCommitRef.current(updates[0].selection, updates[0].next, {
165-
plainTranslate: updates[0].plainTranslate,
166-
});
167-
const saved = Promise.resolve(commit)
168-
.catch(() => {
169-
for (const member of session.members) {
170-
if (isStudioManualEditGestureCurrent(member.element, member.gestureToken)) {
171-
restoreStudioPathOffset(member.element, member.initialPathOffset);
162+
let saved: Promise<unknown> | undefined;
163+
try {
164+
const commit = session.isGroup
165+
? p.onGroupPathOffsetCommitRef.current(updates)
166+
: p.onPathOffsetCommitRef.current(updates[0].selection, updates[0].next, {
167+
plainTranslate: updates[0].plainTranslate,
168+
});
169+
saved = Promise.resolve(commit)
170+
.catch(() => {
171+
for (const member of session.members) {
172+
if (isStudioManualEditGestureCurrent(member.element, member.gestureToken)) {
173+
restoreStudioPathOffset(member.element, member.initialPathOffset);
174+
}
172175
}
173-
}
174-
})
175-
.finally(() => endManualOffsetDragMembers(session.members));
176-
session.endPendingEdit(saved);
177-
return saved;
176+
})
177+
.finally(() => endManualOffsetDragMembers(session.members));
178+
return saved;
179+
} finally {
180+
session.endPendingEdit(saved);
181+
}
178182
};
179183
const commitSessionRef = useRef(commitSession);
180184
commitSessionRef.current = commitSession;

‎packages/studio/src/hooks/useEditHistoryActions.paint.test.tsx‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ async function studio() {
8383
showToast: () => {},
8484
syncHistoryPreviewAfterApply: persistence.syncHistoryPreviewAfterApply,
8585
showHistoryRestoreNow: persistence.showHistoryRestoreNow,
86-
waitForPendingDomEditSaves: persistence.waitForPendingDomEditSaves,
86+
waitForPendingDomEditSaves: persistence.settlePendingEdits,
8787
});
8888
return null;
8989
}
@@ -225,16 +225,22 @@ it("an undo pressed while a nudge waits for more keys never shows the move befor
225225
expect(s.box()).toBe("50px");
226226
});
227227

228-
it("an undo pressed while an edit's save fails undoes the edit before it, file and box alike", async () => {
228+
it("an undo pressed while a queued save fails undoes the edit before it, file and box alike", async () => {
229229
const s = await studio();
230230
await s.edit();
231231
const box = s.element("box");
232232
let fail!: () => void;
233233
const handleDomStyleCommit = vi.fn(async () => {
234234
box.style.left = "70px";
235-
await new Promise<void>((resolve) => (fail = resolve));
236-
box.style.left = "50px";
237-
throw new Error("The save failed.");
235+
try {
236+
await s.persistence().queueDomEditSave(async () => {
237+
await new Promise<void>((resolve) => (fail = resolve));
238+
throw new Error("The save failed.");
239+
});
240+
} catch (error) {
241+
box.style.left = "50px";
242+
throw error;
243+
}
238244
});
239245
let actions!: ReturnType<typeof useDomEditActionsContext>;
240246
function Canvas() {
@@ -252,6 +258,7 @@ it("an undo pressed while an edit's save fails undoes the edit before it, file a
252258
const failed = actions.handleDomStyleCommit("left", "70px");
253259

254260
const undone = s.actions().undo();
261+
await vi.waitFor(() => expect(fail).toBeTypeOf("function"));
255262
fail();
256263
await expect(failed).rejects.toThrow("The save failed.");
257264
await act(() => undone);

‎packages/studio/src/hooks/usePreviewPersistence.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ export function usePreviewPersistence({
159159
if (result.status !== "clean") throw result.error;
160160
}, [drainPendingDomEditSaves]);
161161

162+
const settlePendingEdits = useCallback(async (): Promise<void> => {
163+
await flushStudioPendingEdits();
164+
await domEditSaveQueueRef.current?.waitForIdle();
165+
}, []);
166+
162167
const resetDomEditSaveQueueBreaker = useCallback(() => {
163168
domEditSaveQueueRef.current?.reset();
164169
setDomEditSaveQueuePaused(null);
@@ -256,6 +261,7 @@ export function usePreviewPersistence({
256261
queueDomEditSave,
257262
drainPendingDomEditSaves,
258263
waitForPendingDomEditSaves,
264+
settlePendingEdits,
259265
domEditSaveQueuePaused,
260266
resetDomEditSaveQueueBreaker,
261267
applyCurrentStudioManualEditsToPreview,

0 commit comments

Comments
 (0)