Skip to content

System Ports

System ports are special ports that the SDK uses to deliver framework-level messages to components. They follow a naming convention with underscore prefix.

System Port Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│                         SYSTEM PORTS                                         │
└─────────────────────────────────────────────────────────────────────────────┘

                              ┌─────────────────┐
                              │                 │
           _settings ─────────▶   Component    │
           _control ──────────▶               │
           _reconcile ────────▶               │──────▶ output
           _client ───────────▶               │
                              │                 │
                              └─────────────────┘

   Port          | Constant                    | Purpose
   ──────────────┼─────────────────────────────┼────────────────────────────
   _settings     | v1alpha1.SettingsPort       | Configuration data
   _control      | v1alpha1.ControlPort        | UI button clicks
   _reconcile    | v1alpha1.ReconcilePort      | TinyNode CR updates
   _client       | v1alpha1.ClientPort         | HTTP client requests

_settings Port

Delivers configuration to components.

Constant

go
v1alpha1.SettingsPort = "_settings"

Message Type

Your custom Settings struct.

Usage

go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
    if port == v1alpha1.SettingsPort {
        c.settings = msg.(Settings)
        return nil
    }
    return nil
}

Delivery Timing

  • Delivered before any other messages
  • Delivered on settings change from UI
  • Delivered on pod startup

Port Definition

go
{
    Name:     v1alpha1.SettingsPort,
    Label:    "Settings",
    Source:   true,
    Position: module.PositionTop,
    Schema:   schema.FromGo(Settings{}),
}

_control Port

Delivers UI button click events.

Constant

go
v1alpha1.ControlPort = "_control"

Message Type

Your custom Control struct with button fields.

Usage

go
type Control struct {
    Start bool `json:"start,omitempty" title:"Start" format:"button"`
    Stop  bool `json:"stop,omitempty" title:"Stop" format:"button"`
}

func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
    if port == v1alpha1.ControlPort {
        if !utils.IsLeader(ctx) {
            return nil  // Only leader handles control
        }

        control := msg.(Control)
        if control.Start {
            return c.start(ctx, output)
        }
        if control.Stop {
            return c.stop()
        }
    }
    return nil
}

Delivery Timing

  • Delivered when user clicks a button in the UI
  • Creates a TinySignal CR in Kubernetes
  • Leader pod processes the signal

Port Definition

go
{
    Name:     v1alpha1.ControlPort,
    Label:    "Control",
    Source:   true,
    Position: module.PositionTop,
    Schema:   schema.FromGo(c.getControl()),
}

_reconcile Port

Delivers TinyNode CR state and triggers status updates.

Constant

go
v1alpha1.ReconcilePort = "_reconcile"

Message Types

Input: v1alpha1.TinyNode - The current TinyNode CR

Output: func(*v1alpha1.TinyNode) - Callback to modify node status

Usage - Reading State

go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
    if port == v1alpha1.ReconcilePort {
        node, ok := msg.(v1alpha1.TinyNode)
        if !ok {
            return nil
        }

        c.nodeName = node.Name

        // Read shared state from metadata
        if val, ok := node.Status.Metadata["my-key"]; ok {
            c.cachedValue = val
        }
    }
    return nil
}

Usage - Writing State

go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
    if port == v1alpha1.ReconcilePort && utils.IsLeader(ctx) {
        // Write to TinyNode status
        output(ctx, v1alpha1.ReconcilePort, func(node *v1alpha1.TinyNode) {
            if node.Status.Metadata == nil {
                node.Status.Metadata = make(map[string]string)
            }
            node.Status.Metadata["my-key"] = "my-value"
        })
    }
    return nil
}

Usage - Triggering UI Update

go
// Send nil to trigger port schema refresh
output(context.Background(), v1alpha1.ReconcilePort, nil)

Delivery Timing

  • On component startup
  • When TinyNode CR changes
  • Periodically (every 5 minutes by default)
  • When you send to _reconcile to update status

Port Definition

Usually implicit - no need to define in Ports().

_client Port

For HTTP-serving components to receive requests.

Constant

go
v1alpha1.ClientPort = "_client"

Message Type

Incoming HTTP request data (component-specific).

Usage

go
type ClientRequest struct {
    Method  string            `json:"method"`
    Path    string            `json:"path"`
    Headers map[string]string `json:"headers"`
    Body    any               `json:"body"`
}

type ClientResponse struct {
    StatusCode  int               `json:"statusCode"`
    ContentType string            `json:"contentType"`
    Body        string            `json:"body"`
}

func (h *HTTPServer) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
    if port == v1alpha1.ClientPort {
        req := msg.(ClientRequest)

        // Forward to flow
        result := output(ctx, "request", req)

        // Get response from flow
        if resp, ok := result.(ClientResponse); ok {
            return resp
        }
    }
    return nil
}

Delivery Timing

  • When HTTP request arrives at component's exposed port
  • Only for components that expose HTTP endpoints

Port Delivery Order

┌─────────────────────────────────────────────────────────────────────────────┐
│                         PORT DELIVERY ORDER                                  │
└─────────────────────────────────────────────────────────────────────────────┘

