refactor(canvas): extract sync engine hook

This commit is contained in:
2026-04-03 21:11:04 +02:00
parent f2f22b66a7
commit c060c57ad8
4 changed files with 1889 additions and 1634 deletions

View File

@@ -0,0 +1,77 @@
import { describe, expect, it, vi } from "vitest";
import type { Id } from "@/convex/_generated/dataModel";
import { createCanvasSyncEngineController } from "@/components/canvas/use-canvas-sync-engine";
const asCanvasId = (id: string): Id<"canvases"> => id as Id<"canvases">;
const asNodeId = (id: string): Id<"nodes"> => id as Id<"nodes">;
describe("useCanvasSyncEngine", () => {
it("hands off an optimistic create to the real node id before replaying a deferred move", async () => {
const enqueueSyncMutation = vi.fn(async () => undefined);
const runBatchRemoveNodes = vi.fn(async () => undefined);
const runSplitEdgeAtExistingNode = vi.fn(async () => undefined);
const controller = createCanvasSyncEngineController({
canvasId: asCanvasId("canvas-1"),
isSyncOnline: true,
enqueueSyncMutation,
runBatchRemoveNodes,
runSplitEdgeAtExistingNode,
});
controller.pendingMoveAfterCreateRef.current.set("req-1", {
positionX: 320,
positionY: 180,
});
await controller.syncPendingMoveForClientRequest("req-1", asNodeId("node-real"));
expect(enqueueSyncMutation).toHaveBeenCalledWith("moveNode", {
nodeId: asNodeId("node-real"),
positionX: 320,
positionY: 180,
});
expect(
controller.pendingLocalPositionUntilConvexMatchesRef.current.get("node-real"),
).toEqual({ x: 320, y: 180 });
expect(controller.resolvedRealIdByClientRequestRef.current.get("req-1")).toBe(
asNodeId("node-real"),
);
expect(controller.pendingMoveAfterCreateRef.current.has("req-1")).toBe(false);
expect(runBatchRemoveNodes).not.toHaveBeenCalled();
expect(runSplitEdgeAtExistingNode).not.toHaveBeenCalled();
});
it("defers resize and data updates for an optimistic node until the real id is known", async () => {
const enqueueSyncMutation = vi.fn(async () => undefined);
const controller = createCanvasSyncEngineController({
canvasId: asCanvasId("canvas-1"),
isSyncOnline: true,
enqueueSyncMutation,
runBatchRemoveNodes: vi.fn(async () => undefined),
runSplitEdgeAtExistingNode: vi.fn(async () => undefined),
});
await controller.queueNodeResize({
nodeId: asNodeId("optimistic_req-2"),
width: 640,
height: 360,
});
await controller.queueNodeDataUpdate({
nodeId: asNodeId("optimistic_req-2"),
data: { label: "Updated" },
});
expect(enqueueSyncMutation).not.toHaveBeenCalled();
await controller.syncPendingMoveForClientRequest("req-2", asNodeId("node-2"));
expect(enqueueSyncMutation.mock.calls).toEqual([
["resizeNode", { nodeId: asNodeId("node-2"), width: 640, height: 360 }],
["updateData", { nodeId: asNodeId("node-2"), data: { label: "Updated" } }],
]);
expect(controller.pendingResizeAfterCreateRef.current.has("req-2")).toBe(false);
expect(controller.pendingDataAfterCreateRef.current.has("req-2")).toBe(false);
});
});