Yantrix
Yantrix is a TypeScript framework that provides a set of tools to create robust and self-documented functional applications by code generation. The business logic is represented by declarative, event-driven finite state machines, while the application state is an Anemic Domain Model, making it great a counterpart to any traditional state manager like Redux, while allowing devs to focus on describing contracts and workflows, rather than writing and debugging the actual code.
Lends itself perfectly to Architecture-as-Code paradigm and no-code/less-code tools for developers, like n8n.
Quick Start
Let's build a slider component. First, we need a diagram for a State Machine describing its behaviour. The diagram is written in Yantrix subsyntax of Mermaid and can be visualized directly with that awesome library. Click on <> button to see the source code:
The details of what's going on here are explained below. For now, assume it's saved to a file named slider.mermaid. Then, we pass that source to a code generator and choose a language we need
shell
$ yantrix codegen ./slider.mermaid --outfile slider_controller.js --language JavaScript --className SliderNow, we import the generated file into your projects:
javascript
import Slider from './slider_controller.js';
const SLIDER_MAX = 1000;
const SLIDER_MIN = -1000;
const SliderController = new Slider();
SliderController.dispatch(
Slider.createAction('RESET', {
min: SLIDER_MIN,
max: SLIDER_MAX,
}),
); // adjusted to local needs
/*
* ...somewhere later on at UI ...
*/
// bind these to buttons and/or keys
const rightHandler = () => SliderController.dispatch(Slider.createAction('INCREASE', { by: 50 }));
const leftHandler = () => SliderController.dispatch(Slider.createAction('DECREASE', { by: 50 }));
// bind this to a click handler, using relative coordinate
const setHandler = percentage =>
SliderController.dispatch(
Slider.createAction('SET', {
value: SLIDER_MIN + percentage * (SLIDER_MAX - SLIDER_MIN),
}),
);
// use this to display value in the GUI
const getSliderValue = () => SliderController.getContext().value;
// and this - to prevent UI from changing the slider value
const isDisabled = () => Slider.hasState(SliderConroller, 'DISABLED');Or you can use one of the available Integrations with a framework of your choice, i.e. Redux
Syntax Breakdown
Let's see what's happening in depth. The diagram describes an FSM, — a finite state machine, a control object that is characterized by a limited set of States, and it always resides in one and only one of them.
States
Every State is independent on another and difference between States is qualitative, i.e. they represent some scenarios in App that are independent on each other and never happen simultaneously:
- Slider is usable and idle (enabled)
- Slider can't be interacted with (disabled)
- Slider is being switched to a particular position
- Slider is being nudged left or right
Actions
To change the State of the machine, an Action must be invoked. Every Action has a unique name, and the first layer of diagram is essentially a graph that depicts which Actions lead to which States.
Also note that not every Action can be invoked from every State by default. A [*] node is used to define an Action that can be invoked from every State. The binding between States and Action define a many-to-many relation, called Transition Matrix, that is another way to define the FSM. Every item in this matrix is either null or an Action name, while columns and rows correspond to States in the same order.
| From/To | ENABLED | DISABLED |
|---|---|---|
| any | RESET | |
| ENABLED | DISABLE | |
| DISABLED | ENABLE |
Payload
Every Action can carry a Payload, which is a plain data object that represents some meta-information about the change requested by it. For instance, when resetting (instantiating) a Slider, it's wise to provide its min and max values. Payload is represented with a list of parameters in parentheses, each possible having a default value:
Technically, nudging Slider can be processed the same way as setting it directly, by an only Action, carrying a polymoprhic Payload, allowing to either adjust value or to set it directly:
However, different behaviours are better to be represented with different States.
Intermediary States
While FSM is transactional and synchronous (i.e. no further computation is done until the invoked Action has been processed), most interactions are asynchronous by nature, and they have some in-between state, usually called "pending" in this context.
In fact, even synchronous operations could have an intermediate State which actually does computations, and this would be an advised design pattern. As you can guess, those operations then could be easily plugged in with async functionality, behaving as a Promise.
here we define 3 Actions and 3 corresponding intermediate States for them:
SET (value)– a direct assignment of the Slider value, leading to SETTINGStateand carrying new value inPayloadINCREASE (by)– an increment of the Slider value, leading to INCREMENTStateand carrying a difference inPayloadDECREASE (by)– a decrement of the Slider value, leading to DECREMENTStateand carrying a difference inPayload
Here we use [-] labels on Actions, which is a special symbol for ByPass Action. Now, to emphasize that we need to add Notes to States to describe their behaviour:
Our Transition Matrix now looks like this:
| From/To | ENABLED | DISABLED | INCREMENT | DECREMENT | SETTING |
|---|---|---|---|---|---|
| any | RESET | ||||
| ENABLED | DISABLE | INCREASE | DECREASE | SET | |
| DISABLED | ENABLE | ||||
| INCREMENT | INCREASE | ||||
| DECREMENT | DECREASE | ||||
| SETTING | SET |
Context
Now, we need to define Context — values that are stored within FSM along with the current State and represent some of its quantitative properties. We define it with Reducers along with State, and they are calculated whenever the FSM is switched to that State. The Reducers defined at [*] node are run on every transition, and the Context properties found there are persistent through every Action, unless specified otherwise.
here we have defined 3 Context properties, that are copied from the previous values with every Action:
- value stores current Slider position
- min stores minimal selectable value
- max stores maximal selectable value
Initial Context
When RESET Action is invoked, a new min and max values can be set with Payload. Since that Action resolves to ENABLED State, that's where the related Reducer belongs:
Here we have also specified that ENABLED is the starting State of the FSM with Init flag.
Operations on data
Now we specify the arithmetics for intermediate "nudge" States INCREMENT and DECREMENT, which will add or substract, respectively, the provided Payload property by to/from stored value in Context :
Note that we use built-in Transformer Functions min,max,add and diff here in such a manner, that the resulting value always stays within min and max bounds.
Finally, we write a simple assignment Reducer for SETTING intermediate State, that will copy value from Paylod to Context, and we are done:
Now, it's only about dispatching proper Payloads to the FSM.
What's next?
- See Sample FSM Designs and generate some code
- Learn Yantrix Syntax and try making an
FSMof your own - Try some Integrations with your favorite frameworks to achieve fast results
- Read more on framework architecture and how to expand beyond one
FSMintoSlices and built a wholeApplicationon Yantrix - If you like what you get, consider Contributing or resolving some Issues