Skip to content
github-actions[bot] edited this page Jul 29, 2026 · 1 revision

Overview

A plugin is a named bundle of initialization. You pass it to a constructor, its Init runs once, and whatever it set up — hooks, ports, metadata, pipes — is simply part of the thing you constructed.

That is all a plugin is. Its value is that it makes a concern portable: written once, it can be attached to any component or any mesh without editing either.

Plugins come at two levels, with the same shape:

Level Interface Attach with Init receives Query
Component component.Plugin component.WithPlugins(...) *component.Component c.PluginRegistered(name)
Mesh fmesh.Plugin fmesh.WithPlugins(...) *fmesh.FMesh fm.PluginRegistered(name)

Both interfaces are two methods:

GetName() string
Init(T) error

Rules that hold at both levels:

  • Names are unique. Attaching two plugins with the same name is a construction error.
  • A failing Init fails construction. New returns nil and the error; you never get a half-initialized object.
  • Init runs in sorted name order. Plugins mostly register hooks, and hooks fire in registration order — so init order is visible in behavior, and sorting keeps it identical between runs.

Component plugins

Init runs during component.New, after every other option has been applied. The component is fully formed at that point, so a plugin can add ports, metadata, state and hooks:

type PricePlugin struct{}

func (PricePlugin) GetName() string { return "price" }

func (PricePlugin) Init(c *component.Component) error {
    // Extend the component's interface
    if err := c.AddInputs("price_in"); err != nil {
        return err
    }
    if err := c.AddOutputs("price_out"); err != nil {
        return err
    }

    // Extend its behavior
    c.SetupHooks(func(hooks *component.Hooks) {
        hooks.OnCreation(func(this *component.Component) error {
            this.State().Set("base_price", 77.5)
            return nil
        })
        hooks.OnActivation(func(this *component.Component) error {
            return this.OutputByName("price_out").PutPayloads(999.0)
        })
    })

    // Extend its metadata
    c.AddLabel("plugin/price/version", "v1.2.4")
    return nil
}
c, err := component.New("priced",
    component.WithInputs("i1"),
    component.WithOutputs("o1"),
    component.WithActivationFunc(myLogic),
    component.WithPlugins(PricePlugin{}),
)

Order inside component.New: options → plugin Inits → OnCreation hooks. A plugin registering an OnCreation hook still has it run, because OnCreation fires after all plugins are initialized.


Mesh plugins

A mesh plugin bundles initialization for a whole mesh, which makes it the right home for anything cross-cutting — measuring, tracing, exporting, asserting, wiring. None of those belong to a single component, and all of them would otherwise have to be repeated in every one.

Init runs during fmesh.New, after the options, so the plugin sees a fully configured mesh.

The one thing to know: you cannot walk components in Init

A mesh is constructed empty and filled by AddComponents afterwards. At Init time there is nothing to walk:

func (p *MyPlugin) Init(fm *fmesh.FMesh) error {
    fm.Components().ForEach(...)   // always empty — the mesh has no components yet
    return nil
}

Instead, register an OnComponentAdded hook and instrument each component as it arrives:

type ActivationCounter struct {
    count atomic.Int64
}

func (p *ActivationCounter) GetName() string { return "activation-counter" }

func (p *ActivationCounter) Init(fm *fmesh.FMesh) error {
    fm.SetupHooks(func(hooks *fmesh.Hooks) {
        hooks.OnComponentAdded(func(ctx *fmesh.ComponentAddedContext) error {
            ctx.Component.SetupHooks(func(ch *component.Hooks) {
                ch.AfterActivation(func(*component.ActivationContext) error {
                    p.count.Add(1)
                    return nil
                })
            })
            return nil
        })
    })
    return nil
}

func (p *ActivationCounter) Count() int64 { return p.count.Load() }
counter := &ActivationCounter{}
fm, err := fmesh.New("mesh", fmesh.WithPlugins(counter))
// ... AddComponents, Run ...
fmt.Println(counter.Count())

That indirection is the whole trick: the plugin observes every activation in the mesh, and no component knows it exists.

