From 6a57e668dd1adc33314b25cd4d57780a45df3f69 Mon Sep 17 00:00:00 2001 From: Zimeng Xiong Date: Fri, 21 Nov 2025 22:45:14 -0800 Subject: [PATCH] Fix save issue --- .gitignore | 1 + backend/prisma/dev.db | Bin 86016 -> 0 bytes backend/src/index.ts | 35 +++++++ frontend/src/pages/Editor.tsx | 190 +++++++++++++++++++++++++++++----- frontend/src/utils/sync.ts | 2 +- 5 files changed, 202 insertions(+), 26 deletions(-) delete mode 100644 backend/prisma/dev.db diff --git a/.gitignore b/.gitignore index 600b6b8..fcda649 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ frontend/node_modules .DS_Store +backend/prisma/*.db \ No newline at end of file diff --git a/backend/prisma/dev.db b/backend/prisma/dev.db deleted file mode 100644 index 3ccc57ff5d2ab894a42b3217e29b53a0bfe4746a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86016 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 { app.get("/drawings/:id", async (req, res) => { try { const { id } = req.params; + console.log("[API] Fetching drawing", { id }); const drawing = await prisma.drawing.findUnique({ where: { id } }); if (!drawing) { + console.warn("[API] Drawing not found", { id }); return res.status(404).json({ error: "Drawing not found" }); } + console.log("[API] Returning drawing", { + id, + elementCount: (() => { + try { + const parsed = JSON.parse(drawing.elements); + return Array.isArray(parsed) ? parsed.length : null; + } catch (_err) { + return null; + } + })(), + }); + res.json({ ...drawing, elements: JSON.parse(drawing.elements), @@ -195,6 +209,15 @@ app.put("/drawings/:id", async (req, res) => { const { id } = req.params; const { name, elements, appState, collectionId, preview } = req.body; + console.log("[API] Updating drawing", { + id, + hasElements: elements !== undefined, + elementCount: + elements && Array.isArray(elements) ? elements.length : undefined, + hasAppState: appState !== undefined, + hasPreview: preview !== undefined, + }); + const data: any = { version: { increment: 1 }, }; @@ -210,6 +233,18 @@ app.put("/drawings/:id", async (req, res) => { data, }); + console.log("[API] Update complete", { + id, + storedElementCount: (() => { + try { + const parsed = JSON.parse(updatedDrawing.elements); + return Array.isArray(parsed) ? parsed.length : null; + } catch (_err) { + return null; + } + })(), + }); + res.json({ ...updatedDrawing, elements: JSON.parse(updatedDrawing.elements), diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index 7c239e0..b563034 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -22,6 +22,20 @@ interface ElementVersionInfo { versionNonce: number; } +const haveSameElements = (a: readonly any[] = [], b: readonly any[] = []) => { + if (!a || !b) return false; + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + const left = a[i]; + const right = b[i]; + if (!left || !right) return false; + if (left.id !== right.id) return false; + if ((left.version ?? 0) !== (right.version ?? 0)) return false; + if ((left.versionNonce ?? 0) !== (right.versionNonce ?? 0)) return false; + } + return true; +}; + // Move UIOptions outside to prevent re-creation on every render const UIOptions = { canvasActions: { @@ -41,6 +55,7 @@ export const Editor: React.FC = () => { const [isRenaming, setIsRenaming] = useState(false); const [newName, setNewName] = useState(''); const [initialData, setInitialData] = useState(null); + const [isSceneLoading, setIsSceneLoading] = useState(true); const [peers, setPeers] = useState([]); const [me] = useState(getUserIdentity()); @@ -48,9 +63,14 @@ export const Editor: React.FC = () => { const socketRef = useRef(null); const lastCursorEmit = useRef(0); const elementVersionMap = useRef>(new Map()); + const isBootstrappingScene = useRef(true); + const hasHydratedInitialScene = useRef(false); + const isUnmounting = useRef(false); const isSyncing = useRef(false); const cursorBuffer = useRef>(new Map()); const animationFrameId = useRef(0); + const latestElementsRef = useRef([]); + const latestFilesRef = useRef(null); const recordElementVersion = useCallback((element: any) => { elementVersionMap.current.set(element.id, { @@ -69,6 +89,13 @@ export const Editor: React.FC = () => { return previous.version !== nextVersion || previous.versionNonce !== nextNonce; }, []); + useEffect(() => { + isUnmounting.current = false; + return () => { + isUnmounting.current = true; + }; + }, []); + useEffect(() => { if (!id || !isReady) return; @@ -146,6 +173,7 @@ export const Editor: React.FC = () => { }); excalidrawAPI.current.updateScene({ elements: mergedElements }); + latestElementsRef.current = mergedElements; isSyncing.current = false; }); @@ -200,16 +228,26 @@ export const Editor: React.FC = () => { setIsReady(true); }, []); + const buildEmptyScene = useCallback(() => ({ + elements: [], + appState: { + viewBackgroundColor: '#ffffff', + gridSize: null, + collaborators: new Map(), + }, + scrollToContent: true, + }), []); + // ------------------------------------------------------------------ // 1. STABLE SAVE LOGIC (The Fix) // We use a Ref to hold the save function so the debounce wrapper // doesn't need to be recreated on every render. // ------------------------------------------------------------------ - const saveDataRef = useRef<(elements: any, appState: any) => Promise>(null); - const savePreviewRef = useRef<(elements: any, appState: any, files: any) => Promise>(null); + const saveDataRef = useRef<((elements: readonly any[], appState: any) => Promise) | null>(null); + const savePreviewRef = useRef<((elements: readonly any[], appState: any, files: any) => Promise) | null>(null); // Update the ref on every render to ensure it has access to the latest props/state - saveDataRef.current = async (elements, appState) => { + saveDataRef.current = async (elements: readonly any[], appState: any) => { if (!id) return; try { @@ -217,34 +255,56 @@ export const Editor: React.FC = () => { viewBackgroundColor: appState.viewBackgroundColor, gridSize: appState.gridSize, }; - - await api.updateDrawing(id, { - elements, + + const snapshot = latestElementsRef.current ?? elements; + const persistableElements = Array.from(snapshot); + + console.log("[Editor] Saving drawing", { + drawingId: id, + elementCount: persistableElements.length, + hasRenderableElements: persistableElements.some((el: any) => !el?.isDeleted), appState: persistableAppState, }); + + await api.updateDrawing(id, { + elements: persistableElements, + appState: persistableAppState, + }); + + console.log("[Editor] Save complete", { drawingId: id }); } catch (err) { console.error('Failed to save drawing', err); toast.error("Failed to save changes"); } }; - savePreviewRef.current = async (elements, appState, files) => { + savePreviewRef.current = async (elements: readonly any[], appState: any, files: any) => { if (!id) return; try { + const currentSnapshot = latestElementsRef.current ?? elements; + const currentFiles = latestFilesRef.current ?? files; + // Generate preview const svg = await exportToSvg({ - elements, + elements: currentSnapshot, appState: { ...appState, exportBackground: true, viewBackgroundColor: appState.viewBackgroundColor || '#ffffff', }, - files, + files: currentFiles, }); const preview = svg.outerHTML; + console.log("[Editor] Saving preview", { + drawingId: id, + elementCount: currentSnapshot.length, + }); + await api.updateDrawing(id, { preview }); + + console.log("[Editor] Preview save complete", { drawingId: id }); } catch (err) { console.error('Failed to save preview', err); } @@ -298,33 +358,57 @@ export const Editor: React.FC = () => { // 2. DATA LOADING // ------------------------------------------------------------------ useEffect(() => { + isBootstrappingScene.current = true; + hasHydratedInitialScene.current = false; + elementVersionMap.current.clear(); + latestElementsRef.current = []; + latestFilesRef.current = null; + excalidrawAPI.current = null; + setIsReady(false); + setIsSceneLoading(true); + setInitialData(null); + const loadData = async () => { - if (!id) return; + if (!id) { + setInitialData(buildEmptyScene()); + setIsSceneLoading(false); + return; + } try { const data = await api.getDrawing(id); setDrawingName(data.name); const elements = convertToExcalidrawElements(data.elements || []); + latestElementsRef.current = elements; + latestFilesRef.current = null; - // Initialize version tracking with loaded data elements.forEach((el: any) => { recordElementVersion(el); }); + const persistedAppState = data.appState || {}; + const hydratedAppState = { + ...persistedAppState, + viewBackgroundColor: persistedAppState.viewBackgroundColor ?? '#ffffff', + gridSize: persistedAppState.gridSize ?? null, + collaborators: new Map(), + }; + setInitialData({ elements, - appState: { - ...data.appState, - collaborators: new Map(), - }, + appState: hydratedAppState, scrollToContent: true, }); } catch (err) { console.error('Failed to load drawing', err); + toast.error("Failed to load drawing"); + setInitialData(buildEmptyScene()); + } finally { + setIsSceneLoading(false); } }; loadData(); - }, [id, recordElementVersion]); + }, [id, recordElementVersion, buildEmptyScene]); // ------------------------------------------------------------------ // 3. HANDLERS @@ -336,9 +420,11 @@ export const Editor: React.FC = () => { if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); if (excalidrawAPI.current && saveDataRef.current && savePreviewRef.current) { - const elements = excalidrawAPI.current.getSceneElements(); + const elements = excalidrawAPI.current.getSceneElementsIncludingDeleted(); const appState = excalidrawAPI.current.getAppState(); const files = excalidrawAPI.current.getFiles() || null; + latestElementsRef.current = elements; + latestFilesRef.current = files; // Call save immediately, bypassing debounce await saveDataRef.current(elements, appState); // Also update preview @@ -352,6 +438,11 @@ export const Editor: React.FC = () => { }, []); const handleCanvasChange = useCallback((elements: readonly any[], appState: any) => { + if (isUnmounting.current) { + console.log("[Editor] Ignoring change during unmount", { drawingId: id }); + return; + } + // 4. STOP THE ECHO // If this change was caused by a socket update, do NOT broadcast it back if (isSyncing.current) return; @@ -361,14 +452,54 @@ export const Editor: React.FC = () => { ? excalidrawAPI.current.getSceneElementsIncludingDeleted() : elements; + if (!hasHydratedInitialScene.current) { + const matchesInitialSnapshot = haveSameElements(allElements, latestElementsRef.current); + hasHydratedInitialScene.current = true; + isBootstrappingScene.current = false; + + if (matchesInitialSnapshot) { + console.log("[Editor] Skipping hydration change", { + drawingId: id, + elementCount: allElements.length, + }); + return; + } + + console.log("[Editor] First live change after hydration", { + drawingId: id, + elementCount: allElements.length, + }); + } + + latestElementsRef.current = allElements; + + const hasRenderableElements = allElements.some((el: any) => !el?.isDeleted); + if (isBootstrappingScene.current && !hasRenderableElements) { + console.log("[Editor] Bootstrapping guard active", { + drawingId: id, + elementCount: allElements.length, + }); + return; + } + // Trigger Sync (Throttled) broadcastChanges(allElements); // Trigger Fast Save + console.log("[Editor] Queueing save", { + drawingId: id, + elementCount: allElements.length, + hasRenderableElements, + }); debouncedSave(allElements, appState); // Trigger Slow Preview Gen const files = excalidrawAPI.current?.getFiles() || null; + latestFilesRef.current = files; + console.log("[Editor] Queueing preview save", { + drawingId: id, + fileCount: files ? Object.keys(files).length : 0, + }); debouncedSavePreview(allElements, appState, files); }, [debouncedSave, debouncedSavePreview, broadcastChanges]); @@ -456,14 +587,23 @@ export const Editor: React.FC = () => {
- + {initialData ? ( + + ) : ( +
+ + {isSceneLoading ? 'Loading drawing...' : 'Preparing canvas...'} + +
+ )}
diff --git a/frontend/src/utils/sync.ts b/frontend/src/utils/sync.ts index cdaa154..38ecf49 100644 --- a/frontend/src/utils/sync.ts +++ b/frontend/src/utils/sync.ts @@ -15,7 +15,7 @@ export const reconcileElements = ( const getVersionNonce = (element: any) => element?.versionNonce ?? 0; const getUpdated = (element: any) => { const value = element?.updated; - return typeof value === 'number' ? value : Number(value) || 0; + return typeof value === "number" ? value : Number(value) || 0; }; remoteElements.forEach((remoteEl) => {