diff --git a/automata.d.ts b/automata.d.ts new file mode 100644 index 0000000..1f511ed --- /dev/null +++ b/automata.d.ts @@ -0,0 +1,104 @@ +declare module FSM { + + export interface FSMDefinitionStateTimer { + timeout : number; + event : FSM.TransitionMessage; + } + + export interface FSMDefinitionState { + name : string; + initial : boolean; + onTimer : FSMDefinitionStateTimer; + onEnter : string|FSM.StateCallback; + onExit : string|StateCallback; + } + + export interface FSMDefinitionSubState { + name : string; + } + + export interface FSMDefinitionTransition { + event :string; + from : string; + to : string; + onTransition : string|TransitionCallback; + onPreGuard : string|TransitionCallback; + onPostGuard : string|TransitionCallback; + } + + export interface FSMDefinition { + name : string; + state : FSMDefinitionState[] | FSMDefinitionSubState[]; + transition : FSMDefinitionTransition[]; + onEnter : string | StateCallback; + onExit : string | StateCallback; + } + + export interface TransitionMessage { + msgId : string; + data? : any; + } + + export interface ConsumeCallback { + (session:Session):void + } + + export interface TransitionCallback { + (state:State, transition:Transition, message:TransitionMessage):void; + } + + export interface StateCallback { + (state:State, transition:Transition, message:TransitionMessage):void; + } + + class Session { + + consume( message : TransitionMessage, consumeCallback? : FSM.ConsumeCallback ); + addListener( sl : SessionListener ); + removeListener( sl : SessionListener ); + printStackTrace(); + addProperty( key:string, value:any ); + removeProperty( key:string ); + getProperty( key:string ) : any; + start( callback:ConsumeCallback ) : void; + } + + class GuardException { + + msg : string; + toString() : string; + } + + class SessionListener { + contextCreated( obj ); + contextDestroyed( obj ); + finalStateReached( obj ); + stateChanged( obj ); + customEvent( obj ); + guardPreCondition( obj ); + guardPostCondition( obj ); + } + + class State { + getName() : string; + isFinal() : boolean; + } + + class Transition { + getEvent() : string; + } + + export interface StateTransitionCallback { + ( session:Session, state:State, transition:Transition, message:TransitionMessage ) : void; + } +} + +declare module Automata { + + export function registerFSM( object:FSM.FSMDefinition ); + export function registerFDA( object:FSM.FSMDefinition ); + export function createSession( fda_name : string, controller : any ) : FSM.Session; + export function newGuardException( message : string ) : FSM.GuardException; + export function newSessionListener( obj : any ) : FSM.SessionListener; + +} \ No newline at end of file diff --git a/automata.js b/automata.js index 5b5abee..9354aa6 100644 --- a/automata.js +++ b/automata.js @@ -10,35 +10,147 @@ var TIMER_CHECK_RESOLUTION= 200; + /** * requireJS available ??? */ root.module = {}; + /** + * @callback ConsumeCallback + * @param session {FSM.Session} + */ /** - * @name TransitionCallback - * @type function + * @callback TransitionCallback * @param state {FSM.State} * @param transition {FSM.Transition} - * @param message {object} + * @param message {FSM.TransitionMessage} */ /** - * @name StateCallback - * @type function + * @callback StateCallback * @param state {FSM.State} * @param transition {FSM.Transition} - * @param message {object} + * @param message {FSM.TransitionMessage} + */ + + + /** + * @name FSM + * @namespace + * + * Local module definition. + */ + var FSM= {}; + + /** + * @typedef {{ message :FSM.TransitionMessage, callback : ConsumeCallback}} + */ + FSM.MessageCallbackTuple; + + /** + * @typedef {{ event : FSM.TransitionMessage, timeout : number}} + */ + FSM.StateTimeTransitionInfo; + + /** + * @typedef {{ msgId : string, data? : object }} + */ + FSM.TransitionMessage; + + /** + * @typedef {{ fda : string, controller? : Object }} + */ + FSM.SessionCreationData; + + /** + * @typedef {{ session : FSM.Session }} + */ + FSM.SessionFinalStateReachedEvent; + + /** + * @typedef {{ session : FSM.Session, context : FSM.SessionContext }} + */ + FSM.SessionContextEvent; + + /** + * @typedef {{ + * session : FSM.Session, + * context : FSM.SessionContext, + * prevState : FSM.State, + * state : FSM.State, + * message : FSM.TransitionMessage + * }} + */ + FSM.SessionStateChangeEvent; + + /** + * @typedef {{ + * session : FSM.Session, + * transition : FSM.Transition, + * message : FSM.TransitionMessage, + * exception : string, + * }} */ + FSM.TransitionGuardEvent; /** - * @name StateTimeTransitionInfo - * @type object - * @param event {object} - * @param timeout {number} + * @typedef {{ + * session : FSM.Session, + * data : Object, + * }} */ + FSM.CustomEvent; + /** + * @typedef {{ + * timeout : number, + * event : FSM.TransitionMessage + * }} + */ + var FSMDefinitionStateTimer; + + /** + * @typedef {{ + * name : string, + * initial : boolean=, + * onTimer : FSMDefinitionStateTimer=, + * onEnter : (string|StateCallback), + * onExit : (string|StateCallback) + * }} + */ + var FSMDefinitionState; + + /** + * @typedef {{ + * name : string, + * }} + */ + var FSMDefinitionSubState; + + /** + * @typedef {{ + * event : string, + * from : string, + * to : string, + * onTransition : (string|TransitionCallback), + * onPreGuard : (string|TransitionCallback), + * onPostGuard : (string|TransitionCallback) + * }} + */ + var FSMDefinitionTransition; + + /** + * @typedef {{ + * name : string, + * state : Array, + * transition : Array, + * onEnter : (string|StateCallback), + * onExit : (string|StateCallback) + * }} + */ + var FSMDefinition; /** * Regular extension mechanism. @@ -107,18 +219,10 @@ */ var fsmContext= null; - /** - * @name FSM - * @namespace - * - * Local module definition. - */ - var FSM= {}; - /** * @memberOf FSM * - * @class FSMTimerTask + * @class TimerTask * @classdesc * * This object encapsulates a task timer. @@ -126,7 +230,7 @@ * * @constructor * @param session a session object - * @param event a message object. + * @param event a message object. * @param time an integer specifying milliseconds. */ FSM.TimerTask= function( session, event, time ) { @@ -134,7 +238,7 @@ /** * Session to forward the event to on timeout. * @name session - * @memberOf FSM.TimerTask + * @memberOf TimerTask.prototype * @type {FSM.Session} */ this.session= session; @@ -143,15 +247,15 @@ * This event will be forwarded to the task session owner when timeout. * This is an event to be sent to a transition. * @name event - * @memberOf FSM.TimerTask - * @type {Object} + * @memberOf TimerTask.prototype + * @type {FSM.TransitionMessage} */ this.event= event; /** * Milliseconds to consider this task expired. * @name triggerTime - * @memberOf FSM.TimerTask + * @memberOf TimerTask.prototype * @type {number} */ this.triggerTime= time; @@ -160,7 +264,7 @@ * TimerTask id. * This id is returned whenever a timed-transition is set. Thus, timed events can be cancelled. * @name id - * @memberOf FSM.TimerTask + * @memberOf TimerTask.prototype * @type {number} */ this.id= __TimerIndex++; @@ -169,7 +273,7 @@ * Cache session's current state. When the task times-out, it is checked whether the session is still in the * same state. If so, the timeout event is sent. * @name contextState - * @memberOf FSM.TimerTask + * @memberOf TimerTask.prototype * @type {FSM.State} */ this.contextState= session.getCurrentState(); @@ -177,7 +281,7 @@ /** * Time when the timer task was created. More or less at scheduleTime + triggerTime the task times out. * @mame scheduleTime - * @memberOf FSM.TimerTask + * @memberOf TimerTask.prototype * @type {number} */ this.scheduleTime= new Date().getTime(); @@ -185,7 +289,7 @@ /** * Internal flag of timer task validity. * @name consumed - * @memberOf FSM.consumed + * @memberOf TimerTask.prototype * @type {boolean} */ this.consumed = false; @@ -264,7 +368,7 @@ /** * Array of pending timer tasks. * @name timerTask - * @memberOf FSM.FSMContext + * @memberOf FSMContext.prototype * @type {Array} */ this.timerTasks= []; @@ -274,7 +378,7 @@ * From each entry a FSM session object can be built. * * @name registry - * @memberOf FSM.FSMContext + * @memberOf FSMContext.prototype * @type {map} */ this.registry= {}; @@ -282,7 +386,7 @@ /** * This timer is used to check all the TimerTask timeouts. * @name timerId - * @memberOf FSM.FSMContext + * @memberOf FSMContext.prototype * @type {number} */ this.timerId= root.setInterval( this.__checkTimers.bind(this), TIMER_CHECK_RESOLUTION ); @@ -346,7 +450,6 @@ * Get a FSM.FSM registered instance. * * @param name {string} get a FSM.FSM previously registered object. - * @private */ getFSM : function( name ) { return this.registry[ name ]; @@ -354,18 +457,20 @@ /** * Create a given FSM session. - * @param fromFSM {string} a FSM name. Must be previously registered by calling registerFSM function. - * @param args {Array.<*>} an array of parameters passed from context.createSession() + * + * @param sessionData {FSM.SessionCreationData} + * * @return {FSM.Session} an initialized session object. */ - createSession : function( fromFSM, args ) { + createSession : function( sessionData ) { - var fsm= this.registry[ fromFSM ]; + var automata= sessionData.fda; + var fsm= this.registry[ automata ]; if ( typeof fsm==="undefined" ) { - throw "FSM "+fromFSM+" does not exist."; + throw "FSM "+automata+" does not exist."; } - return fsm.createSession(args); + return fsm.createSession(sessionData.controller); }, /** @@ -376,7 +481,7 @@ * Should not be called directly. * * @param session {FSM.Session} a session object - * @param event {object} a message object + * @param event {FSM.TransitionMessage} a message object * @param time {number} an integer indicating milliseconds. * * @return {number} a unique timertask id. @@ -415,30 +520,47 @@ * * An Automata specific exception raised when a guard fails. * - * @param msg {Object} - * @returns {FSM.GuardException} + * @param msg {string} + * + * @return {FSM.GuardException} * @constructor */ FSM.GuardException= function(msg) { - this.msg= msg; - this.toString= function() { - return this.msg.toString(); - }; + /** + * @name msg + * @memberOf GuardException.prototype + * @type {string} + */ + this.msg = msg; return this; }; + /** + * @lend FSM.GuardException.prototype + */ + FSM.GuardException.prototype= { + + toString : function() { + return this.msg.toString(); + } + }; + /** * @memberOf FSM * - * @class FSMTransition + * @class Transition * @classdesc * * An Automata framework transition. * This class is private and should not be used directly. * Any given Transition which belongs to a FSM object is a unique instance. * + * @param event {string} + * @param initialState {FSM.State} + * @param finalState {FSM.State} + * * @constructor */ FSM.Transition= function( event, initialState, finalState ) { @@ -446,24 +568,23 @@ /** * An string identifying an event this transition will be fired by. * @name event - * @memberOf FSM.Transition + * @memberOf Transition.prototype * @type string */ this.event= event; /** * Transition initial State. - * All transition but the 'inititial transition' have a initial state. * @name initialState - * @memberOf FSM.Transition - * @type FSM.State + * @memberOf FSM.Transition.prototype + * @type {FSM.State} */ this.initialState= initialState; /** * Transition final State. * @name finalState - * @memberOf FSM.Transition + * @memberOf Transition.prototype * @type FSM.State */ this.finalState= finalState; @@ -474,7 +595,7 @@ * If a function, it will be invoked. * * @name onTransition - * @memberOf FSM.Transition + * @memberOf Transition.prototype * @type {string|TransitionCallback} */ this.onTransition= null; @@ -485,7 +606,7 @@ * If a function, it will be invoked. * * @name onPreGuard - * @memberOf FSM.Transition + * @memberOf Transition.prototype * @type {string|TransitionCallback} */ this.onPreGuard= null; @@ -496,7 +617,7 @@ * If a function, it will be invoked. * * @name onPreGuard - * @memberOf FSM.Transition + * @memberOf Transition.prototype * @type {string|TransitionCallback} */ this.onPostGuard= null; @@ -522,7 +643,7 @@ }, /** - * Set this transition's pre guard function or function name form the logic object. + * Set this transition's pre guard function or function name form the controller object. * * @param m {TransitionCallback|string} */ @@ -533,14 +654,14 @@ /** * Create a GuardException. - * @param msg {object} + * @param msg {string} */ createThrowable : function( msg ) { throw new FSM.GuardException(msg); }, /** - * Set this transition's post guard function or function name form the logic object. + * Set this transition's post guard function or function name form the controller object. * * @param m {TransitionCallback|string} */ @@ -560,7 +681,7 @@ /** * Do this transition's pre-transition code - * @param msg {object} + * @param msg {FSM.TransitionMessage} * @param session {FSM.Session} */ firePreTransition : function( msg, session) { @@ -575,7 +696,7 @@ /** * Do this transition's post-transition code - * @param msg {object} + * @param msg {FSM.TransitionMessage} * @param session {FSM.Session} */ firePostTransition : function( msg, session) { @@ -586,7 +707,7 @@ * Do this transition's pre-transition code. Though it may seem equal to firePreTransition it is handled * in another function because an exception could be throws. In such case a pre-guard is assumed to have * been fired. - * @param msg {object} + * @param msg {FSM.TransitionMessage} * @param session {FSM.Session} */ firePreTransitionGuardedByPostCondition : function( msg, session ) { @@ -603,7 +724,7 @@ * Do this transition's post-transition code. Though it may seem equal to firePreTransition it is handled * in another function because an exception could be throws. In such case a pre-guard is assumed to have * been fired. - * @param msg {object} + * @param msg {FSM.TransitionMessage} * @param session {FSM.Session} */ firePostTransitionGuardedByPostCondition : function( msg, session ) { @@ -615,7 +736,7 @@ /** * Fire pre-Guard code. * If the method throws an exception, this transition is aborted as if it hadn't been fired. - * @param msg {object} + * @param msg {FSM.TransitionMessage} * @param session {FSM.Session} */ checkGuardPreCondition : function( msg, session ) { @@ -626,7 +747,7 @@ * Fire post-Guard code. * If the method throws an exception, this transition is vetoed, and it will issue an auto-transition instead * of a state-to-state transition. - * @param msg {object} + * @param msg {FSM.TransitionMessage} * @param session {FSM.Session} */ checkGuardPostCondition : function( msg, session ) { @@ -635,7 +756,8 @@ /** * Notify observers about this transition fire event. - * @param msg {object} the message which fired this transition + * + * @param msg {FSM.TransitionMessage} the message which fired this transition * @param session {FSM.Session} * * @private @@ -659,7 +781,7 @@ /** * @memberOf FSM - * @class FSMState + * @class State * @classdesc * * This object defines a FSM state. There's a finite number of states, and each session can only be in one such @@ -675,16 +797,16 @@ * Exiting transitions from this State. * * @type {map} - * @memberOf FSM.State + * @memberOf State.prototype * @name exitTransitions */ this.exitTransitions= {}; /** * Number of exit transitions. Needed to know which State is a final state (no exit transitions). - * @name FSM.State.exitTransitionsCount + * @name exitTransitionsCount * @type {number} - * @memberOf FSM.State + * @memberOf State.prototype */ this.exitTransitionsCount= 0; @@ -692,7 +814,7 @@ * State name. * * @type {string} - * @memberOf FSM.State + * @memberOf State.prototype * @name name */ this.name= name || ( "state"+__StateIndex++ ); @@ -701,7 +823,7 @@ * On State Enter action. * @type {string|StateCallback} * @name onEnter - * @memberOf FSM.State + * @memberOf State.prototype */ this.onEnter= null; @@ -709,15 +831,15 @@ * On State Exit action. * @type {string|StateCallback} * @name onEnter - * @memberOf FSM.State + * @memberOf State.prototype */ this.onExit= null; /** * Described a timed transition to this State. - * @type {StateTimeTransitionInfo} + * @type {FSM.StateTimeTransitionInfo} * @name onTimer - * @memberOf FSM.State + * @memberOf FSM.State.prototype */ this.onTimer= null; @@ -726,7 +848,7 @@ * * @type {FSM.FSM} * @name subState - * @memberOf FSM.State + * @memberOf State.prototype */ this.subState= null; @@ -794,7 +916,7 @@ /** * Add a timed transition to this state. - * @param c {StateTimeTransitionInfo} + * @param c {FSM.StateTimeTransitionInfo} */ setOnTimer : function( c ) { this.onTimer= c; @@ -802,9 +924,13 @@ /** * Get a transition for the defined typeof message. - * @param msg {string} + * @param msg {FSM.TransitionMessage} */ getTransitionFor : function( msg ) { + if (!msg || !msg.msgId ) { + // WTF ?? + return null; + } return this.exitTransitions[ msg.msgId ]; }, @@ -820,7 +946,7 @@ * It may seem to set a timer, and calling the optional onEnter callback function. * @param session {FSM.Session} * @param transition {FSM.Transition} - * @param msg {object} + * @param msg {FSM.TransitionMessage} */ callOnEnter : function( session, transition, msg ) { if ( this.onTimer ) { @@ -838,7 +964,7 @@ * * @param session {FSM.Session} * @param transition {FSM.Transition} - * @param msg {object} + * @param msg {FSM.TransitionMessage} */ callOnExit : function( session, transition, msg ) { if( this.onTimer ) { @@ -858,6 +984,8 @@ /** * @memberOf FSM * @class FSM + * @extends FSM.State + * * @classdesc * * FSM defines a complete finite state machine. @@ -869,37 +997,35 @@ * supplied as parameter. * * @constructor - * @param sessionObjectFactory {Function} object factory * @param name {string} FSM name * */ - FSM.FSM= function( sessionObjectFactory, name ) { + FSM.FSM= function( name ) { FSM.FSM.superclass.constructor.call(this, name); /** - * Session factory. - * - * @name sessionObjectFactory - * @memberOf FSM.FSM - * @type {Function} + * @name onEnter + * @type {string|StateCallback} + * @memberOf FSM.prototype */ - this.sessionObjectFactory= sessionObjectFactory; + this.onEnter= this.getName()+"_enter"; /** - * @type {string} - * @private + * @name onExit + * @type {string|StateCallback} + * @memberOf FSM.prototype */ - this._onEnter= this.name+"_enter"; + this.onExit= this.getName()+"_exit"; /** - * FSM initial transition. + * Defines the FDA's initial state. * - * @name initialTransition - * @type {FSM.Transition} - * @memberOf FSM.FSM + * @memberOf FSM.prototype + * @name initialState + * @type {FSM.State} */ - this.initialTransition= null; + this.initialState= null; return this; }; @@ -909,7 +1035,6 @@ */ FSM.FSM.prototype= { - /** * Initialize a Finite State Machine. * Create the initial transition to the supplied state. @@ -919,30 +1044,7 @@ * @param initialState {FSM.State} */ initialize : function( initialState ) { - - var me= this; - - FSM.FSM.superclass.setOnEnter.call( this, function( session, state, transition, msg ) { - me.initialTransition.fireTransition( { - msgId : __InitialTransitionId - }, - session ); - } ); - - this.initialState= initialState; - this.initialTransition= new FSM.Transition(__InitialTransitionId, null, initialState ); - this.initialTransition.setOnTransition( function( session, state, transition, msg ) { - session.push( initialState ); - }); - }, - - /** - * Set FSM on enter callback. - * @param m {string|StateCallback} - */ - setOnEnter : function( m ) { - this._onEnter= m; - return this; + this.initialState= initialState; }, /** @@ -951,11 +1053,14 @@ * * @param session {FSM.Session} * @param transition {FSM.Transition} - * @param msg {object} + * @param msg {FSM.TransitionMessage} */ callOnEnter : function( session, transition, msg ) { - session.callMethod( this._onEnter, this, transition, msg ); FSM.FSM.superclass.callOnEnter.call( this, session, transition, msg ); + session.consume( { + msgId : __InitialTransitionId + }); + }, /** @@ -974,17 +1079,24 @@ * this way for the sake of simplicity, but will probably change this semantics in the future, * (by adding an Automata with just one substate) which could cause backward incompatibilities. * - * @param args {object} session factory initialization parameters. + * @param sessionController {object} session factory initialization parameters. */ - createSession : function(args) { - - if ( !this.sessionObjectFactory ) { - return null; - } + createSession : function(sessionController ) { + return new FSM.Session(this, sessionController ); + }, - var session= new FSM.Session( new this.sessionObjectFactory(session, args) ); - session.push( this ); - this.callOnEnter( session, null, null ); + /** + * + * @param session {FSM.Session} + * @param callback {ConsumeCallback} + * @returns {FSM.Session} + */ + startSession : function( session, callback ) { + session.push(this); + FSM.FSM.superclass.callOnEnter.call( this, session, null, null ); + session.consume( { + msgId : __InitialTransitionId + }, callback); return session; } @@ -1014,7 +1126,7 @@ * * @name currentState * @type {FSM.State} - * @memberOf FSM.SessionContext + * @memberOf SessionContext.prototype */ this.currentState= state; @@ -1045,7 +1157,7 @@ /** * Get an exiting transition defined by this message for the current State. - * @param msg {object} + * @param msg {FSM.TransitionMessage} */ getTransitionFor : function( msg ) { return this.currentState.getTransitionFor( msg ); @@ -1055,7 +1167,7 @@ * Call this current State onExit callback function. * @param session {FSM.Session} * @param transition {FSM.Transition} - * @param msg {object} + * @param msg {FSM.TransitionMessage} */ exit : function( session, transition, msg) { this.currentState.callOnExit(session, transition, msg); @@ -1065,7 +1177,7 @@ * Print this context current state info. */ printStackTrace : function() { - FSM.Log.d(" "+this.currentState.name); + FSM.Log.d(" "+this.currentState.getName()); } @@ -1163,16 +1275,26 @@ * * @constructor * - * @param logic {object} an object coming from the FSM session factory object. + * @param fsm {FSM.FSM} a FDA. + * @param controller {object} an object coming from the FSM session factory object. */ - FSM.Session= function( logic ) { + FSM.Session= function( fsm, controller ) { + + /** + * FSM.FSM instance this sessio belongs to. + * @name _fda + * @memberOf FSM.Session.prototype + * @type {FSM.FSM} + * @private + */ + this._fda= fsm; /** * Each sub-state accessed during the FSM execution will generated a new context object. * This is the stack-trace of the different sub-states a FSM currently is in. * @type {Array.} * @name sessionContextList - * @memberOf FSM.Session + * @memberOf Session.prototype */ this.sessionContextList= []; @@ -1182,7 +1304,7 @@ * etc. * * @name sessionListeners - * @memberOf FSM.Session + * @memberOf Session.prototype * @type {Array.} */ this.sessionListener= []; @@ -1194,7 +1316,7 @@ * This is a general purpose map holder, use wisely. * * @name properties - * @memberOf FSM.Session + * @memberOf Session.prototype * @type {map} */ this.properties= {}; @@ -1202,53 +1324,191 @@ /** * Session data. An object created form the FSM factory constructor function. * - * @name logic - * @memberOf FSM.Session + * @name controller + * @memberOf Session.prototype * @type {object} an object returned from the FSM factory constructor. */ - this.logic= logic; + this.controller= controller; /** * When a message is sent to a session, that message consumtion may fire new messages sent to the session. * These messages are not consumed immediately. * * @name messages - * @memberOf FSM.Session - * @type {Array.} + * @memberOf Session.prototype + * @type {Array.} + */ + this.messageQueues = []; + + /** + * Internal flag used to signal that 'consume' calls are in the context of a 'callMethod'. + * + * @name _inCallMethod + * @memberOf Session.prototype + * @type {Array.} */ - this.messages = []; + this._inCallMethod = false; + + /** + * Internal flag for session state. + * @name _started + * @memberOf FSM.Session.prototype + * @type {boolean} + * @private + */ + this._started = false; return this; }; + /** + * @class SessionMessageQueue + * @memberOf FSM + * @classdesc + * + * This function creates objects to hold messages for a unit of work. + * The unit of work is a user-made 'consume' method, and all the transitions generated from this call. + * + * This is a FIFO queue. + * + * @param msg {FSM.TransitionMessage} + * @param callback {ConsumeCallback} + * @returns {FSM.SessionMessageQueue} + * + * @constructor + */ + FSM.SessionMessageQueue = function( msg, callback ) { + + /** + * @name _callback + * @type {ConsumeCallback} + * @memberOf SessionMessageQueue.prototype + * @private + */ + this._callback = callback; + + /** + * @name _messageQueue; + * @type {Array} + * @memberOf SessionMessageQueue.prototype + * @private + */ + this._messages = []; + + this.push( msg ); + + return this; + }; + + /** + * @lend FSM.SessionMessageQueue.prototype + */ + FSM.SessionMessageQueue.prototype = { + + /** + * + * @param message {FSM.TransitionMessage} a valid FSM message. can be null if called from the initial state context. + * @param callback {ConsumeCallback?} + */ + push : function( message, callback ) { + if ( message ) { + this._messages.push( { + message: message, + callback: callback + }); + } + }, + + /** + * Get the head of messages. + * @returns {FSM.MessageCallbackTuple} + */ + shift : function() { + return this._messages.shift(); + }, + + /** + * Get number of pending messages. + * @returns {Number} + */ + getNumMessages : function() { + return this._messages.length; + }, + + /** + * Notify this messages queue callback. + * This happens when the unit of work ends, ie the queue gets empty. + * + * @param session {FSM.Session} + */ + notify : function( session ) { + if ( this._callback ) { + this._callback( session ); + } + } + }; + + /** + * @lend FSM.Session.prototype + */ FSM.Session.prototype= { + /** + * Start a Session object. + * The session can be started only once. + * The reason to have a create and start functions, is that you can attach session listeners just after + * creation, and before it is started. Starting a session may imply state transitions. It is not reasonable + * to be able to attach observers after the inital transition executes and not before. + * + * @param callback {ConsumeCallback=} + */ + start : function( callback ) { + if ( this._started ) { + throw "Session is already started."; + } + + this._started= true; + + this._fda.startSession( this, callback ); + }, /** * Never call this method directly. - * For a given Automata event triggering (state.onEnter, state.onExit, transition.onPre/PostGuard, - * transition.onTransition), this method makes the appropriate call, either to the logic object, or to + * For a given Automata event triggering function (state.onEnter, state.onExit, transition.onPre/PostGuard, + * transition.onTransition), this method makes the appropriate call, either to the controller object, or to * the supplied callback function instead. + * This method also sets an internal flag (_inCallMethod) which indicates that `session.consume` calls happening + * inside a called method must not creat a message bucket, but queue messages in the current message bucket. */ callMethod : function( /* method, argument1, ... */ ) { - var args= Array.prototype.slice.call( arguments ); - var method= args.shift(); - if ( null===method ) { // just in case. + + var args = Array.prototype.slice.call(arguments); + var method = args.shift(); + + if (null === method) { // just in case. return; } - args.splice(0,0,this); + args.splice(0, 0, this); + + this._inCallMethod = true; - if ( typeof method==="function" ) { - method.apply( this.logic, args ); + if (typeof method === "function") { + method.apply(this.controller, args); } else { - if ( typeof this.logic[method]!=="undefined" ) { - this.logic[ method ].apply( this.logic, args ); - } else { - // no method with given name on session object data. + if ( this.controller ) { + + if (this.controller && typeof this.controller[method] !== "undefined") { + this.controller[method].apply(this.controller, args); + } else { + // no method with given name on session object data. + } + } } + + this._inCallMethod= false; }, /** @@ -1276,21 +1536,20 @@ * * @param state {FSM.State} * - * @private */ push : function( state ) { var sc= new FSM.SessionContext( state ); this.sessionContextList.push( sc ); this.fireContextCreated( sc ); - this.fireStateChanged( sc, state, __InitialTransitionId ); + this.fireStateChanged( sc, null, state, {msgId : __InitialTransitionId} ); }, /** * Pop and reset the last FSM.Context object level. * * @param transition {FSM.Transition} the firing transition - * @param msg {object} the message that triggered the transition + * @param msg {FSM.TransitionMessage} the message that triggered the transition * * @private */ @@ -1307,31 +1566,80 @@ /** * Asynchronously consume a message. - * @param msg {object} - * @param endCallback {function} + * @param msg {FSM.TransitionMessage} + * @param endCallback {ConsumeCallback?} */ consume : function( msg, endCallback ) { - this.messages.push( msg ); - if ( !this.transitioning ) { - this.__processMessages(endCallback); + + if ( msg.msgId===__InitialTransitionId ) { + this.push( this.getCurrentState().initialState ); + this.messageQueues.push( new FSM.SessionMessageQueue(null, endCallback) ); + this.getCurrentState().callOnEnter( this, null, msg ); + } else { + + // calling consume from a method call, not a user generated consume call. + if ( this._inCallMethod ) { + this.messageQueues[0].push( msg, endCallback ); + } else { + this.messageQueues.push( new FSM.SessionMessageQueue( msg, endCallback ) ); + } } + + this.__doConsume(); + }, + + __doConsume : function() { + setTimeout(this.__processMessages.bind(this), 0); + }, + + isEmpty : function() { + return this.sessionContextList.length===0 }, /** * Consume a message. * A message consumption may imply more messages to be consumed. The callback will be invoked * when no more messages are available to be processed. - * - * @param endCallback {function} a callback function fired when there're no pending messages to be processed. */ - __processMessages : function( endCallback ) { + __processMessages : function( ) { - if ( this.sessionContextList.length===0 ) { + if ( this.messageQueues.length===0 ) { + return; + } + + if ( this.isEmpty() ) { throw "Empty Session"; } - // remove first message - var msg= this.messages.shift(); + var queue= this.messageQueues[0]; + // trivial exit + if ( queue.getNumMessages()===0 ) { + + queue.notify( this ); + this.messageQueues.shift(); + if ( this.messageQueues.length>0 ) { + this.__doConsume(); + } + // sanity clear + this._inCallMethod= false; + return; + } + + /** + * remove first message + * @type FSM.MessageCallbackTuple + */ + var pair= queue.shift(); + + /** + * @type {FSM.TransitionMessage} + */ + var msg= pair.message; + + /** + * @type {ConsumeCallback} + */ + var callback= pair.callback; var firingTransition= null; // FSM.Transition var target= null; // FSM.SessionContext @@ -1345,7 +1653,12 @@ } if ( !firingTransition ) { - throw "No transition on state "+this.getCurrentState().name+" for message "+msg.msgId; + FSM.Log.e( "No transition on state "+this.getCurrentState().name+" for message "+msg.msgId ); + if ( callback ) { + callback(this); + } + this.__doConsume(); + return; } // check guard pre condition. @@ -1353,7 +1666,12 @@ firingTransition.checkGuardPreCondition( msg, this ); } catch( e ) { if ( e instanceof FSM.GuardException ) { + FSM.Log.i(e.toString()); this.fireGuardPreCondition(firingTransition, msg, e); + if ( callback ) { + callback(this); + } + this.__doConsume(); return; // fails on pre-guard. simply return. } else { FSM.Log.e("An error ocurred: "+ e.message); @@ -1361,8 +1679,6 @@ } } - this.transitioning= true; - try { firingTransition.checkGuardPostCondition( msg, this ); @@ -1373,9 +1689,10 @@ firingTransition.firePreTransition( msg, this ); + var currentState= this.getCurrentState(); var newState= firingTransition.finalState; target.setCurrentState( newState ); - this.fireStateChanged( target, newState, msg ); + this.fireStateChanged( target, currentState, newState, msg ); firingTransition.firePostTransition( msg, this ); @@ -1391,9 +1708,10 @@ } } catch( guardException ) { if ( guardException instanceof FSM.GuardException ) { + FSM.Log.i(guardException.toString()); this.fireGuardPostCondition(firingTransition, msg, guardException); firingTransition.firePreTransitionGuardedByPostCondition( msg, this ); - this.fireStateChanged( target, firingTransition.initialState, msg ); + this.fireStateChanged( target, this.getCurrentState(), firingTransition.initialState, msg ); firingTransition.firePostTransitionGuardedByPostCondition( msg, this ); } else { FSM.Log.e("An error ocurred: "+ guardException.toString()); @@ -1401,15 +1719,22 @@ } } + if ( callback ) { + callback(this); + } - if ( this.messages.length===0 ) { - this.transitioning = false; - if ( endCallback ) { - endCallback(); - } - } else { - // differ to next tick execution - setTimeout( this.consume.bind( this, endCallback ), 0 ); + if ( this.isEmpty() ) { + var sess= this; + // the session is empty. + // notify main callback only. + this.messageQueues.forEach( function(mq) { + mq.notify(sess); + }); + this.messageQueues= []; + } + + if ( this.messageQueues.length>0 ) { + this.__doConsume(); } }, @@ -1492,6 +1817,10 @@ } }, + /** + * + * @param sessionContext {FSM.SessionContext} + */ fireContextCreated : function( sessionContext ) { for( var i=0; i} + */ var states_a= fsmd.state; var states= {}; var initial_state= null; @@ -1638,6 +2029,10 @@ throw "No initial state defined."; } + /** + * + * @type {Array} + */ var transitions_a= fsmd.transition; for( i=0; i a FSM registered name. + * + * @param data {FSM.SessionCreationData} */ - function createSession( fsm ) { - var args= Array.prototype.slice.call(arguments); - args.shift(); - return fsmContext.createSession( fsm, args ); + function createSession( data ) { + return fsmContext.createSession( data ); } function guardException( str ) { diff --git a/changelog b/changelog index fac75c6..8c1ca3e 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,13 @@ +07-07-2015 *2.0.0* +------------------ + +* Version incompatible with any 1.x.x version. Read readme for details. +* Fully asynchronous execution. +* Added 'consume message' callback. +* Logic object does not belong to the automata defintion anymore. +* Logic object concept is deprecated in favor of session controller. +* Session lifecycle has been modified: create, start, end. + 07-02-2015 *1.1.1* ------------------ diff --git a/package.json b/package.json index 6109f66..e064d20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "automata", - "version": "1.1.1", + "version": "2.0.0", "main": "automata", "keywords": [ "DFA", diff --git a/readme.md b/readme.md index a51bfa0..92145c7 100644 --- a/readme.md +++ b/readme.md @@ -1,60 +1,80 @@ #Automata - A finite state machine framework. +Current state of automata is version 2.x.x, which is not backward compatible with 1.x.x. + ##Description -Automata is a formal finite state machine (FSM) framework. -Its aims at offering a totally decoupled management of logic and data storage. +Automata is a formal finite state machine (FDA) framework. +It aims at offering a totally decoupled management of logic and data storage. It features all the needed elements to have a modern and flexible finite state machine framework like -* FSM registry +* FDA registry * Timed transitions * Auto transition * Sub states * Guards -* FSM Session as message chroreographer +* FDA Session as message chroreographer +* Asynchronous execution ##How to -Automata works on browsers or Node. +Automata works on browsers or Node and has no dependencies. -To get it: +### To get it: * npm install automata +or * include automata.js script file Automata will then expose an object with some functions: ```javascript module.exports= { - registerFSM, // register a FSM object. + registerFSM, // register a FDA object. registerFDA, // same as registerFSM - createSession, // create a session for an FSM + createSession, // create a session for an FDA guardException, // create a guard exception newSessionListener // create a session listener overriding methods with the parameter object. } ``` +Or typescript definition: + +```typescript +declare module Automata { + + export function registerFSM( object:FSM.FSMDefinition ); + export function registerFDA( object:FSM.FSMDefinition ); + export function createSession( fda_name : string, controller : any ) : FSM.Session; + export function newGuardException( message : string ) : FSM.GuardException; + export function newSessionListener( obj : any ) : FSM.SessionListener; + +} +``` + ##How it works -In Automata, there will be a single instance of every FSM. Think of the FSM as the class or template to build an -automata. From this unique FSM, you can create an undefined amount of sessions. Each session will track the current - State, and the session data. -The Session and its data is created by supplying the FSM definition with a factory constructor function. - -First of all, one or more FSM must be registered in the system by calling either registerFSM ( +In Automata, FDA (finite deterministic automaton) are declaratively defined. It is contstrained to `FSMDefinition` +object. +The Automata definition will be unique, and different execution `Session` objects will be created from there. +Think of the FDA as the class, and the `Session` as the object. +For example, an FDA defines a Scrabble game. The sessions will be specific Scrabble games. Sessions keep track +of the current State as well as per-session Data associated with a session controller object. This controller is an +arbitrary object you supply at session creation time. + +So, first of all, one or more FDA must be registered in the system by calling either registerFSM ( register finite state machine) or registerFDA (register finite deterministic automaton). Both methods do the same, but - i prefer calling registerFDA. + I'd rather call `registerFDA`. In the FDA definition one State must be labeled as initial. This will be the entry point. -A minimal state machine could be: +An example minimal state machine could be: ```javascript fsmContext.registerFSM( { - // FSM registry name + // FDA registry name name : "Test", - logic : constructor_func, // States state : [ @@ -86,52 +106,108 @@ fsmContext.registerFSM( { } ); ``` -To start using this machine, a FSM session must be created from a registered FSM. For example: +Only one State must be labeled as `initial`. +To start using this machine, a `Session` must be created from a registered FDA. For example: ```javascript -var session= fsmContext.createSession("Test"); + +// ControllerObject is an object that holds per-session data and FDA's activity function callbacks. +// Will come to it later. +var session= fsmContext.createSession("Test", new SessionController() ); + +// The session must ultimately be started in order to track FDA's activity: +session.start(); + ``` To send notification events to a session object, call consume method: ```javascript -session.consume( { msgId: "12" } ); +session.consume( { msgId: "12" } ); ``` -By consuming a message in the FDA, new messages being dispatched to the session can be created. Each successive -message will be consumed in the next execution tick. This is why you can call: +This is the most basic workflow, but some things must be taken into account: + +### Why create a session then start ? + +Session creation may internally trigger state changes. +If you want to have a `SessionListener` object registered to track all these state changes, the `Session` lifecycle must +be spanned in two different stages. ```javascript -session.processMessage( {msgId: "12"}, function consumeEndCallback() { - // the session has no more pending messages to be consumed. - }); + +var session= fsmContext.createSession( ... ); +session.addListener( { + ... +} ); + +session.start( function(session) { + // session started +}); ``` -One important thing to note is that the FDA does not really know whether the queued-for-consumption messages come -from consuming a message or from external events. In either case, the **consumeEndCallback** won't be invoked until -the session's message queue is empty. +### Why a callback to `start` session or `consume` ? + +As we said before, a `Session` creation may internally trigger state changes. +For example, an FDA definition states that when entering its initial state, a message will be consumed which will fire +a transition from state 'initial' to another one. +Since Automata's execution is fully asynchronous, by the time the call to `start` or `consume` ends, you definitely +can't be sure whether the session ended starting or not. +The callback is guaranteed to be notified when `start` or `consume` methods and all internally triggered state changes +end. + +Another thing to note is that is will be fully safe to call `consume` right after ending a previous `consume` or +`start` method call. Automata treates user issued `consume` calls differently than `consume` calls triggered by a +state or transition action execution. + +### FDA messages + +The `consume` method accepts as a valid message any object which conforms to the typedef `FSM.TransitionMessage` +which has the following form: -This method accept as a valid message any object which contains a field called **msgId**. To trigger a transition, - any message object's msgId value must be the value defined in the **event** attribute present in the Transition +```json +{ + msgId : string, + data? : object +} +``` + +msgId's values must be the value defined in the **event** attribute present in the Transition FDA definition block. -A session accepts messages until it has reached a final State at its top level. From then and beyond, the session will +A session accepts messages until it has reached a final State. From then and beyond, the session will toss exceptions if it has a message sent for consumption. -##Logic object +### Session execution + +Until Automata V2, all session messages where synchronously consumed. +From V2, all messages are **asynchronously** consumed, which renders Automata V2 incompatible with Automata V1.X. +The synchronous consumption led to some unexpected problems like deep execution stack traces that could led to stackoverflow +errors. +In order to avoid execution callback errors, Automata V2 creates internal message queues. They work as follows: + +* for each user called `session.consume(callback)` method, a new message queue will be created. This queue will not be + executed until all the previous message queues (user issued session.consume calls) end processing their messages. +* for each framework called `session.consume(callback)` method, a new message will be added to the current message queue. +Framework consume calls happen in the controller object, when the FDA callbacks get executed. + +When a message queue gets empty, the callback gets called. + +## Controller object -The FSM logic and state are isolated. The developer supplies a custom object to the FSM via the **logic** value in the -FDA definition object. It must be a constructor function and will create a new object per **Session**. -The logic object will contain per session data, like for example the cards dealt in game, the authorization credentials, -or any other Session specific information. +The FDA logic and state are isolated. The developer supplies a custom FDA controller object when the `Session` is +created. +The `controller` object contains per session data, like for example the cards dealt in game, the authorization credentials, +or any other Session specific information. It also has callback functions for FDA specific hook points like +entering/exiting a `State` or executing a `Transition`. For both, State and Transitions, the calling **this** scope will be the logic object itself. -##Activy hooks +## Activy hooks Automata offers many activy hooks on its activity. The following hooks are available: -State and FSM: +State and FDA: * **onEnter**. Code fired on state enter. * **onExit**. Code fired on state exit. @@ -149,15 +225,15 @@ A natural transition flow of executed actions for a transition from StateA to St StateA.onExit() -> Transition.onTransition() -> StateB.onEnter() ``` -Those hooks are defined in the **FSM JSON** definition as in the example: +Those hooks are defined in the **FDA JSON** definition as in the example: -For example: ```javascript /** - * Define a logic constructor function. + * Define a session controller object. + * @constructor */ -function constructor_func() { +function controller() { this.count= 0; @@ -178,13 +254,12 @@ function constructor_func() { } /** - * Define a FSM + * Define a FDA */ fsmContext.registerFSM( { - // FSM registry name + // FDA registry name name : "Test", - logic : constructor_func, // States state : [ @@ -217,19 +292,22 @@ function constructor_func() { ] } ); - var session= fsmContext.createSession("Test"); - session.dispatch( { msgId: "AB" } ); - // this will print: - // Exit state A - // Transition fire code - // Enter state B + var session= fsmContext.createSession("Test", new controller()); + + session.start( function(session) { + session.dispatch( { msgId: "AB" } ); + // this will print: + // Exit state A + // Transition fire code + // Enter state B + }); + ``` - -The logic object can be notified automatically about Session changes in two different ways: +The controller object can be notified automatically about Session changes in two different ways: * Configuration: supply callback functions in the FDA definition object. -* Convention: the framework will automatically try to find methods in the logic object as follows: +* Convention: the framework will automatically try to find methods in the controller object as follows: * * State enter: state.getName() + "_enter" * * State exit: state.getName() + "_exit" @@ -285,7 +363,7 @@ endif pre/post-transition functions. A Guard is expected to throw a GuardException object by calling `transition.createThrowable` method or `module.newGuardException`. Those functions are optional, and must be set in the "transition" block of the - FSM definition as follows: + FDA definition as follows: ```javascript fsmContext.registerFSM( { @@ -345,15 +423,15 @@ Automata offers out of the box timed transitions by defining an **onTimer** bloc ``` This instruments the engine that after 2 seconds of entering this state, an event {msgId: "12"} will be sent to the -FSM session. The timer is handled automatically, and set/canceled on state enter/exit respectively. +FDA session. The timer is handled automatically, and set/canceled on state enter/exit respectively. The timers are checked every 200 milliseconds by the unique instance of FSMContext object. Thus, if you need to have less than 200ms timers, you may want to change TIMER_CHECK_RESOLUTION in the automata.js file. ##SubStates -Automata allows to nest as much as needed substates. In fact, by defining a single FSM, the engine stacks two levels, -one for the FSM, and the other, initially for the FSM's initial state. To define different levels, you must -register more than one FSM in the registry, and then reference one of them as a substate in the "state" section: +Automata allows to nest as much as needed substates. In fact, by defining a single FDA, the engine stacks two levels, +one for the FDA, and the other, initially for the FDA's initial state. To define different levels, you must +register more than one FDA in the registry, and then reference one of them as a substate in the "state" section: ```javascript fsmContext.registerFSM( { @@ -377,19 +455,19 @@ register more than one FSM in the registry, and then reference one of them as a } ); ``` -Then, the transition section will identify this FSM as a substate by its name, STest. A "subState" can't have a +Then, the transition section will identify this FDA as a substate by its name, STest. A "subState" can't have a regular name, nor onEnter/onExit functions. The name is the one of the FDA itself, and the activity hooks are overridden to do the stacking. The stacking of different subStates is done transparently, and they are handled by the "session" object. For each - stacked level, a FSM.Context object is created. A context object is just a holder for the current state for each + stacked level, a FDA.Context object is created. A context object is just a holder for the current state for each nesting level. ##Transition from Substates The way in which Automata manages state changes is made hierarchycally. That means, the engine will try to find a suitable transition for a given incoming message regardless of its nesting level. -So for any given FSM stacktrace, the engine will traverse upwards trying to find a suitable state to fire a +So for any given FDA stacktrace, the engine will traverse upwards trying to find a suitable state to fire a transition for the dispatched event. (Warning, offending ascii art. States between parenthesis, transitions between square brackets.) @@ -407,7 +485,7 @@ transition for the dispatched event. For example, given the previous example, ```javascript -session.dispatch( {msgId : "T_S1_S2" } ); +session.consume( {msgId : "T_S1_S2" } ); ``` means the session is on state SS1, and the stackTrace will be the following: @@ -416,7 +494,7 @@ ROOT, SUB_STATE, SS1 By calling ```javascript -session.dispatch( {msgId : "T_SS_S3" } ); +session.consume( {msgId : "T_SS_S3" } ); ``` on the session at state SS1, SS1 will be removed from the stack (since SS2 is a final state), and the session will @@ -425,13 +503,13 @@ Additionally, this session will be finished since S3 is a final State (this nest and so it is ROOT, which causes the session to be emptied. -##FSM listeners +##FDA listeners -Any FSM session activity can be monitored by adding a listener. +Any FDA session activity can be monitored by adding a listener. For example: ```javascript -session.addListener( new FSM.SessionListener() ); +session.addListener( new FDA.SessionListener() ); ``` or @@ -461,18 +539,13 @@ session.addListener( module.newSessionListener( { The obj parameter for each listener object function contains the following parameters: -* **contextCreated**: function( session, context ) -* **contextDestroyed**: function( session, context ) -* **finalStateReached**: function( session ) -* **stateChanged**: function( session, context, newState, message ) -* **customEvent**: function( session, message ) - -In all cases: - -* **session**: is the FSM created session. -* **context**: is an internal FSM object. A context is just a holder for the current state for each subState the system enters. -* **newState**: a FSM state object. -* **message**: a message object. The only constraint for these message objects is they must have a "msgId" field. +* **contextCreated**: FSM.SessionContextEvent +* **contextDestroyed**: FSM.SessionContextEvent +* **finalStateReached**: FSM.SessionFinalStateReachedEvent +* **stateChanged**: FSM.SessionStateChangeEvent +* **preGuard**: FSM.TransitionGuardEvent +* **postGuard**: FSM.TransitionGuardEvent +* **customEvent**: FSM.CustomEvent ##Custom events @@ -481,63 +554,79 @@ The preferred way for sending custom events will be by calling: session.fireCustomEvent( a_json_object ); ``` -and have a listener/observer object attached to the sending FSM session. +and have a listener/observer object attached to the sending FDA session. This method will be notified on the method ```javascript -customEvent : function( { session: session, customEvent: a_json_object } ) { +customEvent : function( ev : FSM.CustomEvent ) { ``` #Samples -##Sample 1 - Simple FSM +##Sample 1 - Simple FDA -This sample shows how to define common FSM session callback points. Either on logic object, or by defining a callback. +This sample shows how to define common FDA session callback points. Either on logic object, or by defining a callback. In either case, **this** is defined to be the session's logic object. ```javascript context= module.exports; -var Logic= function() { +var Controller= function() { - this.enter= function( session, state, transition, msg ) { - console.log("enter "+state.toString()); + this.a_enter= function( session, state, transition, msg ) { + console.log("a enter "+state.toString()); }; - this.exit= function( session, state, transition, msg ) { - console.log("exit "+state.toString()); + this.a_exit= function( session, state, transition, msg ) { + console.log("a exit "+state.toString()); }; - this.action= function( session, state, transition, msg ) { + this.b_enter= function( session, state, transition, msg ) { + console.log("b enter "+state.toString()); + }; + + this.b_exit= function( session, state, transition, msg ) { + console.log("b exit "+state.toString()); + }; + + this.c_exit= function( session, state, transition, msg ) { + console.log("c exit "+state.toString()); + }; + + this.ab_transition= function( session, state, transition, msg ) { console.log("transition: "+transition.toString()); }; + + this.bc_transition= function( session, state, transition, msg ) { + console.log("transition: "+transition.toString()); + }; + + this.Test1_enter= function( session, state, transition, msg ) { + console.log("test1 enter "+state.toString()); + }; + + this.Test1_exit= function( session, state, transition, msg ) { + console.log("test1 exit "+state.toString()); + }; }; context.registerFSM( { name : "Test1", - logic : Logic, state : [ { name : "a", - initial : true, - onEnter : "enter", - onExit : "exit" + initial : true }, { - name : "b", - onEnter : "enter", - onExit : "exit" + name : "b" }, { name : "c", onEnter : function( session, state, transition, msg ) { console.log("Enter c"); - }, - onExit : function( session, state, transition, msg ) { - console.log("Exit c"); } } ], @@ -546,38 +635,41 @@ context.registerFSM( { { event : "ab", from : "a", - to : "b", - onTransition: "action" + to : "b" }, { event : "bc", from : "b", - to : "c", - onTransition: "action" + to : "c" } ] } ); -var session= context.createSession("Test1"); -session.consume( { msgId: "ab" } ); +var session= context.createSession({ + fda: "Test1", + controller: new Controller() +} ); +session.start( function onStartProcessEnds(session) { + session.consume( { msgId: "ab" } ); + session.consume( { msgId: "bc" } ); + } +); -var session2= context.createSession("Test1"); -session2.consume( { msgId: "ab" } ); ``` -##Sample 2 - FSM with timed events +##Sample 2 - FDA with timed events -This sample show how to define a timed transition. +This sample show how to define a timed transition. Note this example has no FDA Controller. ```javascript context= module.exports; + context.registerFSM( { name : "Test2", - logic : function() { return this; }, state : [ { @@ -618,9 +710,16 @@ context.registerFSM( { ] } ); -var session1= context.createSession("Test2"); +var session1= context.createSession({ + fda: "Test2" +}); +session1.start(); -var session2= context.createSession("Test2"); +var session2= context.createSession({ + fda : "Test2" +} ); + +session2.start(); session2.consume( {msgId : "ab"} ); /* @@ -637,6 +736,8 @@ Enter b after 4 seconds from session1. */ +*/ + ``` ## Sample 3 - Guards @@ -644,7 +745,7 @@ after 4 seconds from session1. This sample shows how transition guards work on Automata. To fire a transition, first of all an optional **pre-guard** function is tested. If this function throws an exception, Automata interprets a veto on this transition fire. During pre-guard stage, a veto means transition disposal, so no auto-transition is performed. This is useful for example, in -a multiplayer game where while playing, a user abbadons the game and the game can continue playing. So instead of +a multiplayer game where while playing, a user abandons the game and the game can continue playing. So instead of transitioning from State-playing to State-EndGame, a guard can decide to veto the transition. By definition, a guard **should not** modify the model, in this case, a Logic object. @@ -660,14 +761,14 @@ If not, the transition continues its natural flow and transition's next state is context= module.exports; -var Logic= function() { + +var Controller= function() { this.count= 0; this.enter_b= function() { console.log("enter b"); - this.count++; - } + }; this.enter= function( session, state, transition, msg ) { console.log("enter "+state.toString()); @@ -705,7 +806,6 @@ var Logic= function() { context.registerFSM( { name : "Test3", - logic : Logic, state : [ { @@ -728,7 +828,7 @@ context.registerFSM( { name : "d", onEnter : "enter", onExit : "exit" - }, + } ], transition : [ @@ -755,21 +855,28 @@ context.registerFSM( { ] } ); -var session= context.createSession("Test3"); +var session= context.createSession({ + fda: "Test3", + controller: new Controller() +}); + +session.addListener( context.newSessionListener( { + finalStateReached : function( obj ) { + console.log("SessionListener finalStateReached " ); + }, + + /** + * + * @param obj {FSM.SessionStateChangeEvent} + */ + stateChanged : function( obj ) { + var ps= obj.prevState ? obj.prevState.getName() : "none"; + console.log("SessionListener stateChanged "+ps+" --> "+obj.state.getName() ); + } +} ) ); -session.addListener( - context.newSessionListener( { - contextCreated : function( obj ) { }, - contextDestroyed : function( obj ) { }, - finalStateReached : function( obj ) { - console.log("SessionListener finalStateReached"); - }, - stateChanged : function( obj ) { - console.log("SessionListener stateChanged"); - }, - customEvent : function( obj ) { } - } ) -); +// start session. +session.start(); console.log(""); console.log("Sent 'ab'"); @@ -803,12 +910,12 @@ session.consume( { msgId: "bc" } ); ## Sample 4 - SubStates -Sub States is an Automata feature which allows to nest different registered FSM as states of other FSM. -The mechanism is straightforward, just define a **substate** block in an FSM **state** definition block. -Automata will handle automatically all the nesting procedure, call the FSM action hooks and set the system's new +Sub States is an Automata feature which allows to nest different registered FDA as states of other FDA. +The mechanism is straightforward, just define a **substate** block in an FDA **state** definition block. +Automata will handle automatically all the nesting procedure, call the FDA action hooks and set the system's new current state. -A substate, or a FSM does not define neither onEnter nor onExit function callbacks. +A substate, or a FDA does not define neither onEnter nor onExit function callbacks. It is done as follows: @@ -816,7 +923,8 @@ It is done as follows: var context= module.exports; -var Logic= function() { + +var Controller= function() { this.enter= function( session, state, transition, msg ) { console.log("Enter "+state.toString()); @@ -837,8 +945,6 @@ var Logic= function() { context.registerFSM( { name : "SubStateTest", - // in a sub state FSM a Logic object constructor function is optional - state : [ { name : "1", @@ -869,7 +975,16 @@ context.registerFSM( { from : "2", to : "3" } - ] + ], + + onExit : function() { + console.log(" --> Exit sub-automata SubStateTest"); + }, + + onEnter : function() { + console.log(" --> Enter sub-automata SubStateTest"); + } + } ); // register another FSM model @@ -877,7 +992,6 @@ context.registerFSM( { context.registerFSM( { name : "Test4", - logic : Logic, state : [ { @@ -920,32 +1034,51 @@ context.registerFSM( { to : "c", onTransition: "transition" } - ] -} ); + ], -var session= context.createSession("Test4"); -session.consume( { msgId : "ab" } ); -session.consume( { msgId : "bc" }, function() { - - // The session is now in State-1 on STest FSM. - session.printStackTrace(); - - // The stack trace is: - // Test4 - // SubStateTest - // 1 -} ); + onExit : function() { + console.log(" --> Exit automata Test4"); + }, + + onEnter : function() { + console.log(" --> Enter automata Test4"); + } -session.consume( { msgId : "cd" }, function() { - - // Although neither State-1 on SubStateTest, nor SubStateTest have a transition to "cd", Automata's engine traverses - // current Session's stack trace upwards trying to find a suitable State with an exit transition to "cd". In this case, - // SubStateTest itself consumes the transition, meaning the last Session's context will be poped out and the control flow - // will be transitioning from SubStateTest to State-c. - - // After that call, the session will be empty, since State-c is final, and every context is poped out the session. - session.printStackTrace(); - - // prints: session empty. } ); + +var session= context.createSession({ + fda : "Test4", + controller : new Controller() +}); + +session.start( function(session) { + + session.consume({msgId: "ab"}); + session.consume({msgId: "bc"}, function () { + + // The session is now in State-1 on STest FSM. + session.printStackTrace(); + + // The stack trace is: + // Test4 + // SubStateTest + // 1 + + session.consume( { msgId : "cd" }, function() { + + // Although neither State-1 on SubStateTest, nor SubStateTest have a transition to "cd", Automata's engine traverses + // current Session's stack trace upwards trying to find a suitable State with an exit transition to "cd". In this case, + // SubStateTest itself consumes the transition, meaning the last Session's context will be poped out and the control flow + // will be transitioning from SubStateTest to State-c. + + // After that call, the session will be empty, since State-c is final, and every context is poped out the session. + session.printStackTrace(); + + // prints: session empty. + } ); + + }); + +}); + ``` diff --git a/test/test.html b/test/test.html index 29f2818..df8c4cd 100644 --- a/test/test.html +++ b/test/test.html @@ -11,15 +11,11 @@ return window.Automata; } - - - + + + \ No newline at end of file diff --git a/test/test1.js b/test/test1.js index 7e6804b..501b855 100644 --- a/test/test1.js +++ b/test/test1.js @@ -15,7 +15,7 @@ context= require("automata"); -var Logic= function() { +var Controller= function() { this.a_enter= function( session, state, transition, msg ) { console.log("a enter "+state.toString()); @@ -57,7 +57,6 @@ var Logic= function() { context.registerFSM( { name : "Test1", - logic : Logic, state : [ { @@ -89,7 +88,13 @@ context.registerFSM( { ] } ); -var session= context.createSession("Test1"); -session.consume( { msgId: "ab" } ); -session.consume( { msgId: "bc" } ); +var session= context.createSession({ + fda: "Test1", + controller: new Controller() +} ); +session.start( function onStartProcessEnds(session) { + session.consume( { msgId: "ab" } ); + session.consume( { msgId: "bc" } ); + } +); diff --git a/test/test2.js b/test/test2.js index e5b4fe0..a17e341 100644 --- a/test/test2.js +++ b/test/test2.js @@ -15,7 +15,6 @@ context= require("automata"); context.registerFSM( { name : "Test2", - logic : function() { return this; }, state : [ { @@ -56,9 +55,16 @@ context.registerFSM( { ] } ); -var session1= context.createSession("Test2"); +var session1= context.createSession({ + fda: "Test2" +}); +session1.start(); -var session2= context.createSession("Test2"); +var session2= context.createSession({ + fda : "Test2" +} ); + +session2.start(); session2.consume( {msgId : "ab"} ); /* diff --git a/test/test3.js b/test/test3.js index e8bd56f..feb68c7 100644 --- a/test/test3.js +++ b/test/test3.js @@ -26,13 +26,13 @@ context= require("automata"); -var Logic= function() { +var Controller= function() { this.count= 0; this.enter_b= function() { console.log("enter b"); - } + }; this.enter= function( session, state, transition, msg ) { console.log("enter "+state.toString()); @@ -70,7 +70,6 @@ var Logic= function() { context.registerFSM( { name : "Test3", - logic : Logic, state : [ { @@ -120,20 +119,29 @@ context.registerFSM( { ] } ); -var session= context.createSession("Test3"); +var session= context.createSession({ + fda: "Test3", + controller: new Controller() +}); session.addListener( context.newSessionListener( { - contextCreated : function( obj ) { }, - contextDestroyed : function( obj ) { }, finalStateReached : function( obj ) { - console.log("SessionListener finalStateReached"); + console.log("SessionListener finalStateReached " ); }, + + /** + * + * @param obj {FSM.SessionStateChangeEvent} + */ stateChanged : function( obj ) { - console.log("SessionListener stateChanged"); - }, - customEvent : function( obj ) { } + var ps= obj.prevState ? obj.prevState.getName() : "none"; + console.log("SessionListener stateChanged "+ps+" --> "+obj.state.getName() ); + } } ) ); +// start session. +session.start(); + console.log(""); console.log("Sent 'ab'"); session.consume( { msgId: "ab" } ); diff --git a/test/test4.js b/test/test4.js index d9e47f9..be32734 100644 --- a/test/test4.js +++ b/test/test4.js @@ -17,7 +17,7 @@ context= require("automata"); -var Logic= function() { +var Controller= function() { this.enter= function( session, state, transition, msg ) { console.log("Enter "+state.toString()); @@ -38,8 +38,6 @@ var Logic= function() { context.registerFSM( { name : "SubStateTest", - // in a sub state FSM a Logic object constructor function is optional - state : [ { name : "1", @@ -87,7 +85,6 @@ context.registerFSM( { context.registerFSM( { name : "Test4", - logic : Logic, state : [ { @@ -142,29 +139,38 @@ context.registerFSM( { } ); -var session= context.createSession("Test4"); -session.consume( { msgId : "ab" } ); -session.consume( { msgId : "bc" }, function() { +var session= context.createSession({ + fda : "Test4", + controller : new Controller() +}); - // The session is now in State-1 on STest FSM. - session.printStackTrace(); +session.start( function(session) { - // The stack trace is: - // Test4 - // SubStateTest - // 1 + session.consume({msgId: "ab"}); + session.consume({msgId: "bc"}, function () { -} ); + // The session is now in State-1 on STest FSM. + session.printStackTrace(); + + // The stack trace is: + // Test4 + // SubStateTest + // 1 + + session.consume( { msgId : "cd" }, function() { + + // Although neither State-1 on SubStateTest, nor SubStateTest have a transition to "cd", Automata's engine traverses + // current Session's stack trace upwards trying to find a suitable State with an exit transition to "cd". In this case, + // SubStateTest itself consumes the transition, meaning the last Session's context will be poped out and the control flow + // will be transitioning from SubStateTest to State-c. + + // After that call, the session will be empty, since State-c is final, and every context is poped out the session. + session.printStackTrace(); -session.consume( { msgId : "cd" }, function() { + // prints: session empty. + } ); - // Although neither State-1 on SubStateTest, nor SubStateTest have a transition to "cd", Automata's engine traverses - // current Session's stack trace upwards trying to find a suitable State with an exit transition to "cd". In this case, - // SubStateTest itself consumes the transition, meaning the last Session's context will be poped out and the control flow - // will be transitioning from SubStateTest to State-c. + }); - // After that call, the session will be empty, since State-c is final, and every context is poped out the session. - session.printStackTrace(); +}); - // prints: session empty. -} ); \ No newline at end of file