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

Let's build your first functional mesh. We'll compute the min, max, and sum of a list of numbers — but instead of doing it sequentially, the three calculations will run concurrently, and a reporter will combine their results. This is a tiny example, yet it already shows the three things F-Mesh does for you that plain functions don't: fan-out, concurrency without goroutines, and fan-in with automatic synchronization.

Here is the topology we are going to build:

                  
                  these 3 run concurrently    
                     ┌──> [min] ──┐                
[source] ──fan-out───┼──> [max] ──┼── fan-in ──> [report]
                     └──> [sum] ──┘                

A runnable version of this example lives in the fmesh-examples repository.

Creating a mesh

To start, we create a mesh object. Each mesh is uniquely identified by its name, which helps in managing multiple meshes:

fm, err := fmesh.New("stats")
if err != nil {
    // handle error
}

There are optional configuration options (cycles limit, time limit, error handling strategy, logger), but for now we do not need them.

Creating a component

A component encapsulates one unit of work in its activation function. Let's create a reusable worker that reads every number on its input port and reduces it to a single result:

worker := func(name string, reduce func([]int) int) *component.Component {
    c, _ := component.New(name,
        component.WithInputs("in"),   // one input port
        component.WithOutputs("out"), // one output port
        component.WithActivationFunc(func(this *component.Component) error {
            // Read all numbers buffered on the input port
            var nums []int
            for _, s := range this.InputByName("in").Signals().All() {
                nums = append(nums, s.PayloadOrNil().(int))
            }

            // Reduce them and put the result on the output port
            return this.OutputByName("out").PutSignals(signal.New(reduce(nums)))
        }),
    )
    return c
}

min := worker("min", func(n []int) int {
    m := n[0]
    for _, v := range n {
        if v < m {
            m = v
        }
    }
    return m
})

max := worker("max", func(n []int) int {
    m := n[0]
    for _, v := range n {
        if v > m {
            m = v
        }
    }
    return m
})

sum := worker("sum", func(n []int) int {
    s := 0
    for _, v := range n {
        s += v
    }
    return s
})

Notice the shape of the activation function: it reads inputs, produces outputs, and exits — no shared state, no side effects. In F-Mesh you do not read a signal from a port, you just get it, because when a component is activated all its input signals are already buffered on its input ports. Likewise, writing is non-blocking: you put signals on a port rather than send them. This creates a frozen or discrete time experience — activation functions involve no async I/O.

The source

We want one place to feed numbers in, and have them distributed to all three workers. A tiny source component does that — it just forwards whatever arrives to its output:

source, _ := component.New("source",
    component.WithInputs("in"),
    component.WithOutputs("out"),
    component.WithActivationFunc(func(this *component.Component) error {
        return port.ForwardSignals(this.InputByName("in"), this.OutputByName("out"))
    }),
)

The broadcasting itself happens in the pipes (we wire them below), not in the component. In a real mesh, the source is usually an entry point that reads a file or queries a database — broadcasting via pipes lets all consumers share that work.

The reporter

Finally, a reporter with three input ports — one per worker — combines the results into a summary:

report, _ := component.New("report",
    component.WithInputs("min", "max", "sum"),
    component.WithOutputs("out"),
    component.WithActivationFunc(func(this *component.Component) error {
        summary := fmt.Sprintf("min=%v  max=%v  sum=%v",
            this.InputByName("min").Signals().FirstPayloadOrNil(),
            this.InputByName("max").Signals().FirstPayloadOrNil(),
            this.InputByName("sum").Signals().FirstPayloadOrNil(),
        )
        return this.OutputByName("out").PutSignals(signal.New(summary))
    }),
)

The reporter activates only once all three results are present. Because min, max, and sum sit at the same depth in the graph, their results arrive at the reporter in the same cycle — F-Mesh synchronizes the fan-in for you. (For paths of unequal length you can wait explicitly; see Waiting for inputs.)

