Enable offline delete and reconnect queue sync

This commit is contained in:
Matthias
2026-04-01 10:37:20 +02:00
parent da576c1400
commit b6187210c7
7 changed files with 441 additions and 77 deletions

View File

@@ -1,36 +1,52 @@
import { useCallback } from "react";
import { useRef } from "react";
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
import { reconnectEdge, type Connection, type Edge as RFEdge } from "@xyflow/react";
import type { Id } from "@/convex/_generated/dataModel";
import { isOptimisticEdgeId } from "./canvas-helpers";
type UseCanvasReconnectHandlersParams = {
canvasId: Id<"canvases">;
edgeReconnectSuccessful: MutableRefObject<boolean>;
isReconnectDragActiveRef: MutableRefObject<boolean>;
setEdges: Dispatch<SetStateAction<RFEdge[]>>;
runCreateEdgeMutation: (args: {
canvasId: Id<"canvases">;
sourceNodeId: Id<"nodes">;
targetNodeId: Id<"nodes">;
sourceHandle?: string;
targetHandle?: string;
}) => Promise<unknown>;
runRemoveEdgeMutation: (args: { edgeId: Id<"edges"> }) => Promise<unknown>;
};
export function useCanvasReconnectHandlers({
canvasId,
edgeReconnectSuccessful,
isReconnectDragActiveRef,
setEdges,
runCreateEdgeMutation,
runRemoveEdgeMutation,
}: UseCanvasReconnectHandlersParams): {
onReconnectStart: () => void;
onReconnect: (oldEdge: RFEdge, newConnection: Connection) => void;
onReconnectEnd: (_: MouseEvent | TouchEvent, edge: RFEdge) => void;
} {
const pendingReconnectRef = useRef<{
oldEdge: RFEdge;
newConnection: Connection;
} | null>(null);
const onReconnectStart = useCallback(() => {
edgeReconnectSuccessful.current = false;
isReconnectDragActiveRef.current = true;
pendingReconnectRef.current = null;
}, [edgeReconnectSuccessful, isReconnectDragActiveRef]);
const onReconnect = useCallback(
(oldEdge: RFEdge, newConnection: Connection) => {
edgeReconnectSuccessful.current = true;
pendingReconnectRef.current = { oldEdge, newConnection };
setEdges((currentEdges) => reconnectEdge(oldEdge, newConnection, currentEdges));
},
[edgeReconnectSuccessful, setEdges],
@@ -40,6 +56,7 @@ export function useCanvasReconnectHandlers({
(_: MouseEvent | TouchEvent, edge: RFEdge) => {
try {
if (!edgeReconnectSuccessful.current) {
pendingReconnectRef.current = null;
setEdges((currentEdges) =>
currentEdges.filter((candidate) => candidate.id !== edge.id),
);
@@ -48,10 +65,6 @@ export function useCanvasReconnectHandlers({
return;
}
if (isOptimisticEdgeId(edge.id)) {
return;
}
void runRemoveEdgeMutation({ edgeId: edge.id as Id<"edges"> }).catch(
(error) => {
console.error("[Canvas edge remove failed] reconnect end", {
@@ -64,12 +77,54 @@ export function useCanvasReconnectHandlers({
},
);
}
const pendingReconnect = pendingReconnectRef.current;
pendingReconnectRef.current = null;
if (
pendingReconnect &&
pendingReconnect.newConnection.source &&
pendingReconnect.newConnection.target
) {
void runCreateEdgeMutation({
canvasId,
sourceNodeId: pendingReconnect.newConnection.source as Id<"nodes">,
targetNodeId: pendingReconnect.newConnection.target as Id<"nodes">,
sourceHandle: pendingReconnect.newConnection.sourceHandle ?? undefined,
targetHandle: pendingReconnect.newConnection.targetHandle ?? undefined,
}).catch((error) => {
console.error("[Canvas edge reconnect failed] create edge", {
oldEdgeId: pendingReconnect.oldEdge.id,
source: pendingReconnect.newConnection.source,
target: pendingReconnect.newConnection.target,
error: String(error),
});
});
if (pendingReconnect.oldEdge.className !== "temp") {
void runRemoveEdgeMutation({
edgeId: pendingReconnect.oldEdge.id as Id<"edges">,
}).catch((error) => {
console.error("[Canvas edge reconnect failed] remove old edge", {
oldEdgeId: pendingReconnect.oldEdge.id,
error: String(error),
});
});
}
}
edgeReconnectSuccessful.current = true;
} finally {
isReconnectDragActiveRef.current = false;
}
},
[edgeReconnectSuccessful, isReconnectDragActiveRef, runRemoveEdgeMutation, setEdges],
[
canvasId,
edgeReconnectSuccessful,
isReconnectDragActiveRef,
runCreateEdgeMutation,
runRemoveEdgeMutation,
setEdges,
],
);
return { onReconnectStart, onReconnect, onReconnectEnd };