1 /** 2 * Emily.js - http://flams.github.com/emily/ 3 * Copyright(c) 2012-2014 Olivier Scherrer <pode.fr@gmail.com> 4 * MIT Licensed 5 */ 6 "use strict"; 7 8 var Tools = require("./Tools"); 9 10 /** 11 * @class 12 * Creates a stateMachine 13 * 14 * @param initState {String} the initial state 15 * @param diagram {Object} the diagram that describes the state machine 16 * @example 17 * 18 * diagram = { 19 * "State1" : [ 20 * [ message1, action, nextState], // Same as the state's add function 21 * [ message2, action2, nextState] 22 * ], 23 * 24 * "State2" : 25 * [ message3, action3, scope3, nextState] 26 * ... and so on .... 27 * 28 * } 29 * 30 * @return the stateMachine object 31 */ 32 module.exports = function StateMachineConstructor($initState, $diagram) { 33 34 /** 35 * The list of states 36 * @private 37 */ 38 var _states = {}, 39 40 /** 41 * The current state 42 * @private 43 */ 44 _currentState = ""; 45 46 /** 47 * Set the initialization state 48 * @param {String} name the name of the init state 49 * @returns {Boolean} 50 */ 51 this.init = function init(name) { 52 if (_states[name]) { 53 _currentState = name; 54 return true; 55 } else { 56 return false; 57 } 58 }; 59 60 /** 61 * Add a new state 62 * @private 63 * @param {String} name the name of the state 64 * @returns {State} a new state 65 */ 66 this.add = function add(name) { 67 if (!_states[name]) { 68 var transition = _states[name] = new Transition(); 69 return transition; 70 } else { 71 return _states[name]; 72 } 73 }; 74 75 /** 76 * Get an existing state 77 * @private 78 * @param {String} name the name of the state 79 * @returns {State} the state 80 */ 81 this.get = function get(name) { 82 return _states[name]; 83 }; 84 85 /** 86 * Get the current state 87 * @returns {String} 88 */ 89 this.getCurrent = function getCurrent() { 90 return _currentState; 91 }; 92 93 /** 94 * Tell if the state machine has the given state 95 * @param {String} state the name of the state 96 * @returns {Boolean} true if it has the given state 97 */ 98 this.has = function has(state) { 99 return _states.hasOwnProperty(state); 100 }; 101 102 /** 103 * Advances the state machine to a given state 104 * @param {String} state the name of the state to advance the state machine to 105 * @returns {Boolean} true if it has the given state 106 */ 107 this.advance = function advance(state) { 108 if (this.has(state)) { 109 _currentState = state; 110 return true; 111 } else { 112 return false; 113 } 114 }; 115 116 /** 117 * Pass an event to the state machine 118 * @param {String} name the name of the event 119 * @returns {Boolean} true if the event exists in the current state 120 */ 121 this.event = function event(name) { 122 var nextState; 123 124 nextState = _states[_currentState].event.apply(_states[_currentState].event, Tools.toArray(arguments)); 125 // False means that there's no such event 126 // But undefined means that the state doesn't change 127 if (nextState === false) { 128 return false; 129 } else { 130 // There could be no next state, so the current one remains 131 if (nextState) { 132 // Call the exit action if any 133 _states[_currentState].event("exit"); 134 _currentState = nextState; 135 // Call the new state's entry action if any 136 _states[_currentState].event("entry"); 137 } 138 return true; 139 } 140 }; 141 142 /** 143 * Initializes the StateMachine with the given diagram 144 */ 145 Tools.loop($diagram, function (transition, state) { 146 var myState = this.add(state); 147 transition.forEach(function (params){ 148 myState.add.apply(null, params); 149 }); 150 }, this); 151 152 /** 153 * Sets its initial state 154 */ 155 this.init($initState); 156 }; 157 158 /** 159 * Each state has associated transitions 160 * @constructor 161 */ 162 function Transition() { 163 164 /** 165 * The list of transitions associated to a state 166 * @private 167 */ 168 var _transitions = {}; 169 170 /** 171 * Add a new transition 172 * @private 173 * @param {String} event the event that will trigger the transition 174 * @param {Function} action the function that is executed 175 * @param {Object} scope [optional] the scope in which to execute the action 176 * @param {String} next [optional] the name of the state to transit to. 177 * @returns {Boolean} true if success, false if the transition already exists 178 */ 179 this.add = function add(event, action, scope, next) { 180 181 var arr = []; 182 183 if (_transitions[event]) { 184 return false; 185 } 186 187 if (typeof event == "string" && 188 typeof action == "function") { 189 190 arr[0] = action; 191 192 if (typeof scope == "object") { 193 arr[1] = scope; 194 } 195 196 if (typeof scope == "string") { 197 arr[2] = scope; 198 } 199 200 if (typeof next == "string") { 201 arr[2] = next; 202 } 203 204 _transitions[event] = arr; 205 return true; 206 } 207 208 return false; 209 }; 210 211 /** 212 * Check if a transition can be triggered with given event 213 * @private 214 * @param {String} event the name of the event 215 * @returns {Boolean} true if exists 216 */ 217 this.has = function has(event) { 218 return !!_transitions[event]; 219 }; 220 221 /** 222 * Get a transition from it's event 223 * @private 224 * @param {String} event the name of the event 225 * @return the transition 226 */ 227 this.get = function get(event) { 228 return _transitions[event] || false; 229 }; 230 231 /** 232 * Execute the action associated to the given event 233 * @param {String} event the name of the event 234 * @param {params} params to pass to the action 235 * @private 236 * @returns false if error, the next state or undefined if success (that sounds weird) 237 */ 238 this.event = function event(newEvent) { 239 var _transition = _transitions[newEvent]; 240 if (_transition) { 241 _transition[0].apply(_transition[1], Tools.toArray(arguments).slice(1)); 242 return _transition[2]; 243 } else { 244 return false; 245 } 246 }; 247 }