Skip to content

Commit f59b48c

Browse files
committed
more checks when connecting edges
Signed-off-by: Teo Koon Peng <[email protected]>
1 parent b5ef3a7 commit f59b48c

File tree

4 files changed

+171
-15
lines changed

4 files changed

+171
-15
lines changed

diagram-editor/frontend/diagram-editor.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import React from 'react';
3030
import AddOperation from './add-operation';
3131
import CommandPanel from './command-panel';
3232
import type { DiagramEditorEdge } from './edges';
33-
import { EDGE_TYPES } from './edges';
33+
import { createBaseEdge, EDGE_TYPES } from './edges';
3434
import {
3535
EditorMode,
3636
type EditorModeContext,
@@ -49,7 +49,7 @@ import {
4949
} from './nodes';
5050
import { useTemplates } from './templates-provider';
5151
import { autoLayout } from './utils/auto-layout';
52-
import { getValidEdgeTypes } from './utils/connection';
52+
import { checkValidEdgeSimple, getValidEdgeTypes } from './utils/connection';
5353
import { exhaustiveCheck } from './utils/exhaustive-check';
5454
import { exportTemplate } from './utils/export-diagram';
5555
import { calculateScopeBounds, LAYOUT_OPTIONS } from './utils/layout';
@@ -527,6 +527,10 @@ function DiagramEditor() {
527527
closeAllPopovers();
528528
}}
529529
onConnect={(conn) => {
530+
if (!reactFlowInstance.current) {
531+
return;
532+
}
533+
530534
const sourceNode = reactFlowInstance.current?.getNode(conn.source);
531535
const targetNode = reactFlowInstance.current?.getNode(conn.target);
532536
if (!sourceNode || !targetNode) {
@@ -541,16 +545,22 @@ function DiagramEditor() {
541545
return;
542546
}
543547

544-
setEdges((prev) =>
545-
addEdge(
546-
{
547-
...conn,
548-
type: validEdges[0],
549-
data: defaultEdgeData(validEdges[0]),
550-
},
551-
prev,
552-
),
548+
const newEdge = {
549+
...createBaseEdge(conn.source, conn.target),
550+
type: validEdges[0],
551+
data: defaultEdgeData(validEdges[0]),
552+
} as DiagramEditorEdge;
553+
554+
const validationResult = checkValidEdgeSimple(
555+
newEdge,
556+
reactFlowInstance.current,
553557
);
558+
if (!validationResult.valid) {
559+
showErrorToast(validationResult.error);
560+
return;
561+
}
562+
563+
setEdges((prev) => addEdge(newEdge, prev));
554564
}}
555565
isValidConnection={(conn) => {
556566
const sourceNode = reactFlowInstance.current?.getNode(conn.source);

diagram-editor/frontend/edges/create-edge.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { MarkerType } from '@xyflow/react';
22
import { v4 as uuidv4 } from 'uuid';
33
import type { DiagramEditorEdge, EdgeData } from '.';
44

5-
function createBaseEdge(
5+
export function createBaseEdge(
66
source: string,
77
target: string,
88
): Pick<DiagramEditorEdge, 'id' | 'source' | 'target' | 'markerEnd'> {

diagram-editor/frontend/forms/edit-edge-form.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ const EDGE_DEFAULT_DATA = {
4646
splitRemaining: {},
4747
streamOut: { name: '' },
4848
unzip: { seq: 0 },
49-
} satisfies { [k in EdgeTypes]: EdgeData[k] };
49+
} satisfies Record<EdgeTypes, EdgeData<EdgeTypes>>;
5050

51-
export function defaultEdgeData(type: EdgeTypes): EdgeData[EdgeTypes] {
51+
export function defaultEdgeData(type: EdgeTypes): EdgeData<EdgeTypes> {
5252
return { ...EDGE_DEFAULT_DATA[type] };
5353
}
5454

diagram-editor/frontend/utils/connection.ts

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import type { EdgeTypes } from '../edges';
1+
import type { ReactFlowInstance } from '@xyflow/react';
2+
import type { DiagramEditorEdge, EdgeTypes } from '../edges';
23
import type { DiagramEditorNode, NodeTypes } from '../nodes';
4+
import { exhaustiveCheck } from './exhaustive-check';
35

46
const ALLOWED_OUTPUT_EDGES: Record<NodeTypes, Set<EdgeTypes>> = {
57
buffer: new Set<EdgeTypes>(['bufferKey', 'bufferSeq']),
@@ -67,3 +69,147 @@ export function getValidEdgeTypes(
6769
: new Set([]);
6870
return Array.from(setIntersection(allowedOutputEdges, allowedInputEdges));
6971
}
72+
73+
enum CardinalityType {
74+
Single,
75+
Pair,
76+
Many,
77+
}
78+
79+
function getOutputCardinality(type: NodeTypes): CardinalityType {
80+
switch (type) {
81+
case 'fork_clone':
82+
case 'unzip':
83+
case 'buffer':
84+
case 'section':
85+
case 'split': {
86+
return CardinalityType.Many;
87+
}
88+
case 'fork_result': {
89+
return CardinalityType.Pair;
90+
}
91+
case 'node':
92+
case 'buffer_access':
93+
case 'join':
94+
case 'serialized_join':
95+
case 'listen':
96+
case 'scope':
97+
case 'stream_out':
98+
case 'transform':
99+
case 'start':
100+
case 'terminate':
101+
case 'sectionBuffer':
102+
case 'sectionInput':
103+
case 'sectionOutput': {
104+
return CardinalityType.Single;
105+
}
106+
default: {
107+
exhaustiveCheck(type);
108+
throw new Error('unknown op type');
109+
}
110+
}
111+
}
112+
113+
export type EdgeValidationResult =
114+
| { valid: true; validEdgeTypes: EdgeTypes[] }
115+
| { valid: false; error: string };
116+
117+
function createValidationError(error: string): EdgeValidationResult {
118+
return { valid: false, error };
119+
}
120+
121+
/**
122+
* Perform a quick check if an edge is valid.
123+
* This only checks if the edge type is valid, does not check for conflicting edges, data correctness etc.
124+
* Complexity is O(1).
125+
*/
126+
export function checkValidEdgeQuick(
127+
edge: DiagramEditorEdge,
128+
reactFlow: ReactFlowInstance<DiagramEditorNode, DiagramEditorEdge>,
129+
): EdgeValidationResult {
130+
const sourceNode = reactFlow.getNode(edge.source);
131+
const targetNode = reactFlow.getNode(edge.target);
132+
133+
if (!sourceNode || !targetNode) {
134+
return createValidationError('cannot find source or target node');
135+
}
136+
137+
const validEdgeTypes = getValidEdgeTypes(sourceNode, targetNode);
138+
if (!validEdgeTypes.includes(edge.type)) {
139+
return createValidationError('invalid edge type');
140+
}
141+
142+
return { valid: true, validEdgeTypes };
143+
}
144+
145+
/**
146+
* Perform a simple check of the validity of edges.
147+
* A more complete check than `checkValidEdgeQuick`, but still does not do complicated checks like type compatibility.
148+
* Complexity is O(numOfEdges), so it is not recommended to call this very frequently.
149+
*/
150+
export function checkValidEdgeSimple(
151+
edge: DiagramEditorEdge,
152+
reactFlow: ReactFlowInstance<DiagramEditorNode, DiagramEditorEdge>,
153+
): EdgeValidationResult {
154+
const quickCheck = checkValidEdgeQuick(edge, reactFlow);
155+
if (!quickCheck.valid) {
156+
return quickCheck;
157+
}
158+
159+
const sourceNode = reactFlow.getNode(edge.source);
160+
if (!sourceNode) {
161+
return createValidationError('cannot find source or target node');
162+
}
163+
164+
// Check if the source supports emitting multiple outputs.
165+
// NOTE: All nodes supports "Many" inputs so we don't need to check that.
166+
const outputCardinality = getOutputCardinality(sourceNode.type);
167+
switch (outputCardinality) {
168+
case CardinalityType.Single: {
169+
if (reactFlow.getEdges().some((edge) => edge.source === sourceNode.id)) {
170+
return createValidationError('source node already has an edge');
171+
}
172+
break;
173+
}
174+
case CardinalityType.Pair: {
175+
let count = 0;
176+
for (const edge of reactFlow.getEdges()) {
177+
if (edge.source === sourceNode.id) {
178+
count++;
179+
}
180+
if (count > 1) {
181+
return createValidationError('source node already has two edges');
182+
}
183+
}
184+
break;
185+
}
186+
case CardinalityType.Many: {
187+
break;
188+
}
189+
default: {
190+
exhaustiveCheck(outputCardinality);
191+
throw new Error('unknown output cardinality');
192+
}
193+
}
194+
195+
return { valid: true, validEdgeTypes: quickCheck.validEdgeTypes };
196+
}
197+
198+
/**
199+
* Perform a full check of the validity of edges.
200+
* This can be slow so it is not recommended to call this frequently.
201+
*/
202+
export async function checkValidEdgeFull(
203+
edge: DiagramEditorEdge,
204+
reactFlow: ReactFlowInstance<DiagramEditorNode, DiagramEditorEdge>,
205+
): Promise<EdgeValidationResult> {
206+
const simpleCheck = checkValidEdgeSimple(edge, reactFlow);
207+
if (!simpleCheck.valid) {
208+
return simpleCheck;
209+
}
210+
211+
// TODO: check message type compatibility. Writing the same logic as `bevy_impulse` is hard, it might
212+
// be better to introduce a validation endpoint.
213+
214+
return { valid: true, validEdgeTypes: simpleCheck.validEdgeTypes };
215+
}

0 commit comments

Comments
 (0)