Skip to content

ChainGraph API Documentation / @badaitech/chaingraph-types

@badaitech/chaingraph-types

Core type definitions and utilities for the ChainGraph flow-based programming framework. This package serves as the foundation for the entire ChainGraph ecosystem, providing a robust and type-safe infrastructure for building visual programming nodes, ports, flows, and execution engines.

License

Overview

@badaitech/chaingraph-types provides:

  • Type-Safe Port System: Define ports with rich type systems including primitives, objects, arrays, streams, and more
  • Decorator-Based Node Creation: Build nodes using intuitive TypeScript decorators
  • Unified Port Storage: Advanced caching and storage system with automatic synchronization
  • Operations Layer: Type-safe, composable operations for modifying node state
  • Event Management: Powerful event handling mechanisms for node and flow communication
  • Flow Execution Engine: Core classes for executing computational graphs
  • Serialization Utilities: Robust serialization/deserialization support for all components

This package is designed to handle the complex type relationships between nodes, ports, and flows while providing a developer-friendly API for building visual programming components.

Installation

bash
# Before installing, make sure you have set up authentication for GitHub Packages
npm install @badaitech/chaingraph-types
# or
yarn add @badaitech/chaingraph-types
# or
pnpm add @badaitech/chaingraph-types

Authentication for GitHub Packages

To use this package, you need to configure npm to authenticate with GitHub Packages:

  1. Create a personal access token (PAT) with the read:packages scope on GitHub.
  2. Add the following to your project's .npmrc file or to your global ~/.npmrc file:
@badaitech:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_PAT

Replace YOUR_GITHUB_PAT with your actual GitHub personal access token.

Usage

Creating a Node with Decorators

typescript
import { 
  BaseNodeCompositional, 
  ExecutionContext, 
  NodeExecutionResult,
  Node, 
  Input, 
  Output, 
  String, 
  Number 
} from '@badaitech/chaingraph-types';

@Node({
  type: 'AdditionNode',
  title: 'Addition Node',
  description: 'Adds two numbers together',
  category: 'math',
})
class AdditionNode extends BaseNodeCompositional {
  @Input()
  @Number({ defaultValue: 0 })
  a: number = 0;

  @Input()
  @Number({ defaultValue: 0 })
  b: number = 0;

  @Output()
  @Number()
  result: number = 0;

  async execute(context: ExecutionContext): Promise<NodeExecutionResult> {
    this.result = this.a + this.b;
    return {};
  }
}

Handling Complex Port Types

Object Ports

typescript
import { ObjectSchema, PortObject, String, Number } from '@badaitech/chaingraph-types';

@ObjectSchema({
  type: 'UserProfile'
})
class UserProfile {
  @String()
  name: string = '';

  @Number({ min: 0, max: 120 })
  age: number = 0;
}

// In your node class:
@PortObject({
  schema: UserProfile,
  defaultValue: new UserProfile()
})
profile: UserProfile = new UserProfile();

Array Ports

typescript
import { PortArray, PortArrayNumber } from '@badaitech/chaingraph-types';

// Array of numbers
@PortArrayNumber({ defaultValue: [1, 2, 3] })
numbers: number[] = [];

// Array with custom item configuration
@PortArray({
  itemConfig: { 
    type: 'string', 
    minLength: 2 
  }
})
strings: string[] = [];

Stream Ports

typescript
import { PortStream, MultiChannel } from '@badaitech/chaingraph-types';

// Stream of strings
@PortStream({
  itemConfig: { type: 'string' }
})
dataStream: MultiChannel<string> = new MultiChannel<string>();

// Later in your code:
async processStream() {
  // Send data to the stream
  this.dataStream.send("Hello");
  this.dataStream.send("World");
  
  // Close the stream when done
  this.dataStream.close();
}

// Reading from a stream
async readStream() {
  for await (const item of this.dataStream) {
    console.log(item); // Prints "Hello", then "World"
  }
}

Enum Ports

typescript
import { 
  StringEnum, 
  NumberEnum, 
  PortEnumFromNative 
} from '@badaitech/chaingraph-types';

// String enum
@StringEnum(['Red', 'Green', 'Blue'], { defaultValue: 'Red' })
color: string = 'Red';

// Number enum
@NumberEnum([10, 20, 30], { defaultValue: '10' })
size: string = '10';

// From TypeScript enum
enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT'
}

@PortEnumFromNative(Direction, { defaultValue: Direction.Up })
direction: Direction = Direction.Up;

Creating and Executing a Flow

typescript
import { 
  Flow, 
  ExecutionContext, 
  ExecutionEngine 
} from '@badaitech/chaingraph-types';

// Create a flow
const flow = new Flow({ name: "Simple Calculation Flow" });

// Add nodes
const addNode = new AdditionNode('add1');
addNode.initialize();
flow.addNode(addNode);

