npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@easy-nodes/core

v1.0.3

Published

A React-based node graph editor built on [React Flow](https://reactflow.dev). Define nodes declaratively with JSON, wire them together visually, and let the built-in evaluation engine run your graph in topological order. Supports sync and async evaluation

Readme

Graphly

A React-based node graph editor built on React Flow. Define nodes declaratively with JSON, wire them together visually, and let the built-in evaluation engine run your graph in topological order. Supports sync and async evaluation, cycle detection, configurable fields, inline JavaScript operations, a custom operation registry, and partial re-evaluation for performance-sensitive graphs.


Table of Contents


Installation

npm install @easy-nodes/core

Peer Dependencies

Graphly requires these peer dependencies. Install them if they are not already in your project:

npm install react react-dom @xyflow/react

| Peer Dependency | Version | | ----------------- | --------- | | react | ^19.2.4 | | react-dom | ^19.2.4 | | @xyflow/react | ^12.10.1|

Graphly imports @xyflow/react/dist/style.css internally, so your bundler must be configured to handle CSS imports (Vite, Next.js, and Create React App handle this out of the box).


Quick Start

import { GraphlyCanvas } from '@easy-nodes/core';

function App() {
  const nodeDefinitions = {
    number: {
      label: 'Number',
      category: 'generator',
      operationCode: 'return { val: state.config?.val ?? 10 };',
      configurable: [
        { id: 'val', label: 'Value', type: 'number', configLocation: 'inline' },
      ],
      outputs: [{ id: 'val', label: 'Value', type: 'number' }],
    },
    add: {
      label: 'Add',
      category: 'transformer',
      operationCode: 'return { sum: (Number(inp_a) || 0) + (Number(inp_b) || 0) };',
      inputs: [
        { id: 'a', label: 'A', type: 'number' },
        { id: 'b', label: 'B', type: 'number' },
      ],
      outputs: [{ id: 'sum', label: 'Sum', type: 'number' }],
    },
    output: {
      label: 'Output',
      category: 'output',
      isOutput: true,
      operationCode: 'return { value: inp_value };',
      inputs: [{ id: 'value', label: 'Value', type: 'any' }],
    },
  };

  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <GraphlyCanvas nodeDefinitions={nodeDefinitions} />
    </div>
  );
}

This renders a full canvas with a draggable node sidebar, a context menu for adding nodes, bezier edges, a minimap, zoom controls, and a background grid. Users can drag nodes from the sidebar, connect them with edges, and the graph evaluates automatically.


Node Definitions

Node definitions are a Record<string, Partial<DynamicNodeData>> object passed to GraphlyCanvas via the nodeDefinitions prop. Each key is a unique identifier for that node type, and the value describes the node's behavior, ports, and UI.

Basic Structure

const nodeDefinitions = {
  myNode: {
    label: 'My Node',          // Display name shown in the node header
    category: 'transformer',    // Affects header color: 'generator' | 'transformer' | 'output' | 'generic'
    inputs: [...],              // Array of input ports
    outputs: [...],             // Array of output ports
    configurable: [...],        // Array of user-editable config fields
    operationCode: '...',       // Inline JavaScript string (evaluated via new Function)
    operationType: '...',       // OR: key into the operations registry
    isOutput: false,            // Mark as a final output node
    settings: { ... },         // Arbitrary metadata (not used by the engine directly)
  },
};

Inputs and Outputs

Each input and output port is a DynamicNodePort:

interface DynamicNodePort {
  id: string;       // Unique within the node. Used as handle ID for edge connections.
  label: string;    // Display label next to the handle.
  type?: string;    // 'number' | 'string' | 'text' | 'boolean' | 'any' | 'array' | 'object'
  options?: string[];  // For dropdown controls: display labels.
  values?: any[];      // For dropdown controls: actual values (paired with options by index).
}

