var _ = require('underscore'); /* machina.js Author: Jim Cowart License: Dual licensed MIT (http://www.opensource.org/licenses/mit-license) & GPL (http://www.opensource.org/licenses/gpl-license) Version 0.1.0 */ var slice = [].slice, NEXT_TRANSITION = "transition", NEXT_HANDLER = "handler", transformEventListToObject = function(eventList){ var obj = {}; _.each(eventList, function(evntName) { obj[evntName] = []; }); return obj; }, parseEventListeners = function(evnts) { var obj = evnts; if(_.isArray(evnts)) { obj = transformEventListToObject(evnts); } return obj; }, utils = { makeFsmNamespace: (function(){ var machinaCount = 0; return function() { return "fsm." + machinaCount++; }; })(), getDefaultOptions: function() { return { initialState: "uninitialized", eventListeners: { "*" : [] }, states: {}, eventQueue: [], namespace: utils.makeFsmNamespace() }; } }, Fsm = function(options) { var opt, initialState, defaults = utils.getDefaultOptions(); if(options) { if(options.eventListeners) { options.eventListeners = parseEventListeners(options.eventListeners); } if(options.messaging) { options.messaging = _.extend({}, defaults.messaging, options.messaging); } } opt = _.extend(defaults , options || {}); initialState = opt.initialState; delete opt.initialState; _.extend(this,opt); this.state = undefined; this._priorAction = ""; this._currentAction = ""; if(initialState) { this.transition(initialState); } machina.fireEvent("newFsm", this); }; Fsm.prototype.fireEvent = function(eventName) { var args = arguments; _.each(this.eventListeners["*"], function(callback) { try { callback.apply(this,slice.call(args, 0)); } catch(exception) { if(console && typeof console.log !== "undefined") { console.log(exception.toString()); } } }); if(this.eventListeners[eventName]) { _.each(this.eventListeners[eventName], function(callback) { try { callback.apply(this,slice.call(args, 1)); } catch(exception) { if(console && typeof console.log !== "undefined") { console.log(exception.toString()); } } }); } }; Fsm.prototype.handle = function(msgType) { // vars to avoid a "this." fest var states = this.states, current = this.state, args = slice.call(arguments,0), handlerName; this.currentActionArgs = args; if(states[current] && (states[current][msgType] || states[current]["*"])) { handlerName = states[current][msgType] ? msgType : "*"; this._currentAction = current + "." + handlerName; this.fireEvent.apply(this, ["Handling"].concat(args)); states[current][handlerName].apply(this, args.slice(1)); this.fireEvent.apply(this, ["Handled"].concat(args)); this._priorAction = this._currentAction; this._currentAction = ""; this.processQueue(NEXT_HANDLER); } else { this.fireEvent.apply(this, ["NoHandler"].concat(args)); } this.currentActionArgs = undefined; }; Fsm.prototype.transition = function(newState) { if(this.states[newState]){ var oldState = this.state; this.state = newState; if(this.states[newState]._onEnter) { this.states[newState]._onEnter.call( this ); } this.fireEvent.apply(this, ["Transitioned", oldState, this.state ]); this.processQueue(NEXT_TRANSITION); return; } this.fireEvent.apply(this, ["InvalidState", this.state, newState ]); }; Fsm.prototype.processQueue = function(type) { var filterFn = type === NEXT_TRANSITION ? function(item){ return item.type === NEXT_TRANSITION && ((!item.untilState) || (item.untilState === this.state)); } : function(item) { return item.type === NEXT_HANDLER; }, toProcess = _.filter(this.eventQueue, filterFn, this); this.eventQueue = _.difference(this.eventQueue, toProcess); _.each(toProcess, function(item, index){ this.handle.apply(this, item.args); }, this); }; Fsm.prototype.deferUntilTransition = function(stateName) { if(this.currentActionArgs) { var queued = { type: NEXT_TRANSITION, untilState: stateName, args: this.currentActionArgs }; this.eventQueue.push(queued); this.fireEvent.apply(this, [ "Deferred", this.state, queued ]); } }; Fsm.prototype.deferUntilNextHandler = function() { if(this.currentActionArgs) { var queued = { type: NEXT_TRANSITION, args: this.currentActionArgs }; this.eventQueue.push(queued); this.fireEvent.apply(this, [ "Deferred", this.state, queued ]); } }; Fsm.prototype.on = function(eventName, callback) { if(!this.eventListeners[eventName]) { this.eventListeners[eventName] = []; } this.eventListeners[eventName].push(callback); }; Fsm.prototype.off = function(eventName, callback) { if(this.eventListeners[eventName]){ this.eventListeners[eventName] = _.without(this.eventListeners[eventName], callback); } }; var machina = { Fsm: Fsm, bus: undefined, utils: utils, on: function(eventName, callback) { if(!this.eventListeners[eventName]) { this.eventListeners[eventName] = []; } this.eventListeners[eventName].push(callback); }, off: function(eventName, callback) { if(this.eventListeners[eventName]){ this.eventListeners[eventName] = _.without(this.eventListeners[eventName], callback); } }, fireEvent: function(eventName) { var i = 0, len, args = arguments, listeners = this.eventListeners[eventName]; if(listeners && listeners.length) { _.each(listeners, function(callback) { callback.apply(null,slice.call(args, 1)); }); } }, eventListeners: { newFsm : [] } }; module.exports = machina;