A minimalistic finite state machine library for browser and node implemented using promises.
- How to use
- Configuring promise library
- Create finite state machine
- Callbacks
- Handling Errors
- Recipes
- UML visualization
- Contributing
- License
- Credits
Run npm install fsm-as-promised
to get up and running. Then:
var StateMachine = require('fsm-as-promised');
Use manually with browserify for now...
StateMachine.Promise = YourChoiceForPromise
You can choose from the following promise libraries:
If the environment does not provide Promise
support, the default implementation is es6-promise.
The library works also with the promise implementation bundled with es6-shim.
A state machine object can be created by providing a configuration object:
var fsm = StateMachine({
events: [
{ name: 'wait', from: 'here'},
{ name: 'jump', from: 'here', to: 'there' },
{ name: 'walk', from: ['there', 'somewhere'], to: 'here' }
],
callbacks: {
onwait: function () {
// do something when executing the transition
},
onleavehere: function () {
// do something when leaving state here
},
onleave: function () {
// do something when leaving any state
},
onentersomewhere: function () {
// do something when entering state somewhere
},
onenter: function () {
// do something when entering any state
},
onenteredsomewhere: function () {
// do something after entering state somewhere
// transition is complete and events can be triggered safely
},
onentered: function () {
// do something after entering any state
// transition is complete and events can be triggered safely
}
}
});
The state machine configuration contains an array of event that convey information about what transitions are possible. Typically a transition is triggered by an event identified by name, and happens between from and to states.
The state machine configuration can define callback functions that are invoked when leaving or entering a state, or during the transition between the respective states. The callbacks must return promises or be thenable.
You can define the initial state by setting the initial property:
var fsm = StateMachine({
initial: 'here',
events: [
{ name: 'jump', from: 'here', to: 'there' }
]
});
console.log(fsm.current);
// here
otherwise the finite state machine's initial state is none
.
You can define the final state or states by setting the final property:
var fsm = StateMachine({
initial: 'here',
final: 'there', //can be a string or array
events: [
{ name: 'jump', from: 'here', to: 'there' }
]
});
An existing object can be augmented with a finite state machine:
var target = {
key: 'value'
};
StateMachine({
events: [
{ name: 'jump', from: 'here', to: 'there' }
],
callbacks: {
onjump: function (options) {
// accessing target properties
console.log(target.key === this.key);
}
}
}, target);
target.jump();
The following arguments are passed to the callbacks:
var fsm = StateMachine({
events: [
{ name: 'jump', from: 'here', to: 'there' }
],
callbacks: {
onjump: function (options) {
// do something with jump arguments
console.log(options.args);
// do something with event name
console.log(options.name);
// do something with from state
console.log(options.from);
// do something with to state
console.log(options.to);
return options;
}
}
});
fsm.jump('first', 'second');
You can define synchronous callbacks as long as the callback returns the options object that is going to be passed to the next callback in the chain:
var fsm = StateMachine({
events: [
{ name: 'jump', from: 'here', to: 'there' }
],
callbacks: {
onjump: function (options) {
// do something
return options;
}
}
});
fsm.jump();
You can define asynchronous callbacks as long as the callback returns a new promise that resolves with the options object when the asynchronous operation is completed. If the asynchronous operation is unsuccessful, you can throw an error that will be propagated throughout the chain.
var fsm = StateMachine({
events: [
{ name: 'jump', from: 'here', to: 'there' }
],
callbacks: {
onjump: function (options) {
return new Promise(function (resolve, reject) {
// do something
resolve(options);
});
}
}
});
fsm.jump();
The callbacks are called in the following order:
callback | state in which the callback executes |
---|---|
onleave{stateName} | from |
onleave | from |
on{eventName} | from |
onenter{stateName} | from |
onenter | from |
onentered{stateName} | to |
onentered | to |
A state is locked if there is an ongoing transition between two different states. While the state is locked no other transitions are allowed.
If the transition is not successful (e.g. an error is thrown from any callback), the state machine returns to the state in which it is executed.
By default, each callback in the promise chain is called with the options
object.
Callbacks can pass values that can be used by subsequent callbacks in the promise chain.
var fsm = StateMachine({
initial: 'one',
events: [
{ name: 'start', from: 'one', to: 'another' }
],
callbacks: {
onleave: function (options) {
options.foo = 2;
},
onstart: function (options) {
// can use options.foo value here
if (options.foo === 2) {
options.foo++;
}
},
onenter: function (options) {
// options.foo === 3
}
}
});
This also includes callbacks added to the chain by the user.
fsm.start().then(function (options) {
// options.foo === 3
});
The options
object can be hidden from the promises added by the end user by setting the options.res property. This way the subsequent promises that are not part of the state machine work do not receive the options
object.
var fsm = StateMachine({
initial: 'one',
events: [
{ name: 'start', from: 'one', to: 'another' }
],
callbacks: {
onstart: function (options) {
options.res = {
val: 'result of running start'
};
}
}
});
fsm.start().then(function (data) {
console.log(data);
// { val: 'result of running start' }
});
By default, the callback names start with on
. You can omit the prefix by setting it to empty string or assign any other prefix:
StateMachine.callbackPrefix = 'customPrefix';
Errors thrown by any of the callbacks called during a transition are propagated through the promise chain and can be handled like this:
fsm.jump().catch(function (err) {
// do something with err...
// err.trigger - the event that triggered the error
// err.current - the current state of the state machine
// err.message - described bellow...
});
The library throws errors with the following messages:
message | explanation | note |
---|---|---|
Ambigous transition | The state machine has one transition that starts from one state and ends in multiple | must be fixed during design time |
Previous transition pending | The previous transition is in progress preventing new ones until it has completed | - |
Invalid event in current state | The state machine is in a state that does not allow the requested transition | - |
It is not advisable to let the errors that can be handled gracefully at callback level to propagate to the end of the promise chain.
The following is an example where the error is handled inside a synchronous callback:
var fsm = StateMachine({
initial: 'green',
events: [
{ name: 'warn', from: 'green', to: 'yellow' }
],
callbacks: {
onwarn: function (options) {
try {
throw new Error('TestError');
} catch (err) {
// handle error
return options;
}
}
}
});
fsm.warn().then(function () {
fsm.current === 'yellow';
// true
});
The same inside an asynchronous callback:
var fsm = StateMachine({
initial: 'green',
events: [
{ name: 'warn', from: 'green', to: 'yellow' }
],
callbacks: {
onwarn: function (options) {
return new StateMachine.Promise(function (resolve, reject) {
reject(new Error('TestError'));
}).catch(function (err) {
// handle error
return options;
});
}
}
});
fsm.warn().then(function () {
fsm.current === 'yellow';
// true
})
The library provides a way to define conditional transitions:
StateMachine({
events: [
{ name: 'conditional',
from: 'init',
to: ['one', 'two'],
condition: function (options) {
return 0; // transition to state 'one'
}
}
]
});
The above is equivalent to:
StateMachine({
events: [
{ name: 'conditional',
from: 'init',
to: ['one', 'two'],
condition: function (options) {
return 'one'; // transition to state 'one'
}
}
]
});
The condition callback must return the to
Array's index of the selected state, the name of the selected state, or a promise which resolves to either. The condition callback is executed after on{eventName}
callback.
If the above is not suitable, complex conditional transitions can be achieved through transitioning explicitly to a pseuso state where the condition is checked, then the appropriate event is triggered:
StateMachine({
events: [
{ name: 'trigger', from: 'existing', to: 'pseudo' },
{ name: 'triggerOptionA', from: 'pseudo', to: 'option-a' },
{ name: 'triggerOptionB', from: 'pseudo', to: 'option-b' }
],
callbacks: {
onenteredpseudo: function () {
if (condition) {
this.triggerOptionA();
} else {
this.triggerOptionB();
}
}
}
});
If your pseudo state's callback returns a Promise, you must return the call to the event function; e.g. return this.triggerOptionA()
.
The state machine definitions can be visualized as UML diagrams using fsm2dot.
Install fsm2dot and graphviz, then:
fsm2dot -f fsm.js -o fsm.dot
dot -Tpdf fsm.dot -o fsm.pdf
Install the library and run tests:
npm install
npm test
The library is available under the MIT license.
The framework is heavily influenced by Jake Gordon's javascript-state-machine.