refactor(canvas): debounce and extract local snapshot persistence
This commit is contained in:
@@ -42,11 +42,9 @@ import {
|
||||
dropCanvasOpsByEdgeIds,
|
||||
dropCanvasOpsByNodeIds,
|
||||
enqueueCanvasOp,
|
||||
readCanvasSnapshot,
|
||||
remapCanvasOpNodeId,
|
||||
resolveCanvasOp,
|
||||
resolveCanvasOps,
|
||||
writeCanvasSnapshot,
|
||||
} from "@/lib/canvas-local-persistence";
|
||||
import {
|
||||
ackCanvasSyncOp,
|
||||
@@ -140,6 +138,7 @@ import { getImageDimensions } from "./canvas-media-utils";
|
||||
import { useCanvasReconnectHandlers } from "./canvas-reconnect";
|
||||
import { useCanvasScissors } from "./canvas-scissors";
|
||||
import { CanvasSyncProvider } from "./canvas-sync-context";
|
||||
import { useCanvasLocalSnapshotPersistence } from "./use-canvas-local-snapshot-persistence";
|
||||
|
||||
interface CanvasInnerProps {
|
||||
canvasId: Id<"canvases">;
|
||||
@@ -1907,7 +1906,6 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
// ─── Lokaler State (für flüssiges Dragging) ───────────────────
|
||||
const nodesRef = useRef<RFNode[]>(nodes);
|
||||
nodesRef.current = nodes;
|
||||
const [hasHydratedLocalSnapshot, setHasHydratedLocalSnapshot] = useState(false);
|
||||
const [connectionDropMenu, setConnectionDropMenu] =
|
||||
useState<ConnectionDropMenuState | null>(null);
|
||||
const connectionDropMenuRef = useRef<ConnectionDropMenuState | null>(null);
|
||||
@@ -1919,19 +1917,13 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
>(null);
|
||||
const [navTool, setNavTool] = useState<CanvasNavTool>("select");
|
||||
|
||||
useEffect(() => {
|
||||
const snapshot = readCanvasSnapshot<RFNode, RFEdge>(canvasId as string);
|
||||
if (snapshot) {
|
||||
setNodes(snapshot.nodes);
|
||||
setEdges(snapshot.edges);
|
||||
}
|
||||
setHasHydratedLocalSnapshot(true);
|
||||
}, [canvasId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasHydratedLocalSnapshot) return;
|
||||
writeCanvasSnapshot(canvasId as string, { nodes, edges });
|
||||
}, [canvasId, edges, hasHydratedLocalSnapshot, nodes]);
|
||||
useCanvasLocalSnapshotPersistence<RFNode, RFEdge>({
|
||||
canvasId: canvasId as string,
|
||||
nodes,
|
||||
edges,
|
||||
setNodes,
|
||||
setEdges,
|
||||
});
|
||||
|
||||
const handleNavToolChange = useCallback((tool: CanvasNavTool) => {
|
||||
if (tool === "scissor") {
|
||||
|
||||
105
components/canvas/use-canvas-local-snapshot-persistence.ts
Normal file
105
components/canvas/use-canvas-local-snapshot-persistence.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
} from "react";
|
||||
import {
|
||||
readCanvasSnapshot,
|
||||
writeCanvasSnapshot,
|
||||
} from "@/lib/canvas-local-persistence";
|
||||
|
||||
type UseCanvasLocalSnapshotPersistenceParams<TNode, TEdge> = {
|
||||
canvasId: string;
|
||||
nodes: TNode[];
|
||||
edges: TEdge[];
|
||||
setNodes: Dispatch<SetStateAction<TNode[]>>;
|
||||
setEdges: Dispatch<SetStateAction<TEdge[]>>;
|
||||
debounceMs?: number;
|
||||
};
|
||||
|
||||
type PendingSnapshot<TNode, TEdge> = {
|
||||
canvasId: string;
|
||||
nodes: TNode[];
|
||||
edges: TEdge[];
|
||||
};
|
||||
|
||||
const DEFAULT_SNAPSHOT_DEBOUNCE_MS = 350;
|
||||
|
||||
export function useCanvasLocalSnapshotPersistence<TNode, TEdge>({
|
||||
canvasId,
|
||||
nodes,
|
||||
edges,
|
||||
setNodes,
|
||||
setEdges,
|
||||
debounceMs = DEFAULT_SNAPSHOT_DEBOUNCE_MS,
|
||||
}: UseCanvasLocalSnapshotPersistenceParams<TNode, TEdge>): void {
|
||||
const hasHydratedLocalSnapshotRef = useRef(false);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const pendingSnapshotRef = useRef<PendingSnapshot<TNode, TEdge> | null>(null);
|
||||
|
||||
const flushPendingSnapshot = useCallback(() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
|
||||
const pendingSnapshot = pendingSnapshotRef.current;
|
||||
if (!pendingSnapshot) return;
|
||||
|
||||
writeCanvasSnapshot(pendingSnapshot.canvasId, {
|
||||
nodes: pendingSnapshot.nodes,
|
||||
edges: pendingSnapshot.edges,
|
||||
});
|
||||
pendingSnapshotRef.current = null;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
hasHydratedLocalSnapshotRef.current = false;
|
||||
flushPendingSnapshot();
|
||||
|
||||
const snapshot = readCanvasSnapshot<TNode, TEdge>(canvasId);
|
||||
if (snapshot) {
|
||||
setNodes(snapshot.nodes);
|
||||
setEdges(snapshot.edges);
|
||||
}
|
||||
|
||||
hasHydratedLocalSnapshotRef.current = true;
|
||||
}, [canvasId, flushPendingSnapshot, setEdges, setNodes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasHydratedLocalSnapshotRef.current) return;
|
||||
|
||||
pendingSnapshotRef.current = { canvasId, nodes, edges };
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
const pendingSnapshot = pendingSnapshotRef.current;
|
||||
if (!pendingSnapshot) return;
|
||||
|
||||
writeCanvasSnapshot(pendingSnapshot.canvasId, {
|
||||
nodes: pendingSnapshot.nodes,
|
||||
edges: pendingSnapshot.edges,
|
||||
});
|
||||
pendingSnapshotRef.current = null;
|
||||
timeoutRef.current = null;
|
||||
}, debounceMs);
|
||||
}, [canvasId, debounceMs, edges, nodes]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => flushPendingSnapshot();
|
||||
}, [flushPendingSnapshot]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("beforeunload", flushPendingSnapshot);
|
||||
window.addEventListener("pagehide", flushPendingSnapshot);
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", flushPendingSnapshot);
|
||||
window.removeEventListener("pagehide", flushPendingSnapshot);
|
||||
};
|
||||
}, [flushPendingSnapshot]);
|
||||
}
|
||||
Reference in New Issue
Block a user