createStateMachine

createStateMachine :: FSM_Def -> FSM_Settings -> Stateful_FSM

Description

This FSM factory function takes parameters defining the behavior of the state machine and returns the created state machine. The created state machine is a regular JavaScript function that you call with the inputs to be processed by the machine. The function computes the machine’s outputs. The syntax for an input is {[eventLabel] : eventData}, i.e. an input is an object with exactly one key, which is the event identifier, and the value matching the key is the event data.

The machine additionally may carry over environment variables, which are accessible in guards, and action factories. This helps to maintain such functions pure and testable. Environment variables can also be used to parameterize the state machine’s behavior.

Better than words, we recall here the counter application example:

counter machine

...

const fsmDef = {
  states: { counting: "" },
  events: ["clicked"],
  initialExtendedState: { count: 0 },
  initialControlState: "counting",
  transitions: [
    {
      from: "counting",
      event: "clicked",
      to: "counting",
      action: incCounterAndDisplay
    }
  ],
  updateState
};

...

const settings = { debug: { console }, devTool: { tracer } };
const fsm = createStateMachine(fsmDef, settings);

...
  const commands = fsm({ clicked: void 0 });

You can go back and forth to the code sample while you are reviewing the detailed type information from subsequent sections. Let’s now dive into the details. Additionally, you may also refer to the test examples from the test directory.

FSM_Def

The type is as follows:

/**
 * @typedef {Object} FSM_Def
 * @property {FSM_States} states Object whose every key is a control state admitted by the
 * specified state machine. The value associated to that key is unused in the present version of the library. The
 * hierarchy of the states correspond to property nesting in the `states` object
 * @property {Array<EventLabel>} events A list of event monikers the machine is configured to react to
 * @property {Array<Transition>} transitions An array of transitions the machine is allowed to take
 * @property {*} initialExtendedState The initial value for the machine's extended state
 * @property {function(ExtendedState, ExtendedStateUpdate) : ExtendedState} updateState function
 * which update the extended state of the state machine
 */

states

states reflects the state hierarchy of the machine in its object hierarchy:

Graph States object
chess game no undo
const states = {
  [OFF]: "",
  [WHITE_TURN]: {
    [WHITE_PLAYS]: "",
    [WHITE_PIECE_SELECTED]: ""
  },
  [BLACK_TURN]: {
    [BLACK_PLAYS]: "",
    [BLACK_PIECE_SELECTED]: ""
  },
  [GAME_OVER]: "",
};

Note how in the last example the White turn ccmpound state’s two sub-states are encoded in the states object.

events

You can refer to the tutorials for examples. For the password meter, events = [TYPED_CHAR, CLICKED_SUBMIT, START];

transitions

The transitions is the most complex parameter of the createStateMachine factory function as it encodes the machine graph. The translation of the machine into the transitions object is however relatively simple. There are two kind of transition records: inconditional transition, i.e. transitions that do not have guards applying, and conditional transitions, which do.

The settings parameter passed to the guard is identical to the one passed at machine creation’s time. The extended state passed as first parameter is that which is encapsulated in the machine at the moment when the event is received. The event data is the data that is carried with the event being processed by the machine.

The settings parameter passed to the action factory is identical to the one passed at machine creation’s time. The extended state passed as first parameter is that which is encapsulated in the machine at the moment when the event is received. The event data is the data that is carried with the event being processed by the machine.

The important point here is that action factories return Actions that produce two things: updates to the encapsulated state of the machine; and the machine outputs. The machine outputs are the actual commands that are to be executed by the machine clients. The updates of the encapsulated state are not visible from an API client. Again, we refer the reader to the tutorials for concrete examples of using action factories.

The reader can refer to the chess game tutorial (with undo functionality) to see an example of complex configuration that involves history states, atomic states, compound states, and guards. Here goes an excerpt that features eventless transitions (event: void 0), transitions to history states, transitions from compound states to the initial state of the compound state, and transitions between atomic states:

const transitions = [
  { from: OFF, event: START, to: GAME_ON, action: ACTION_IDENTITY },
  { from: GAME_ON, event: INIT_EVENT, to: WHITE_TURN, action: resetAndStartTimer },
  { from: WHITE_TURN, event: INIT_EVENT, to: WHITE_PLAYS, action: displayInitScreen },
  { from: GAME_ON, event: TICK, to: UPDATING_CLOCK, action: updateAndDisplayClock },
  { from: UPDATING_CLOCK, event: void 0, to: historyState(DEEP, GAME_ON), action: ACTION_IDENTITY },
  { from: GAME_ON, event: CLOCK_CLICKED, to: PAUSED_CLOCK, action: pauseClock },
  { from: PAUSED_CLOCK, event: CLOCK_CLICKED, to: historyState(DEEP, GAME_ON), action: resumeClock },
  (...)
];

If there is any missing information, you may refer to the types file in the project repository. You may also leave us a comment, we will update the documentation to reflect the missing information.

initial extended state

This parameter holds the initial extended state for the machine.

updateState

This parameter is a reducer function that takes a state and a series of updates to compute an update state: updateState: function(ExtendedState, ExtendedStateUpdates) : ExtendedState. ExtendedStateUpdates MUST be an array. The empty array is used by default to indicate the absence of modifications of the extended state.

