-
-
Notifications
You must be signed in to change notification settings - Fork 0
1. Quick start
Let's build your first functional mesh (or just check the full example in go-playground).
For simplicity it will be a mesh with just 1 component, which receives single input signal and generates "Hello World!" string as it's output signal.
To start, we create a mesh object. Each mesh is uniquely identified by its name, which helps in managing multiple meshes:
fm := fmesh.New("simple mesh")
As you can see it has a name, so you can use it when managing multiple meshes in your code. Also there are some other properties like Description or Configuration, but for now we do not need them.
Next, let’s create our first component, concat. This component concatenates two input strings and produces a single output string.
c := component.New("concat"). //Unique name is required
WithDescription("Concatenates 2 strings"). //Optional
WithInputs("i1", "i2"). //Optional (ports must have unique names)
WithOutputs("o1"). //Optional (ports must have unique names)
WithActivationFunc(func(this *component.Component) error {
//Read input signals
payload1 := this.InputByName("i1").FirstSignalPayloadOrDefault("").(string)
payload2 := this.InputByName("i2").FirstSignalPayloadOrDefault("").(string)
//Generate output signal
resultSignal := signal.New(payload1 + payload2)
//Put the signal on port
this.OutputByName("o1").PutSignals(resultSignal)
//Inform the framework that activation finished successfully
return nil
})
The concat component:
- Takes inputs from ports i1 and i2.
- Concatenates them.
- Outputs the result on port o1.
In general that is the work of any well encapsulated system (ideally a pure function): read inputs, provide outputs and leave zero side effects. In F-Mesh you do not read the signal from port, you just get it, because when a component is activated all input signals are already there, buffered on input ports. Same with writes, it is non-blocking operation, that why in F-Mesh we do not send, rather we put signals on the ports. This creates a frozen or discrete time experience, where activation functions involve no async I/O, you just take inputs, put outputs and exit with or without error.
The job done by the component sounds pretty simple and is equivalent of:
func concat(i1,i2 string) (string, error) {
return i1 + i2, nil
}
So why use a more complex and verbose approach? Of course, it doesn't make sense to use F-Mesh for simple tasks like concatenating two strings or similar operations. However, when dealing with a component that has tens or even hundreds of input and output ports, that's when F-Mesh truly shines with its declarative syntax!
Next, let's add the component to the mesh so it recognizes it
// Add component to mesh
fm.WithComponents(c)
Important
All components must be explicitly added to the mesh.
So far so good, now we can pass some data into the mesh:
// Pass input signals to mesh
c.InputByName("i1").PutSignals(signal.New("Hello"))
c.InputByName("i2").PutSignals(signal.New(" World!"))
There is no special API for initialising the mesh, you just put some signals on input ports of any component you want. In real-world scenario you probably will know which components are entry points for the initial data. Putting signals does not trigger anything, it is just like appending an item to slice.
Now everything is ready to start the computations and run the mesh:
// Run and check errors
_, err := fm.Run()
if err != nil {
fmt.Println(fmt.Errorf("F-Mesh returned an error: %w", err))
}
First, we pass the input signals, and only then do we run the mesh. The mesh is not a long running process by default (though it can be in more complex use-cases). It behaves more like a computational graph: you build it from components, initialize it with some state (input signals), and then execute it. Running an F-Mesh means triggering activation cycles 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 ports. You can learn more about scheduling rules here.
Note
Run() is a blocking operation — it will block until the mesh reaches a terminal state. As a result of running the mesh, you will receive an activation cycles report and any errors that occurred. The report can be useful for debugging or visualizing the mesh. In the code above, the report is replaced with blank identifiers for simplicity.
// Extract results from mesh
resultPayload := fm.Components().ByName("concat").OutputByName("o1").FirstSignalPayloadOrNil().(string)
fmt.Println("Got from mesh: ", resultPayload)
There are no surprises here—simply retrieve the signals from the output ports of the respective components (you or the mesh author should know where to place inputs and where to retrieve outputs). And that’s essentially it. If everything is working correctly, you should see something like:
Got from mesh: Hello World!
By now, we hope you have a sense of how small chunks of data (signals) flow in and out of components. That’s what FBP and F-Mesh are all about: rather than writing imperative code, you describe how your data flows, making it more natural for your task.
Congratulations! You've just built your first F-Mesh!
The framework utilizes a chained API to minimize repetitive error checks, allowing you to focus on actual programming. You only need to check for errors once at the end of the chain:
if sig := c.Inputs().ByName("invalid port name").Buffer().First(); sig.HasErr() {
// Handle error
}
All main APIs have HasErr() and Err() methods, which can be used to check if an error occurred in the chain of calls. Errors are propagated up the chain to the parent level (e.g., signal → signal group/collection → port → port group/collection → component → components collection → F-Mesh).