Time ────────────────────────────────────────────────────────────────────────▶

1. _settings
   │  Configuration delivered first


2. _reconcile
   │  Initial TinyNode state


3. Regular ports (input, etc.)
   │  Normal message flow begins


4. _control (as needed)
   │  User clicks buttons


5. _reconcile (periodic)
      Status updates, requeue

Complete Example

Component using all system ports:

go
package mycomponent

import (
    "context"
    "sync"

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

type Settings struct {
    Interval int `json:"interval" title:"Interval (ms)" default:"1000"`
}

type Control struct {
    Start bool `json:"start,omitempty" title:"Start" format:"button"`
    Stop  bool `json:"stop,omitempty" title:"Stop" format:"button"`
}

type Input struct {
    Data string `json:"data"`
}

type Output struct {
    Result string `json:"result"`
}

type MyComponent struct {
    settings   Settings
    nodeName   string
    isRunning  bool
    cancelFunc context.CancelFunc
    mu         sync.Mutex
}

func (c *MyComponent) GetInfo() module.Info {
    return module.Info{
        Name:        "my-component",
        Description: "Demonstrates all system ports",
        Icon:        "IconBox",
    }
}

func (c *MyComponent) Ports() []module.Port {
    return []module.Port{
        // System ports
        {
            Name:     v1alpha1.SettingsPort,
            Label:    "Settings",
            Source:   true,
            Position: module.PositionTop,
            Schema:   schema.FromGo(Settings{}),
        },
        {
            Name:     v1alpha1.ControlPort,
            Label:    "Control",
            Source:   true,
            Position: module.PositionTop,
            Schema:   schema.FromGo(c.getControl()),
        },
        // Regular ports
        {
            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 (c *MyComponent) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
    switch port {
    // System ports
    case v1alpha1.SettingsPort:
        c.settings = msg.(Settings)
        return nil

    case v1alpha1.ControlPort:
        return c.handleControl(ctx, output, msg)

    case v1alpha1.ReconcilePort:
        return c.handleReconcile(ctx, output, msg)

    // Regular ports
    case "input":
        return c.handleInput(ctx, output, msg)
    }

    return nil
}

func (c *MyComponent) handleControl(ctx context.Context, output module.Handler, msg any) error {
    if !utils.IsLeader(ctx) {
        return nil
    }

    control := msg.(Control)

    c.mu.Lock()
    defer c.mu.Unlock()

    if control.Start && !c.isRunning {
        ctx, c.cancelFunc = context.WithCancel(ctx)
        c.isRunning = true
        output(context.Background(), v1alpha1.ReconcilePort, nil)
        go c.run(ctx, output)
    }

    if control.Stop && c.isRunning {
        if c.cancelFunc != nil {
            c.cancelFunc()
        }
        c.isRunning = false
        output(context.Background(), v1alpha1.ReconcilePort, nil)
    }

    return nil
}

func (c *MyComponent) handleReconcile(ctx context.Context, output module.Handler, msg any) error {
    node, ok := msg.(v1alpha1.TinyNode)
    if !ok {
        return nil
    }

    c.nodeName = node.Name

    // Read shared state
    if val, ok := node.Status.Metadata["last-run"]; ok {
        // Use cached value
        _ = val
    }

    // Leader: update shared state
    if utils.IsLeader(ctx) {
        output(ctx, v1alpha1.ReconcilePort, func(n *v1alpha1.TinyNode) {
            if n.Status.Metadata == nil {
                n.Status.Metadata = make(map[string]string)
            }
            n.Status.Metadata["component-status"] = "ready"
        })
    }

    return nil
}

func (c *MyComponent) handleInput(ctx context.Context, output module.Handler, msg any) error {
    input := msg.(Input)
    result := Output{Result: "Processed: " + input.Data}
    return output(ctx, "output", result)
}

func (c *MyComponent) run(ctx context.Context, output module.Handler) {
    // Background work
    <-ctx.Done()
}

func (c *MyComponent) getControl() Control {
    c.mu.Lock()
    defer c.mu.Unlock()
    return Control{
        Start: !c.isRunning,
        Stop:  c.isRunning,
    }
}

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

System Port Constants

go
package v1alpha1

const (
    SettingsPort  = "_settings"
    ControlPort   = "_control"
    ReconcilePort = "_reconcile"
    ClientPort    = "_client"
)

Best Practices

1. Check Port Before Processing

go
switch port {
case v1alpha1.SettingsPort:
    // Handle settings
case v1alpha1.ControlPort:
    // Handle control
default:
    // Handle regular ports
}

2. Leader Check for Control

go
if port == v1alpha1.ControlPort {
    if !utils.IsLeader(ctx) {
        return nil
    }
    // Process control
}

3. Thread Safety

go
c.mu.Lock()
c.settings = msg.(Settings)
c.mu.Unlock()

4. Nil Checks for Reconcile

go
if port == v1alpha1.ReconcilePort {
    node, ok := msg.(v1alpha1.TinyNode)
    if !ok {
        return nil  // Handle callback case
    }
    // Process node
}

Next Steps

Build flow-based applications on Kubernetes