Skip to content
github-actions[bot] edited this page Jul 23, 2026 · 2 revisions

Overview

Components are the primary building blocks in F-Mesh, encapsulating a unit of behavior in a single function known as the Activation Function. This design ensures that components remain lightweight and transient, activating only when needed and completing execution promptly. Unlike traditional FBP systems, F-Mesh components are not long-running processes like goroutines; instead, they execute their logic when activated and return. The framework decides when to activate a component based on whether it has input signals.

F-Mesh components are generally stateless by default, but they now have the capability to maintain state between activations. Each component contains its own State—a key-value store that persists data across cycles (safe without locks, because a component activates at most once per cycle in a single goroutine). This allows for more complex and persistent behavior without sacrificing the modularity, predictability, and reusability of the design. The activation function is invoked every time F-Mesh schedules the component. If the component fails to complete its execution promptly, it can cause the entire mesh to hang, so it's critical to design components to handle small, focused tasks: read input signals, process them, and produce output signals.

Key Elements of a Component

  1. Name
    Must be unique within a single mesh.
    Used for identification and debugging.

  2. Inputs and Outputs
    Collections of ports where signals are received and sent.
    Components can have an unlimited number of input and output ports.

  3. Activation Function
    Defines the logic of the component.
    Executes when the component is scheduled.

  4. State
    Simple key-value storage which can be used to persist data between activations.
    You can initialize a component state calling WithInitialState when building it.

  5. Labels & Scalars
    String and numeric metadata for categorizing and organizing components.
    Useful for filtering, routing, and implementing cross-cutting concerns.
    See the Labels documentation for details.

  6. Logger
    Each component is instrumented with a simple log.Logger from standard library. The logger has the component name set as a prefix.

You can optionally provide a description for a component. This description can be useful for visualization when exporting or documenting a mesh.

Note

While components can have many ports, you are not required to use all of them in every activation.
The usage of ports depends entirely on your component's design and logic.

To explore the full capabilities of components, refer to the component package API.

Activation function

The Activation Function is the heart of every component, defining its core behavior. To design elegant and maintainable systems, always strive to keep your activation functions simple and concise.

All activation functions in F-Mesh share the same signature:

func(this *component.Component) error

This signature allows you to access everything you need, like:

  • this.Inputs(): Collection of input ports.
  • this.Outputs(): Collection of output ports.
  • this.State(): Current state.
  • this.Logger(): Logger.

The function returns an error to indicate if the activation encountered any issues.

Both inputs and outputs are of type port.Collection, allowing you to use the same API to interact with ports. For more information on working with collections, see Collections and Groups. Typically, you will:

  • Read signals from input ports.
  • Process these signals.
  • Optionally read from or write to state.
  • Write results as signals to output ports.

When used properly the pattern resembles the concept of pure functions in functional programming, promoting clean and predictable behavior.

Example: Summing Input Signals

Here's a straightforward activation function that demonstrates how to use input and output ports:

func(this *component.Component) error {
    sum := 0

    // Approach 1: Using All() to get a slice
    for _, sig := range this.InputByName("i1").Signals().All() {
        sum += sig.PayloadOrNil().(int)
    }

    // Approach 2: Using ForEach() (cleaner for iteration)
    // _, err := this.InputByName("i1").Signals().ForEach(func(sig *signal.Signal) error {
    //     sum += sig.PayloadOrNil().(int)
    //     return nil
    // })

    // Create a new signal with the sum and put it on output port "o1"
    return this.OutputByName("o1").PutSignals(signal.New(sum))
}

Explanation:

  • The function calculates the sum of all integer payloads received on input port i1.
  • A new signal with the computed sum is sent to output port o1.
  • The loop iterates over all signals in the input port's buffer, allowing flexible handling of multiple signals.

Stateful component

As it is mentioned earlier components are stateless by default, but when you need you can make them stateful which means you can preserve some state between activation function invocations. Check the State type for all available methods.

Here is an example of stateful counter:

counter, _ := component.New("stateful_counter",
    component.WithDescription("counts all observed signals and bypasses them down the stream"),
    component.WithInputs("bypass_in"),
    component.WithOutputs("bypass_out"),
    component.WithInitialState(func(state component.State) {
        // Explicitly initialize state with the key we want to use (state is just wrapper around map[string]any)
        state.Set("observed_signals_count", 0)
    }),
    component.WithActivationFunc(func(this *component.Component) error {
        // Read the current value from state
        count := this.State().Get("observed_signals_count").(int)

        defer func() {
            // Once we finished activation we want to write back (persist) our state
            this.State().Set("observed_signals_count", count)
        }()

        count += this.InputByName("bypass_in").Signals().Len()
        this.Logger().Println("so far signals observed ", count)

        return port.ForwardSignals(this.InputByName("bypass_in"), this.OutputByName("bypass_out"))
    }),
)

