The StateMachines Library
/* @provengo summon StateMachines */
StateMachines is library for describing, well, state machines. In Provengo, a state machine consists of a set of states, some inter-connections between them, and a set of state handlers - functions to run in each state.
While it is running, a state machine is always at one of its states. When it gets to a new state, it runs the state handler specified for that state (if any). After the handler completes its execution, the machine moves to one of the states that are connected to its current one. If there are no such states, the machine stops. Provengo’s state machines support cycles and embedding. As they progress, they emit events that allow other parts of the specification to follow them (using waitFor
), or alter their behavior (e.g. through block
-ing related events).
For example, here is a state machine describing a visitor journey at a famous hotel in the west coast:
// @provengo summon StateMachines
const eagle = StateMachine("hotel California");
eagle.connect("dark desert highway")
.to("reception")
.to("check in")
.to("stay")
.to("checkout");
eagle.connect("checkout").to("stay");
eagle.connect("reception").to("leave");
A state machine structure can’t change after the machine has been started. Use sm.next.mustBe , sm.next.cannotBe or general BP techniques to alter the way it can move from other b-threads (e.g. block(sm.enterEvent(doNotGoInHere)) ).
|
Constants and functions that relate to state machines in general live in the StateMachines
object.
StateMachines are normally defined at the model level, as top-level constants (that is, not inside bthread s). This makes them usable for the rest of the model, e.g. when defining test goals for ensembles, or when an external bthread needs to block a state machine from entering one of its states.
|
Configuration Verification
Warnings
Before a state machine starts to run, it automatically inspects itself to find anomalies and possible errors. Checks include:
-
Unreachable states
-
States with no handlers
-
Handlers registered at nonexistent states
By default, these problems are found and reported as warnings in the console. This is because during models development, state machines are often incomplete, so these anomalies are just something to warn the model developer about. Some error message examples are shown below.
[EXEC>random] INFO [BP][Warn] Violation found on state machine 'main': Graph contains unreachable states: maiin.
[EXEC>random] INFO [BP][Warn] Violation found on state machine 'main': States without any handler: index,second screen,third screen,maiin.
[EXEC>random] INFO [BP][Warn] Violation found on state machine 'main': Handlers to nonexistent states: notThere1,notThere2.
Once a state machine is complete, it is possible to programmatically keep it that way, by setting its tolerance level to StateMachines.TOLERANCE_LEVEL.STRICT
. In its order to turn the warnings off entirely, set the state machine’s tolerance level to StateMachines.TOLERANCE_LEVEL.LENIENT
.
Errors
Provengo’s State Machines support multiple start states, through sm.addStart(stateName)
. If, at the time the machine starts, one of the additional start states was not created, the the model will halt its execution and complain.
// @provengo summon StateMachines
const sm = new StateMachine("Nonexistent Start Example");
sm.connect("A").to("B").to("C"); (1)
sm.addStart("B"); (2)
sm.addStart("D"); (3)
1 | Adds an initial path, and implicitly defines state A as a start state. |
2 | Defines B as an additional start state (this call is OK) |
3 | Defines D as an additional start state, even though D itself is not defined yet. |
If, after running the above code, state D
won’t be added to sm
, the following error will be generated:
[EXEC>random] ERR Error on state machine "Nonexistent Start Example": Start node D does not exist. (dsls/stateMachines.js#257)
Classes and Methods
To create a new state machine, call the StateMachine(name, props)
builder function. This function accepts the machine name, and a parameter object that allows adjusting the new machine’s behavior. Every state machine has a set of inter-connected states. While running, a state machine must be in one of its states.
StateMachine(name, properties)
Creates a new state machine called name
.
name
-
Name of the created machine.
properties
-
Customization parameters object for the machine. See below.
The properties
object contains the following fields:
Field Name | Field Type | Description |
---|---|---|
|
boolean |
When |
|
string |
string represent color name or hex color, this color will represent the state machine elements on analyze pdf output. |
|
One of
|
|
sm.addStart(s1)
Allows sm
to start from state s1
. This does not prevent sm
from starting at other states, e.g. those marked by other addStart
calls.
s1
-
Name of a state at which
sm
may start its runs.
sm.connect(s1).to(s2)
Connecting state s1
to state s2
, creating them if needed. To allow easy definition of consequent states, the calls may be chained like so: sm.connect("a").to("b").to("c")
.
s1
-
Name of the origin state (String);
s2
-
Name of the target state (String);
By default, the first state defined through connect is the starting state. To explicitly define the starting state, use sm.addStart(stateName)
|
sm.at(stateName).run(handler)
Makes sm
run handler
whenever it gets to state stateName
. This call overrides the handler previously set for state stateName
(if any).
stateName
-
String. The name of the state at which
handler
should run. handler
-
Function. Executed in the
sm
'sbthread
, wheneversm
gets to statestateName
. This is "code-block" function - it does not take any parameters, and its return value is ignored.
sm.at(stateName).embed(sub)
Embeds state machine sub
in sm
's state stateName
. This allows state machine composition - whenever the sm
(the top-level machine) gets to state stateName
, it runs the sub
state machine. sm
does not leave state stateName
until sm
has completed its run.
stateName
-
String. The name of the state in which we embed
sub
. sub
-
Function. Executed in the
sm
'sbthread
, wheneversm
gets to statestateName
. This is "code-block" function - it does not take any parameters, and its return value (if any) is ignored.
In many cases, when a state machine is embedded in another one, the embedded machine should not be auto-starting (that is, when creating it, the properties parameter should contain an autoStart: false field). If you embed a state machine, make sure to set its autoStart flag according to your needs.
|
sm.next.cannotBe(s1, s2, s3…/[s1, s2, s3])
Prevent the state machine from entering any of the passed states in its next move.
s1, s2, s3…
-
Names of the destination states.
[s1, s2, s3]
-
Array containing the names of the destination states.
sm.next.mustBe(s1, s2, s3…/[s1, s2, s3])
Force the state machine to enter one of the passed states in its next move. This only applies for states that are directly connected to the machine’s current state - it won’t enter non-directly connected states anyway.
s1, s2, s3…
-
Names of the destination states.
[s1, s2, s3]
-
Array containing the names of the destination states.
sm.removeStart(s1)
Prevents sm
from starting runs at state s1
. If state s1
is already not a starting state, this call has no effect.
s1
-
A name of a state where
sm
should not start at.
sm.setToleranceLevel(toleranceLevel)
Sets how sm
will behave if it finds problems in its configurations. according to tolerance Level see toleranceLevel
.
Events
StateMachines.allEvents
An event set containing all events that were emitted from any StateMachines element.
sm.anyStateChange
An event set that contains all events where sm
enters a state. To prevent sm
from advancing, block
this event set. To follow sm
's changes, waitFor
it.
sm.enterEvent(stateName)
Event emitted by sm
when it enters state stateName
. waitFor
this event to be notified when sm
enters state stateName
; block
this event to prevent sm
from entering it.
stateName
-
The name of the state that
sm
just entered.
This event is also available as sm.enters(stateName) , a non-standard form that some find more readable in some cases.
|
StateMachines
This object contains constants and methods that relate to state machines in general - not to any specific machine.
StateMachines.TOLERANCE_LEVEL
How a state machine reacts to detected warnings and structural issues.
StateMachines.allEvents
An event set containing all State Machine events. For example, the following code will block all state machines from advancing until event Event("proceed")
is selected:
bthread(function(){
sync({
block: StateMachines.allEvents,
waitFor: Event("proceed")
});
});