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

@@ -32,6 +32,9 @@ import "@xyflow/react/dist/style.css";
import { toast } from "@/lib/toast";
import { msg } from "@/lib/toast-messages";
import {
dropCanvasOpsByClientRequestIds,
dropCanvasOpsByEdgeIds,
dropCanvasOpsByNodeIds,
enqueueCanvasOp,
readCanvasSnapshot,
remapCanvasOpNodeId,
@@ -43,6 +46,9 @@ import {
ackCanvasSyncOp,
type CanvasSyncOpPayloadByType,
countCanvasSyncOps,
dropCanvasSyncOpsByClientRequestIds,
dropCanvasSyncOpsByEdgeIds,
dropCanvasSyncOpsByNodeIds,
dropExpiredCanvasSyncOps,
enqueueCanvasSyncOp,
listCanvasSyncOps,
@@ -404,31 +410,6 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
const createNodeWithEdgeSplit = useMutation(api.nodes.createWithEdgeSplit);
const batchRemoveNodes = useMutation(api.nodes.batchRemove).withOptimisticUpdate(
(localStore, args) => {
const nodeList = localStore.getQuery(api.nodes.list, { canvasId });
const edgeList = localStore.getQuery(api.edges.list, { canvasId });
if (nodeList === undefined || edgeList === undefined) return;
const removeSet = new Set<string>(
args.nodeIds.map((id: Id<"nodes">) => id as string),
);
localStore.setQuery(
api.nodes.list,
{ canvasId },
nodeList.filter((n: Doc<"nodes">) => !removeSet.has(n._id)),
);
localStore.setQuery(
api.edges.list,
{ canvasId },
edgeList.filter(
(e: Doc<"edges">) =>
!removeSet.has(e.sourceNodeId) && !removeSet.has(e.targetNodeId),
),
);
},
);
const createEdge = useMutation(api.edges.create).withOptimisticUpdate(
(localStore, args) => {
const edgeList = localStore.getQuery(api.edges.list, {
@@ -465,18 +446,8 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
api.nodes.createWithEdgeToTarget,
);
const createEdgeRaw = useMutation(api.edges.create);
const removeEdge = useMutation(api.edges.remove).withOptimisticUpdate(
(localStore, args) => {
const edgeList = localStore.getQuery(api.edges.list, { canvasId });
if (edgeList === undefined) return;
localStore.setQuery(
api.edges.list,
{ canvasId },
edgeList.filter((e: Doc<"edges">) => e._id !== args.edgeId),
);
},
);
const batchRemoveNodesRaw = useMutation(api.nodes.batchRemove);
const removeEdgeRaw = useMutation(api.edges.remove);
const [nodes, setNodes] = useState<RFNode[]>([]);
const [edges, setEdges] = useState<RFEdge[]>([]);
@@ -802,6 +773,10 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
setEdgeSyncNonce((value) => value + 1);
} else if (op.type === "createEdge") {
await createEdgeRaw(op.payload);
} else if (op.type === "removeEdge") {
await removeEdgeRaw(op.payload);
} else if (op.type === "batchRemoveNodes") {
await batchRemoveNodesRaw(op.payload);
} else if (op.type === "moveNode") {
await moveNode(op.payload);
} else if (op.type === "resizeNode") {
@@ -862,6 +837,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
await refreshPendingSyncCount();
}
}, [
batchRemoveNodesRaw,
canvasId,
createEdgeRaw,
createNodeRaw,
@@ -871,6 +847,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
moveNode,
refreshPendingSyncCount,
remapOptimisticNodeLocally,
removeEdgeRaw,
removeOptimisticCreateLocally,
resizeNode,
updateNodeData,
@@ -968,14 +945,61 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
);
const runBatchRemoveNodesMutation = useCallback(
async (args: Parameters<typeof batchRemoveNodes>[0]) => {
if (!isSyncOnline) {
notifyOfflineUnsupported("Löschen");
async (args: { nodeIds: Id<"nodes">[] }) => {
const ids = args.nodeIds.map((id) => id as string);
const optimisticNodeIds = ids.filter((id) => isOptimisticNodeId(id));
const persistedNodeIds = ids.filter((id) => !isOptimisticNodeId(id));
const createClientRequestIds = optimisticNodeIds
.map((id) => clientRequestIdFromOptimisticNodeId(id))
.filter((id): id is string => id !== null);
if (createClientRequestIds.length > 0) {
const droppedSync = await dropCanvasSyncOpsByClientRequestIds(
canvasId as string,
createClientRequestIds,
);
const droppedLocal = dropCanvasOpsByClientRequestIds(
canvasId as string,
createClientRequestIds,
);
for (const clientRequestId of createClientRequestIds) {
removeOptimisticCreateLocally({
clientRequestId,
removeNode: true,
removeEdge: true,
});
}
resolveCanvasOps(canvasId as string, droppedSync);
resolveCanvasOps(canvasId as string, droppedLocal);
}
if (persistedNodeIds.length === 0) {
await refreshPendingSyncCount();
return;
}
await batchRemoveNodes(args);
const droppedSyncByNode = await dropCanvasSyncOpsByNodeIds(
canvasId as string,
persistedNodeIds,
);
const droppedLocalByNode = dropCanvasOpsByNodeIds(
canvasId as string,
persistedNodeIds,
);
resolveCanvasOps(canvasId as string, droppedSyncByNode);
resolveCanvasOps(canvasId as string, droppedLocalByNode);
await enqueueSyncMutation("batchRemoveNodes", {
nodeIds: persistedNodeIds as Id<"nodes">[],
});
},
[batchRemoveNodes, isSyncOnline, notifyOfflineUnsupported],
[
canvasId,
enqueueSyncMutation,
refreshPendingSyncCount,
removeOptimisticCreateLocally,
],
);
const runCreateEdgeMutation = useCallback(
@@ -1001,14 +1025,37 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
);
const runRemoveEdgeMutation = useCallback(
async (args: Parameters<typeof removeEdge>[0]) => {
if (!isSyncOnline) {
notifyOfflineUnsupported("Kante entfernen");
async (args: { edgeId: Id<"edges"> }) => {
const edgeId = args.edgeId as string;
setEdges((current) => current.filter((edge) => edge.id !== edgeId));
if (isOptimisticEdgeId(edgeId)) {
const clientRequestId = clientRequestIdFromOptimisticEdgeId(edgeId);
if (clientRequestId) {
const droppedSync = await dropCanvasSyncOpsByClientRequestIds(
canvasId as string,
[clientRequestId],
);
const droppedLocal = dropCanvasOpsByClientRequestIds(
canvasId as string,
[clientRequestId],
);
resolveCanvasOps(canvasId as string, droppedSync);
resolveCanvasOps(canvasId as string, droppedLocal);
}
await refreshPendingSyncCount();
return;
}
await removeEdge(args);
const droppedSync = await dropCanvasSyncOpsByEdgeIds(canvasId as string, [edgeId]);
const droppedLocal = dropCanvasOpsByEdgeIds(canvasId as string, [edgeId]);
resolveCanvasOps(canvasId as string, droppedSync);
resolveCanvasOps(canvasId as string, droppedLocal);
await enqueueSyncMutation("removeEdge", {
edgeId: edgeId as Id<"edges">,
});
},
[isSyncOnline, notifyOfflineUnsupported, removeEdge],
[canvasId, enqueueSyncMutation, refreshPendingSyncCount],
);
const splitEdgeAtExistingNodeMut = useMutation(
@@ -1322,7 +1369,6 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
const { onBeforeDelete, onNodesDelete, onEdgesDelete } = useCanvasDeleteHandlers({
canvasId,
isOffline: !isSyncOnline,
nodes,
edges,
deletingNodeIds,
@@ -1333,9 +1379,11 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
});
const { onReconnectStart, onReconnect, onReconnectEnd } = useCanvasReconnectHandlers({
canvasId,
edgeReconnectSuccessful,
isReconnectDragActiveRef,
setEdges,
runCreateEdgeMutation,
runRemoveEdgeMutation,
});
@@ -1349,7 +1397,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
if (!convexEdges) return;
setEdges((prev) => {
const prevConvexSnap = convexNodeIdsSnapshotForEdgeCarryRef.current;
const currentConvexIdList =
const currentConvexIdList: string[] =
convexNodes !== undefined
? convexNodes.map((n: Doc<"nodes">) => n._id as string)
: [];
@@ -1362,8 +1410,8 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
const tempEdges = prev.filter((e) => e.className === "temp");
const sourceTypeByNodeId =
convexNodes !== undefined
? new Map(
convexNodes.map((n: Doc<"nodes">) => [n._id as string, n.type]),
? new Map<string, string>(
convexNodes.map((n: Doc<"nodes">) => [n._id as string, n.type as string]),
)
: undefined;
const glowMode = resolvedTheme === "dark" ? "dark" : "light";