State API

component.State is a map[string]any with helper methods:

Method Description
Has(key) Returns true if the key exists
Get(key) Returns value or nil
GetOrDefault(key, default) Returns value or default
Set(key, value) Sets a value
Delete(key) Removes a key
SetIfAbsent(key, value) Sets only if key is absent; returns whether it was set
Upsert(key, fn) Applies fn to current value (or nil) and stores result
Update(key, fn) Like Upsert but only when key exists
MustGetTyped[T](state, key) Typed get; panics on missing key or wrong type

Use WithInitialState to initialize keys at construction time, and ResetState() to wipe state between runs.

Indexed ports

Create many similarly named ports with a prefix and index range:

c, _ := component.New("worker",
    component.WithIndexedInputs("in", 0, 3),  // in0, in1, in2, in3
    component.WithIndexedOutputs("out", 0, 3),
    component.WithActivationFunc(myFunc),
)

Or call AddIndexedInputs(prefix, start, end) / AddIndexedOutputs after construction. Invalid ranges return port.ErrInvalidRangeForIndexedGroup.

LoopbackPipe

Connect an output port to an input port on the same component without manual lookup:

_ = c.LoopbackPipe("o1", "i1") // equivalent to c.OutputByName("o1").PipeTo(c.InputByName("i1"))

Nested meshes

F-Mesh has no special "sub-mesh" type. Instead, a component can run another mesh inside its activation function, treating the inner mesh as a black box. The inner mesh is not added to the outer mesh via AddComponents — you build it separately and call Run() imperatively.

This pattern composes complex workflows hierarchically: the outer component forwards input signals to the inner mesh, runs it, and maps results back to its outputs.

factorizer, _ := component.New("factorizer",
    component.WithInputs("in"),
    component.WithOutputs("out"),
    component.WithActivationFunc(func(this *component.Component) error {
        inner := buildFactorizationMesh() // your inner *fmesh.FMesh

        _, err := this.InputByName("in").Signals().ForEach(func(sig *signal.Signal) error {
            // Bridge: outer signal → inner mesh input
            _ = inner.ComponentByName("starter").InputByName("in").PutSignals(sig)

            if _, err := inner.Run(); err != nil {
                return fmt.Errorf("inner mesh failed: %w", err)
            }

            // Bridge: inner mesh output → outer signal
            factors, err := inner.ComponentByName("results").OutputByName("factors").Signals().AllPayloads()
            if err != nil {
                return err
            }
            return this.OutputByName("out").PutSignals(signal.New(myResult{
                Num:     sig.PayloadOrNil().(int),
                Factors: factors,
            }))
        })
        return err
    }),
)

Key points:

  • Blockinginner.Run() inside an activation function blocks until the inner mesh completes.
  • Separate registries — inner mesh components are invisible to the outer mesh scheduler.
  • Manual bridging — you copy signals in and out; pipes connect only within each mesh.
  • Recursive — an inner mesh component can itself wrap another mesh, to any depth.
  • Reuse — build the inner mesh once (e.g. in a factory function) and run it per signal or per activation.

See the full nesting example (prime factorization as a sub-mesh).

Returning Errors

In most cases, your activation function will complete successfully, and you can simply return nil. However, if an issue arises, returning an error is the proper way to communicate it to F-Mesh. Here's how error handling works:

  • Error Propagation: When your activation function returns an error, it notifies F-Mesh of the problem.

  • Mesh Behavior: Returning an error does not halt the entire mesh unless the error-handling strategy is explicitly set to StopOnFirstErrorOrPanic. This allows your mesh to continue processing other components even if one encounters an issue.

  • Error Handling Strategy: If you expect components to occasionally fail and want the mesh to proceed regardless, choose an error-handling strategy that tolerates errors when creating the mesh. Examples of such strategies include logging errors or collecting them for later inspection without stopping execution.

  • Signal Flushing: Errors do not affect how signals are drained from output ports. Any signals that were added to the output ports before the error occurred will still be forwarded as usual.

  • Inspecting errors: Run() returns an error when the mesh stops abnormally. Check hooks (OnError, OnPanic) for per-component details during execution. To inspect per-component results after the run, see Inspecting a run.

Example

Here's a simple example that demonstrates returning an error:

func(this *component.Component) error {
    // This signal will be successfully transferred, as it is put before any error is returned
    if err := this.OutputByName("log").PutSignals(signal.New("component activated")); err != nil {
        return err
    }

    firstPayload := this.InputByName("i1").Signals().FirstPayloadOrNil()
    if firstPayload == nil {
        return fmt.Errorf("no signals received on input port 'i1'")
    }

    number, ok := firstPayload.(int)
    if !ok {
        return fmt.Errorf("expected integer payload on 'i1', but got %T", firstPayload)
    }

    return this.OutputByName("o1").PutSignals(signal.New(number * 2))
}

