Skip to content

Instantly share code, notes, and snippets.

@robspassky
Last active August 1, 2017 04:05
Show Gist options
  • Save robspassky/bc21002eb2e80325d70d43d6d3819c3c to your computer and use it in GitHub Desktop.
Save robspassky/bc21002eb2e80325d70d43d6d3819c3c to your computer and use it in GitHub Desktop.
FSM (finite-state-machine) for javascript
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