Input ports appear on the left side of the node. If the port type is number, text, or string, an inline input control is rendered directly on the node so users can type values manually. When an edge is connected to the input, the inline control is disabled and the value comes from the upstream node.

Output ports appear on the right side of the node.

Example with multiple inputs and outputs:

{
  "divide": {
    "label": "Divide",
    "category": "transformer",
    "operationCode": "const a = Number(inp_dividend) || 0; const b = Number(inp_divisor) || 1; return { result: a / b, remainder: a % b };",
    "inputs": [
      { "id": "dividend", "label": "Dividend", "type": "number" },
      { "id": "divisor", "label": "Divisor", "type": "number" }
    ],
    "outputs": [
      { "id": "result", "label": "Result", "type": "number" },
      { "id": "remainder", "label": "Remainder", "type": "number" }
    ]
  }
}

Categories

The category field determines the header color of the node:

| Category | Color | | ------------- | ------------ | | generator | Dark Green | | transformer | Indigo | | output | Dark Red | | generic | Dark Gray |

If omitted, defaults to generic.

Operation Code (Inline JavaScript)

operationCode is a JavaScript string evaluated with new Function(). The function receives these named arguments:

| Argument pattern | Source | Example | | --- | --- | --- | | inp_<inputId> | Resolved value of each input port | inp_a, inp_value | | conf_<configId> | Current value of each configurable field | conf_gravityConstant | | state | The node's full internal state object | state.outputs, state.config |

The function must return an object whose keys match the output port IDs:

// For a node with inputs: [{ id: 'a' }, { id: 'b' }] and outputs: [{ id: 'sum' }]
"return { sum: (Number(inp_a) || 0) + (Number(inp_b) || 0) };"

For generator nodes with no inputs, use state.config to read user-editable values:

// For a node with configurable: [{ id: 'val' }] and outputs: [{ id: 'val' }]
"return { val: state.config?.val ?? 10 };"

The compiled function is cached internally, so repeated evaluations of the same operationCode do not recompile.

Operation Type (Registry)

Instead of inline code, you can reference a function from an external registry by setting operationType:

// Node definition
const defs = {
  fetchUrl: {
    label: 'Fetch URL',
    category: 'transformer',
    operationType: 'fetchUrl',  // key into the registry
    inputs: [{ id: 'url', label: 'URL', type: 'string' }],
    outputs: [{ id: 'data', label: 'Data', type: 'any' }],
  },
};

// Operation registry
const operations = {
  fetchUrl: async (inputs, state) => {
    const res = await fetch(inputs.url || '');
    const data = await res.json();
    return { data };
  },
};

<GraphlyCanvas
  nodeDefinitions={defs}
  operations={operations}
  evaluationMode="async"
/>

Registry operations receive (inputs: Record<string, any>, state: Record<string, any>) and return Record<string, any> (or a Promise of one in async mode).

If both operationCode and operationType are set, operationCode takes priority.

Configurable Fields

Configurable fields let users edit values that are not connected via edges. They are stored in state.config and accessible in operationCode as conf_<id>.

interface ConfigurableField {
  id: string;
  label: string;
  type?: string;          // 'number' | 'string' | 'text' | 'boolean' | dropdown (when options is set)
  options?: string[];     // Display labels for a dropdown
  values?: any[];         // Actual stored values (paired with options by index)
  configLocation?: 'inline' | 'panel' | 'both';  // Default: 'panel'
}

configLocation controls where the control appears:

| Value | Inline on node | In config panel | | -------- | -------------- | --------------- | | inline | Yes | No | | panel | No | Yes (default) | | both | Yes | Yes |

Dropdown example with separate display labels and stored values:

{
  "dropdown_input": {
    "label": "Mode Selector",
    "category": "generator",
    "operationCode": "return { selected: state.config?.selected ?? 1 };",
    "configurable": [
      {
        "id": "selected",
        "label": "Mode",
        "type": "string",
        "options": ["Option 1", "Option 2", "Option 3"],
        "values": [1, 2, 3],
        "configLocation": "inline"
      }
    ],
    "outputs": [{ "id": "selected", "label": "Selected", "type": "number" }]
  }
}

When options is provided without values, the option label string is stored directly. When values is provided, the value at the matching index is stored instead.

Output Nodes

Set isOutput: true to mark a node as a terminal output. This is a semantic marker your application can use to identify which nodes produce final results. The engine does not treat them differently, but the demo app uses it to render an "App Outputs" panel.

{
  "output_display": {
    "label": "Output Display",
    "isOutput": true,
    "category": "output",
    "operationCode": "return { value: inp_value };",
    "inputs": [{ "id": "value", "label": "Value", "type": "any" }]
  }
}

GraphlyCanvas Props

GraphlyCanvas extends all React Flow props (except onLoad) and adds:

| Prop | Type | Default | Description | | --- | --- | --- | --- | | nodeDefinitions | Record<string, Partial<DynamicNodeData>> | {} | Node type definitions. Populates the sidebar and context menu. | | operations | OperationRegistry | {} | Map of operationType keys to functions. | | evaluationMode | 'sync' \| 'async' | 'sync' | When 'async', registry operations that return Promises are awaited. | | evaluationOptions | EvaluateGraphOptions | — | Callbacks for diagnostics: onNodeEvaluated, onError, onCycleDetected. | | onSave | (data: GraphData) => Promise<void> \| void | — | Called when the user presses Cmd/Ctrl+S or calls ref.save(). | | onLoad | () => Promise<GraphData \| null> \| GraphData \| null | — | Called on mount to load initial graph data. | | showControls | boolean | true | Show React Flow zoom/fit controls. | | showMiniMap | boolean | true | Show the React Flow minimap. | | showBackground | boolean | true | Show the dot grid background. | | nodeTypes | Record<string, ComponentType> | — | Additional custom React Flow node types (merged with the built-in dynamicNode). | | children | ReactNode | — | Extra children rendered inside the React Flow canvas (e.g., custom <Panel>). | | ref | Ref<GraphlyCanvasHandle> | — | Imperative handle for save/load/getGraphData. |

All other React Flow props (e.g., fitView, minZoom, maxZoom, panOnDrag, defaultViewport, etc.) are forwarded directly to <ReactFlow>.


Evaluation Engine

The engine evaluates nodes in dependency order using Kahn's algorithm (topological sort). For each node it:

  1. Gathers inputs from connected upstream node outputs and from state.inputs (manual/default values).
  2. Runs the node's operation (operationCode or operationType from the registry).
  3. Writes state.inputs, state.outputs, and state.engineMeta back to the node.

The graph re-evaluates automatically whenever edges change, a new node is added, or a configurable field value changes.

Sync Evaluation (Default)

import { evaluateGraph } from '@easy-nodes/core';

// Used internally by GraphlyCanvas; also available for standalone use:
const evaluatedNodes = evaluateGraph(nodes, edges, registry, options);

All operations must return Record<string, any> synchronously. If an operation returns a Promise in sync mode, the engine attaches an error: "This node requires async evaluation mode.".

Async Evaluation

Enable by setting evaluationMode="async":

<GraphlyCanvas
  evaluationMode="async"
  operations={operations}
  nodeDefinitions={defs}
/>

Registry operations may return Promise<Record<string, any>>:

const operations = {
  fetchUrl: async (inputs) => {
    const res = await fetch(inputs.url || '');
    const data = await res.json();
    return { data };
  },
};

The standalone function is also available:

import { evaluateGraphAsync } from '@easy-nodes/core';

const evaluatedNodes = await evaluateGraphAsync(nodes, edges, registry, options);

Two-Phase Results

For operations that have both an immediate synchronous result and a deferred async result, return a two-phase object:

const operations = {
  fetchWithStatus: (inputs) => ({
    immediate: { loading: true },
    promise: fetch(inputs.url)
      .then(res => res.json())
      .then(data => ({ loading: false, data })),
  }),
};

The immediate outputs are applied right away (and can unblock downstream nodes), while promise outputs are merged in when resolved. This only works in async evaluation mode.

Partial Re-evaluation

When only part of the graph changes, you can re-evaluate just the affected subgraph:

import { evaluateSubgraph } from '@easy-nodes/core';

const evaluatedNodes = evaluateSubgraph(nodes, edges, registry, {
  startNodeIds: ['node-1', 'node-2'],
  onNodeEvaluated: (id, { outputs }) => console.log(id, outputs),
});

Only startNodeIds and their downstream dependents are re-evaluated. All other nodes retain their existing state.outputs.

Cycle Detection

The engine automatically detects cycles. Nodes involved in a cycle receive:

  • state.engineMeta.error = { message: 'Cycle detected' }
  • state.engineMeta.cyclesInvolving = ['node-A', 'node-B', ...]

The DynamicNode component renders a red error banner on nodes with errors.

Engine Metadata

Each evaluated node gets state.engineMeta (type EngineMeta):

interface EngineMeta {
  lastEvaluatedAt?: number;      // Timestamp of last evaluation
  error?: {
    message: string;
    stackSnippet?: string;       // First 3 lines of the stack trace
  };
  cyclesInvolving?: string[];    // IDs of all nodes in the detected cycle
}

Evaluation Options (Diagnostics)

Pass evaluationOptions to receive callbacks during evaluation:

<GraphlyCanvas
  evaluationOptions={{
    onNodeEvaluated: (nodeId, { inputs, outputs }) => {
      console.log(`Node ${nodeId} evaluated:`, outputs);
    },
    onError: (nodeId, error) => {
      console.error(`Node ${nodeId} failed:`, error);
    },
    onCycleDetected: (cycleNodeIds) => {
      console.warn('Cycle detected:', cycleNodeIds);
    },
  }}
  nodeDefinitions={defs}
/>

Save and Load

Using the Ref Handle

GraphlyCanvas exposes an imperative handle via React.forwardRef:

import { useRef } from 'react';
import { GraphlyCanvas, GraphlyCanvasHandle, GraphData } from '@easy-nodes/core';

function App() {
  const canvasRef = useRef<GraphlyCanvasHandle>(null);

  const handleSaveClick = () => {
    canvasRef.current?.save(); // triggers onSave callback
  };

  const handleExport = () => {
    const data: GraphData = canvasRef.current!.getGraphData();
    // data contains { nodes, edges, viewport }
    console.log(JSON.stringify(data));
  };

  const handleImport = (data: GraphData) => {
    canvasRef.current?.loadGraphData(data);
  };

  return (
    <GraphlyCanvas
      ref={canvasRef}
      onSave={async (data) => {
        localStorage.setItem('my-graph', JSON.stringify(data));
      }}
      onLoad={() => {
        const raw = localStorage.getItem('my-graph');
        return raw ? JSON.parse(raw) : null;
      }}
      nodeDefinitions={defs}
    />
  );
}

GraphlyCanvasHandle methods:

| Method | Description | | --- | --- | | save(): Promise<void> | Calls onSave with current { nodes, edges, viewport }. Sets isSaving and clears hasUnsavedChanges. | | getGraphData(): GraphData | Returns current { nodes, edges, viewport } without triggering save. | | loadGraphData(data: GraphData): void | Replaces nodes, edges, and viewport. Resets hasUnsavedChanges. |

onSave / onLoad Callbacks

| Callback | Signature | When | | --- | --- | --- | | onSave | (data: GraphData) => Promise<void> \| void | Called by ref.save() or Cmd/Ctrl+S. | | onLoad | () => Promise<GraphData \| null> \| GraphData \| null | Called once on initial mount. Return null for an empty canvas. |

GraphlyProjectFile

A convenience type for consumers who store metadata alongside the graph:

type GraphlyProjectFile = {
  version?: number;
  name?: string;
  updatedAt?: string;   // ISO date string
  graph: GraphData;
};

The library does not perform any I/O itself. This type is provided for consistency if you build project management features.

Keyboard Shortcut

Cmd+S (Mac) / Ctrl+S (Windows/Linux) triggers onSave automatically.


Store

Graphly uses a Zustand store internally. You can access it directly for advanced use cases:

import { useGraphlyStore } from '@easy-nodes/core';

function MyComponent() {
  const nodes = useGraphlyStore((s) => s.nodes);
  const edges = useGraphlyStore((s) => s.edges);
  const hasUnsavedChanges = useGraphlyStore((s) => s.hasUnsavedChanges);
  const updateNodeData = useGraphlyStore((s) => s.updateNodeData);
  // ...
}

GraphlyStoreState fields and methods:

| Field / Method | Type | Description | | --- | --- | --- | | nodes | Node[] | Current React Flow nodes. | | edges | Edge[] | Current React Flow edges. | | onNodesChange | OnNodesChange | Handler for React Flow node changes (position, selection, removal). | | onEdgesChange | OnEdgesChange | Handler for React Flow edge changes. | | onConnect | OnConnect | Handler for new connections. Automatically replaces existing edges on the same input handle. | | setNodes(nodes) | (Node[]) => void | Replace all nodes. | | setEdges(edges) | (Edge[]) => void | Replace all edges. | | addNode(node) | (Node) => void | Append a single node. | | updateNodeData(id, patch) | (string, Partial<DynamicNodeData>) => void | Shallow-merge a data patch into a specific node. | | configPanelNodeId | string \| null | ID of the node whose config panel is open, or null. | | setConfigPanelNodeId(id) | (string \| null) => void | Open/close the config panel for a node. | | isSaving | boolean | True while a save operation is in progress. | | setIsSaving(v) | (boolean) => void | — | | hasUnsavedChanges | boolean | True when the graph has been modified since the last save/load. | | setHasUnsavedChanges(v) | (boolean) => void | — |

The store enforces single-connection-per-input-handle: when a new edge connects to a target handle, any existing edge on that same target+targetHandle is removed.


Components

GraphlyCanvas

The main component. Renders the full editor: React Flow canvas, node sidebar, config panel, context menu, minimap, controls, and background. See GraphlyCanvas Props for the full API.

DynamicNode

The built-in node component registered as dynamicNode. Renders:

  • A colored header based on category.
  • Input handles on the left with optional inline controls (for number/text/string types).
  • Output handles on the right.
  • Inline configurable fields (when configLocation is 'inline' or 'both').
  • A gear button to open the config panel.
  • An error banner when state.engineMeta.error is present.

You do not need to use or import DynamicNode directly; it is automatically registered as a node type.

NodeSidebar

A draggable panel (top-left) listing all node definitions. Users drag items from the sidebar onto the canvas to create nodes. Only renders when at least one definition is provided.

NodeConfigPanel

A panel (top-right) that opens when the user clicks the gear icon on a node or selects "Open config panel" from the context menu. Displays all configurable fields whose configLocation is 'panel' or 'both' (or unset, since 'panel' is the default). Close with Escape or the Close button.

NodeConfigControls / PortControl

PortControl renders the appropriate HTML control for a port or configurable field based on its type:

| Port type | Control | | --- | --- | | number | <input type="number"> | | string / text | <input type="text"> | | boolean | <input type="checkbox"> | | Has options | <select> dropdown | | Anything else | <input type="text"> |

Props:

interface PortControlProps {
  port: PortOrConfigurable;     // { id, label, type?, options?, values? }
  value: unknown;
  onChange: (value: unknown) => void;
  compact?: boolean;            // Smaller styling for inline node UI
  disabled?: boolean;           // Disabled when an edge is connected to the input
  className?: string;
}

DeletableEdge

