# Introduction
statem is a JavaScript state/turing machine framework which lets you manage your application's total state (i.e. named states, transitions, data, and rewriting of inputs).
TIP
StateM is largely based on Erlang OTP’s gen_statem behavior.
# Event Driven
StateM is an event-driven state machine where the input is an event that triggers a state transition and the output is actions executed during the state transition. Events drive the state machine and are externally triggered or internally generated by the state machine. Pending events are tracked on a priority queue that preserves entry order.
# Handlers
State machines are specified as an ordered list of handlers keyed on Event x State tuples (wildcards and patterns are allowed to combine handlers).
# Handling Events
If the state machine is in state S and event E occurs, the state machine will perform actions A and make a transition to state S'(S' can be equal to S and A can be empty).
State(S) x Event(E) -> Actions(A), State(S')
To do this, statem will match the tuple ExS against the keys of the ordered list of handlers for the first matching handler, execute its actions A and transition to the
# Arbitrary Data
State machines can hold arbitrary data which is provided to, and can be mutated by, it's event handlers.
# Total State
Statem provides management of total state, i.e.:
- Management of states and transitions as in a DFA,
- Custom read/write memory available to handlers,
- Ability to rewrite inputs by postponing them.
# Features
- Co-located code: Event, states, transitions and actions in one place.
- Inserted Events: Insert events from within the specification.
- State Entry Events: Automatically generates Entry events on state change.
- Timeouts: Install timeouts for state transitions, new events, or just plain timeouts.
# Installation
Install with npm:
npm install --save gen-statem
# Usage
Create a state machine by calling StateMachine's constructor and passing it a list of handlers, the initial state and optional data.
NOTE
The state machine’s data type TData is a type argument (StateMachine<TData>).
For example, the state machine below toggles states ONE and TWO on event next.
let sm = new StateMachine<void>({
handlers: [
['cast#next#ONE', 'TWO'],
['cast#next#TWO', () => nextState('ONE')], ],
initialState: "ONE", })
sm.startSM()
sm.on('stateChanged', (state, old, data, event) => {
console.log(`${old} --> ${state} on ${event ? event.toString() : ''}`)
})
sm.cast('next') // ONE --> TWO on Cast@Low { context: 'next' }
sm.cast('next') // TWO --> ONE on Cast@Low { context: 'next' }
# Subclassing StateMachine
Extending the StateMachine class lets you declare and implement a public API for your state machine (and wrap call/cast dispatch calls).
TIP
Handler functions are called with this set to the state machine instance.
class PingPong extends StateMachine<void> {
handlers: Handlers<void> = [
['cast#next#ONE', 'TWO'],
['cast#next#TWO', 'ONE'], ]
initialState = "ONE"
// Define our public API
next() {
this.cast('next')
}
}
let sm = new PingPong()
sm.startSM()
sm.on('stateChanged', (state, old, data, event) => {
console.log(`${old} --> ${state} on ${event ? event.toString() : ''}`)
})
sm.next() // ONE --> TWO on Cast@Low { context: 'next' }
sm.next() // TWO --> ONE on Cast@Low { context: 'next' }
# In the Browser
Fetch it from npm via unpkg:
<script src="https://unpkg.com/gen-statem/dist/umd/gen-statem.js"></script>
# React State Management
StateM accepts a DataProxy object to synchronize its internal data with external objects such as React components.
Terminology: State machine data == React component state
class App extends React.Component {
constructor() {
this.sm = new StateMachine( {
dataProxy: {
get: () => this.state,
set: ( data, state ) => this.setState( { ...data, currentState: state } ),
},
} )
}
}
# How It Works
# Routes
StateM uses the url path to parameterized route matching seen in express et. al.
# Current Event Routes
The current event and current state are mapped to a route string as:
<current event>#<event context>#<current state>
For example:
| Event | Event Context | Current State | Route |
|---|---|---|---|
| cast | "flip" | off | "cast#flip#off" |
| cast | {button: 2} | locked | "cast#button/2#off" |
| call | "getInfo" | one | "call/internalId#getInfo/2#one" |
# Event Handler Routes
In addition to string literals, handler routes can include:
- Parameter capture patterns (
:param) capture up to the next/,#or the end of the route. - Splats (
*param) capture from up to#or the end of the route. - Parts of the route can be marked optional by wrapping in parenthesis. Optional parts can include parameter capture and splats.
# Examples
cast#flip#:statewill match acast(flip)event in any state and provide the current state as an argument (args.param) to the handler.call/:from#getInfo#:statewill capture the callerId and state asargs.fromandargs.staterespectively."cast#button/:digit#lockedwill capture a button press in thelockedstate and provide the digit value inargs.digit."cast#*context#openintercepts anycastevents in stateopenregardless of the parameters passed when cast was invoked (note: the splat will be available asargs.context).
# Event Handlers
A key part of a state machine specification is the list of (route, handler) tuples:
(string | Array<string>, function | string | [string, string | number])
Note: Event handlers are specified as an array to preserve order (vs. objects, where propertyiteration order is arbitrary).
# Multiple Routes to a Handler
When a route is specified as an array, it is treated as a boolean OR, i.e. if any route matches an incoming event route, the corresponding handler is invoked.
# Handler Functions
Handler functions receive the following:
type HandlerOpts<TData> = {
args: { [k in string]: string },
current: State,
data: TData,
event: Event,
route: string
}
current: the state machine’s current state.data: the state machine’s current data.args: any arguments or splats parsed from the incoming event’s route.event: the actual incoming event.route: the event’s route.
Handler functions can return:
ResultBuildera fluent builder forResults.Result- verbose, not recommended.void- interpreted askeepStateAndData
# string Handlers
Instead of a handler function, you can provide a
state: string- interpreted as a next state directive.[state: string, timeout: string | number]- interpreted as a next state directive and event timeout.
# Result Builder
StateM provides a fluent interface (ResultBuilder) for specifying state transitions and actions in handler functions.
The following functions return a ResultBuilder:
- keepState: Instructs the state machine to keep the current state (i.e. transition to the same state). Does not emit a state entry event.
- repeatState: Like
keepState, but emits a state entry event. - nextState(state): Instructs the state machine to transition to the given
state(can be the current state).
# ResultBuilder Methods
ResultBuilders provide the following chainable methods:
- data(spec): Instructs the state machine to mutate state data with the given
spec. - eventTimeout(time): Starts the event timer which may result in a
EventTimeoutEventevent if a new event is not received. - stateTimeout(time): Starts the state timer which may result in a
StateTimeoutEventevent if a state transition does not occur. - timeout(time, name): Starts a generic timer with an optional name which may result in a
GenericTimeoutEvent. Callingtimeout(name)will cancel the event if it has not yet fired. - nextEvent(type, context, extra): Inserts an event of the given type at the front of the queue so that it is executed before pending events.
- internalEvent(context, extra): Like
nextEvent, this method inserts anInternalEventevent. - postpone: Instructs the state machine to postpone the current until the state changes at which point any postponed events are delivered before pending events.
- reply(from, msg): Instructs the state machine to reply to the sender with id
from; the result of a prior invocation ofcall.
# Mutating State Machine Data
State machine data is mutated by calling the data on ResultBuilder with immutability-helper commands, including:
{$push: array}push()all the items inarrayon the target.{$unshift: array}unshift()all the items inarrayon the target.{$splice: array of arrays}for each item inarrayscallsplice()on the target with the parameters provided by the item. Note: The items in the array are applied sequentially, so the order matters. The indices of the target may change during the operation.* *{$set: any}replace the target entirely.{$toggle: array of strings}toggles a list of boolean fields from the target object.{$unset: array of strings}remove the list of keys inarrayfrom the target object.{$merge: object}merge the keys ofobjectwith the target.{$apply: function}passes in the current value to the function and updates it with the new returned value.{$add: array of objects}add a value to aMaporSet. When adding to aSetyou pass in an array of objects to add, when adding to a Map, you pass in[key, value]arrays like so:update(myMap, {$add: [['foo', 'bar'], ['baz', 'boo']]}){$remove: array of strings}remove the list of keys in array from aMaporSet.
# Event Types
# CallEvent
Sends a event of type call to the state machine and returns a pending Promise.
* Call call(context, extra) to emit.
* Event handlers can reply with reply() action which resolves the pending Promise returned by call().
* Internally, call generates a from id to identify the caller.
* The event route for call is: call/<from>#<context>#<state>.
# CastEvent
Sends a event of type cast to the state machine and returns without waiting for a result.
* Call cast(context, extra) to emit.
* The event route for cast is: cast#<context>#<state>.
# EnterEvent
Sends a enter event to the state machine.
* Internally generated by the state machine on a state transition. If the state machine is transitioning to the same state, enter is emitted if the previous event handler returns repeatState (and not for either of keepState, or nextState(same state)).
# EventTimeoutEvent
Sends a eventTimeout event to the state machine.
* Internally generated by the state machine when the eventTimeout timer fires.
* The eventTimeout timer is started by invoking eventTimeout(timeout) in a event handler.
* The event route for eventTimeout is: eventTimeout#<context>#<state>.
* Can be cancelled by calling eventTimeout() without a timeout argument from an event handler.
# GenericTimeoutEvent
Sends a genericTimeout event to the state machine.
* Internally generated by the state machine when the (optionally named) genericTimeout timer fires.
* A genericTimeout timer is started by invoking genericTimeout(timeout [, name]) in a event handler.
* The event route for genericTimeout is: genericTimeout#<context>#<state>.
* Can be cancelled by calling genericTimeout([name]) without a timeout argument from an event handler.
# StateTimeoutEvent
Sends a stateTimeout event to the state machine.
* Internally generated by the state machine when the stateTimeout timer fires.
* The stateTimeout timer is started by invoking stateTimeout(timeout) in a event handler.
* The event route for stateTimeout is: stateTimeout#<context>#<state>.
* Can be cancelled by calling stateTimeout() without arguments from an event handler.
# InternalEvent
Sends a internal event to the state machine. This is a deliberately named event to let the state machine know that the event is internal.
* Internally generated by invoking internal(context, extra) from a event handler.
* The event route for internal is: internal#<context>#<state>.
# Processing Events
The state machine looks for the first event handler whose key matches the incoming event x current state, or, a catch-all handler.
The matched handler is invoked with the incoming event, route matching arguments, the current state machine state and data.
The result of the handler invocation can include a state transition directive and transition actions, which are immediately executed, potentially changing the state machine’s state, mutating the internal data as well as the event queue.