The updateState property is mandatory. We successfully used JSON patch operations for model updates, but you can choose to use the immutable library of your choice or a simple reducer. The important point is that the extended state should not be modified in place, i.e. updateState should be a pure function. You can relax that condition in some trivial cases where you do not need immutability (e.g., specific and limited tests). In the general case, do not shoot yourself in the feet. Bugs due to mutability are incredibly hard to trace back to their origin.

FSM_Settings

The settings.debug.checkContracts, when set, represents contracts that the state machine must fulfill. The kingly library comes by default with a set of contracts enforcing most of the syntax and semantics of the state machine format chosen. The contracts will for instance prevent you from using control states that are never used in transitions (e.g., unreachable control states), or eventless transitions that return to the origin control state (possibility of blocking infinite loop). If you are just starting to use Kingly, we recommend setting the option to avoid trivial errors that will appear as you get familiarized with the API and state machine semantics. The contracts have been tested across a few dozen users and should catch most of the common errors.

Kingly’s contracts can be imported with import { fsmContracts } from "kingly" and assigned to the
settings.debug.checkContracts property.

The settings.debug.console, when set, represents a console object through which tracing and debug info will flow. This is useful in development and can be turned off in production. This console object must have the following properties: log, info, debug, warn, error, group, and groupEnd.

Example of use of debugging parameters can be found in the counter application tutorial.

Stateful_FSM

The machine returned by createStateMachine is a function that take one single parameter that is an object with only one property: the moniker of the event to be processed. The value of that property is the event data:

/**
 * @callback Stateful_FSM
 * @param {Object.<EventLabel, EventData>} input
 * @returns {FSM_Outputs|Error}
 */

The machine returns an array of outputs or an Error object. The machine may actually also throw if an unexpected error occurs. Unexpected error should be internal errors, as user-provided functions are
guarded against exceptions.

Example of use of the type can be found in the counter application tutorial.

Contracts

The enforced contracts, taken from the codebase as of March 2021, are as follows:


// S2. State names must be unique
// S1. State name cannot be a reserved state name (for now only INIT_STATE)
// S4. At least one control state (other than the initial state) muat be declared
// S5. check initial control state is a defined state in states
// E0. `fsmDef.events` msut be an array of strings
// T1. There must be configured at least one transition away from the initial state
// T2. A transition away from the initial state can only be triggered by the initial event
// T7b. The initial state must have a valid transition INIT_STATE -INIT-> defined which does not have a history
// state as target
// T23. We allow conditional initial transitions, but what about the action ? should it be always identity? We
// can't run any actions. We can update internal state, but we can't trace it, so we loose tracing properties and
// debugging!. So enforce ACTIONS to be identity
// T15. Init transitions can only occur from compound states or the initial state, i.e. A -INIT-> B iff A is a compound
// state or A is the initial state
// T5. Every compound state NOT the initial state A must have a valid transition A -INIT-> defined
// T7a. Every compound state NOT the initial state must have a valid INCONDITIONAL transition A -INIT-> defined which
// does not have a history state as target
// NOTE: actually we could limit it to history state of the containing compound state to avoid infinity loop
// T8. Every compound state NOT the initial state must have a valid INCONDITIONAL transition A -INIT-> defined which
// does not have the history state as target and has a target control state that is one of its substates (no
// out-of-hierarchy INIT transitions)
// T11. If there is an eventless transition A -eventless-> B, there cannot be a competing A -ev-> X
// T24. Check that we have this implicitly : Compound states must not have eventless transitions
// defined on them (would introduce ambiguity with the INIT transition).
// T12. All transitions A -ev-> * must have the same transition index, i.e. all associated guards must be together
// in a single array and there cannot be two transition rows showcasing A -ev-> * transitions
// T14. Conflicting transitions are not allowed, i.e. A -ev-> B and A < OUTER_A
// with ev non reserved event (init event or eventless) is not compatible with OUTER_A-ev->C.
// The event `ev` could trigger a transition towards either B or C
// T16.a History states must be target states
// T16.b History states must be compound states
// T17 An history state must refer to an existing state
// T18. Transitions have a valid format, and are either inconditional (no guards) or conditional
// events are strings
// guards are functions
// action factories are functions
// T25. SS1 - as of v0.13 settings is no longer mandatory
// T22. There are no incoming transitions to the reserved initial state, check if implemented or not, prob. not
// T23. eventless self-transitions are forbidden (while theoretically possible, the feature is of
// little practical value, though being a possible source of ambiguity or infinite loops)
// A -_> A impossible on compound states because there is A -INIT-> X
// so only possibility is A -_> A with A atomic state

- All [previously mentioned](https://github.com/brucou/kingly#contracts) contracts apply.
- [Type contracts](https://github.com/brucou/kingly/blob/master/src/types.js)
- The `settings.updateState` property is mandatory!
- The `settings` property **should not be modified** after being passed as parameter (i.e. should be a constant): it is not cloned and is passed to all relevant functions (guards, etc.)

Some of these contracts are type contracts that are checked at runtime. Others are semantic contracts that filter out many pathological machines, possibly saving you hours of debugging sessions.