Skip to content

Instantly share code, notes, and snippets.

@joelhooks
Last active December 14, 2015 20:59
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joelhooks/5147792 to your computer and use it in GitHub Desktop.
Save joelhooks/5147792 to your computer and use it in GitHub Desktop.
This is a port of a port. Very basic finite state machine. Requires underscore.
//this is an event dispatcher at its bare minimum.
var EventDispatcher;
(function () {
"use strict";
/**
* Minimal event dispatcher
* @see http://stackoverflow.com/q/7026709/87002
* @constructor
*/
EventDispatcher = function EventDispatcher() {
var listeners = {};
this.addEventListener = function (event, listener, context) {
if (listeners.hasOwnProperty(event)) {
listeners[event].push([listener, context]);
} else {
listeners[event] = [
[listener, context]
];
}
};
this.removeEventListener = function (event, listener) {
var i;
if (listeners.hasOwnProperty(event)) {
for (i in listeners[event]) {
if (listeners[event][i][0] == listener) {
listeners[event].splice(i, 1);
return true;
}
}
}
return false;
};
this.dispatchEvent = function (event) {
var i;
if (event.name && listeners.hasOwnProperty(event.name)) {
for (i in listeners[event.name]) {
if (typeof listeners[event.name][i][0] == 'function') {
listeners[event.name][i][0].call(listeners[event.name][i][1], event);
}
}
}
};
};
}());
module fsm {
export interface EventDispatcher {
dispatchEvent: Function;
addEventListener(eventName:String, listener:(event:any) => void);
}
export class State {
transitions:any = {};
constructor(public name:string, public entering:string = null, public exiting:string = null, public changed:string = null) {
}
defineTrans(action:string, target:string) {
if (this.getTarget(action) != null) {
return;
}
this.transitions[action] = target;
}
removeTrans(action:string) {
this.transitions[action] = null;
}
getTarget(action:string) {
return this.transitions[action];
}
}
export class StateEvent {
static ACTION:string = "stateMachineEvent/action";
static CANCEL:string = "stateMachineEvent/cancel";
static CHANGED:string = "stateMachineEvent/changed";
constructor(public name:string, public action:string = null, public data:any = null) {
}
}
export class StateMachine {
private _currentState:State;
private _canceled:bool = false;
private _states:any = {};
private _initial:State;
constructor(public eventDispatcher:EventDispatcher) {
}
getCurrentStateName() {
return this._currentState ? this._currentState.name : '';
}
onRegister() {
this.eventDispatcher.addEventListener(StateEvent.ACTION, this.handleStateAction.bind(this));
this.eventDispatcher.addEventListener(StateEvent.CANCEL, this.handleStateCancel.bind(this));
if (this._initial) {
this.transitionTo(this._initial);
}
}
handleStateAction(event:StateEvent) {
var newStateTarget:string = this._currentState.getTarget(event.action);
var newState:State = this._states[newStateTarget];
if (newState) {
this.transitionTo(newState, event.data);
}
}
handleStateCancel(event:String) {
this._canceled = true;
}
registerState(state:State, initial:bool = false) {
if (state == null || state[state.name] != null) {
return;
}
this._states[state.name] = state;
if (initial) {
this._initial = state;
}
}
removeState(stateName:string) {
this._states[stateName] = null;
}
transitionTo(nextState:State, data:any = null) {
if (!nextState) {
return;
}
this._canceled = false;
if (this._currentState && this._currentState.exiting) {
this.eventDispatcher.dispatchEvent(new StateEvent(this._currentState.exiting, null, data))
}
if (this._canceled) {
this._canceled = false;
return;
}
if (nextState.entering) {
this.eventDispatcher.dispatchEvent(new StateEvent(nextState.entering, null, data));
}
if (this._canceled) {
this._canceled = false;
return;
}
this._currentState = nextState;
if (nextState.changed) {
this.eventDispatcher.dispatchEvent(new StateEvent(nextState.changed, null, data));
}
}
}
export class FSMInjector {
private stateList:Array = null;
private fsm:any = null;
constructor(fsm:any, public eventDispatcher:EventDispatcher) {
this.fsm = fsm;
}
getStates() {
if (!this.stateList) {
this.stateList = [];
_.each(this.fsm.states, (state) => {
this.stateList.push(this.createState(state));
})
}
return this.stateList;
}
inject(stateMachine:StateMachine) {
_.each(this.getStates(), (state) => {
stateMachine.registerState(state, this.isInitial(state.name));
});
stateMachine.onRegister();
}
createState(state:any) {
var newState = new State(state.name, state.entering, state.exiting, state.changed);
_.each(state.transitions, (transition:any) => {
newState.defineTrans(transition.action, transition.target);
});
return newState;
}
isInitial(stateName:string):bool {
return stateName == this.fsm.initial;
}
}
}
var machine = {
initial: <string> 'state/STARTING',
states: [
<Object> {
name: <string> 'state/STARTING',
transitions: [
<Object> {
action: <string> 'action/completed/STARTED',
target: <string> 'state/CONSTRUCTING' //name of the state to move to when event received
},
<Object> {
action: <string> 'action/START_FAILED',
target: <string> 'state/FAILING'
}
]
},
<Object> {
name: <string> 'state/CONSTRUCTING',
changed: <string> 'event/CONSTRUCT',
exiting: <string> 'event/CONSTRUCTION_EXIT',
entering: <string> 'action/CONSTRUCTION_ENTERING',
transitions: [
<Object> {
action: <string> 'action/completed/CONSTRUCTED', //action is the event constant
target: <string> 'state/COMPLETE' //name of the state to move to when event received
},
<Object> {
action: <string> 'action/CONSTRUCTION_FAILED',
target: <string> 'state/FAILING'
}
]
},
<Object> {
name: <string> 'state/COMPLETE',
changed: <string> 'event/COMPLETE'
//no transitions because this is effectively the end of the road
},
<Object> {
name: <string> 'state/FAILING',
changed: <string> 'event/FAIL'
}
]
};
var FSM = new fsm.FSMInjector(machine);
var dispatcher = new EventDispatcher();
var machine = new fsm.StateMachine(dispatcher);
FSM.inject(machine);
dispatcher.dispatchEvent(new fsm.StateEvent(fsm.StateEvent.ACTION, 'action/completed/STARTED'));
console.log(machine.getCurrentStateName());
dispatcher.dispatchEvent(new fsm.StateEvent(fsm.StateEvent.ACTION, 'action/completed/CONSTRUCTED'));
console.log(machine.getCurrentStateName());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment