@f5io/machine v1.0.14
@f5io/machine
An asynchronous finite state machine library for Node.js and the browser.
Installation
This library requires async/await and Proxy support in your Node.js runtime, so ideally node>=7.4.
$ npm install --save @f5io/machineor
$ yarn add @f5io/machineConcepts
With this library, a finite state machine is defined with an object containing a list of transitions, in the format:
{
[transitionName]: { from: [...states], to: [...states] },
...more transitions
}A named transition defines an edge on a graph that allows a transition between the from and to states. The supplied from and to properties can either be a singular state string or an array of state strings.
Further to this, a state machine can be supplied with handlers which hook into the life-cycle of the machine. A state transition would flow through handlers in a particular order:
onBefore{T} -> onLeave{CS} -> on{T} -> onEnter{TS} -> on{TS} -> onAfter{T}Where T is equal to a transition name, CS is equal to the current state and TS is equal to the new target state.
For example, on a machines' transition from state A to state B over a transition foo, the handlers order would fire like so:
onBeforeFoo -> onLeaveA -> onFoo -> onEnterB -> onB -> onAfterFooThese handlers are supplied the context that is used at initialisation of the machine. In contrast to many other state machine implementations, a state machine created by this library can be initialised in any state without forced transitioning. This allows state machines to be wrapped over data structures at any time in their life-cycle.
API
The library exposes one function which is used to create a machine factory.
createMachineFactory({ stateKey = 'state', allowCyclicalTransitions = false, handlers = {}, transitions }) -> MachineFactory
The machine factory creator takes an options object containing 3 properties:
stateKey- defaults to'state', determines the key on which the state will be defined on the contextallowCyclicalTransitions- defaults tofalse, determines whether the machine should allow cyclical transitionshandlers- defaults to{}, defines optional life-cycle hooks for the machinetransitions- required, defines transitions keyed by name containingfrom/toattributes of the typestring|number|array<string|number>
A machine factory is returned which is used to initialise a machine.
machineFactory(context) -> Machine
This function requires a context object to be passed in. It will check whether a valid state is defined at context[stateKey] (see above).
The returned Machine will contain the following default methods:
Machine.can(to)->Boolean- Thecanmethod takes a state to transition to and will return aBooleanas to whether the machine can transition directly to that stateMachine.to(to)->Promise- Thetomethod will attempt to transition the machine to the supplied state, otherwise will throw an error if unavailableMachine.edge(to)->string- Theedgemethod will return the name of the transition that fulfils the transtion to the supplied state, otherwise will throw an error if none is availableMachine.will(...to)->Boolean- Thewillmethod takes any number of states and attempts to find a shortest path between the current state and each state supplied, eventually ending at the last supplied state, returning aBooleanMachine.thru(...to)->Promise- Thethrumethod, similarly to thewillmethod, takes any number of states and attempts to find a shortest path between the current state and each state supplied, eventually ending at the last supplied state, then enacts the change to the machine by transitioning through all the statesMachine.path(...to)->Boolean|Array<Array<String>>- Thepathmethod, similarly to thewillmethod takes any number of states and returns eitherfalsedenoting an invalid transition, or anArrayof pair tuples, ie.[ ['A', 'B'], ['B', 'C'] ]denoting the state changes it would take to achieve the transition, without effecting any changeMachine.transitions->array<string>- Thetransitionsmethods will return an array of all available transition names from the current state
The Machine also will contain methods that are derived from the transitions object passed to the createMachineFactory function. For example, given the transitions object:
{
foo: { from: 'A', to: 'B' },
bar: { from: 'B', to: 'C' },
}The Machine will have both a foo and a bar method which both return a Promise and, once called, enact that transition on the machine.
Example Usage
Below is a simple state machine example and how it could be used.
Constructing the machine
The createMachineFactory function expects a configuration object that contains the parameters for the state machine including transitions and life-cycle behaviour. This function will return a factory method (machineFactory below) that, when called, will create an instantiated instance of the defined state machine with a given context.
const machineFactory = createMachineFactory({
/**
* The transitions describe all the states the machines can be in
* and the transitions available between those states.
*/
transitions: {
init: { from: 'A', to: 'B' },
effect: { from: [ 'A', 'B', 'D' ], to: 'C' },
dispute: { from: 'C', to: 'D' }
},
/**
* Handlers can be optionally supplied for any life-cycle
* event available on the machine. All handlers are run
* asynchronously.
*/
handlers: {
onInit: (ctx) => {
/**
* you can mutate the context, however you will not
* be able to directly mutate the `ctx[stateKey]`.
*/
ctx.hasInitialised = true;
},
onEffect: async (ctx) => {
/**
* all handlers are run asynchronously.
*/
await timeout(200);
}
},
/**
* The state key option defines the key of the state within
* the context object that will be supplied to the `initMachine`
* function.
*/
stateKey: 'stateId',
});Using the machineFactory
Once you have created your machineFactory function, you can instantiate instances of your machine with a given context object. This context object must contain the attribute defined by stateKey in the createMachineFactory method, and the value of this key must be a valid state (as derived from the supplied transitions).
const machine = machineFactory({
stateId: 'A',
anything: 'canBeSupplied',
functionality: () => 'foo',
etc: [ 1, 2, 3 ],
});Transitioning states
The machine is now constructed and has some default methods, plus methods that are derived from your transition names.
(async () => {
/**
* `can` is a default method which takes a state and will
* will return a boolean as to whether the machine can
* directly transition to the supplied state.
*/
if (machine.can('B')) { // returns true as there is an edge from `A` to `B`
/**
* `edge` is a default method which takes a state to transition
* to and will return the name of a transition if available.
*/
const edge = machine.edge('B'); // returns `init`
/**
* `to` is a default method which takes a state to attempt
* to transition to. If it is in an invalid transition it will
* throw an error.
*/
await machine.to('B');
/**
* The machine is now in state `B` and according to the `onInit` handler
* `machine.hasInitialised` should exist and be equal to `true`. As described
* above, the machine also exposes methods derived from the supplied `transition`
* names.
*/
await machine.effect();
/**
* The machine has taken around 200ms to transition into state `C` as defined
* by the `timeout` in the `onEffect` handler.
*/
}
})();Shortest path transitions
The library also contains a mechanism for transitioning along a shortest path to a desired state.
const machine = initMachine({
stateId: 'A',
});
(async () => {
/**
* `will` is a default method that takes a variadic number of states to pass thru
* on it's way to the target state, and will return a boolean as to whether the transition can be
* achieved ie. from the current state to the last in the arguments via the rest of the arguments.
* The following `will` call is equivalent to `machine.will('C', 'D')`.
*/
if (machine.will('D')) { // the machine has found a path to `D` thru `C` from `A`
/**
* `path` is a default method which will return an array of tuples denoting the changes in state
* that will be made to enact change. In this case it will return `[ ['A', 'C'], ['C', 'D'] ]`.
* If `path` is passed states that make up an invalid transition, it will simply return `false`.
*/
const pairs = machine.path('D');
/**
* `thru` is a default method that will enact a chain of state changes to reach the supplied
* target state. It works in the same way as the `will` method.
*/
await machine.thru('D');
/**
* The machine is now in state `D` having transitioned through every state on the way, in the
* shortest possible path and passing through each of the handlers.
*/
}
})();CLI
A command-line application is included within the package for creating svg diagrams of a defined state machine.
$ `npm bin`/visualise --help
Usage: visualise [options]
a tool for outputting svgs from finite state machines
Options:
-V, --version output the version number
-i, --input <value> input to be visualised in the format .json, .js or .dot
-g, --graph <value> supply a name for the graph
-f, --format <value> output format, either .svg or .dot, defaults to .svg
-o, --output <value> output to file, if none supplied will output to stdout
-s, --styles <value> supply a css file of .dot styles
-h, --help output usage informationSupported input types include:
.jsfiles, which default export is a machine initialiser, ie. see here..jsonfiles, which define transitions for a machine, ie. see here..dotfiles, which define a graphviz representation of a graph, ie. see here.
Output can be either .dot or .svg and can be styled with a CSS-like syntax, shown here. Below is an example of the svg output of using the following command with examples from this repo.
$ `npm bin`/visualise -i ./test/test.fsm.js -s ./test/test.fsm.css > ./test/test.fsm.svgContributions
Contributions are welcomed and appreciated!
- Fork this repository.
- Make your changes, documenting your new code with comments.
- Submit a pull request with a sane commit message.
Feel free to get in touch if you have any questions.
License
Please see the LICENSE file for more information.