Explanation:

  • If no signals are received on the input port i1, an error is returned with a descriptive message.
  • If the payload type is not an integer, an error is returned indicating the type mismatch.
  • If everything is fine, the function processes the input and sends a signal to the output port o1 with the doubled value.

Key Considerations

  • Use meaningful error messages to help diagnose issues during execution.
  • Ensure your mesh's error-handling strategy aligns with your system's requirements.
  • Errors are a tool to maintain clarity and predictability in your system without disrupting the entire flow unnecessarily.

Waiting for inputs

In some cases, you may need to delay activation of a component until specific signals appear on one or more of its ports. F-Mesh provides a basic synchronization mechanism for such scenarios, allowing you to return a special sentinel error from the activation function to signal that the component should wait.

Let's examine the following mesh setup:

Initializing the Mesh

Here's how we initialize the mesh with input signals:

// Put one signal into each chain to start them in the same cycle
_ = fm.ComponentByName("d1").InputByName("i1").PutSignals(signal.New(1))
_ = fm.ComponentByName("d4").InputByName("i1").PutSignals(signal.New(2))

This configuration starts execution at the topmost components (d1 and d4) and progresses downward in parallel. Execution proceeds in waves:

  1. d4, d1
  2. d5, d2
  3. sum, d3
  4. sum

The Synchronization Problem

Suppose the sum component needs to compute the sum of the signals from both vertical chains. A problem arises at cycle #3 because the left chain is shorter, causing its signal to arrive at sum earlier. To resolve this, we can instruct F-Mesh to wait until both input ports of sum have signals before activating it.

Implementation

Here's how to implement this behavior:

s, _ := component.New("sum",
    component.WithInputs("i1", "i2"),
    component.WithOutputs("o1"),
    component.WithActivationFunc(func(this *component.Component) error {
        // Wait until both input ports have signals
        if !this.Inputs().ByNames("i1", "i2").AllHaveSignals() {
            return component.ErrWaitingForInputsKeep
        }

        inputNum1 := this.InputByName("i1").Signals().FirstPayloadOrDefault(0)
        inputNum2 := this.InputByName("i2").Signals().FirstPayloadOrDefault(0)

        return this.OutputByName("o1").PutSignals(signal.New(inputNum1.(int) + inputNum2.(int)))
    }),
)

Two sentinel errors control waiting behavior:

Error Behavior
component.ErrWaitingForInputs Wait and clear input buffers (default)
component.ErrWaitingForInputsKeep Wait and keep input signals for the next cycle

Use ErrWaitingForInputsKeep when every signal is important and you want to collect multiple signals on each port. Use ErrWaitingForInputs when the presence of signals on specific ports matters more than their content.

By using this mechanism, you can control when a component should activate and ensure proper synchronization in your mesh.

Component Hooks

Hooks allow you to extend component behavior without modifying the activation function. They provide clean separation between core logic and cross-cutting concerns like logging, monitoring, and error handling.

Available Hooks

Components support several lifecycle hooks:

  • OnCreation - Called during component.New (after all options and plugins)
  • BeforeActivation - Called before the activation function runs
  • OnActivation - Extra activation functions appended after the main one (run sequentially, share the error path)
  • AfterActivation - Called after activation completes (regardless of outcome)
  • OnSuccess - Called when activation function returns nil
  • OnError - Called when activation function returns an error
  • OnPanic - Called when activation function panics
  • OnWaitingForInputs - Called when component is waiting for inputs

Setting Up Hooks

c, _ := component.New("monitored",
    component.WithInputs("in"),
    component.WithOutputs("out"),
    component.WithActivationFunc(func(c *component.Component) error {
        // Your component logic
        return nil
    }),
)
c.SetupHooks(func(h *component.Hooks) {
    h.BeforeActivation(func(c *component.Component) error {
        c.Logger().Println("Starting activation")
        return nil
    })
    
    h.OnSuccess(func(ctx *component.ActivationContext) error {
        ctx.Component.Logger().Println("Activation succeeded")
        return nil
    })
    
    h.OnError(func(ctx *component.ActivationContext) error {
        ctx.Component.Logger().Printf("Error: %v\n", ctx.Result.ActivationError())
        return nil // or return error to escalate
    })
})

Use Cases

  • Logging: Track component activations and results
  • Metrics: Measure execution time and success rates
  • Error Recovery: Implement fallback behavior on errors
  • Debugging: Inspect component state at each activation
  • Testing: Verify component behavior without modifying logic

For complete documentation on hooks, see the Hooks wiki page.

Clone this wiki locally