// Set values
addNode.a = 5;
addNode.b = 10;

// Execute the flow
const abortController = new AbortController();
const context = new ExecutionContext(flow.id, abortController);
const executionEngine = new ExecutionEngine(flow, context);

// Subscribe to events
executionEngine.onAll((event) => {
  console.log(`Event: ${event.type}`, event.data);
});

// Run the flow
await executionEngine.execute();

// Check results
console.log(`Result: ${addNode.result}`); // Should print 15

Port Visibility Rules

typescript
import { 
  PortVisibility, 
  Boolean, 
  String 
} from '@badaitech/chaingraph-types';

class ConditionalNode extends BaseNodeCompositional {
  @Boolean({ defaultValue: false })
  showAdvanced: boolean = false;

  @PortVisibility({
    showIf: (node) => (node as ConditionalNode).showAdvanced
  })
  @String()
  advancedOption: string = '';
  
  // The advancedOption port will only be visible when showAdvanced is true
}

Core Components

Nodes

BaseNodeCompositional is the foundation for all computational nodes and implements the INodeComposite interface using a compositional architecture. It provides:

  • Port management via VirtualPortManager (lazy creation + caching)
  • Unified storage for all port configurations and values
  • Event handling via NodeEventManager
  • Serialization/deserialization via NodeSerializer
  • Execution lifecycle methods
  • Operations layer for type-safe state mutations

Architecture Highlights:

  • Each port manager component has a single responsibility
  • No complex inheritance hierarchies
  • Easy to test and extend

Ports

Ports are the inputs and outputs of nodes. The system supports various port types:

  • Primitive Ports: String, Number, Boolean
  • Complex Ports: Array, Object
  • Special Ports: Stream, Enum, Any, Secret

All ports are managed through a unified storage system that ensures:

  • Single source of truth for all port data
  • Consistent synchronization across all views
  • Efficient memory usage even with deeply nested structures

Flows

The Flow class represents a computational graph composed of nodes and edges. It provides:

  • Node and edge management
  • Graph validation
  • Event propagation
  • Serialization/deserialization

Execution Engine

The ExecutionEngine handles flow execution with features like:

  • Parallel execution support
  • Dependency resolution
  • Error handling
  • Execution events
  • Debugging capabilities

Advanced Architecture

Unified Port Storage System

The Unified Port Storage system solves the challenge of maintaining consistency across deeply nested ports with a dual storage strategy:

Storage Architecture

UnifiedPortStorage
├─ Flat Config Storage (O(1) lookups)
│  └─ Every port gets an entry: "config", "config.database", "config.database.host"
└─ Hierarchical Value Storage (Memory efficient)
   └─ Only root ports stored: "config" → { database: { host: "..." } }

Key Benefits

  • Single Source of Truth: All port data centralized in one place
  • Automatic Synchronization: Changes propagate instantly to all views
  • Memory Efficient: 1000-item arrays use 5 storage entries instead of 4000
  • Fast Lookups: O(1) for configs, O(d) for values where d = depth
  • Path-Based Access: Ports accessed using dot notation for objects, bracket notation for arrays and streams
  • Hierarchical Organization: Parent-child relationships maintained efficiently

Virtual Port Manager

The VirtualPortManager provides lazy port creation and caching with automatic synchronization:

Key Features

  • Lazy Creation: Ports created only when first accessed
  • Caching: Created ports cached for performance
  • No Watchers: WrappedPort always reads fresh from storage
  • Bidirectional Sync: IPort ↔ UnifiedPortStorage transparent integration

Usage

typescript
// First access - creates port
const port1 = node.getPort('prompt')  // WrappedPort created and cached

// Second access - returns cached
const port2 = node.getPort('prompt')  // Same instance

// Both always see fresh data from storage
port1.setValue('hello')
const value = port2.getValue()        // Gets 'hello'

WrappedPort Caching

WrappedPort is a transparent wrapper that integrates ports with unified storage:

Interception Pattern

typescript
// getValue/setValue read/write from unified storage
port.setValue("new value")  // Writes to storage, not just inner port

// getConfig builds dynamic proxy trees for complex types
const config = port.getConfig()
config.schema.properties   // Proxied - always fresh from storage

// setConfig writes to storage with automatic propagation
port.setConfig({ ui: { hidden: true } })  // Syncs to all views

Unwrapping

Access the underlying port instance when needed:

typescript
const wrapped = node.getPort('config')  // WrappedPort<ObjectPortConfig>
const inner = wrapped.unwrap()          // ObjectPort

if (inner instanceof ObjectPort) {
  inner.addField('field', config)       // Direct port methods
}

Operations Layer

The Operations Layer provides type-safe, composable operations for modifying node state:

