Finite State Automaton (or finite-state machine) in JavaScript
OTHER License
JSFSA is an abbreviation for JavaScript Finite State Automaton (or Automata), it is a small lib for creating asynchronous, non-deterministic, hierarchical finite-state machines in JavaScript.
JSFSA is in a stable state. The reason why it's not v1.x yet is because I want to nail down the API first.
You can contact me on Twitter with questions/remarks : @camillereynders
or send me a mail at: info [at] creynders [dot] be
var offState = new jsfsa.State( 'off' )
.addTransition( 'ignite', 'on' )
.isInitial = true
;
var onState = new jsfsa.State( 'on' )
.addTransition( 'shutdown', 'off' )
;
var fsm = new jsfsa.Automaton()
.addState( offState )
.addState( onState )
.doTransition( 'ignite' )
;
console.log( fsm.getCurrentState().name );//outputs 'on'
var offState = new State( 'off', {
transitions : {
'ignite' : 'on'
},
isInitial : true
} );
var onState = new jsfsa.State( 'on', {
transitions : {
'shutdown' : 'off'
}
} );
var fsm = new jsfsa.Automaton()
.addState( offState )
.addState( onState )
.doTransition( 'ignite' )
;
console.log( fsm.getCurrentState().name );//outputs 'on'
var offState = new State( 'off', {
'ignite' : 'on',
isInitial : true
} );
var onState = new jsfsa.State( 'on', {
'shutdown' : 'off'
} );
var fsm = new jsfsa.Automaton()
.addState( offState )
.addState( onState )
.doTransition( 'ignite' )
;
console.log( fsm.getCurrentState().name );//outputs 'on'
var config = {
'off' : {
'ignite' : 'on',
isInitial : true
},
'on' : {
'shutdown' : 'off'
}
};
var fsm = new jsfsa.Automaton( config )
.doTransition( 'ignite' )
;
console.log( fsm.getCurrentState().name );//outputs 'on'
Guards control entries and exits of states, they need to return a true
to continue transition. Anything falsy will terminate the transition.
//loose syntax
var blockEntry = function(){
return false;
}
var config = {
'off' : {
'ignite' : 'on',
isInitial : true
},
'on' : {
guards : {
entry : blockEntry
},
'shutdown' : 'off'
}
};
var fsm = new jsfsa.Automaton( config )
.doTransition( 'ignite' )
;
console.log( fsm.getCurrentState().name );//outputs 'off'
There are 4 events that can be listened to, either directly on a state instance or on the statemachine instance itself: 'entered', 'exited', 'entryDenied', 'exitDenied'. The automaton dispatches 2 additional events: 'transitionDenied', dispatched if the transition name was not registered for the current state or if the transition target could not've been found and 'changed', dispatched after a transition has been completed.
//loose syntax
var outputEvent = function( e ){
console.log( e.type + ' from ' + e.from + ' to ' + e.to );
}
var config = {
'off' : {
listeners : {
exited : outputEvent
},
'ignite' : 'on',
isInitial : true
},
'on' : {
listeners : {
entered : outputEvent
},
'shutdown' : 'off'
}
};
var fsm = new jsfsa.Automaton( config )
.doTransition( 'ignite' )
;
//output in console:
//exited from off to on
//entered from off to on
//loose syntax
var outputEvent = function( e ){
console.log( e.type + ' from ' + e.from + ' to ' + e.to );
}
var config = {
'off' : {
'ignite' : 'on',
isInitial : true
},
'on' : {
'shutdown' : 'off'
}
};
var fsm = new jsfsa.Automaton( config )
.addListener( jsfsa.StateEvent.CHANGED, outputEvent )
.doTransition( 'ignite' )
;
//output in console:
//changed from off to on
var config = {
"off" : {
isInitial : true,
"powerOn" : "on"
},
"off/standby" : {
isInitial : true
},
"off/kaput" : {
},
"off/kaput/fixable" :{
isInitial : true,
"fixed" : "off/standby"
},
"off/kaput/pertetotale":{
},
"on" : {
"powerOff" : "off",
"fail" : "off/kaput",
"vandalize" : "off/kaput/pertetotale"
},
"on/green" : {
isInitial : true,
"next" : "on/orange"
},
"on/orange" : {
"next" : "on/red"
},
"on/red" : {
"next" : "on/green"
}
};
var config = {
"off" : {
isInitial : true,
"powerOn" : "on"
},
"standby" : {
parent : "off"
isInitial : true
},
"kaput" : {
parent : "off"
},
"fixable" :{
parent : "kaput",
isInitial : true,
"fixed" : "standby"
},
"pertetotale":{
parent : "kaput"
},
"on" : {
"powerOff" : "off",
"fail" : "kaput",
"vandalize" : "pertetotale"
},
"green" : {
parent : "on",
isInitial : true,
"next" : "orange"
},
"orange" : {
parent : "on",
"next" : "red"
},
"red" : {
parent : "on"
"next" : "green"
}
};
var fsm;
var config = {
"green" : {
isInitial : true,
listeners : {
exited : function(){
fsm.pause(); //sets the fsm in a waiting state
setTimeout( 500, function(){
fsm.proceed(); //automaton proceeds transitioning to "orange" state
} );
}
},
"next" : "orange"
},
"orange" : { "next" : "red" },
"red" : { "next" : "green" }
};
fsm = new jsfsa.Automaton( config )
.doTransition( 'next' )
;
doTransition
method are automatically passed to all guards and listenersvar config = {
'off' : {
'ignite' : 'on'
isInitial : true,
listeners : {
exited : function( e, payload ){
console.log( payload ); //outputs 'foo'
}
}
},
'on' : {
'shutdown' : 'off'
}
};
var fsm = new jsfsa.Automaton( config )
.doTransition( 'ignite', 'foo' )
;
So far we have seen examples for statically defined transition targets. However, transitions can also be dynamic, i.e. the transition itself can be conditional, transitions can have an effect and it is possible to determine the target state of a transition based on some calculation within the transition. The event handler has access to payload data.
goal[valid]/goals++ +----------+
---------------------------> | kickOff |
+----------+
When a goal occurs, the effect is that the goals count is increased, but only if the goal is valid. Afterwards the game will be in the kickOff state.
This can be especially useful if there are different effects depending on the event and the condition which brings you to a new state. E.g. you can have one state kickOff which only contains the entry acttions that are common to all kickOffs, rather than distinguishing a kickOffAfterGoal which increases the goal count in its entry action from a kickOffAtBeginning and a kickOffAfterHalftime which don't do that.
For such a guarded transition with effect, define a function as the transition target and return the name of the target state if you want to allow the transition.
...
transitions : {
'goal' : function(e, valid) {
if(valid === true) {
goals++;
return 'kickOff';
}
}
}
...
// a valid goal occurs:
sm.doTransition('goal', true)
Note that the function returns the target state 'kickOff' if the goal was valid, but it returns undefined
if the goal was not valid. In the latter case, the transition is denied and we stay in the current state.
+-----------------------------------------------+
| Entering password |
+-----------------------------------------------+
| passwordEntered[invalid]/failed++ |
| |
+-----------------------------------------------+
For such an internal transition the event handler function must always return a falsy value.
...
transitions : {
'passwordEntered' : function(e, password) {
if(!passwordValid(password) {
failed++;
}
return undefined;
}
}
...
// wrong password:
sm.doTransition('passwordEntered', 'wr0n5')
Finally, this technique also allows you to express a choice, i.e. a transition whose target is determined dynamically. Consider the transitions below which describe what happens if a team scores a goal during a football match in tie state.
+-----------+
| Tie |
+-----------+
|
| goal
/ \
__ / \__
| \ / |
[homeTeam] | \ / |[visitingTeam]
/goals.homeTeam++ | | /goals.visitingTeam++
| |
+----------+<--* *-->+----------+
| Lead | | Lead |
| Home | | Visiting |
| Team | | Team |
+----------+ +----------+
The goal event can lead to two different transitions. The transition from Tie to LeadHomeTeam occurs only if the goal was scored by the home team. It effectively increments the goals of the home team (and vice versa). For such a choice, you could write:
...
transitions : {
'goal' : function(e, scorer) {
goals[scorer]++;
if(scorer === 'homeTeam') {
return 'leadHomeTeam';
} else {
return 'leadVisitingTeam';
}
}
}
...
// The home team scores a goal:
sm.doTransition('goal', 'homeTeam')
JSFSA has no dependencies on other frameworks.
JSFSA is inspired by the fsm's of