-
-
Notifications
You must be signed in to change notification settings - Fork 1
302. Ports
Ports are the connection points of components in F-Mesh. Unlike systems that follow the actor model (e.g., Akka), where components communicate directly, F-Mesh components are isolated and communicate only through signals passed via ports.
This design promotes modularity and separation of concerns: a component only interacts with its own ports and does not need to know where its signals are routed. The abstraction ensures that the logic and behavior of each component remain self-contained and decoupled from the overall mesh structure.
You can explore the full API for ports here.
There are two types of ports in F-Mesh:
- Input Ports: Receive signals for the component to process.
- Output Ports: Send signals to connected components.
Both input and output ports are instances of the same type, port.Port.
However, F-Mesh enforces type-safe connections:
- Pipes cannot be created between two input ports or two output ports.
- Connections are strictly from an output port to an input port.
There are two approaches to creating ports:
The most common way is to use WithInputs() / WithOutputs() options or AddInputs() / AddOutputs() on components:
c, err := component.New("processor",
component.WithInputs("data", "config", "control"),
component.WithOutputs("result", "logs"),
component.WithActivationFunc(myFunc),
)Or after construction:
c, _ := component.New("processor", component.WithActivationFunc(myFunc))
_ = c.AddInputs("data", "config", "control")
_ = c.AddOutputs("result", "logs")This is simple and works well for most cases.
For more control, you can create ports directly with descriptions and labels:
// Create ports with configuration
dataPort, _ := port.NewInput("data",
port.WithDescription("Primary data input for processing"),
port.WithLabel("type", "primary"),
port.WithLabel("required", "true"),
)
configPort, _ := port.NewInput("config",
port.WithDescription("Configuration parameters"),
)
configPort.SetLabels(map[string]string{
"type": "configuration",
"optional": "true",
})
resultPort, _ := port.NewOutput("result",
port.WithDescription("Processing results"),
)
resultPort.AddLabel("type", "data")
logsPort, _ := port.NewOutput("logs",
port.WithDescription("Diagnostic logs"),
)
logsPort.AddLabel("type", "diagnostic")
// Add pre-configured ports to component
c, _ := component.New("processor", component.WithActivationFunc(myFunc))
_ = c.AttachInputPorts(dataPort, configPort)
_ = c.AttachOutputPorts(resultPort, logsPort)This approach is useful when you need to:
- Add descriptions for documentation or export
- Configure labels during port creation
- Set up complex port configurations
- Reuse port configurations
Each port has five core elements:
- Name: Unique name used to access port.
- Signal Buffer: Holds an unlimited number of signals.
- Pipes List: Tracks outbound connections to other ports.
- Labels: String key-value metadata for categorizing and filtering ports. See the Labels documentation for details.
-
Scalars: Numeric metadata (
string→float64).
Important
The port buffer does not guarantee any specific order for the signals it holds.
Your program must not rely on the observed order of signals.
Ports accept signals via two methods:
// From existing signal objects
err := port.PutSignals(signal.New("data"), signal.New(42))
// From raw payloads (convenience)
err := port.PutPayloads("data", 42)Both methods return an error and trigger port hooks if configured.
Every port has a Direction: DirectionIn, DirectionOut, or DirectionUndefined (misconfigured). Use IsInput() / IsOutput() in logic. Piping validates direction at call time — connecting two outputs or two inputs returns port.ErrInvalidPipeDirection.
Package-level functions copy signals between ports without clearing the source:
// Copy all signals
err := port.ForwardSignals(source, dest)
// Copy only matching signals
err := port.ForwardWithFilter(source, dest, signal.LabelEquals("type", "data"))
// Transform then copy
err := port.ForwardWithMap(source, dest, func(s *signal.Signal) *signal.Signal {
return s.MapPayload(func(p any) any { return strings.ToUpper(p.(string)) })
})PutSignalGroups adds all signals from one or more signal.Group values:
_ = port.PutSignalGroups(group1, group2)Port descriptions are set at construction via the WithDescription option. Labels and scalars use the mutating API (AddLabel, SetLabels, AddScalar, etc.) — see Labels page.
ParentComponent() returns the owning component (as a ParentComponent interface with Name()).
| Error | When |
|---|---|
ErrInvalidPipeDirection |
PipeTo between same-direction ports |
ErrWrongPortDirection |
Attaching output port as input (or vice versa) |
ErrNilPort |
Nil port in pipe validation |
ErrInvalidRangeForIndexedGroup |
Invalid index range for indexed port creation |
Ports support hooks that allow you to intercept signal operations. Hooks are set up on individual ports:
c, _ := component.New("logger",
component.WithInputs("data"),
component.WithActivationFunc(myFunc),
)
// Setup hook on individual port
c.InputByName("data").SetupHooks(func(h *port.Hooks) {
h.OnSignalsAdded(func(ctx *port.SignalsAddedContext) error {
fmt.Printf("Port '%s' received %d signals\n",
ctx.Port.Name(),
len(ctx.SignalsAdded))
return nil
})
})Port hooks are useful for:
- Tracking signals flowing through specific ports
- Validating signal payloads
- Debugging signal flow
- Collecting metrics
For complete documentation on hooks at all levels (F-Mesh, Component, and Port), see the Hooks page.
When working with multiple ports, F-Mesh provides two utility types: Group and Collection:
- Collection - Indexed structure where ports are accessed by name
- Group - Ordered structure for iterating over ports without names
These are part of F-Mesh's consistent API for managing multiple entities. For complete documentation on how Collections and Groups work across F-Mesh, see the Collections and Groups page.
port.Group is an ordered list of ports — used for outbound pipes on a port and for batch operations. Create groups with:
group := port.NewInputGroup("p1", "p2", "p3") // or NewOutputGroup(...)
indexed, err := port.NewIndexedInputGroup("worker", 0, 3) // worker0 .. worker3 (or NewIndexedOutputGroup)port.Group supports Without, ForEachIf, MapIf, and the usual group iteration API. (Cross-entity scalar aggregation — SumScalar and friends — exists on signal.Group only.)