Core Concepts

  • IOperation: Generic interface with full TypeScript typing for parameters and results
  • OperationContext: Traceability (userId, source, requestId, metadata)
  • OperationResult: Wrapper with success/error/data/events
  • OperationExecutor: Handles execution + event integration

Operation Types

Port Operations:

  • SetValueOperation: Update port value
  • UpdateConfigOperation: Update port configuration
  • UpdateUIOperation: Update port UI settings

Schema Operations:

  • AddFieldOperation: Add property to object port
  • RemoveFieldOperation: Remove property from object port
  • AppendItemOperation: Append item to array port
  • RemoveItemOperation: Remove item from array port

Usage Examples

typescript
import { SetValueOperation, AddFieldOperation } from '@badaitech/chaingraph-types';

// Set port value
const op = new SetValueOperation(
  { portPath: 'prompt', value: 'Hello world' },
  { userId: 'user123', source: 'ui', requestId: 'req_abc' }
)
const result = await node.executeOperation(op)

if (result.success) {
  console.log('Previous:', result.data.previousValue)
  console.log('New:', result.data.newValue)
  console.log('Events:', result.events)  // Auto-emitted
}

// Add field to object
const addOp = new AddFieldOperation(
  {
    parentPath: 'config',
    key: 'newDatabase',
    config: { type: 'object', schema: { properties: { host: { type: 'string' } } } },
    nodeId: node.id
  },
  { userId: 'user123' }
)
const addResult = await node.executeOperation(addOp)
// addResult.data.childPath === 'config.newDatabase'

Why Use Operations?

  • Type Safety: Full TypeScript support with generics
  • Traceability: Every change tracked with context (who/what/when)
  • Event Integration: Operations emit existing NodeEvents automatically
  • Testability: Mock storage, no complex state setup needed
  • Auditability: Operation context provides audit trail

Advanced Features

Decorators

The package includes a rich set of decorators for defining nodes and ports:

  • @Node: Main node class decorator (requires type field)
  • @ObjectSchema: Define object schemas (requires type field)
  • @Input/@Output: Port direction decorators
  • @String, @Number, @Boolean: Basic type decorators
  • @PortArray, @PortObject, @PortStream: Complex type decorators
  • @PortVisibility: Conditional visibility control

Event System

A comprehensive event system allows communication between components:

  • Node events (status change, port updates)
  • Flow events (node additions, edge connections)
  • Execution events (start, completion, errors)
  • Operation events (created via IOperation.toEvents())

Debugging Tools

Built-in debugging capabilities include:

  • Breakpoints
  • Step-by-step execution
  • Execution event monitoring

Performance Optimizations

The unified port storage system provides significant performance improvements:

MetricBenefit
Config LookupsO(1) via flat map (vs O(d) tree traversal)
Memory Usage1000x reduction for arrays (flat storage vs per-instance)
WatchersZero needed (WrappedPort always reads fresh)
Nested UpdatesStructural sharing (only clone changed path)
Port CreationLazy on-demand with caching

Example: 1000-Item Array

typescript
const items = Array.from({ length: 1000 }, (_, i) => ({
  id: i,
  name: `Item ${i}`,
  status: 'active'
}))

node.items = items
StorageOld ApproachNew ApproachReduction
Configs~4,000 entries~4 entries1000x
Values~4,000 entries1 entry4000x
Total~8,000 entries~5 entries1600x

Migration Guide

For Node Developers

No changes needed! The API is identical:

typescript
// Works exactly the same as before
node.prompt = "hello"
const value = node.prompt

// Port operations work the same
const port = node.getPort("prompt")
port.setValue("hello")
const value = port.getValue()

The unified storage system is transparent to node code.

For Framework Developers

If you're working with internal APIs, here are the key changes:

Old API (Deprecated):

typescript
// Manual port management
const port = node.getPort('config')
port.setValue(newValue)
node.updatePort(port)  // Manual propagation

New API (Recommended):

typescript
import { SetValueOperation } from '@badaitech/chaingraph-types'

// Type-safe operations with auto-propagation
const op = new SetValueOperation(
  { portPath: 'config', value: newValue },
  { source: 'system' }
)
await node.executeOperation(op)  // Handles all propagation

Accessing New Features

typescript
// Access unified storage (if needed)
const stats = node.storage.getStats()
console.log(`Configs: ${stats.configCount}`)
console.log(`Root Values: ${stats.rootValueCount}`)

// Get virtual port manager for advanced usage
const manager = node['virtualPortManager']
const port = manager.getPort('config')

// Use unwrap() for type checking
const wrapped = node.getPort('items')
if (wrapped.unwrap() instanceof ArrayPort) {
  // Access array-specific methods
}

Best Practices

Working with Unified Storage

DO:

typescript
// Update configs via port instances
const port = node.getPort("config.database.host")
port.setConfig({ ui: { hidden: true } })