Two consequences of instrumenting on arrival

  • Components are inspected only when added. Ports created later with AddInputs/AddOutputs are invisible to the plugin. Declare ports in component.New.
  • Component hooks run concurrently. Components in a cycle activate in parallel, so a plugin holding shared state must guard it — and if it is timing anything, take the clock readings outside its own lock, or each component gets charged for the contention caused by the others.

Bundled plugins

The plugin package ships ready-made mesh plugins.

import "github.com/hovsep/fmesh/plugin"

Profiler — where the time goes

Profiler times whole runs, single cycles, and every component's activations. The per-component numbers are the reason it exists: a Go CPU profile of a mesh is dominated by the scheduler and barely distinguishes one component from another, because every component's work is the same handful of runtime calls. Timing activations directly names the slow one.

prof := plugin.NewProfiler()

fm, err := fmesh.New("mesh", fmesh.WithPlugins(prof))
// ... AddComponents, Run ...

fmt.Println(prof.Report())
runs:   1, total 2.41ms, avg 2.41ms
cycles: 7, total 2.28ms, avg 325µs

component                           count        total          avg          min          max
tokenizer                               7       1.42ms        202µs        180µs        310µs
counter                                 7        612µs         87µs         71µs        140µs
Method Returns
Runs() Stat for whole runs
Cycles() Stat for individual cycles
Components() []ComponentStat, slowest total first
TopN(n) the n busiest components, by activation count
Report() the table above, as a string
Reset() discards everything measured so far

Stat carries Count, Total, Min, Max and an Avg() method. Stats accumulate across runs, which is what you want when comparing a mesh against itself; call Reset() between runs that should not be pooled. One Profiler belongs to one mesh.

TopN and Components() answer different questions. Components() sorts by total time — what is slow. TopN sorts by activation count — what is hot. A component that activates every cycle and does almost nothing can matter more than one that is individually slow but rarely runs.

Autowire — pipes by naming convention

A mesh of any size accumulates long stretches of wiring that say nothing a reader could not have guessed: everything that keeps time gets the clock, everything that wants the weather gets the weather. Written out, that is a list to maintain, and forgetting it fails silently — the component sits there, looking perfectly connected, waiting on inputs that never arrive.

Autowire makes the list derivable. Give it a rule mapping an output port to the input port name that should receive it, and it wires every match:

fm, err := fmesh.New("mesh",
    fmesh.WithPlugins(plugin.AutowireBroadcast("time")),
)

// No PipeTo calls: every component with an input named "time"
// is wired to every output named "time".
err = fm.AddComponents(clock, heart, lung)

Three ready-made conventions plus the general form:

Constructor Rule
plugin.AutowireBroadcast("time") every output named time → every input named time
plugin.AutowireBroadcastAs("tick", "time") every output named tick → every input named time
plugin.AutowirePrefixed("env_") an output → the input named <prefix><component>_<port>, e.g. env_sun_uvi
&plugin.Autowire{Name: func(source *component.Component, output *port.Port) string {...}} your own rule; return "" to decline

Wiring happens on every arrival, in both directions: a new component is offered every output already in the mesh, and every output it brings is offered to the components already there. So the order of AddComponents does not matter — which is the property that makes a convention safe to lean on.

A mesh often wants more than one convention at once — a clock reaching everything that keeps time, and a set of environmental factors reaching whatever asked for them by name. Those are separate rules, so they are separate plugins:

fm, err := fmesh.New("habitat", fmesh.WithPlugins(
    plugin.AutowireBroadcastAs("tick", "time"),
    plugin.AutowirePrefixed("env_"),
))

Each carries its own PluginName, so they coexist. Two conventions that happen to agree on the same output/input pair wire it once, not twice.

Two limits worth knowing:

  • A component is never wired to itself. A convention is not how you express a loopback — use c.LoopbackPipe(out, in).
  • Ports must exist before AddComponents, since arrival is the only moment a component is looked at.

Choosing a level

You want to... Use
Give one kind of component extra ports, state or behavior Component plugin
Observe or wire every component, without touching any of them Mesh plugin + OnComponentAdded
React to a lifecycle event in one specific mesh, once Just a hook — see 501. Hooks

A plugin is worth the wrapper when the setup is reusable or belongs to a concern rather than to a component. For a one-off, SetupHooks directly is simpler and says more.


For complete API documentation, see FMesh, Component and the plugin package.

Clone this wiki locally