npm package: @picoware/state
Description: Tiny composable statecharts library for javascript
Author: Elisabeth Rousset
Latest version: 0.1.11
Deprecated: This package has been moved to https://www.npmjs.com/package/@ersbeth/picomachine

# @picoware/state

A tiny full-featured library for composable [finite state machines](https://statecharts.dev/what-is-a-state-machine.html) and [ statecharts](https://statecharts.dev/what-is-a-statechart.html) :

* enter/exit/transition hooks for side effects
* nested states
* parallel states
* transient states
* multiple transitions
* guarded transitions
* delayed transitions
* async (promise-based) states
* history
* child/parent communication
* splitting and composition

`@picoware/state` aims at providing a seamless developper experience. The API is carefully crafted to be legible and allows  easy splitting and composition of statemachines. Types are provided for intellisense.

The library runs both in browser and on Node.js

## Installation
```  npm install @picoware/state```

## Usage
### Finite state machine

#### Basic example

Here is a simple toggle machine. Comments show what is logged in the console.

```js

import { parseMachine } from '@picoware/state';

// Machine structure definition
const machine = parseMachine("root", {
    initial: 'OFF', // initial state
    states: { // keys are states names
        ON: {
            events: { // keys are events names
                TOGGLE: { target: "OFF" }
            }
        },
        OFF: {
            events:
            {
                TOGGLE: { target: "ON" }
            }
        }
    }
});

// Add listener
machine.status.onChanged((status) => console.log(status.state))

// Start machine
machine.start();
// -> [ 'root' ]
// -> [ 'root.OFF' ]

machine.send('TOGGLE');
// -> [ 'root.ON' ]

machine.send('TOGGLE');
// -> [ 'root.OFF' ]
```

#### Actions

*Actions* are functions which perform side-effects.  FSM have several hooks where you can call actions:

* when entering a state
* when leaving a state
* when executing a transition

The hooks execution order is *exit>transition>enter* .

**warning:** actions should always be sync function. If you need to perform promised-based side-effect use activities instead (see below).

```js
import { parseMachine } from '@picoware/state';

const actions = {
    enterON: () => console.log("enter ON"),
    exitON: () => console.log("exit ON"),
    enterOFF: () => console.log("enter OFF"),
    exitOFF: () => console.log("exit OFF"),
    toON: () => console.log("to ON"),
    toOFF: () => console.log("to OFF")
}

const machine = parseMachine("root", {
    initial: 'OFF',
    states: {
        ON: {
            enter: actions.enterON, // enter actions (can be an array)
            exit: actions.exitON, // exit actions (can be an array)
            events: {
                TOGGLE: { target: "OFF", actions: actions.toOFF } // transition actions (can be an array)
            }
        },
        OFF: {
            enter: actions.enterOFF,
            exit: actions.exitOFF,
            events:
            {
                TOGGLE: { target: "ON", actions: actions.toON }
            }
        }
    }
});

// Add listener
machine.status.onChanged((status) => console.log(status.state))

// Start machine
machine.start();
// [ 'root' ]
// enter OFF
// [ 'root.OFF' ]

machine.send('TOGGLE');
// exit OFF
// to ON
// enter ON
// [ 'root.ON' ]

machine.send('TOGGLE');
// exit ON
// to OFF
// enter OFF
// [ 'root.OFF' ]
```

It is possible to send a data object with an event to the state machine :

```js
machine.send('EVENT', someData)
```

This data object will be passed as parameter to the actions of the triggered transition where you can use it :

```js
events: {
  EVENT: {target: "SOMEWHERE", actions: (data) => doStuff(data)}
}
```

#### Self transitions

It is possible to transit from a node to itself. There are two types of self-transtions:

* external : it will trigger enter/exit hooks. For this use "self" as target.
* internal: it will not trigger enter/exit hooks. For this use "internal" as target

#### Guarded transitions

You can add guards to transitions so that they trigger only under certain circumstances. For example here when going out we take an umbrella if it's raining or a hat if it's hot:

``` js
const guards = {
    isRaining: () => true, // return something that depends on the context
    isHot: () => false
}

const machine = parseMachine("root", {
    initial: 'GOING_OUT',
    states: {
        GOING_OUT: {
            events: {
                CHECK_WEATHER: [
                    { target: "TAKING_UMBRELLA", guard: guards.isRaining }, // guard is true so we take this one
                    { target: "TAKING_HAT", guard: guards.isHot }
                ]
            }
        },
        TAKING_HAT: {},
        TAKING_UMBRELLA: {},

    }
});

machine.status.onChanged((status) => console.log(status.state))

machine.start();
// [ 'root' ]
// [ 'root.GOING_OUT' ]

machine.send('CHECK_WEATHER');
// [ 'root.TAKING_UMBRELLA' ]
```

Transitions are evaluated in the same order as they are declared in the array. The first whith a true guard is taken, and the other are ignored.

### Delayed transitions

The execution of a transition can be delayed by adding a `delay` field to it:

```js
...
events: {
  MY_EVENT: { target: "SOMEWHERE", delay: 500}
}
...
```

if we send "MY_ENVENT" to the state machine, the transition will execute after 500ms.

* if the transition is guarded, the guard is evaluated immediately.
* if the state machine exits the state before the end of the delay then the transition is canceled.

### Transient transitions

A transient transition is automatically triggered on enter. These transitions are useful when combined with guards. Use the `always` field to declare them:

```js
...
MY_STATE: {
  always: {target: "SOMEWHERE"}
}
...
```

A transient transition is always executed **after** the enter actions.

### Nested States

#### Basic example

Just add  a `states` field with substates and an `initial` state. Here is a basic example of a player machine. The player can be toggled ON/OFF, and when it's ON it can be PLAYING or PAUSED :

```js
import { parseMachine } from '@picoware/state';

const actions = {
    enterON: () => console.log("enter ON"),
    exitON: () => console.log("exit ON"),
    enterOFF: () => console.log("enter OFF"),
    exitOFF: () => console.log("exit OFF"),
    enterPLAYING: () => console.log("enter PLAYING"),
    exitPLAYING: () => console.log("exit PLAYING"),
    enterPAUSED: () => console.log("enter PAUSED"),
    exitPAUSED: () => console.log("exit PAUSED"),
    toON: () => console.log("to ON"),
    toOFF: () => console.log("to OFF"),
    play: () => console.log("play"),
    pause: () => console.log("pause"),
}

const machine = parseMachine("root", {
    initial: 'OFF',
    states: {
        ON: {
            enter: actions.enterON,
            exit: actions.exitON,
            events: {
                TOGGLE: { target: "OFF", actions: actions.toOFF }
            },
            initial: "PLAYING",
            states: {
                PLAYING: {
                    enter: actions.enterPLAYING,
                    exit: actions.exitPLAYING,
                    events: {
                        PAUSE: { target: "PAUSED", actions: actions.pause }
                    }
                },
                PAUSED: {
                    enter: actions.enterPAUSED,
                    exit: actions.exitPAUSED,
                    events: {
                        PLAY: { target: "PLAYING", actions: actions.play }
                    }
                }
            }
        },
        OFF: {
            enter: actions.enterOFF,
            exit: actions.exitOFF,
            events:
            {
                TOGGLE: { target: "ON", actions: actions.toON }
            }
        }
    }
});

// Add listener
machine.status.onChanged((status) => console.log(status.state))

// Start machine
machine.start();
// [ 'root' ]
// enter OFF
// [ 'root.OFF' ]

machine.send('TOGGLE');
// exit OFF
// to ON
// enter ON
// [ 'root.ON' ]
// enter PLAYING
// [ 'root.ON.PLAYING' ]

machine.send('PAUSE');
// exit PLAYING
// pause
// enter PAUSED
// [ 'root.ON.PAUSED' ]

machine.send('TOGGLE');
// exit PAUSED
// exit ON
// to OFF
// enter OFF
// [ 'root.OFF' ]

```

#### Final states

There are two reserved names for final states : "ERROR" and "DONE". When entering this states, the transition "error" or "done" is triggered on the parent. Here is an example of a machine which retries a computation until it gets a result:

```js
const machine = parseMachine("root", {
    initial: 'COMPUTE',
    states: {
        COMPUTE: {
            done: { target: "RESULT" },
            error: { target: "self" }, // will re-trigger computations until we get a result

            initial: "CALCULATE",
            states: {
                CALCULATE: {
                    events: {
                        SUCCEED: { target: "DONE" },
                        FAIL: { target: "ERROR" },
                    }
                },
                ERROR: {},
                DONE: {},
            }
        },
        RESULT: {}
    }
});

machine.status.onChanged((status) => console.log(status.state))

machine.start();
// [ 'root' ]
// [ 'root.COMPUTE' ]
// [ 'root.COMPUTE.CALCULATE' ]

machine.send('SUCCEED');
// [ 'root.COMPUTE.DONE' ]
// [ 'root.RESULT' ]
```

It is possible to have multiple error and done states with custom names :

```js
// default final states
ERROR:{}
DONE:{}
// custom final states
MY_ERROR: {
  final: "error"
}
MY_DONE: {
  final: "done"
}
```

The `done` and `error` transition are called with the name of the reached final state as argument.

Such In case of multiple final/error states you can use guarded transitions (see below) to decide what to do on the parent:

```js
PARENT: {
  done: [
    {target: "OUTPUT_1", guard: (data)=> data=="MY_DONE_1"},
    {target: "OUTPUT_2", guard: (data)=> data=="MY_DONE_2"},
  ]
  ...
}
```

#### History state

History can be activated in a state. It will keep track of the last active substate before quiting the state. When entering back the state the machine will enter the substate kept in history instead of the initial state. Let's take a look back at our previous *player* example. Now with history enabled when we switch on the player it will go to the last state where it was before:

```js
const machine = parseMachine("root", {
    initial: 'OFF',
    states: {
        ON: {
            history: true, // history activation
            events: {
                TOGGLE: { target: "OFF" }
            },

            initial: "PLAYING",
            states: {
                PLAYING: {
                    events: {
                        PAUSE: { target: "PAUSED" }
                    }
                },
                PAUSED: {
                    events: {
                        PLAY: { target: "PLAYING" }
                    }
                }
            }
        },
        OFF: {
            events:
            {
                TOGGLE: { target: "ON" }
            }
        }
    }
});

machine.status.onChanged((status) => console.log(status.state))

machine.start();
// [ 'root' ]
// [ 'root.OFF' ]

machine.send('TOGGLE');
// [ 'root.ON' ]
// [ 'root.ON.PLAYING' ]

machine.send('PAUSE');
// [ 'root.ON.PAUSED' ]

machine.send('TOGGLE');
// [ 'root.OFF' ]

machine.send('TOGGLE');
// [ 'root.ON' ]
// [ 'root.ON.PAUSED' ] Here without history we would have been in PLAYING state
```

### Parallel states

#### Basic example

To make a parrallel state you must activate the `parallel` flag. Here is an example of a styling machine which manages bold/underline/italics in parallel :

```js
const machine = parseMachine("root", {
    parallel: true,
    states: {
        BOLD: {
            initial: 'OFF',
            states: {
                ON: {
                    events: {
                        TOGGLE_BOLD: { target: "OFF" }
                    }
                },
                OFF: {
                    events: {
                        TOGGLE_BOLD: { target: "ON" }
                    }
                }
            }
        },
        UNDERLINE: {
            initial: 'OFF',
            states: {
                ON: {
                    events: {
                        TOGGLE_UNDERLINE: { target: "OFF" }
                    }
                },
                OFF: {
                    events: {
                        TOGGLE_UNDERLINE: { target: "ON" }
                    }
                }
            }
        },
        ITALICS: {
            initial: 'OFF',
            states: {
                ON: {
                    events: {
                        TOGGLE_ITALICS: { target: "OFF" }
                    }
                },
                OFF: {
                    events: {
                        TOGGLE_ITALICS: { target: "ON" }
                    }
                }
            }
        },
    }
});

machine.status.onChanged((status) => console.log(status.state))

machine.start();
// [ 'root.BOLD', 'root.UNDERLINE', 'root.ITALICS' ]
// [ 'root.BOLD.OFF', 'root.UNDERLINE', 'root.ITALICS' ]
// [ 'root.BOLD.OFF', 'root.UNDERLINE.OFF', 'root.ITALICS' ]
// [ 'root.BOLD.OFF', 'root.UNDERLINE.OFF', 'root.ITALICS.OFF' ]

machine.send('TOGGLE_BOLD');
// [ 'root.BOLD.ON', 'root.UNDERLINE.OFF', 'root.ITALICS.OFF' ]

machine.send('TOGGLE_UNDERLINE');
// [ 'root.BOLD.ON', 'root.UNDERLINE.OFF', 'root.ITALICS.OFF' ]

machine.send('TOGGLE_ITALICS');
// [ 'root.BOLD.ON', 'root.UNDERLINE.ON', 'root.ITALICS.ON' ]
```

#### Final states

There isn't final states in a parrallel state, but substates can have final states inside them.

* the `done` transition is triggered when **all** the substates wich have final states are done
* the `error` transition is triggered when **one** of the substate which have error states is in error.

### Child/Parent communication

Any state can raise an event on its parent trough an action. This is especially useful to allow communication between children of a parrallel state. The raised event is added to the [javascript event queue](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop), so that the current transition can end up before starting a new one.

Here is an example of parrallel children communication:

```js
import { parseMachine, raise } from '@picoware/state';

const actions = {
    doStuffA: () => console.log("Do stuff A"),
    doStuffB: () => console.log("Do stuff B"),
}

const machine = parseMachine("root", {
    parallel: true,
    states: {

        STATE_A: {
            events: {
                DO_STUFF_A: {
                    target: "self",
                    actions: [
                        actions.doStuffA,
                        raise("DO_STUFF_B"), // here we raise a new event on the parent
                    ]
                }
            }
        },

        STATE_B: {
            events: {
                DO_STUFF_B: { target: "self", actions: actions.doStuffB },
            }
        },
    }
});

machine.status.onChanged((status) => console.log(status.state))

machine.start();
// ['root.STATE_A', 'root.STATE_B']

machine.send('DO_STUFF_A');
// Do stuff A
// [ 'root.STATE_A', 'root.STATE_B' ]
// Do stuff B
// [ 'root.STATE_A', 'root.STATE_B' ]
```

### Activity States

You will often have to perform side-effects with are promised-based (eg. fetching data from the network). We call these side-effects "activities". They can be managed with an activity state, which has built-in error and done hooks that are triggered when the promise resolves. Let's see our computation example with a promised-base function:

```js
const activities = {
    calculate: () => Promise.resolve()// return  instead a promise whose resolution depends on the context
}

const machine = parseMachine("root", {
    initial: 'COMPUTING',
    states: {
        COMPUTING: {
            done: { target: "RESULT" },
            error: { target: "self" }, // will re-trigger computations until we get a result

            activity: activities.calculate // here we declare our activity
        },
        RESULT: {}
    }
});

machine.status.onChanged((status) => console.log(status.state))

machine.start();
// [ 'root' ]
// [ 'root.COMPUTING' ]
// [ 'root.RESULT' ]
```

### Composition

The structure of a statemachine can be spread across different nodes, either in the same file or in different files. This brings a better modularity and allows to reuse some parts of your machine in different places. To split a machine, use the `src` field.

```js
// these nodes could be defined in another file and imported here
const stateA = parseMachine("A", {...})
const stateB = parseMachine("B", {...})

// combine them under the root node
const machine = parseMachine("root", {
  states:{
    A:{
      src: stateA, // here you indicate the source node
    },
    B:{
      src: stateB,
    }
  }
})
```

You can define transitions and actions either in the parent node or in the child node or event in both: everything will be merged.

**warning**: in the child node you can only define self-transitions as the siblings are not known. you must define siblings transitions on the parent node.

## Acknowledgment
This library is heavily inspired by [xstate](https://xstate.js.org/) for the API, but the implementation is completely different and much more minimal. If you need more feature than what we provide, go for xstate it's awesome !

## License
MIT