Component Interface Reference

Complete reference for the module.Component interface that all TinySystems components must implement.

Source of truth: module/component.go, module/lifecycle.go, module/result.go in the SDK (github.com/tiny-systems/module).

Interface Definition

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

    // Handle processes a message arriving on `port`; emits via `output`
    Handle(ctx context.Context, output Handler, port string, message any) Result

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

    // Instance creates a new component instance with default settings
    Instance() Component
}

Methods

GetInfo

Returns metadata about the component.

func (c *Component) GetInfo() module.ComponentInfo

Returns: module.ComponentInfo

type ComponentInfo struct {
    Name        string   // Unique component identifier
    Description string   // Short human-readable description
    Info        string   // Longer help text
    Tags        []string // Categorization tags
}

Example:

func (c *Component) GetInfo() module.ComponentInfo {
    return module.ComponentInfo{
        Name:        "array_get",
        Description: "Get an array element by index",
        Info:        "Returns {item, index} for the requested 1-based index, or an error when out of range.",
        Tags:        []string{"array", "utility"},
    }
}

Notes:

  • Name must be unique within the module; use lowercase snake_case (e.g. array_get, http_server)
  • Name must not contain dots — full node names have the form {project-prefix}.{module}.{component}
  • There is no Icon field

Handle

Processes incoming messages on a data port. Returns module.Result, never a bare error or any.

func (c *Component) Handle(
    ctx context.Context,
    output module.Handler,
    port string,
    message any,
) module.Result

Parameters:

ParameterTypeDescription
ctxcontext.ContextRequest context with tracing, cancellation
outputmodule.HandlerFunction to emit output messages; itself returns Result
portstringName of the port receiving the message
messageanyThe incoming message data

Returns: module.Result — construct with module.Ok(v) / module.Fail(err); the runner unwraps it via Err() / Value().

Chain the Result returned by output up the call stack: blocking-I/O callers (e.g. http-server) receive the synchronous response that flows back from downstream nodes through these returns.

System ports never arrive at Handle. _settings, _control, _reconcile, _client and _identity are dispatched exclusively through the capability interfaces below; there is no legacy fallback into Handle. See the System Ports Reference.

Example:

func (c *Component) Handle(
    ctx context.Context,
    output module.Handler,
    port string,
    message any,
) module.Result {
    switch port {
    case "input":
        in, ok := message.(Input)
        if !ok {
            return module.Fail(fmt.Errorf("invalid input type: %T", message))
        }
        result, err := c.process(in)
        if err != nil {
            return module.Fail(err)
        }
        return output(ctx, "output", result)
    default:
        return module.Fail(fmt.Errorf("unknown port: %s", port))
    }
}

Error retryability: unmarked errors are never retried. Wrap transient failures with module.Retryable(err) so per-edge retry policies and the retry component can act on them; module.Permanent(err) marks the opposite explicitly. Emit module.NewError(ctx, err) on error ports to produce the canonical {context, error, retryable} payload. See the Handler Interface Reference for details.


Ports

Returns the component's port definitions.

func (c *Component) Ports() []module.Port

Returns: []module.Port

type Port struct {
    Source                bool            // true = OUTPUT (source of data), false = input
    Position              Position        // Top(0), Right(1), Bottom(2), Left(3)
    Name                  string          // lower-case programmatic name
    Label                 string          // human-readable name
    Configuration         interface{}     // request schema struct
    ResponseConfiguration interface{}     // response schema struct (blocking ports)
    Schema                json.RawMessage // raw JSON schema override (runtime-shaped forms)
}

See the Port Interface Reference for field semantics.

Example:

func (c *Component) Ports() []module.Port {
    return []module.Port{
        {
            Name:          v1alpha1.SettingsPort,
            Label:         "Settings",
            Configuration: Settings{},
        },
        {
            Name:          "input",
            Label:         "Input",
            Position:      module.Left,
            Configuration: Input{},
        },
        {
            Name:          "output",
            Label:         "Output",
            Source:        true, // output port
            Position:      module.Right,
            Configuration: Output{},
        },
        {
            Name:          "error",
            Label:         "Error",
            Source:        true, // output port
            Position:      module.Bottom,
            Configuration: module.ErrorMessage{},
        },
    }
}

Instance

Creates a new instance of the component.

func (c *Component) Instance() module.Component

Returns: module.Component — a new, independent instance

Example:

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

Notes:

  • Must return a fresh instance
  • Do not share state between instances
  • Each TinyNode gets its own instance

Registration

Register the component with the module's registry in an init function:

import "github.com/tiny-systems/module/registry"

func init() {
    registry.Register((&Component{}).Instance())
}

Capability Interfaces

Optional interfaces the framework detects on the component instance. System ports are delivered only through these — never through Handle.

