/** * Abstract node layer: shared types, hook, and factory for flow node components. * * - **AbstractNodeProps** — Typed props (id, data, width?, height?, selected?) for your node. * - **useAbstractNode(id, data)** — Flow context plus helpers: nodes, edges, setNodes, setEdges, * updateData(partial), incomingEdges, outgoingEdges, sourceIds, targetIds. Calling updateData() * also marks this node as a connection-path trigger so edges update on data changes. * - **createAbstractNodeComponent(displayName, Component)** — Wraps with memo + nodePropsAreEqual. * * **Node lifecycle / connection status:** Nodes that can be updating, paused, or in error should * call useSyncConnectionStatus(id, { updating, error, paused }) from @/lib/graph/nodeLifecycle so edge * colors and path animation stay correct. See nodeLifecycle.ts for the full contract. * * Example: define NodeData type, Props = AbstractNodeProps, use useAbstractNode in the * component, then export const MyNode = createAbstractNodeComponent('MyNode', MyNodeComponent). */ import React, { useCallback, useContext, useMemo, useRef } from 'react' import { GraphContext, ConnectionPathContext } from './flowContext' import { nodePropsAreEqual } from './flowUtils' import type { AppNode } from './nodeTypes' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- /** Props passed by React Flow to custom node components. Extend data with your node's shape. */ export type AbstractNodeProps> = { id: string data: TData width?: number height?: number selected?: boolean } /** Edge shape used in flow context (minimal for connection logic). */ export type FlowEdge = { id: string; source: string; target: string; [k: string]: unknown } /** Node shape used in flow context (minimal for reading graph). */ export type FlowNode = { id: string; type?: string; data?: unknown; position?: { x: number; y: number }; [k: string]: unknown } /** Result of useAbstractNode: flow context plus helpers scoped to this node. */ export type AbstractNodeContext> = { id: string data: TData nodes: FlowNode[] edges: FlowEdge[] setNodes: (updater: (nodes: FlowNode[]) => FlowNode[]) => void setEdges: (updater: (edges: FlowEdge[]) => FlowEdge[]) => void /** Merge partial data into this node's data. Stable reference. */ updateData: (partial: Partial) => void /** Incoming edge IDs (edges whose target is this node). */ incomingEdges: FlowEdge[] /** Outgoing edge IDs (edges whose source is this node). */ outgoingEdges: FlowEdge[] /** Source node IDs connected to this node (incoming). */ sourceIds: string[] /** Target node IDs this node connects to (outgoing). */ targetIds: string[] } /** Returns nodes that are connected to this node (in sourceIds) and have the given type. */ export function getConnectedNodesByType( nodes: FlowNode[], sourceIds: string[], type: string ): T[] { return nodes.filter((n) => sourceIds.includes(n.id) && n.type === type) as T[] } // --------------------------------------------------------------------------- // Hook // --------------------------------------------------------------------------- /** * Provides flow context and helpers for the current node. Use in any node component * that receives id and data; updateData(partial) merges into this node's data. */ export function useAbstractNode>( id: string, data: TData ): AbstractNodeContext { const graphCtx = useContext(GraphContext) const pathCtx = useContext(ConnectionPathContext) const addTriggerRef = useRef(pathCtx?.addConnectionPathTrigger) addTriggerRef.current = pathCtx?.addConnectionPathTrigger const nodes = graphCtx?.graphRef?.current?.nodes ?? [] const edges = graphCtx?.edges ?? [] const setNodes = graphCtx?.setNodes const setEdges = graphCtx?.setEdges const updateData = useCallback( (partial: Partial) => { if (!setNodes) return setNodes((nds: AppNode[]) => nds.map((n) => n.id === id ? { ...n, data: { ...(n.data as object), ...partial } } : n ) as AppNode[] ) addTriggerRef.current?.(id) }, [id, setNodes] ) const incomingEdges = useMemo( () => (edges as FlowEdge[]).filter((e) => e.target === id), [edges, id] ) const outgoingEdges = useMemo( () => (edges as FlowEdge[]).filter((e) => e.source === id), [edges, id] ) const sourceIds = useMemo( () => incomingEdges.map((e) => e.source).sort(), [incomingEdges] ) const targetIds = useMemo( () => outgoingEdges.map((e) => e.target).sort(), [outgoingEdges] ) return { id, data, nodes, edges, setNodes: ((setNodes ?? (() => {})) as AbstractNodeContext['setNodes']), setEdges: setEdges ?? (() => {}), updateData, incomingEdges, outgoingEdges, sourceIds, targetIds, } } // --------------------------------------------------------------------------- // Component factory // --------------------------------------------------------------------------- /** * Wraps a node component with React.memo and nodePropsAreEqual so only id/data/width/height/selected * changes trigger re-renders. Use with AbstractNodeProps for typed props. */ export function createAbstractNodeComponent>( displayName: string, Component: React.ComponentType> ): React.MemoExoticComponent>> { const Wrapped = React.memo(Component, nodePropsAreEqual) as React.MemoExoticComponent< React.ComponentType> > Wrapped.displayName = displayName return Wrapped }