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");
process diagram for a guest in hotel california
Figure 1. A visualization of the above program. Once checked in, you can check out any time you like but you can never 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 bthreads). 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

autoStart

boolean

When true, the machine starts running when the spec starts. When false, the machine waits for someone to call machine.doStart(). Defaults to true.

color

string

string represent color name or hex color, this color will represent the state machine elements on analyze pdf output.

toleranceLevel

One of StateMachines.TOLERANCE_LEVEL's fields:

  • LENIENT

  • PASSIVE_AGGRESSIVE

  • STRICT

StateMachines.TOLERANCE_LEVEL.LENIENT

The state machine will not inspect or validate its status.

StateMachines.TOLERANCE_LEVEL.PASSIVE_AGGRESSIVE

(Default) The state machine will inspect its status, and will report all found problems as warnings to the console.

StateMachines.TOLERANCE_LEVEL.STRICT

The state machine will halt model execution if it finds it has configuration problems.

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.doStart()

Starts sm. Required only if the sm was not defined as "autoStart" when constructed.

sm.doStop()

Makes sm stop. Note that sm.doneEvent is still emitted.

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's bthread, whenever sm gets to state stateName. 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's bthread, whenever sm gets to state stateName. 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.name

Name of the state machine.

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.

toleranceLevel

tolerance level is an enum that represent graph problems handling, whether to throw exception, print the problems or do nothing.

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.doneEvent

The event emitted when sm has finished it’s execution.

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")
    });
});