// Access nested schemas via getConfig()
const config = objectPort.getConfig()
const schema = config.schema

// Use operations for complex state changes
const op = new AddFieldOperation(...)
await node.executeOperation(op)

// Watch for changes reactively
node.storage.watchConfig("config.database", (config) => {
  updateUI(config)
})

DON'T:

typescript
// Don't manually propagate to parent schemas (not needed!)
// ❌ parent.schema.properties.child = childPort.getConfig()

// Don't cache config objects (they become stale)
// ❌ const cached = port.getConfig()
// ❌ setTimeout(() => cached.ui, 1000)  // Might be stale

// Don't bypass storage for nested updates
// ❌ Manually updating deeply nested objects without storage

Debugging and Inspection

typescript
// Inspect storage statistics
const stats = node.storage.getStats()
console.log(`Total configs: ${stats.configCount}`)
console.log(`Total values: ${stats.rootValueCount}`)

// Get all child paths
const children = node.storage.getChildPaths("config")
console.log("Config children:", children)

// Verify proxy synchronization
const port = node.getPort("config")
const config1 = port.getConfig()
config1.schema.properties.database.ui = { hidden: true }
const config2 = port.getConfig()
console.log(config2.schema.properties.database.ui.hidden)  // true ✅

Testing

Unit Testing Nodes

typescript
import { describe, it, expect } from 'vitest'

describe('MyNode', () => {
  it('should execute operation', async () => {
    const node = new MyNode('test-id')
    node.initialize()

    const op = new SetValueOperation(
      { portPath: 'input', value: 'test' }
    )
    
    const result = await node.executeOperation(op)
    
    expect(result.success).toBe(true)
    expect(result.data.newValue).toBe('test')
  })
})

Testing Operations

typescript
import { UnifiedPortStorage, SetValueOperation } from '@badaitech/chaingraph-types'

it('should update storage', async () => {
  const storage = new UnifiedPortStorage()
  // ... register config ...
  
  const op = new SetValueOperation({
    portPath: 'prompt',
    value: 'new value'
  })
  
  const result = await op.execute(storage)
  expect(result.success).toBe(true)
})

Run tests with:

bash
# Test unified storage
pnpm --filter @badaitech/chaingraph-types test \
  src/node/implementations/__tests__/unified-port-storage.test.ts

# Test wrapped ports
pnpm --filter @badaitech/chaingraph-types test \
  src/port/instances/__tests__/wrapped-port.test.ts

# Test operations
pnpm --filter @badaitech/chaingraph-types test \
  src/node/implementations/__tests__/

Architecture Reference

Key Files

Core Architecture:

  • src/node/base-node-compositional.ts: Node base class with compositional architecture
  • src/node/implementations/unified-port-storage.ts: Unified storage engine
  • src/node/implementations/virtual-port-manager.ts: Lazy port creation and caching
  • src/port/instances/WrappedPort.ts: Storage-integrated port wrapper

Operations Layer:

  • src/node/operations/types/core-types.ts: Operation interfaces
  • src/node/operations/executor.ts: Operation execution engine
  • src/node/operations/port/: Port-specific operations
  • src/node/operations/schema/: Schema-specific operations

Port System:

  • src/port/base/: Base port interfaces and implementations
  • src/port/instances/: Concrete port types
  • src/port/plugins/: Port extensions and plugins

Important Concepts

Nodes:

  • Inherit from BaseNodeCompositional
  • Implement async execute(context: ExecutionContext) method
  • Use decorators to define ports
  • Can emit events during execution
  • Support batch updates via unified storage

Ports:

  • Typed connection points with validation
  • Support scalars and complex types
  • Managed through VirtualPortManager
  • Wrapped transparently with WrappedPort
  • Connected via Edges in Flows

Flows:

  • Directed graphs of nodes and edges
  • Event propagation engine for synchronization
  • Serializable and cloneable
  • Support validation and debugging

Execution:

  • Context provides access to nodes, events, abort controller
  • Engine handles dependency resolution and parallel execution
  • Supports child execution spawning via emitted events
  • Tracks execution depth to prevent infinite recursion

Type Safety:

  • Full TypeScript support with strict typing
  • Zod schemas for runtime validation
  • Operation generics for type-safe API
  • Decorator metadata for compile-time configuration

License

BUSL-1.1 - Business Source License

  • @badaitech/chaingraph-frontend: Frontend components for visual flow programming
  • @badaitech/chaingraph-backend: Backend services for flow execution
  • @badaitech/chaingraph-nodes: Collection of pre-built nodes
  • @badaitech/chaingraph-executor: Distributed execution engine with recovery

Support

For detailed documentation, see:

Enumerations

Classes

Interfaces

Type Aliases

Variables

Functions

Licensed under BUSL-1.1