Skip to content

Component Model

Components are the building blocks of TinySystems modules. Understanding the component model is essential for building effective modules.

What is a Component?

A component is a reusable unit of logic that:

  • Receives messages on input ports
  • Processes data according to its logic
  • Sends results to output ports
  • Maintains optional internal state
                    ┌────────────────────────────────────┐
                    │           COMPONENT                 │
                    │                                     │
    Input Port ────▶│   ┌─────────────────────────┐     │────▶ Output Port
                    │   │      Handle()            │     │
    Input Port ────▶│   │                          │     │────▶ Output Port
                    │   │   - Process message      │     │
   Settings Port ──▶│   │   - Apply logic          │     │
                    │   │   - Call output()        │     │
   Control Port ◀──▶│   └─────────────────────────┘     │
                    │                                     │
                    │         Component State             │
                    │         (settings, etc.)            │
                    └────────────────────────────────────┘

Component Interface

Every component implements this interface:

go
type Component interface {
    // Metadata about the component
    GetInfo() ComponentInfo

    // Process incoming messages
    Handle(ctx context.Context, output Handler, port string, message any) any

    // Define available ports
    Ports() []Port

    // Factory to create new instances
    Instance() Component
}

GetInfo()

Returns component metadata displayed in the UI:

go
func (c *MyComponent) GetInfo() module.ComponentInfo {
    return module.ComponentInfo{
        Name:        "my-component",      // Unique identifier
        Description: "Brief description", // Short summary
        Info:        "Detailed info...",  // Extended documentation
        Tags:        []string{"utility", "transform"},
    }
}

Handle()

The core processing function. Called for every message:

go
func (c *MyComponent) Handle(
    ctx context.Context,      // Request context (deadlines, tracing)
    output module.Handler,    // Callback to send output
    port string,              // Which port received the message
    message any,              // The message data (typed)
) any {                       // Return nil for success, error for failure
    // Process based on port
    switch port {
    case "input":
        result := process(message)
        return output(ctx, "output", result)
    case v1alpha1.SettingsPort:
        c.settings = message.(Settings)
        return nil
    }
    return nil
}

Ports()

Defines the component's interface:

go
func (c *MyComponent) Ports() []module.Port {
    return []module.Port{
        {
            Name:          "input",
            Label:         "Input",
            Position:      module.Left,
            Source:        false,           // Input port
            Configuration: InputType{},
        },
        {
            Name:          "output",
            Label:         "Output",
            Position:      module.Right,
            Source:        true,            // Output port
            Configuration: OutputType{},
        },
    }
}

Instance()

Factory method creating new component instances:

go
func (c *MyComponent) Instance() module.Component {
    return &MyComponent{
        settings: Settings{
            DefaultValue: "default",
        },
    }
}

Important: Always return a new instance with default values. Don't return c itself.

Component Lifecycle

1. REGISTRATION (at module startup)
   ┌─────────────────────────────────────────┐
   │  registry.Register(&MyComponent{})      │
   │  Component added to global registry     │
   └─────────────────────────────────────────┘


2. DISCOVERY (TinyNode created)
   ┌─────────────────────────────────────────┐
   │  TinyNode CRD references component      │
   │  Scheduler calls Instance()             │
   │  New component instance created         │
   └─────────────────────────────────────────┘


3. INITIALIZATION (Settings port)
   ┌─────────────────────────────────────────┐
   │  Handle(ctx, out, "_settings", config)  │
   │  Component stores configuration         │
   └─────────────────────────────────────────┘


4. RUNNING (Message processing)
   ┌─────────────────────────────────────────┐
   │  Handle(ctx, out, "input", message)     │
   │  Process message, call output()         │
   │  Repeat for each incoming message       │
   └─────────────────────────────────────────┘


5. RECONCILIATION (Periodic)
   ┌─────────────────────────────────────────┐
   │  Handle(ctx, out, "_reconcile", node)   │
   │  Refresh state, cleanup resources       │
   │  Every 5 minutes by default             │
   └─────────────────────────────────────────┘


6. DESTRUCTION (TinyNode deleted)
   ┌─────────────────────────────────────────┐
   │  Context cancelled                       │
   │  Component instance garbage collected   │
   └─────────────────────────────────────────┘

Stateless vs Stateful Components

Stateless Components

Most components should be stateless - they only use:

  • Incoming message data
  • Settings (from settings port)
go
type StatelessComponent struct {
    settings Settings  // Only store settings
}

func (c *StatelessComponent) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
    // All data comes from msg or c.settings
    input := msg.(Input)
    result := transform(input, c.settings)
    return output(ctx, "output", result)
}

Stateful Components

Some components need to maintain state:

  • Accumulators, counters
  • Connection pools
  • Running timers
go
type StatefulComponent struct {
    settings   Settings
    counter    int64
    cancelFunc context.CancelFunc
    mu         sync.Mutex  // Protect concurrent access
}

func (c *StatefulComponent) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
    c.mu.Lock()
    defer c.mu.Unlock()

    c.counter++
    // Use c.counter in processing
    return nil
}

Important: Stateful components require careful handling for:

  • Thread safety (use mutexes)
  • Resource cleanup (respect context cancellation)
  • Multi-replica scenarios (see Scalability)

Component State Sharing

For multi-replica scenarios, share state via TinyNode metadata:

go
func (c *MyComponent) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
    if port == v1alpha1.ReconcilePort {
        // Receive TinyNode with shared metadata
        node := msg.(v1alpha1.TinyNode)
        sharedValue := node.Status.Metadata["my-key"]
        // Use shared value
    }

    // Update shared state (leader only)
    if utils.IsLeader(ctx) {
        output(ctx, v1alpha1.ReconcilePort, func(node *v1alpha1.TinyNode) {
            node.Status.Metadata["my-key"] = "new-value"
        })
    }
}

See CR-Based State Propagation for details.

Best Practices

1. Keep Components Focused

Each component should do one thing well:

go
// Good: Single responsibility
type JSONParser struct{}    // Only parses JSON
type HTTPClient struct{}    // Only makes HTTP requests

// Bad: Multiple responsibilities
type DoEverything struct{}  // Parses, requests, transforms, logs...

2. Use Typed Messages

Always define typed structs for ports:

go
// Good: Typed messages
type Input struct {
    UserID string `json:"userId"`
    Action string `json:"action"`
}

// Bad: Untyped
func (c *Component) Handle(..., msg any) any {
    data := msg.(map[string]interface{})  // Fragile
}

3. Handle All Ports

Don't ignore unknown ports:

go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
    switch port {
    case "input":
        // Handle input
    case v1alpha1.SettingsPort:
        // Handle settings
    case v1alpha1.ReconcilePort:
        // Handle reconcile
    default:
        return fmt.Errorf("unknown port: %s", port)
    }
    return nil
}

4. Respect Context

Always check context for cancellation:

go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
    for _, item := range items {
        select {
        case <-ctx.Done():
            return ctx.Err()  // Stop processing
        default:
            process(item)
        }
    }
    return nil
}

Next Steps

Build flow-based applications on Kubernetes