A bezier edge used as the default edge type. All edges are rendered as bezier curves. Edges can be deleted via the context menu or by selecting and pressing Delete.

CanvasContextMenu

A right-click context menu rendered as a portal. Behavior depends on where you right-click:

Pane (canvas background):

  • Add node (submenu listing all definitions)
  • Paste (if clipboard has content)
  • Delete selection (if nodes are selected)

Node:

  • Open config panel
  • Delete
  • Copy
  • Cut

Hooks and Context

useGraphlyData

A hook for data loading and saving, used internally by GraphlyCanvas:

import { useGraphlyData } from '@easy-nodes/core';

const { save, isSaving, hasUnsavedChanges, nodes, edges } = useGraphlyData({
  onSave: async (data) => { /* persist data */ },
  onLoad: async () => { /* return GraphData or null */ },
});

useGraphlyConfig / GraphlyConfigContext

GraphlyConfigContext provides the onConfigChange callback used by DynamicNode and NodeConfigPanel to propagate configurable field changes. GraphlyCanvas sets this up automatically.

import { useGraphlyConfig } from '@easy-nodes/core';

const config = useGraphlyConfig();
// config?.onConfigChange(nodeId, fieldId, newValue)

Types Reference

All types are exported from the package root:

import type {
  GraphData,
  GraphlyProjectFile,
  GraphlyCanvasHandle,
  GraphlyStoreState,
  SyncCallbacks,
  DynamicNodePort,
  ConfigurableField,
  NodeCategory,
  EngineMeta,
  DynamicNodeData,
  DynamicGraphlyNode,
  NodeOperation,
  OperationRegistry,
  EvaluateGraphOptions,
  EvaluateSubgraphOptions,
} from '@easy-nodes/core';

Key types at a glance

type GraphData = {
  nodes: Node[];
  edges: Edge[];
  viewport?: Viewport;
};

type NodeCategory = 'generator' | 'transformer' | 'output' | 'generic';

interface DynamicNodeData extends Record<string, unknown> {
  label: string;
  inputs?: DynamicNodePort[];
  outputs?: DynamicNodePort[];
  configurable?: ConfigurableField[];
  settings?: Record<string, any>;
  category?: NodeCategory;
  isOutput?: boolean;
  operationType?: string;
  operationCode?: string;
  state?: Record<string, any>;   // { inputs, outputs, config, engineMeta }
}

type DynamicGraphlyNode = Node<DynamicNodeData>;

type NodeOperation = (
  inputs: Record<string, any>,
  state: Record<string, any>
) => Record<string, any> | Promise<Record<string, any>>;

type OperationRegistry = Record<string, NodeOperation>;

interface EvaluateGraphOptions {
  onNodeEvaluated?: (nodeId: string, payload: { inputs: Record<string, any>; outputs: Record<string, any> }) => void;
  onError?: (nodeId: string, error: unknown) => void;
  onCycleDetected?: (cycleGroupNodeIds: string[]) => void;
}

interface EvaluateSubgraphOptions extends EvaluateGraphOptions {
  startNodeIds: string[];
}

Re-exported React Flow Utilities

For convenience, Graphly re-exports commonly needed items from @xyflow/react so you don't need to install or import it separately for basic usage:

import {
  Handle,
  Position,
  useReactFlow,
  ReactFlowProvider,
  applyNodeChanges,
  applyEdgeChanges,
  addEdge,
  MarkerType,
} from '@easy-nodes/core';

import type { Node, Edge } from '@easy-nodes/core';

Context Menu

Right-click anywhere on the canvas or on a node to open the context menu.

Canvas right-click:

  • Add node — opens a submenu with all available node definitions. The new node is placed at the click position.
  • Paste — paste previously copied nodes (and their internal edges) at the click position.
  • Delete selection — removes all currently selected nodes and their connected edges.

