Version 3.0 is a significant rewrite from earlier versions in order to support more advanced use cases and to improve the existing use cases. Unfortunately, many of these updates are incompatible with earlier versions, so changes are required in your code when you upgrade to version 3.x. We want to tackle those all in one swoop and avoid any more big-bang changes in the future.
Please read this article carefully if you are upgrading from version 2.x to 3.x.
A summary of the changes required can be found at the end of the article.
- Construction - constructing single instances follows a more idomatic javascript pattern.
- State Machine Factory - constructing multiple instances from a class has been simplified.
- Data and Methods - A state machine can now have additional data and methods.
- Renamed Terminology - A more consistent terminology has been applied.
- Lifecycle Events - (previously called 'callbacks') are camelCased and observable.
- Async Transitions - Asynchronous transitions now use standard Promises.
- Conditional Transitions - A transition can now dynamically choose its target state at run-time.
- Goto - The state can be changed without a defined transition using
goto
. - State History - The state history can now be retained and traversed with back/forward semantics.
- Visualization - A state machine can now be visualized using GraphViz.
- Build System - A new webpack-based build system has been implemented.
Constructing a single state machine now follows a more idiomatic javascript pattern:
Version 2.x:
var fsm = StateMachine.create({ /* ... */ })
Version 3.x:
var fsm = new StateMachine({ /* ... */ }) // <-- more idomatic
Constructing multiple instances from a state machine 'class' has been simplified:
Version 2.x:
function FSM() { }
StateMachine.create({
target: FSM.prototype,
// ...
})
var a = new FSM(),
b = new FSM();
Version 3.x:
var FSM = StateMachine.factory({ /* ... */ }), // <-- generate a factory (a constructor function)
a = new FSM(), // <-- then create instances
b = new FSM();
A state machine can now have additional (arbitrary) data and methods defined:
Version 2.x: not supported.
Version 3.x:
var fsm = new StateMachine({
data: {
color: 'red'
},
methods: {
speak: function() { console.log('hello') }
}
});
fsm.color; // 'red'
fsm.speak(); // 'hello'
A more consistent terminology has been applied:
- A state machine consists of a set of States.
- A state machine changes state by using Transitions.
- A state machine can perform actions during a transition by observing Lifecycle Events.
- A state machine can also have arbitrary Data and Methods.
Version 2.x:
var fsm = StateMachine.create({
initial: 'ready',
events: [ /* ... */ ],
callbacks: { /* ... */ }
});
fsm.current; // 'ready'
Version 3.x:
var fsm = new StateMachine({
init: 'ready', // <-- renamed s/initial/init/
transitions: [ /* ... */ ], // <-- renamed s/events/transitions/
data: { /* ... */ }, // <-- new
methods: { /* ... */ } // <-- renamed s/callbacks/methods/
// ... which can contain arbitrary methods AND lifecycle event callbacks
});
fsm.state; // 'ready' // <-- renamed s/current/state/
Callbacks have been renamed Lifecycle Events and are now declared as methods
on the
state machine using a more traditional javascript camelCase for the method names:
Version 2.x:
var fsm = StateMachine.create({
initial: 'initial-state',
events: [
{ name: 'do-something', from: 'initial-state', to: 'final-state' }
],
callbacks: {
onbeforedosomething: function() { /* ... */ },
onleaveinitialstate: function() { /* ... */ },
onenterfinalstate: function() { /* ... */ },
onafterdosomething: function() { /* ... */ }
}
})
Version 3.x:
var fsm = new StateMachine({
init: 'initial-state',
transitions: [
{ name: 'do-something', from: 'initial-state', to: 'final-state' }
],
methods: { // <-- renamed s/callbacks/methods/
onBeforeDoSomething: function() { /* ... */ }, // <-- camelCase naming convention
onLeaveInitialState: function() { /* ... */ }, // <--
onEnterFinalState: function() { /* ... */ }, // <--
onAfterDoSomething: function() { /* ... */ } // <--
}
})
Lifecycle events are now passed information in a single `lifecycle` argument:
Version 2.x:
var fsm = StateMachine.create({
events: [
{ name: 'step', from: 'none', to: 'complete' }
],
callbacks: {
onbeforestep: function(event, from, to) {
console.log('event: ' + event); // 'step'
console.log('from: ' + from); // 'none'
console.log('to: ' + to); // 'complete'
},
}
});
Version 3.x:
var fsm = new StateMachine({
transitions: [
{ name: 'step', from: 'none', to: 'complete' }
],
methods: {
onBeforeStep: function(lifecycle) { // <-- combined into a single argument
console.log('transition: ' + lifecycle.transition); // 'step'
console.log('from: ' + lifecycle.from); // 'none'
console.log('to: ' + lifecycle.to); // 'complete'
}
}
});
This change allows us to include additional information in the future without having to have a ridiculous number of arguments to lifecycle event observer methods
Lifecycle events are also now observable by others:
Version 2.x: not supported.
Version 3.x:
var fsm = new StateMachine({ /* ... */ });
// observe individual lifecycle events with observer methods
fsm.observe('onBeforeTransition', function() { /* ... */ });
fsm.observe('onLeaveState', function() { /* ... */ });
// or observe multiple lifecycle events with an observer object
fsm.observe({
onBeforeTransition: function() { /* ... */ },
onLeaveState: function() { /* ... */ }
});
The general purpose lifecycle events now use the word `transition` instead of `event` and occur **before** their specialized versions:
Version 2.x, the lifecycle order was:
onbefore<EVENT>
onbeforeevent
onleave<STATE>
onleavestate
onenter<STATE>
onenterstate
on<STATE>
onafter<EVENT>
onafterevent
on<EVENT>
Version 3.x, the lifecycle order is:
onBeforeTransition
- fired before any transitiononBefore<TRANSITION>
- fired before a specific TRANSITIONonLeaveState
- fired when leaving any stateonLeave<STATE>
- fired when leaving a specific STATEonTransition
- fired during any transitiononEnterState
- fired when entering any stateonEnter<STATE>
- fired when entering a specific STATEon<STATE>
- convenience shorthand foronEnter<STATE>
onAfterTransition
- fired after any transitiononAfter<TRANSITION>
- fired after a specific TRANSITIONon<TRANSITION>
- convenience shorthand foronAfter<TRANSITION>
For more details, read Lifecycle Events
Asynchronous transitions are now implemented using standard javascript Promises.
If you return a Promise from any lifecycle event then the entire lifecycle for that transition is put on hold until that Promise gets resolved. If the promise is rejected then the transition is cancelled.
Version 2.x:
var fsm = StateMachine.create({
events: [
{ name: 'step', from: 'none', to: 'complete' }
],
callbacks: {
onbeforestep: function() {
$('#ui').fadeOut('fast', function() {
fsm.transition();
});
return StateMachine.ASYNC;
}
}
});
Version 3.x:
var fsm = new StateMachine({
transitions: [
{ name: 'step', from: 'none', to: 'complete' }
],
methods: {
onBeforeStep: function() {
return new Promise(function(resolve, reject) { // <-- return a Promise instead of StateMachine.ASYNC
$('#ui').fadeOut('fast', resolve); // <-- resolve the promise instead of calling .transition()
});
}
}
});
For more details, read Asynchronous Transitions
A transition can now be conditional and choose the target state at run-time by providing a function
as the to
attribute.
Version 2.x: not supported.
Version 3.x: See Conditional Transitions
The state can now be changed without the need for a predefined transition using a conditional goto
transition:
Version 2.x: not_supported.
Version 3.x: See Goto
A state machine can now track and traverse (back/forward) its state history.
Version 2.x: not supported.
Version 3.x: See State History
A state machine can now be visualized as a directed graph using GraphViz .dot
syntax.
Version 2.x: not_supported.
Version 3.x: See Visualization
A new Webpack based build system has been provided along with an Ava based unit test suite.
Version 2.x: not_supported.
Version 3.x: See Contributing
isFinished
is no longer built-in, you can easily add it to your state machine with a custom method:
var fsm = new StateMachine({
methods: {
isFinished: function() { return this.state === 'done' }
}
})
The following list summarizes the above changes you might need when upgrading to version 3.0
- replace
StateMachine.create()
withnew StateMachine()
- rename:
initial
toinit
events
totransitions
callbacks
tomethods
fsm.current
tofsm.state
- update your callback methods:
- rename them to use traditional javascript
camelCasing
- refactor them to use the single
lifecycle
argument instead of individualevent,from,to
arguments
- rename them to use traditional javascript
- update any asynchronous callback methods:
- return a
Promise
instead ofStateMachine.ASYNC
resolve()
the promise when ready instead of callingfsm.transition()
- return a
- replace
StateMachine.create({ target: FOO })
with:- if FOO is a class -
StateMachine.factory(FOO, {})
- if FOO is an object -
StateMachine.apply(FOO, {})
- if FOO is a class -