Adding components to the mesh

if err := fm.AddComponents(source, min, max, sum, report); err != nil {
    // handle error
}

Important

All components must be explicitly added to the mesh. AddComponents returns an error if a component is invalid or has a duplicate name.

Connecting components with pipes

Pipes go from an output port to one or more input ports. A single PipeTo call with multiple destinations creates a fan-out:

// Fan-out: source broadcasts to all three workers
_ = source.OutputByName("out").PipeTo(
    min.InputByName("in"),
    max.InputByName("in"),
    sum.InputByName("in"),
)

// Fan-in: each worker reports to its own port on the reporter
_ = min.OutputByName("out").PipeTo(report.InputByName("min"))
_ = max.OutputByName("out").PipeTo(report.InputByName("max"))
_ = sum.OutputByName("out").PipeTo(report.InputByName("sum"))

This wiring is the program. To add a fourth statistic (say, a median), you'd add one component and two pipes — no existing code changes.

Passing initial signals

Now feed the numbers into the source. PutPayloads creates one signal per value in a single call:

_ = source.InputByName("in").PutPayloads(4, 8, 15, 16, 23, 42)

There is no special API for initialising the mesh — you just put signals on the input ports of whichever component is your entry point. Putting signals does not trigger anything; it is like appending to a slice.

PutSignals and PutPayloads return an error — always check the result in production code.

Running the mesh

_, err = fm.Run()
if err != nil {
    fmt.Println(fmt.Errorf("F-Mesh returned an error: %w", err))
}

The mesh behaves like a computational graph: you build it from components, initialize it with input signals, then execute it. Running means triggering waves of component activations until a terminal state is reached — an error, a panic, or a natural stop. The "happy path" is the natural stop, which occurs when no component has signals on any input port. You can learn more about when Run() stops here.

Note

Run() is a blocking operation — it will block until the mesh reaches a terminal state. Check the returned error to see if the mesh stopped normally or hit a limit.

Getting the results

Read the summary from the reporter's output port:

result := fm.ComponentByName("report").OutputByName("out").Signals().FirstPayloadOrNil().(string)
fmt.Println("Result:", result)

If everything is working correctly, you'll see:

Result: min=4 max=42 sum=108

Congratulations! You've just built your first F-Mesh.

Why not just a function or goroutines?

For a one-off calculation, a plain function is fine. But the moment you want those three calculations to run concurrently and then combine their results, hand-written Go needs three goroutines, a sync.WaitGroup (or three channels), and manual result collection — plus the bookkeeping to keep it correct.

In F-Mesh you wrote none of that. min, max, and sum run in parallel automatically because they are independent and ready in the same cycle; ports buffer the data; and the reporter fires only once all three results have arrived. As the graph grows — more stages, fan-out, feedback loops — that "free" concurrency and synchronization is what makes F-Mesh worth it. You describe how data flows, and the framework handles the execution.

Next Steps

Now that you've built your first mesh, explore these powerful features:

Error handling

F-Mesh uses direct error returns instead of a chainable error field. Methods that can fail — such as fmesh.New, component.New, AddComponents, PutSignals, PipeTo, and Run — return error as their last return value:

c, err := component.New("processor",
    component.WithInputs("in"),
    component.WithOutputs("out"),
    component.WithActivationFunc(func(c *component.Component) error {
        return c.OutputByName("out").PutSignals(signal.New("done"))
    }),
)
if err != nil {
    // handle construction error
}

if err := fm.AddComponents(c); err != nil {
    // handle add error
}

if err := c.InputByName("in").PutSignals(signal.New("data")); err != nil {
    // handle put error
}

Lookup methods like ComponentByName and InputByName return nil when the entity is not found — check for nil before use:

port := c.InputByName("missing")
if port == nil {
    // port not found
}

For more information on working with collections and groups, see the Collections and Groups page.

Clone this wiki locally