- For fast upgrade, checkout the Migration Guide
- List of all changes are available in Changelog file
- Documentation is updated to reflect the changes in version 5.x.x;
Finite state machine(FSM) is an abstract machine that can be in exactly one of a finite number of states at any given time. The FSM can change from one state to another in response to some external inputs(actions). The change from state to another is called a transition. An FSM definition includes:
const fsmDefinition = {
init : 'none' // initial fsm state
, behavior : [
// [ fromState, action , nextState, transitionFx, chaining ]
[ 'none' , 'activate', 'active' , 'switchON' ] // transition condition
, [ 'active' , 'stop' , 'none' , 'switchOFF' ] // transition condition
]
, stateData : {
// ... all stateData flags are here
}
}
}
- init: string. Initial state of the FSM;
- stateData: Container for all stateData required by the FSM.
- behavior: array of transition conditions. Describe relation among fsm state, action and transition;
- transitionCondition: Array [fromState action nextState transitionFx];
- fromState: string. State as starting point for transition;
- action: string. External input signal. Transition could happen only when fromState and action are described in transitionCondition record;
- nextState: string. The next FSM state if transition become successful;
- transitionFx: string. The function name that will be executed on 'state/action' conditions. Transition should return transitionResult object where parameter "success" will represent transition success. Result of transition will be evaluated by FSM. Positive response will change actual state to 'nextState' from transitionCondition record. Negative result will keep actual state;
- chaining(optional): Array [ positive, negative ]. Optional element of
transition condition
. It's a mechanism to trigger next action based on the result of the transition. If transition result is positive, FSM will trigger action frompositive
position. If transition result is negative, FSM will trigger action fromnegative
position. If only one position will use chaining, set the other as 'false'.
// ... transition conditions
, [ 'ready', 'activate', 'active', 'switchOFF', [ false, 'generator' ] ]
// Chaining will trigger 'generator' action on negative transition result and will do nothing on positive result.
// ... transition conditions
Transitions are functions and they are provided to FSM as functional library - javascript object with functions.
const transitionLibrary = {
switchOn : function () {
// ...code
}
, switchOff : function () {
// ...code
}
}
It's simple and clean way to represent system behaviour but practice shows that is not enough. This implementation was extended to cover some aditional program needs like:
- Transition function could contain asynchronous code;
- Chain-actions in transaction conditions(Optional);
- Chain-actions are possible on positive and negative transition-end;
- Export fsm state as an object: externalState;
- Import externalState;
- Prevent simultaneous updates;
- Fsm-updates cache mechanism and execution in a row;
Install module by:
npm i @peter.naydenov/fsm
Add in your script:
import Fsm from '@peter.naydenov/fsm'
// init new fsm:
const fsm = new Fsm ( machine, lib )
... where machine is fsm-description and lib is a transition-library.
If your project is commonJS, use a dynamic import
function or use version v.3.0.0 or older.
Fsm description is an object that define fsm business logic. Every fsm description should contain init and behavior properties.:
const machine = {
init : 'stopped'
, behavior: [
// [ state , action , nextState, functionName, chainAction(optional) ]
[ 'stopped', 'activate', 'active', 'fnActivate' ] // transition condition
, [ 'active' , 'stop' , 'stoped', 'fnStop' ]
]
, stateData : { greeting: 'hi' }
/** Parameter 'stateData' is not a required parameter. It's a data storage space.
* Storaged data is available in every transition function by using a function 'extractList'.
* Only a predefined state-data-names can be saved in 'stateData'
* /
}
Property 'init' is the initial state of the system. Property 'behavior' describes how system reacts. Every row(transition condition) contains 5 elements:
- Current active state;
- Action;
- Next state;
- Transition function that should be applied;
- Chain action
[ 'stopped', 'activate', 'active', 'fnActivate' ]
// Read this transition condition like:
/**
* When state is 'stopped' and action 'activate' comes, transition 'fnActivate' will be executed. On success state will become 'active'. No chain-actions are defined.
*/
Object that will contain all transition functions:
const lib = {
fnActicate () {
// ... code to activate
}
, fnStop () {
// ... code to stop
}
}
Fsm transition function is a function, member of transition library. Transition functions is kind of middleware. Every function receives this arguments:
const lib = {
fnActivate ( {task, state, dependencies, extractList}, data ) {
// function code is here...
task.done ( end ) // end is a transitionResult {}
}
}
- task - askForPromise object; AskForPromise documentation. Represents asynchronous transition execution. Finish by providing to task object, the transition result object.
task.done( transitionResult )
; - state - string. Current state of the FSM;
- dependencies - object. Contain all external dependencies. Set fsm dependencies by fsm.setDependencies();
- extractList - function. Extract data from FSM stateData;
- data - any(Prefered: object). External data provided from fsm.update( action, data);
Execution of transition function will end on task.done(end)
where end is an transitionResult object.
Only one of the params in this object is required:
{
success : true // Required. Boolean. Transition success;
, response : {} // Optional. Object. update answer response;
, stateData : {} // Optional. Object. 'stateData' if there is stateData changes;
}
setDependencies : 'Insert all external dependencies'
, getDependencies : 'Returns all external dependencies'
, update : 'Trigger an action'
, importState : 'Import externalState object.'
, exportState : 'Export state and stateData as a single object (externalState)'
, reset : 'Revert state and stateData to initial values described during initialization'
, ignoreCachedUpdates : 'Ignore all cached updates'
, getState : 'Returns actual state'
, extractList : 'Extract list of state segments or properties'
Set dependencies for FSM. Dependency object will be provided to every transition function. With dependency injection code will stay testable. Method could be called multiple times. All dependencies will be merged in one object.
const
moment = require ( 'moment' )
, fsm = new Fsm ()
, deps = {
scrollTo : window.scrollTo
, moment
}
;
fsm.setDependencies ( deps )
- deps: object. Object with external dependencies;
- Method returns: void;
Returns all external dependencies.
const
moment = require ( 'moment' )
, fsm = new Fsm ()
, deps = {
scrollTo : window.scrollTo
, moment
}
;
fsm.setDependencies ( deps )
const allDeps = fsm.getDependencies ()
Provide actions to FSM. If conditions 'state/action' exist in description table, FSM will react.
- If action name does not exist in behavior table, FSM will ignore the action;
- If action name exist in behavior table but current state is different, FSM will ignore the action;
- If transition function is not provided, FSM will ignore the action;
fsm.update ( action, altData )
.then ( r => {
// ...do something with the response
})
- action(required): string. The action name.
- altData(optional): any. Additional data provided to the transition;
- Method returns: Promise. Returned value is equal to transitionResult.response;
Will return current current FSM state.
let currentState = fsm.getState ()
- Method returns: string. Current FSM state;
Export state and stateData as a single object (externalState). Export state to:
- Keep fsm history record;
- Possible point of recovery;
- Synchronize fsm states accross the network in large distributed systems;
- Debuging purposes;
const externalState = fsm.exportState ();
/**
* externalState = {
* state : 'actualState'
* , stateData : {
* ... all stateData flags are here
* }
* }
* */
- Method returns: externalState object
Put fsm in specific state by importing externalState object described in 'exportState()' method. Use method to:
-
Recover previous state of the fsm;
-
Testing specific situations by moving fsm directly in required state;
-
Synchronize fsm states accross the network;
-
externalState: externalState object
-
Method returns: void
Returns initial values for state and stateData.
fsm.reset ()
- Method returns: void;
Ignore all cached updates.
const fsm = new Fsm ( description, transitions );
fsm.update ( 'start' ).then ( x => fsm.ignoreCachedUpdates () )
/**
* Update with action 'start' will block all upcoming updates. Subsequent updates
* will wait by using cache mechanism.
* Method 'ignoreCachedUpdates' will remove updates from cache and will close their promises with 'reject' and
* error message.
* Use reject function to customize 'canceled updates' behaviour
*/
fsm.update ( 'move' )
.then ( r => { // resolve function
result = fsm.exportState ()
expect ( result.state ).to.be.equal ( 'initial' )
}
, r => { // reject function
//---> 'r' will contain error message produced by fsm.
// r == "Action 'move' was ignored"
})
- Method returns: void;
Example represents controller for system that require electricity. On fsm.update('start')
controller will try to activate standard electricity source. On fail will trigger chain-action generator
and will try to activate alternative energy source. On success state will become altSrc
(alternativeSource). When standart electricity source is available we can inform the system by calling fsm.update('electricity')
and on success state will become active
. Switch off the system any time by calling fsm.update('stop')
.
const
lib = {
switchON ( task, dependencies, stateData, dt ) {
task.done ({ success : false })
}
, altOn ( task ) {
task.done ({ success: true })
}
, switchOFF ( task ) {
task.done ({ success: true })
}
, primarySource ( task ) {
task.done ({ success: true })
}
}
, machine = {
init : 'none'
, behavior : [
[ 'none' , 'start' , 'active' , 'switchON', [ false, 'generator'] ]
, [ 'none' , 'generator' , 'altSrc' , 'altOn' ]
, [ 'active' , 'stop' , 'none' , 'switchOFF' ]
, [ 'altSrc' , 'stop' , 'none' , 'switchOFF' ]
, [ 'altSrc' , 'electricity' , 'active', 'primarySource' ]
]
, stateData : {}
}
;
const fsm = new Fsm ( machine, lib );
fsm.update ( 'activate' )
.then ( () => {
const r = fsm.getState ();
expect ( r ).to.be.equal ( 'altSrc' )
})
'@peter.naydenov/fsm' was created by Peter Naydenov.
'@peter.naydenov/fsm' is released under the MIT License.