Last active
August 1, 2017 04:05
-
-
Save robspassky/bc21002eb2e80325d70d43d6d3819c3c to your computer and use it in GitHub Desktop.
FSM (finite-state-machine) for javascript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
window.robspassky = window.robspassky || {}; | |
/** | |
* I've found finite state machines to be very helpful for managing the | |
* complexity of a web UI. This short function generates a FSM given a | |
* set of states. | |
* | |
* Each state has optional "entry" and "exit" functions and must contain | |
* a "transitions" object, whose property names are event names, and whose | |
* property values are functions that operate on the arguments of the | |
* event and return a new state name. | |
* | |
* It is required to have a STATE called "INITIAL", which will be the | |
* starting state of the FSM. | |
* | |
* There is only one API for the FSM: | |
* | |
* fsm.process(event, ...) | |
* | |
* For example, the FSM defined by: | |
* | |
* INITIAL -> (startup) -> READY -> (record) -> BEGIN_RECORDING | |
* \-> (exit) -> EXIT | |
* | |
* can be generated with the following: | |
* | |
* let fsm = FSM('sample', {}, { | |
* INITIAL: { transitions: { startup: { return 'READY'; } } }, | |
* READY: { transitions: { | |
* record: { return 'BEGIN_RECORDING'; }, | |
* exit: { return 'EXIT'; } | |
* } | |
* }, | |
* BEGIN_RECORDING: { ... }, | |
* EXIT: { ... } | |
* }); | |
* fsm.process('startup'); // goes to READY state | |
* fsm.process('record'); // goes to BEGIN_RECORDING state | |
* | |
* NOTES | |
* | |
* The "exit" and "transition" function can return false to abort the | |
* state transition. "entry" can also return false, but I decided not | |
* to rewind the transition and the new state will still be in effect. | |
* | |
* I use template literals so ES6-ish javascript is required. | |
* | |
* I just wrote this off the top of my head (after having written | |
* several messier versions) so until I get a chance to refactor it | |
* into my work project there might be typos. | |
**/ | |
window.robspassky.FSM = (name, context, states) => { | |
let _cs = 'INITIAL'; | |
function _msg(msg) { return `FSM ${name}:${_cs} -- ${msg}`; } | |
function _throw(msg) { throw _msg(msg); } | |
function _log(msg) { console.log(_msg(msg)); } | |
function _error(msg) { console.error(_msg(msg)); } | |
if (!states.INITIAL) { _throw '"INITIAL" state required'; } | |
return { | |
process: (event, ...args) => { | |
let s = states[_cs]; | |
if (!s) { _throw 'unknown state'; } | |
let t = s.transitions[event]; | |
if (!t) { _throw `no transition for event ${event}`; } | |
_log(`processing event ${event}`); | |
try { | |
if (s.exit && !s.exit.apply(context, [])) { | |
_log('exit function returned false so not changing state'); | |
return false; | |
} | |
let st = t.apply(context, args); | |
if (!st) { | |
_log('state transition returned false so not changing state'); | |
return false; | |
} | |
_log(`transitioning to new state: ${st}`); | |
_cs = st; | |
s = states[_cs]; | |
if (!s) { _throw 'unknown state'; } | |
if (s.entry && !s.entry.apply(context, [])) { | |
_error('entry function failed, but state already changed, no going back'); | |
} | |
return true; | |
} catch (e) { | |
_log('unexpected error while transitioning'); | |
_throw e; | |
} | |
} | |
}; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment