refactor(canvas): debounce and extract local snapshot persistence
This commit is contained in:
@@ -42,11 +42,9 @@ import {
|
|||||||
dropCanvasOpsByEdgeIds,
|
dropCanvasOpsByEdgeIds,
|
||||||
dropCanvasOpsByNodeIds,
|
dropCanvasOpsByNodeIds,
|
||||||
enqueueCanvasOp,
|
enqueueCanvasOp,
|
||||||
readCanvasSnapshot,
|
|
||||||
remapCanvasOpNodeId,
|
remapCanvasOpNodeId,
|
||||||
resolveCanvasOp,
|
resolveCanvasOp,
|
||||||
resolveCanvasOps,
|
resolveCanvasOps,
|
||||||
writeCanvasSnapshot,
|
|
||||||
} from "@/lib/canvas-local-persistence";
|
} from "@/lib/canvas-local-persistence";
|
||||||
import {
|
import {
|
||||||
ackCanvasSyncOp,
|
ackCanvasSyncOp,
|
||||||
@@ -140,6 +138,7 @@ import { getImageDimensions } from "./canvas-media-utils";
|
|||||||
import { useCanvasReconnectHandlers } from "./canvas-reconnect";
|
import { useCanvasReconnectHandlers } from "./canvas-reconnect";
|
||||||
import { useCanvasScissors } from "./canvas-scissors";
|
import { useCanvasScissors } from "./canvas-scissors";
|
||||||
import { CanvasSyncProvider } from "./canvas-sync-context";
|
import { CanvasSyncProvider } from "./canvas-sync-context";
|
||||||
|
import { useCanvasLocalSnapshotPersistence } from "./use-canvas-local-snapshot-persistence";
|
||||||
|
|
||||||
interface CanvasInnerProps {
|
interface CanvasInnerProps {
|
||||||
canvasId: Id<"canvases">;
|
canvasId: Id<"canvases">;
|
||||||
@@ -1907,7 +1906,6 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
|||||||
// ─── Lokaler State (für flüssiges Dragging) ───────────────────
|
// ─── Lokaler State (für flüssiges Dragging) ───────────────────
|
||||||
const nodesRef = useRef<RFNode[]>(nodes);
|
const nodesRef = useRef<RFNode[]>(nodes);
|
||||||
nodesRef.current = nodes;
|
nodesRef.current = nodes;
|
||||||
const [hasHydratedLocalSnapshot, setHasHydratedLocalSnapshot] = useState(false);
|
|
||||||
const [connectionDropMenu, setConnectionDropMenu] =
|
const [connectionDropMenu, setConnectionDropMenu] =
|
||||||
useState<ConnectionDropMenuState | null>(null);
|
useState<ConnectionDropMenuState | null>(null);
|
||||||
const connectionDropMenuRef = useRef<ConnectionDropMenuState | null>(null);
|
const connectionDropMenuRef = useRef<ConnectionDropMenuState | null>(null);
|
||||||
@@ -1919,19 +1917,13 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
|||||||
>(null);
|
>(null);
|
||||||
const [navTool, setNavTool] = useState<CanvasNavTool>("select");
|
const [navTool, setNavTool] = useState<CanvasNavTool>("select");
|
||||||
|
|
||||||
useEffect(() => {
|
useCanvasLocalSnapshotPersistence<RFNode, RFEdge>({
|
||||||
const snapshot = readCanvasSnapshot<RFNode, RFEdge>(canvasId as string);
|
canvasId: canvasId as string,
|
||||||
if (snapshot) {
|
nodes,
|
||||||
setNodes(snapshot.nodes);
|
edges,
|
||||||
setEdges(snapshot.edges);
|
setNodes,
|
||||||
}
|
setEdges,
|
||||||
setHasHydratedLocalSnapshot(true);
|
});
|
||||||
}, [canvasId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hasHydratedLocalSnapshot) return;
|
|
||||||
writeCanvasSnapshot(canvasId as string, { nodes, edges });
|
|
||||||
}, [canvasId, edges, hasHydratedLocalSnapshot, nodes]);
|
|
||||||
|
|
||||||
const handleNavToolChange = useCallback((tool: CanvasNavTool) => {
|
const handleNavToolChange = useCallback((tool: CanvasNavTool) => {
|
||||||
if (tool === "scissor") {
|
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