# 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#:state will match a cast(flip) event in any state and provide the current state as an argument (args.param) to the handler.
  • call/:from#getInfo#:state will capture the callerId and state as args.from and args.state respectively.
  • "cast#button/:digit#locked will capture a button press in the locked state and provide the digit value in args.digit.
  • "cast#*context#open intercepts any cast events in state open regardless of the parameters passed when cast was invoked (note: the splat will be available as args.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:

  • ResultBuilder a fluent builder for Results.
  • Result - verbose, not recommended.
  • void - interpreted as keepStateAndData

# 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 EventTimeoutEvent event if a new event is not received.
  • stateTimeout(time): Starts the state timer which may result in a StateTimeoutEvent event if a state transition does not occur.
  • timeout(time, name): Starts a generic timer with an optional name which may result in a GenericTimeoutEvent. Calling timeout(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 an InternalEvent event.
  • 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 of call.

# 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 in array on the target.
    • {$unshift: array} unshift() all the items in array on the target.
    • {$splice: array of arrays} for each item in arrays call splice() 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 in array from the target object.
    • {$merge: object} merge the keys of object with 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 a Map or Set. When adding to a Set you 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 a Map or Set.

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