@crinkle/fsm

Simple state machine

Usage no npm install needed!

<script type="module">
  import crinkleFsm from 'https://cdn.skypack.dev/@crinkle/fsm';
</script>

README

JavaScript FSM

Node version NPM Downloads Minified size License: MIT

Simple finite state machines that can be used for state/process management.

Getting started

A simple state machine can be initiated using the fsm function from the package. It allows you to view the current state (machine.current), or invoke a transition via the .send(event: string) function, with an optional delay .

import { fsm } from '@crinkle/fsm';

const config = {
  green: { on: { CHANGE: 'yellow' } },
  yellow: { on: { CHANGE: 'red' } },
  red: { on: { CHANGE: 'green' } },
};

const machine = fsm('green', config);
// machine.current = 'green'
machine.send('CHANGE', { delay?: 3000 });
// machine.current = 'yellow'

Adding a listener

A single listener can be registered on each machine, allowing you to invoke additional side-effects based on the source state, target state and invoked event. The listener will be invoked on each successful transition.

function listener(source, target, event) {
  console.log(source, target, event);
}

machine.listen(listener);

Entry actions & auto-transitions

When entering a state, an entry action can be triggered by setting a function the entry attribute of a state configuration. It provides access to the internal send() function, allowing you to define auto-transitions when entering a state. These auto-transitions are event triggered on the initial state.

const config = {
  left: {
    on: { CHANGE: 'right' },
    entry: (send) => send('CHANGE', { delay: 3000 }),
  },
  right: {
    on: { CHANGE: 'left' },
    entry: (send) => send('CHANGE', { delay: 3000 }),
  },
};

const machine = fsm('left', config);
// machine.current = 'left', after 3000ms, it will be 'right'

When you manually invoke a transition, while a delayed auto-transition did not yet happen (e.g. within the 3000ms delay of the above example), the auto-transition gets canceled to avoid unwanted side-effects.

As the machines of this library are context unaware, you can send an object as a third parameter in the .send() object. This added context is provided as a second parameter in an entry action definition. This allows you to create conditional auto-transitions.

function myEntryAction(send, ctx) {
  if (ctx.isAdmin) send('SUDO');
  else send('ERROR');
}

machine.send('START', {}, { isAdmin: true });

Guarded transitions

Transitions can also be guarded. This allows you to add aa condition that needs to pass, in order for the transition to successfully fire. Similar to entry actions, the context object as the third parameter of the send() can be used allow the guard to operate based on the context. Only when the result is true, will the transition happen.

const config = {
  start: {
    on: {
      CHANGE: {
        target: 'end',
        guard: (ctx) => ctx?.allowed,
      },
    },
  },
  end: {},
};

const machine = fsm('start', config);
machine.send('CHANGE'); // will result in no changes
machine.send('CHANGE', {}, { allowed: false }); // will result in no changes
machine.send('CHANGE', {}, { allowed: true }); // will result changes

React Hook example

import { fsm } from '@crinkle/fsm';
import { useLayoutEffect, useReducer, useRef } from 'react';

// Define the hook, with query for computed parameters
export default function useFsm(initial, config) {
  const [, rerender] = useReducer((c) => c + 1, 0);
  const value = useRef(fsm(initial, config));

  useLayoutEffect(() => {
    value.current.listen(rerender);
  }, []); //eslint-disable-line

  return value.current;
}