InterfaceMethodDelivers
SettingsHandlerOnSettings(ctx context.Context, settings any) errorDeserialized _settings configuration
ControlHandlerOnControl(ctx context.Context, control any) error_control messages (dashboard widgets, Start/Stop)
ReconcileHandlerOnReconcile(ctx context.Context, node v1alpha1.TinyNode) errorThe TinyNode (by value) on every reconcile
IdentityAwareOnIdentity(id v1alpha1.NodeIdentity)Node name, namespace, flow, project
ClientAwareOnClient(client module.K8sClient)Kubernetes client access
NATSAwareOnNATS(js jetstream.JetStream)JetStream handle (nil when TINY_NATS_URL unset)
StatefulOnState(s module.State)Persistent key-value state backend
EmitterAwareOnEmitter(emit module.Handler)Long-lived handler for emitting from goroutines
DestroyerOnDestroy(metadata map[string]string)Called on node destruction (finalizer) with Status.Metadata

Dispatch order on a fresh runner:

  1. OnIdentity
  2. OnClient
  3. OnNATS
  4. OnState
  5. OnReconcile — restore state from metadata, react to spec
  6. OnSettings — user settings applied last, so they win over reconcile-restored state

On subsequent reconciles OnReconcile fires again; OnSettings only re-fires when the configured value changes.

Example — settings and reconcile:

func (c *Component) OnSettings(_ context.Context, msg any) error {
    in, ok := msg.(Settings)
    if !ok {
        return fmt.Errorf("invalid settings")
    }
    c.settings = in
    return nil
}

func (c *Component) OnReconcile(_ context.Context, node v1alpha1.TinyNode) error {
    if v, ok := node.Status.Metadata["http-port"]; ok {
        c.listenPort = v
    }
    return nil
}

module.Base: embed module.Base to satisfy Stateful, IdentityAware, ClientAware and EmitterAware for free; access the injected dependencies via b.State(), b.Identity(), b.Client(), b.Emit(ctx, port, data). Base deliberately does not implement ReconcileHandler or SettingsHandler — those are always component-specific.


Handler Function

The output handler for emitting messages.

type Handler func(ctx context.Context, port string, data any) module.Result
ParameterTypeDescription
ctxcontext.ContextMust pass through the incoming context
portstringOutput port name
dataanyData to emit

Returns: module.ResultErr() is non-nil on delivery/downstream failure; Value() carries the synchronous response that flows back through blocking-I/O chains. Chain it into your own return.

For emitting outside Handle (tickers, watchers, background loops), implement EmitterAware (or embed Base) to receive a long-lived Handler valid for the runner's lifetime.


Complete Example

package multiplier

import (
    "context"
    "fmt"

    "github.com/tiny-systems/module/api/v1alpha1"
    "github.com/tiny-systems/module/module"
    "github.com/tiny-systems/module/registry"
)

const (
    ComponentName = "multiplier"
    InputPort     = "input"
    OutputPort    = "output"
)

type Settings struct {
    Multiplier int `json:"multiplier" title:"Multiplier" default:"1"`
}

type Input struct {
    Value int `json:"value" required:"true" title:"Value"`
}

type Output struct {
    Result int `json:"result" title:"Result"`
}

type Component struct {
    settings Settings
}

func (c *Component) GetInfo() module.ComponentInfo {
    return module.ComponentInfo{
        Name:        ComponentName,
        Description: "Multiplies input value by the configured multiplier",
        Info:        "Emits {result: value * multiplier} for every input message.",
        Tags:        []string{"math", "transform"},
    }
}

// OnSettings receives _settings; the port never reaches Handle.
func (c *Component) OnSettings(_ context.Context, msg any) error {
    in, ok := msg.(Settings)
    if !ok {
        return fmt.Errorf("invalid settings")
    }
    c.settings = in
    return nil
}

func (c *Component) Handle(
    ctx context.Context,
    output module.Handler,
    port string,
    message any,
) module.Result {
    switch port {
    case InputPort:
        in, ok := message.(Input)
        if !ok {
            return module.Fail(fmt.Errorf("invalid input type: %T", message))
        }
        return output(ctx, OutputPort, Output{Result: in.Value * c.settings.Multiplier})
    default:
        return module.Fail(fmt.Errorf("unknown port: %s", port))
    }
}

func (c *Component) Ports() []module.Port {
    return []module.Port{
        {
            Name:          v1alpha1.SettingsPort,
            Label:         "Settings",
            Configuration: Settings{Multiplier: 1},
        },
        {
            Name:          InputPort,
            Label:         "Input",
            Position:      module.Left,
            Configuration: Input{},
        },
        {
            Name:          OutputPort,
            Label:         "Output",
            Source:        true,
            Position:      module.Right,
            Configuration: Output{},
        },
    }
}

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

var (
    _ module.Component       = (*Component)(nil)
    _ module.SettingsHandler = (*Component)(nil)
)

func init() {
    registry.Register((&Component{}).Instance())
}