Skip to content

Add node connection validation: enforce typed port rules between block types #87

Description

@Pvsaint

Summary

React Flow allows any node to connect to any other node with no type checking. This issue implements a fully typed connection validation system — enforcing per-block source/target rules, handle-level port typing, max-edge constraints, and real-time visual rejection feedback — integrated with the existing graph validation pipeline.

Why This Is Hard

  • React Flow's isValidConnection callback fires synchronously on every connection attempt. It must check rules against the current live node/edge state, which means it needs access to the full nodes and edges arrays inside a stable callback — requiring careful use of useCallback with the right dependency array to avoid stale closure bugs.
  • Condition blocks have two distinct output ports: true and false branches. The validation must be handle-aware: the sourceHandle field on the connection identifies which port is being connected, and the system must enforce that each handle can only be used once (one edge per output handle).
  • Cascading validation: the same rule set must be used in three places: (1) the isValidConnection React Flow callback (prevents invalid connections), (2) validateGraphStructure in validate.ts (catches invalid connections in JSON-loaded graphs), and (3) the type inference engine (issue feat(compiler): implement graph-level type inference engine — propagate and validate Rust types across block connections at edit time #125). The rules must be defined once and consumed in all three places — no duplication.
  • Visual rejection UX: React Flow has no built-in rejection animation. When a connection is rejected, a red ghost edge must be rendered briefly using onConnectEnd and temporary edge state, then removed after 600 ms.
  • Dynamic extensibility: as new block types are added (RBACCheck, CrossContractCall, Loop), the connection rule table must be extensible without touching the validation callbacks.

Connection Rule Table

Source Block Valid Target Types Max out-edges Handle-level rule
default (Start) Any 1 Single output handle
Auth Transfer, Storage, Event, Condition 1 Single output handle
Transfer Storage, Event, Condition 1 Single output handle
Storage Transfer, Event, Condition 1 Single output handle
Condition Any except default 2 sourceHandle: "true" and sourceHandle: "false" each used at most once
Event None (terminal) 0 No outputs allowed

Rule Architecture (new file src/lib/compile/connectionRules.ts)

export interface ConnectionRule {
  maxOutEdges: number
  allowedTargets: BlockType[] | "any"
  handleConstraints?: Record<string, { maxUses: number }>
}

export const CONNECTION_RULES: Record<BlockType, ConnectionRule>

export interface ConnectionValidationContext {
  sourceNode: ContractGraphNode
  targetNode: ContractGraphNode
  sourceHandle: string | null
  existingEdges: ContractGraphEdge[]
}

export function validateConnection(ctx: ConnectionValidationContext): { valid: true } | { valid: false; reason: string }

All three consumers import and call validateConnection — no duplication of logic.

validateGraphStructure extension

After the existing reachability check, iterate all edges and call validateConnection for each. Collect all violations and return them as details in the CompileError with code INVALID_CONNECTION.

React Flow integration

const isValidConnection = useCallback((connection: Connection) => {
  const result = validateConnection({
    sourceNode: getNode(connection.source),
    targetNode: getNode(connection.target),
    sourceHandle: connection.sourceHandle,
    existingEdges: edges,
  })
  return result.valid
}, [edges, getNode])

Visual rejection feedback

On connection end when the attempt was invalid (tracked via a ref set on connection start): briefly add a temporary red dashed edge to the canvas for 600 ms using setEdges, then remove it. The red edge renders with a custom invalidEdge edge type showing a tooltip with the rejection reason.

Handle styling

  • Valid connection targets highlight their input handle green during a drag.
  • Invalid targets dim their handle to 30% opacity.
  • Implemented via useStore from React Flow to detect active connection state.

Acceptance Criteria

  • CONNECTION_RULES is defined in connectionRules.ts for all current block types.
  • validateConnection is the single source of truth used by React Flow, validateGraphStructure, and the type inference engine.
  • isValidConnection in BlockEditor prevents invalid edges from being created.
  • Attempting to connect a second output from a Start node is rejected.
  • Connecting to an Event block output is rejected (terminal block).
  • A Condition block allows exactly two outgoing edges (true/false handles); a third is rejected.
  • validateGraphStructure returns an INVALID_CONNECTION error for rule-violating edges in loaded graphs.
  • A red ghost edge appears briefly (>= 400 ms) when a connection attempt is rejected.
  • Valid target handles highlight green during a drag; invalid targets dim.
  • Unit tests in connectionRules.test.ts cover every rule in the table, including handle constraints.
  • A Playwright E2E test: attempts to connect an Event block output, asserts the edge is not created and the red ghost appears.
  • All existing tests pass.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions