From 040a8546a3f8b0395aa33bc0b872c28aa70bb99f Mon Sep 17 00:00:00 2001 From: Kresna <13603341+slaveofcode@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:12:11 +0700 Subject: [PATCH] fix(whiteboard): harden autosave write and surface save failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I could not reproduce data loss in Chromium or WebKit (draw, restore a complex bound scene, delete, reload — all persist), so this hardens the persistence layer against any environment-specific failure that a simple repro misses: - saveScene JSON round-trips the scene before the IndexedDB put, so a live Excalidraw element object can never trigger a silent DataCloneError that drops the write. It returns whether the write succeeded. - flushSave only advances savedKey on a *successful* write (a failed save no longer looks already-saved and now retries), and shows a red 'Save failed' status instead of a false 'Saved'. Also bumps the whiteboard chunk hash so stale service-worker clients pull the fixed code. --- src/islands/draw/Whiteboard.tsx | 29 +++++++++++++++++++++++++---- src/tools/draw/whiteboard.store.ts | 13 ++++++++++--- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/islands/draw/Whiteboard.tsx b/src/islands/draw/Whiteboard.tsx index 21209bd..dc36fd7 100644 --- a/src/islands/draw/Whiteboard.tsx +++ b/src/islands/draw/Whiteboard.tsx @@ -29,6 +29,7 @@ const TR: Record('saved'); + const [saveState, setSaveState] = useState<'saved' | 'unsaved' | 'saving' | 'error'>('saved'); // Optionally hide the navbar (only while expanded) for maximum canvas space. const [navHidden, setNavHidden] = useState(false); @@ -138,10 +141,19 @@ export default function Whiteboard({ lang = 'en' }: { lang?: Lang }) { saving.current = true; dirty.current = false; const scene = latestScene.current; - savedKey.current = computeSceneKey(scene.elements, scene.files, sceneVersionOf.current); + const key = computeSceneKey(scene.elements, scene.files, sceneVersionOf.current); setSaveState('saving'); - await saveScene(scene); + const ok = await saveScene(scene); saving.current = false; + if (!ok) { + // The write failed — re-mark dirty so it retries, and show it honestly + // rather than a false "Saved". Do NOT advance savedKey, or the change + // would look already-saved and never retry. + dirty.current = true; + setSaveState('error'); + return; + } + savedKey.current = key; // A change may have arrived during the write; only show "Saved" if still clean. setSaveState(dirty.current ? 'unsaved' : 'saved'); }; @@ -247,7 +259,16 @@ export default function Whiteboard({ lang = 'en' }: { lang?: Lang }) { return () => { window.removeEventListener('resize', measure); header.style.display = ''; }; }, [expanded, navHidden]); - const statusIndicator = saveState === 'unsaved' ? ( + const statusIndicator = saveState === 'error' ? ( + + ) : saveState === 'unsaved' ? (