Last active
December 14, 2015 20:59
-
-
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 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
//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); | |
} | |
} | |
} | |
}; | |
}; | |
}()); |
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
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