Skip to content

Component Interface

Every TinySystems component must implement the module.Component interface. This interface defines how your component integrates with the SDK and the visual editor.

The Interface

go
package module

type Component interface {
    // GetInfo returns component metadata
    GetInfo() Info

    // Handle processes incoming messages
    Handle(ctx context.Context, output Handler, port string, msg any) error

    // Ports returns the component's port definitions
    Ports() []Port

    // Instance creates a new instance of this component
    Instance() Component
}

Interface Methods

GetInfo()

Returns metadata about your component:

go
func (c *MyComponent) GetInfo() module.Info {
    return module.Info{
        Name:        "my-component",           // Unique identifier
        Description: "Does something useful",  // Shown in UI
        Icon:        "IconBox",                // Tabler icon name
        Tags:        []string{"utility"},      // For categorization
    }
}
FieldDescription
NameUnique identifier within the module (kebab-case)
DescriptionHuman-readable description for the UI
IconTabler icon name (see Tabler Icons)
TagsCategories for filtering in the component palette

Handle()

The core logic of your component. Called when messages arrive:

go
func (c *MyComponent) Handle(
    ctx context.Context,
    output Handler,
    port string,
    msg any,
) error {
    switch port {
    case "input":
        // Process input
        input := msg.(InputMessage)
        result := process(input)
        return output(ctx, "output", result)
    }
    return nil
}
ParameterDescription
ctxContext with cancellation, leader info
outputFunction to send messages to output ports
portName of the port that received the message
msgThe message data (type depends on port)

Ports()

Defines the component's input and output ports:

go
func (c *MyComponent) Ports() []module.Port {
    return []module.Port{
        {
            Name:     "input",
            Label:    "Input",
            Source:   true,                    // Can receive connections
            Position: module.PositionLeft,
            Schema:   schema.FromGo(InputMessage{}),
        },
        {
            Name:     "output",
            Label:    "Output",
            Source:   false,                   // Output only
            Position: module.PositionRight,
            Schema:   schema.FromGo(OutputMessage{}),
        },
    }
}

Instance()

Creates a new instance of the component:

go
func (c *MyComponent) Instance() module.Component {
    return &MyComponent{}
}

This is used when creating new nodes in the visual editor.

Complete Example

go
package mycomponent

import (
    "context"
    "strings"

    "github.com/tiny-systems/module/module"
    "github.com/tiny-systems/module/pkg/schema"
)

type Input struct {
    Text string `json:"text" title:"Text" description:"Text to uppercase"`
}

type Output struct {
    Text string `json:"text" title:"Result" description:"Uppercased text"`
}

type Uppercaser struct{}

func (u *Uppercaser) GetInfo() module.Info {
    return module.Info{
        Name:        "uppercaser",
        Description: "Converts text to uppercase",
        Icon:        "IconLetterCase",
        Tags:        []string{"text", "transform"},
    }
}

func (u *Uppercaser) Handle(
    ctx context.Context,
    output module.Handler,
    port string,
    msg any,
) error {
    if port == "input" {
        input := msg.(Input)
        return output(ctx, "output", Output{
            Text: strings.ToUpper(input.Text),
        })
    }
    return nil
}

func (u *Uppercaser) Ports() []module.Port {
    return []module.Port{
        {
            Name:     "input",
            Label:    "Input",
            Source:   true,
            Position: module.PositionLeft,
            Schema:   schema.FromGo(Input{}),
        },
        {
            Name:     "output",
            Label:    "Output",
            Source:   false,
            Position: module.PositionRight,
            Schema:   schema.FromGo(Output{}),
        },
    }
}

func (u *Uppercaser) Instance() module.Component {
    return &Uppercaser{}
}

Component Lifecycle

┌─────────────────────────────────────────────────────────────────────────────┐
│                         COMPONENT LIFECYCLE                                  │
└─────────────────────────────────────────────────────────────────────────────┘

1. REGISTRATION
   ┌──────────────────────────────────────────────────────────────────────────┐
   │  module.NewWithComponents(                                               │
   │      &MyComponent{},   // Register with module                           │
   │  )                                                                        │
   └──────────────────────────────────────────────────────────────────────────┘


2. DISCOVERY
   ┌──────────────────────────────────────────────────────────────────────────┐
   │  SDK calls GetInfo() and Ports()                                         │
   │  Publishes to TinyModule.Status.Components                               │
   │  Component appears in visual editor palette                              │
   └──────────────────────────────────────────────────────────────────────────┘


3. INSTANTIATION (when user creates node)
   ┌──────────────────────────────────────────────────────────────────────────┐
   │  SDK calls Instance() to create new instance                             │
   │  TinyNode CR created in Kubernetes                                       │
   │  Instance registered with Scheduler                                      │
   └──────────────────────────────────────────────────────────────────────────┘


4. RECONCILIATION
   ┌──────────────────────────────────────────────────────────────────────────┐
   │  Controller watches TinyNode                                             │
   │  Calls Handle(ctx, output, "_reconcile", node)                          │
   │  Component can initialize based on node state                            │
   └──────────────────────────────────────────────────────────────────────────┘


5. MESSAGE HANDLING (ongoing)
   ┌──────────────────────────────────────────────────────────────────────────┐
   │  Messages arrive via gRPC or internal routing                            │
   │  Handle() called for each message                                        │
   │  output() sends to connected nodes                                       │
   └──────────────────────────────────────────────────────────────────────────┘


6. TERMINATION
   ┌──────────────────────────────────────────────────────────────────────────┐
   │  TinyNode deleted                                                        │
   │  Context cancelled                                                       │
   │  Instance removed from Scheduler                                         │
   └──────────────────────────────────────────────────────────────────────────┘

Best Practices

1. Stateless When Possible

go
// Good: Stateless component
type Processor struct{}

func (p *Processor) Handle(ctx context.Context, output Handler, port string, msg any) error {
    // All state comes from msg
    return process(msg)
}

// Acceptable: Settings-based state
type ConfigurableProcessor struct {
    settings Settings  // Updated via _settings port
}

2. Use Meaningful Names

go
// Good
module.Info{
    Name:        "http-request",
    Description: "Makes HTTP requests to external APIs",
}

// Bad
module.Info{
    Name:        "req",
    Description: "Does HTTP stuff",
}

3. Handle Unknown Ports Gracefully

go
func (c *MyComponent) Handle(ctx context.Context, output Handler, port string, msg any) error {
    switch port {
    case "input":
        return c.handleInput(ctx, output, msg)
    default:
        // Unknown port - ignore or log
        return nil
    }
}

4. Respect Context Cancellation

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

Next Steps

Build flow-based applications on Kubernetes