postal.js/example/node/machina.js

199 lines
No EOL
5.7 KiB
JavaScript

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;