Node right-click:

  • Open config panel — opens the config panel for this node.
  • Delete — removes the node and its edges.
  • Copy — copies the node (or all selected nodes) to the clipboard.
  • Cut — copies and then removes the node(s).

Copy/paste duplicates nodes with new IDs and repositions them relative to the paste location.


Performance Tips

  • State comparison: The canvas uses hasStateChanged(oldState, newState) to compare only inputs, outputs, and engineMeta.error/cyclesInvolving before updating the store. This avoids unnecessary re-renders when lastEvaluatedAt changes but nothing else does.

  • Partial re-evaluation: If you know which nodes changed (e.g., after an edge or input edit), call evaluateSubgraph with startNodeIds and update the store with the returned nodes instead of re-evaluating the full graph.

  • Async mode: Only use evaluationMode="async" when you have operations that return Promises. Sync evaluation has less overhead.

  • Operation caching: Inline operationCode strings are compiled once and cached in a Map<string, Function>, so repeated evaluations don't pay the new Function() cost.


Full Example

A complete example with save/load, async operations, diagnostics, and a custom registry operation:

import { useRef, useState, useMemo, useCallback } from 'react';
import {
  GraphlyCanvas,
  GraphlyCanvasHandle,
  GraphData,
  DynamicNodeData,
  EvaluateGraphOptions,
  OperationRegistry,
} from '@easy-nodes/core';

const nodeDefinitions: Record<string, Partial<DynamicNodeData>> = {
  number: {
    label: 'Number',
    category: 'generator',
    operationCode: 'return { val: state.config?.val ?? 0 };',
    configurable: [
      { id: 'val', label: 'Value', type: 'number', configLocation: 'inline' },
    ],
    outputs: [{ id: 'val', label: 'Value', type: 'number' }],
  },
  multiply: {
    label: 'Multiply',
    category: 'transformer',
    operationCode:
      'return { product: (Number(inp_a) || 0) * (Number(inp_b) || 0) };',
    inputs: [
      { id: 'a', label: 'A', type: 'number' },
      { id: 'b', label: 'B', type: 'number' },
    ],
    outputs: [{ id: 'product', label: 'Product', type: 'number' }],
  },
  fetchJson: {
    label: 'Fetch JSON',
    category: 'transformer',
    operationType: 'fetchJson',
    inputs: [{ id: 'url', label: 'URL', type: 'string' }],
    outputs: [{ id: 'data', label: 'Data', type: 'any' }],
  },
  output: {
    label: 'Result',
    category: 'output',
    isOutput: true,
    operationCode: 'return { value: inp_value };',
    inputs: [{ id: 'value', label: 'Value', type: 'any' }],
  },
};

const operations: OperationRegistry = {
  fetchJson: async (inputs) => {
    const res = await fetch(inputs.url || 'https://jsonplaceholder.typicode.com/todos/1');
    return { data: await res.json() };
  },
};

function App() {
  const canvasRef = useRef<GraphlyCanvasHandle>(null);
  const [mode, setMode] = useState<'sync' | 'async'>('async');

  const evalOpts: EvaluateGraphOptions = useMemo(() => ({
    onNodeEvaluated: (id, { outputs }) => console.log(`[${id}]`, outputs),
    onError: (id, err) => console.error(`[${id}] Error:`, err),
    onCycleDetected: (ids) => console.warn('Cycle:', ids),
  }), []);

  const handleSave = useCallback(async (data: GraphData) => {
    localStorage.setItem('my-graph', JSON.stringify(data));
  }, []);

  const handleLoad = useCallback((): GraphData | null => {
    const raw = localStorage.getItem('my-graph');
    return raw ? JSON.parse(raw) : null;
  }, []);

  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <GraphlyCanvas
        ref={canvasRef}
        nodeDefinitions={nodeDefinitions}
        operations={operations}
        evaluationMode={mode}
        evaluationOptions={evalOpts}
        onSave={handleSave}
        onLoad={handleLoad}
      />
    </div>
  );
}

export default App;

License

ISC