Fix canvas transfer timeouts and move images out of snapshots
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { EdgeChange, NodeChange } from "@xyflow/react";
|
||||
import { prepareManagedImages } from "@/lib/canvas-assets";
|
||||
import { api, ApiError } from "@/lib/api";
|
||||
import { canvasReducer, decodeCanvas, type CanvasAction, type CanvasDocument, type CanvasHistory, type CanvasNode } from "@/lib/canvas";
|
||||
import { CanvasSync, canvasFingerprint, canvasPath, decodeSnapshot, readLocalRecovery, type CanvasSnapshot } from "@/lib/canvas-sync";
|
||||
@@ -24,6 +25,7 @@ export function useCanvasTree(root: string, workspace: string, initial: CanvasDo
|
||||
const retryOperation = useRef<(() => Promise<CanvasCommandResult>) | null>(null);
|
||||
const locked = useRef(false), mounted = useRef(true), pause = useRef(paused); pause.current = paused;
|
||||
const writers = useRef(new Map<string, CanvasSync>());
|
||||
const migrationAbort = useRef<AbortController | null>(null);
|
||||
const loading = useRef(new Set<string>());
|
||||
const [retryLoad, setRetryLoad] = useState(0);
|
||||
const [recoveries, setRecoveries] = useState(new Map<string, CanvasDocument>());
|
||||
@@ -54,7 +56,7 @@ export function useCanvasTree(root: string, workspace: string, initial: CanvasDo
|
||||
}
|
||||
// Initialization never writes a snapshot to the server.
|
||||
if (snapshot && !writers.current.has(root)) install(root, snapshot);
|
||||
useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
|
||||
useEffect(() => { mounted.current = true; return () => { mounted.current = false; migrationAbort.current?.abort(); }; }, []);
|
||||
const update = (boardId: string, action: CanvasAction) => {
|
||||
if (locked.current || retryOperation.current) return;
|
||||
const before = current.current.get(boardId); if (!before) return;
|
||||
@@ -74,7 +76,12 @@ export function useCanvasTree(root: string, workspace: string, initial: CanvasDo
|
||||
const adjusted = referenceAfterMoves(root, current.current, view.current.boards, ref.boardId, moves);
|
||||
const next = { ...change, id: ref.id };
|
||||
if (next.type === "position" && next.position) next.position = { x: (next.position.x - adjusted.origin.x) / ref.scale, y: (next.position.y - adjusted.origin.y) / ref.scale };
|
||||
if (next.type === "dimensions" && next.dimensions) next.dimensions = { width: next.dimensions.width / ref.scale, height: next.dimensions.height / ref.scale };
|
||||
if (next.type === "dimensions") {
|
||||
// ResizeObserver measurements are editor state. Only an explicit resize
|
||||
// gesture may write node dimensions back to the owning snapshot.
|
||||
if (!next.resizing) next.setAttributes = false;
|
||||
if (next.dimensions) next.dimensions = { width: next.dimensions.width / ref.scale, height: next.dimensions.height / ref.scale };
|
||||
}
|
||||
groups.set(ref.boardId, [...(groups.get(ref.boardId) || []), next]);
|
||||
}
|
||||
for (const [boardId, changes] of groups) update(boardId, { type: "nodes", changes });
|
||||
@@ -148,7 +155,7 @@ export function useCanvasTree(root: string, workspace: string, initial: CanvasDo
|
||||
for (const [boardId, document] of current.current) try {
|
||||
if (pendingRecovery.current.has(boardId)) continue;
|
||||
const writer = writers.current.get(boardId);
|
||||
localStorage.setItem(`endoleap_canvas_v1:${workspace}:${boardId}:recovery`, JSON.stringify({ document: decodeCanvas(JSON.stringify(document)), baseVersion: writer?.version || 1, dirty: writer ? writer.saved !== canvasFingerprint(document) : false }));
|
||||
localStorage.setItem(`endoleap_canvas_v1:${workspace}:${boardId}:recovery`, JSON.stringify({ document: decodeCanvas(JSON.stringify(document)), baseVersion: writer?.version || 1, dirty: writer ? writer.needsSave(document) : false }));
|
||||
} catch { setError("local_backup_failed"); }
|
||||
}, 250);
|
||||
const save = setTimeout(() => {
|
||||
@@ -162,7 +169,7 @@ export function useCanvasTree(root: string, workspace: string, initial: CanvasDo
|
||||
if (pause.current) return;
|
||||
for (const [boardId, document] of current.current) {
|
||||
if (pendingRecovery.current.has(boardId)) continue;
|
||||
try { const writer = writers.current.get(boardId); localStorage.setItem(`endoleap_canvas_v1:${workspace}:${boardId}:recovery`, JSON.stringify({ document: decodeCanvas(JSON.stringify(document)), baseVersion: writer?.version || 1, dirty: writer ? writer.saved !== canvasFingerprint(document) : false })); } catch { /* Existing draft is retained if storage is full. */ }
|
||||
try { const writer = writers.current.get(boardId); localStorage.setItem(`endoleap_canvas_v1:${workspace}:${boardId}:recovery`, JSON.stringify({ document: decodeCanvas(JSON.stringify(document)), baseVersion: writer?.version || 1, dirty: writer ? writer.needsSave(document) : false })); } catch { /* Existing draft is retained if storage is full. */ }
|
||||
}
|
||||
};
|
||||
window.addEventListener("pagehide", backup);
|
||||
@@ -267,6 +274,21 @@ export function useCanvasTree(root: string, workspace: string, initial: CanvasDo
|
||||
}
|
||||
tick((v) => v + 1);
|
||||
}
|
||||
async function migrateImages() {
|
||||
return exclusive(async () => {
|
||||
await flush();
|
||||
const before = current.current.get(root)!;
|
||||
const controller = new AbortController(); migrationAbort.current = controller;
|
||||
let document: CanvasDocument;
|
||||
try { document = await prepareManagedImages(before, api, workspace, controller.signal); }
|
||||
finally { migrationAbort.current = null; }
|
||||
if (!mounted.current || controller.signal.aborted) throw new DOMException("Aborted", "AbortError");
|
||||
undo.current.push({ boardId: root, before }); redo.current = [];
|
||||
publish(new Map(current.current).set(root, document));
|
||||
const writer = writers.current.get(root)!;
|
||||
await writer.enqueue(document); await writer.settled(); if (writer.error) throw new Error(writer.error);
|
||||
});
|
||||
}
|
||||
const retry = async () => { setError(""); setRetryLoad((v) => v + 1); for (const writer of writers.current.values()) await writer.retry(); };
|
||||
return { documents, current, views, pendingCommand, retryPending: () => retryOperation.current ? exclusive(retryOperation.current) : Promise.resolve(null), prepareApply, recoveries, resolveRecovery, ensure, exportDocuments, dispatch, nodeAction, nodesChanged, edgesChanged, patchEdge, select, flush, command, refresh, read, busy: busy || pendingCommand, error, status, retry, undo: () => undoAction(), redo: () => undoAction(true), canUndo: !!undo.current.length, canRedo: !!redo.current.length, writers, rootDocument: documents.get(root)!, setError };
|
||||
return { documents, current, views, migrateImages, pendingCommand, retryPending: () => retryOperation.current ? exclusive(retryOperation.current) : Promise.resolve(null), prepareApply, recoveries, resolveRecovery, ensure, exportDocuments, dispatch, nodeAction, nodesChanged, edgesChanged, patchEdge, select, flush, command, refresh, read, busy: busy || pendingCommand, error, status, retry, undo: () => undoAction(), redo: () => undoAction(true), canUndo: !!undo.current.length, canRedo: !!redo.current.length, writers, rootDocument: documents.get(root)!, setError };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user