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
}
}| Field | Description |
|---|---|
Name | Unique identifier within the module (kebab-case) |
Description | Human-readable description for the UI |
Icon | Tabler icon name (see Tabler Icons) |
Tags | Categories 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
}| Parameter | Description |
|---|---|
ctx | Context with cancellation, leader info |
output | Function to send messages to output ports |
port | Name of the port that received the message |
msg | The 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
- Defining Ports - Port configuration details
- Handling Messages - Message processing patterns
- System Ports - Built-in port types