From 9269c0f229526e0df81c050e006ee6c214fed821 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 10 Jul 2013 16:51:50 -0700 Subject: [PATCH 1/9] Added ability to limit which states can transition to which states. --- src/fsm.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/fsm.js b/src/fsm.js index 5dbe762..a511554 100644 --- a/src/fsm.js +++ b/src/fsm.js @@ -67,7 +67,23 @@ _.extend( Fsm.prototype, { this.currentActionArgs = undefined; } }, - transition : function ( newState ) { + transitionIsAllowed: function(newState) { + var i, + allowedTransitions; + if (!this.state || !this.states[this.state].allowedTransitions) { return true; } + allowedTransitions = this.states[this.state].allowedTransitions; + for (i=0; i Date: Wed, 10 Jul 2013 17:01:14 -0700 Subject: [PATCH 2/9] changed spaces to tabs fro lint --- lib/machina.js | 19 ++++++++++++++++++- lib/machina.min.js | 2 +- src/fsm.js | 32 ++++++++++++++++---------------- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/lib/machina.js b/lib/machina.js index cda583d..1c2e063 100644 --- a/lib/machina.js +++ b/lib/machina.js @@ -169,7 +169,23 @@ this.currentActionArgs = undefined; } }, + transitionIsAllowed: function(newState) { + var i, + allowedTransitions; + if (!this.state || !this.states[this.state].allowedTransitions) { return true; } + allowedTransitions = this.states[this.state].allowedTransitions; + for (i=0; ie;++e)if(t===i[e])return!0;return!1},transition:function(t){if(!this.transitionIsAllowed.call(this,t))return this.emit.call(this,c,{state:this.state,attemptedState:t}),!1;if(!this.inExitHandler&&t!==this.state){var e;return this.states[t]?(this.targetReplayState=t,this.priorState=this.state,this.state=t,e=this.priorState,this.states[e]&&this.states[e]._onExit&&(this.inExitHandler=!0,this.states[e]._onExit.call(this),this.inExitHandler=!1),this.emit.call(this,h,{fromState:e,action:this._currentAction,toState:t}),this.states[t]._onEnter&&this.states[t]._onEnter.call(this),this.targetReplayState===t&&this.processQueue(s),!0):(this.emit.call(this,c,{state:this.state,attemptedState:t}),!1)}},processQueue:function(e){var i=e===s?function(t){return t.type===s&&(!t.untilState||t.untilState===this.state)}:function(t){return t.type===r},n=t.filter(this.eventQueue,i,this);this.eventQueue=t.difference(this.eventQueue,n),t.each(n,function(t){this.handle.apply(this,t.args)},this)},clearQueue:function(e,i){if(e){var n;e===s?n=function(t){return t.type===s&&(i?t.untilState===i:!0)}:e===r&&(n=function(t){return t.type===r}),this.eventQueue=t.filter(this.eventQueue,n)}else this.eventQueue=[]},deferUntilTransition:function(t){if(this.currentActionArgs){var e={type:s,untilState:t,args:this.currentActionArgs};this.eventQueue.push(e),this.emit.call(this,l,{state:this.state,queuedArgs:e})}},deferUntilNextHandler:function(){if(this.currentActionArgs){var t={type:s,args:this.currentActionArgs};this.eventQueue.push(t),this.emit.call(this,l,{state:this.state,queuedArgs:t})}},on:function(t,e){var i=this;return i.eventListeners[t]||(i.eventListeners[t]=[]),i.eventListeners[t].push(e),{eventName:t,callback:e,off:function(){i.off(t,e)}}},off:function(e,i){e?this.eventListeners[e]&&(this.eventListeners[e]=i?t.without(this.eventListeners[e],i):[]):this.eventListeners={}}}),y.prototype.trigger=y.prototype.emit;var A=function(){},x=function(e,i,n){var s;return s=i&&i.hasOwnProperty("constructor")?i.constructor:function(){e.apply(this,arguments)},t.deepExtend(s,e),A.prototype=e.prototype,s.prototype=new A,i&&t.deepExtend(s.prototype,i),n&&t.deepExtend(s,n),s.prototype.constructor=s,s.__super__=e.prototype,s};y.extend=function(t,e){var i=x(this,t,e);return i.extend=this.extend,i};var L={Fsm:y,utils:p,on:function(t,e){return this.eventListeners[t]||(this.eventListeners[t]=[]),this.eventListeners[t].push(e),e},off:function(e,i){this.eventListeners[e]&&(this.eventListeners[e]=t.without(this.eventListeners[e],i))},trigger:function(e){var i=arguments,s=this.eventListeners[e]||[];s&&s.length&&t.each(s,function(t){t.apply(null,n.call(i,1))})},eventListeners:{newFsm:[]}};return L.emit=L.trigger,L}); \ No newline at end of file diff --git a/src/fsm.js b/src/fsm.js index a511554..121d764 100644 --- a/src/fsm.js +++ b/src/fsm.js @@ -68,22 +68,22 @@ _.extend( Fsm.prototype, { } }, transitionIsAllowed: function(newState) { - var i, - allowedTransitions; - if (!this.state || !this.states[this.state].allowedTransitions) { return true; } - allowedTransitions = this.states[this.state].allowedTransitions; - for (i=0; i Date: Thu, 11 Jul 2013 08:05:33 -0700 Subject: [PATCH 3/9] Adding tests for allowedTransitions --- spec/machina.fsm.spec.js | 75 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/spec/machina.fsm.spec.js b/spec/machina.fsm.spec.js index c1fee9c..7967a10 100644 --- a/spec/machina.fsm.spec.js +++ b/spec/machina.fsm.spec.js @@ -943,4 +943,79 @@ describe( "machina.Fsm", function () { } ); } ); } ); + + describe( "When providing allowedTransitions", function(){ + var invalidstateTriggered = false, + SomeFsm = machina.Fsm.extend( { + initialState : "notStarted", + states : { + "notStarted" : { + start : function () { + this.transition( "started" ); + }, + allowedTransitions: [ + "started" + ] + }, + "started" : { + finish : function () { + this.transition( "finished" ); + }, + allowedTransitions: [ + "finished" + ] + }, + "finished" : { + _onEnter : function () { + + }, + allowedTransitions: [ + // Final state + ] + } + }, + eventListeners: { + invalidstate: [ + function() { + invalidstateTriggered = true; + } + ] + } + }), + someFsm; + + beforeEach(function() { + someFsm = new SomeFsm(); + invalidstateTriggered = false; + }); + + it( " should not transition to disallowed states", function() { + expect(someFsm.state).to.be("notStarted"); + someFsm.transition("finished"); + expect(someFsm.state).to.be("notStarted"); + }); + + it( " should transition to allowed states", function() { + expect(someFsm.state).to.be("notStarted"); + someFsm.transition("started"); + expect(someFsm.state).to.be("started"); + }); + + it( " should not be able to transition out of a final state", function() { + someFsm.transition("started"); + someFsm.transition("finished"); + expect(someFsm.state).to.be("finished"); + someFsm.transition("started"); + expect(someFsm.state).to.be("finished"); + someFsm.transition("notStarted"); + expect(someFsm.state).to.be("finished"); + }); + + it( " should trigger invalidstate when trying to transition to disallowed state", function() { + expect(someFsm.state).to.be("notStarted"); + expect(invalidstateTriggered).to.be(false); + someFsm.transition("finished"); + expect(invalidstateTriggered).to.be(true); + }); + }) } ); From c1a10faa852c065a4336bfaea3c61224658d05cc Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 11 Jul 2013 09:34:50 -0700 Subject: [PATCH 4/9] Added test for when no allowedStates are listed --- spec/machina.fsm.spec.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/spec/machina.fsm.spec.js b/spec/machina.fsm.spec.js index 7967a10..ea61b6e 100644 --- a/spec/machina.fsm.spec.js +++ b/spec/machina.fsm.spec.js @@ -954,7 +954,8 @@ describe( "machina.Fsm", function () { this.transition( "started" ); }, allowedTransitions: [ - "started" + "started", + "allStates" ] }, "started" : { @@ -972,6 +973,11 @@ describe( "machina.Fsm", function () { allowedTransitions: [ // Final state ] + }, + "allStates" :{ + _onEnter : function (){ + + } } }, eventListeners: { @@ -1001,6 +1007,15 @@ describe( "machina.Fsm", function () { expect(someFsm.state).to.be("started"); }); + it ( " should transition to any state if allowed states isnt specified" , function (){ + expect(someFsm.state).to.be("notStarted"); + someFsm.transition("allStates"); + expect(someFsm.state).to.be("allStates"); + someFsm.transition("finished"); + expect(someFsm.state).to.be("finished"); + + }); + it( " should not be able to transition out of a final state", function() { someFsm.transition("started"); someFsm.transition("finished"); From ee3527f8c5f314140884686f9a35b78561a966ba Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 11 Jul 2013 09:45:05 -0700 Subject: [PATCH 5/9] Updated readme --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 9421cf8..528cfd1 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,9 @@ var storageFsm = new machina.Fsm({ _onEnter: function() { this.handle("sync.customer"); }, + allowedTransitions: [ + "offline" + ] "save.customer" : function( payload ) { if( verifyState() ) { @@ -83,6 +86,8 @@ In the above example, the developer has created an FSM with two possible states: In addition to the state/handler definitions, the above code example as shows that this particular FSM will start in the `offline` state, and can generate a `CustomerSyncComplete` custom event. +AllowedTransitions is an array of states that a state can transition to. If you don't specify allowedTransitions then a state can transition to any state. fsm.transition will check if the transition is allowed. If it is not allowed INVALIDSTATE event is fired. + The `verifyState` and `applicationOffline` methods are custom to this instance of the FSM, and are not, of course, part of machina by default. You can see in the above example that anytime the FSM handles an event, it first checks to see if the state needs to be transitioned between offline and online (via the `verifyState` call). States can also have `_onEnter` and `_onExit` methods. `_onEnter` is fired immediately after the FSM transitions into that state and `_onExit` is fired immediately before transitioning to a new state. @@ -104,6 +109,13 @@ eventListeners: { } ``` +`Events` - MyEvent1 could be called in an onEnter by calling `.emit("MyEvent", {data})`. Other events + `handled` - Fired after a .handle() has been completed + `nohandler` - Fired after a .handle() has not found the message on the state you are currently in + `handling` - Fired when a .handle() begins. + `transition` - Fired when a state has called _onExit but we haven't called _onEnter of the desired state. + `invalidstate` - Fired when a .transition() is called to a state that doesn't exist or is not allowed. + `states` - an object detailing the possible states the FSM can be in, plus the kinds of events/messages each state can handle. States can have normal "handlers" as well as a catch-all handler ("*"), an `_onEnter` handler invoked when the FSM has transitioned into that state and an `_onExit` handler invoked when transitioning out of that state. ```javascript From 4a1b98f76cd894afab2c3d077f575f450750106f Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 11 Jul 2013 09:45:56 -0700 Subject: [PATCH 6/9] Updated readme comma I missed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 528cfd1..267f970 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ var storageFsm = new machina.Fsm({ }, allowedTransitions: [ "offline" - ] + ], "save.customer" : function( payload ) { if( verifyState() ) { From 2af4c89b2a125e3aac7c36750b9048c482a24c84 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 11 Jul 2013 09:47:30 -0700 Subject: [PATCH 7/9] Updated readme to space out event listeners --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 267f970..02c35a0 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ In the above example, the developer has created an FSM with two possible states: In addition to the state/handler definitions, the above code example as shows that this particular FSM will start in the `offline` state, and can generate a `CustomerSyncComplete` custom event. -AllowedTransitions is an array of states that a state can transition to. If you don't specify allowedTransitions then a state can transition to any state. fsm.transition will check if the transition is allowed. If it is not allowed INVALIDSTATE event is fired. +allowedTransitions is an array of states that a state can transition to. If you don't specify allowedTransitions then a state can transition to any state. fsm.transition will check if the transition is allowed. If it is not allowed INVALIDSTATE event is fired. The `verifyState` and `applicationOffline` methods are custom to this instance of the FSM, and are not, of course, part of machina by default. @@ -110,10 +110,15 @@ eventListeners: { ``` `Events` - MyEvent1 could be called in an onEnter by calling `.emit("MyEvent", {data})`. Other events - `handled` - Fired after a .handle() has been completed + +`handled` - Fired after a .handle() has been completed + `nohandler` - Fired after a .handle() has not found the message on the state you are currently in + `handling` - Fired when a .handle() begins. + `transition` - Fired when a state has called _onExit but we haven't called _onEnter of the desired state. + `invalidstate` - Fired when a .transition() is called to a state that doesn't exist or is not allowed. `states` - an object detailing the possible states the FSM can be in, plus the kinds of events/messages each state can handle. States can have normal "handlers" as well as a catch-all handler ("*"), an `_onEnter` handler invoked when the FSM has transitioned into that state and an `_onExit` handler invoked when transitioning out of that state. From 51774e81028f078a2588642198ddc6a9425f933f Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 4 Sep 2013 11:16:16 -0700 Subject: [PATCH 8/9] Added jQuery deferreds to handle and transition. Also made them private methods --- src/fsm.js | 143 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 80 insertions(+), 63 deletions(-) diff --git a/src/fsm.js b/src/fsm.js index 121d764..80cf4a5 100644 --- a/src/fsm.js +++ b/src/fsm.js @@ -7,7 +7,76 @@ var Fsm = function ( options ) { this.transition( this.initialState ); } }; - +function _transition (args, deferred){ + var newState = slice.call(args,0,1)[0]; + if (!this.transitionIsAllowed.call(this, newState)) { + this.emit.call( this, INVALID_STATE, { state: this.state, attemptedState: newState } ); + deferred.reject(); + return false; + } + if ( !this.inExitHandler && newState !== this.state) { + var oldState; + if ( this.states[newState] ) { + this.targetReplayState = newState; + this.priorState = this.state; + this.state = newState; + oldState = this.priorState; + if ( this.states[oldState] && this.states[oldState]._onExit ) { + this.inExitHandler = true; + this.states[oldState]._onExit.call( this ); + this.inExitHandler = false; + } + if(this.initialState != newState){ + this.emit.call( this, TRANSITION, { fromState: oldState, action: this._currentAction, toState: newState } ); + } + if ( this.states[newState]._onEnter ) { + this.states[newState]._onEnter.call( this ); + } + deferred.resolve(); + if ( this.targetReplayState === newState ) { + this.processQueue( NEXT_TRANSITION ); + } + return true; + } + this.emit.call( this, INVALID_STATE, { state: this.state, attemptedState: newState } ); + deferred.reject(); + return false; + } +} +function _handle(args, deferred) { + if ( !this.inExitHandler ) { + var states = this.states, current = this.state, inputType = args[0], args = slice.call(args,0), handlerName, handler, catchAll, action; + this.currentActionArgs = args; + if ( states[current][inputType] || states[current]["*"] || this[ "*" ] ) { + handlerName = states[current][inputType] ? inputType : "*"; + catchAll = handlerName === "*"; + if ( states[current][handlerName] ) { + handler = states[current][handlerName]; + action = current + "." + handlerName; + } else { + handler = this[ "*" ]; + action = "*"; + } + if ( ! this._currentAction ) + this._currentAction = action ; + this.emit.call( this, HANDLING, { inputType: inputType, args: args.slice(1) } ); + if (_.isFunction(handler)) + handler = handler.apply( this, catchAll ? args : args.slice(1) ); + if (_.isString(handler)) + this.transition( handler ) ; + this.emit.call( this, HANDLED, { inputType: inputType, args: args.slice(1) } ); + deferred.resolve(); + this._priorAction = this._currentAction; + this._currentAction = ""; + this.processQueue( NEXT_HANDLER ); + } + else { + this.emit.call( this, NO_HANDLER, { inputType: inputType, args: args.slice(1) } ); + deferred.reject(); + } + this.currentActionArgs = undefined; + } +} _.extend( Fsm.prototype, { initialize: function() { }, emit : function ( eventName ) { @@ -35,38 +104,11 @@ _.extend( Fsm.prototype, { }, this ); } }, - handle : function ( inputType ) { - if ( !this.inExitHandler ) { - var states = this.states, current = this.state, args = slice.call( arguments, 0 ), handlerName, handler, catchAll, action; - this.currentActionArgs = args; - if ( states[current][inputType] || states[current]["*"] || this[ "*" ] ) { - handlerName = states[current][inputType] ? inputType : "*"; - catchAll = handlerName === "*"; - if ( states[current][handlerName] ) { - handler = states[current][handlerName]; - action = current + "." + handlerName; - } else { - handler = this[ "*" ]; - action = "*"; - } - if ( ! this._currentAction ) - this._currentAction = action ; - this.emit.call( this, HANDLING, { inputType: inputType, args: args.slice(1) } ); - if (_.isFunction(handler)) - handler = handler.apply( this, catchAll ? args : args.slice( 1 ) ); - if (_.isString(handler)) - this.transition( handler ) ; - this.emit.call( this, HANDLED, { inputType: inputType, args: args.slice(1) } ); - this._priorAction = this._currentAction; - this._currentAction = ""; - this.processQueue( NEXT_HANDLER ); - } - else { - this.emit.call( this, NO_HANDLER, { inputType: inputType, args: args.slice(1) } ); - } - this.currentActionArgs = undefined; - } - }, + handle : function () { + var deferred = $.Deferred(); + _handle.call(this, arguments, deferred); + return deferred.promise(); + }, transitionIsAllowed: function(newState) { var i, allowedTransitions; @@ -79,36 +121,11 @@ _.extend( Fsm.prototype, { } return false; }, - transition : function ( newState ) { - if (!this.transitionIsAllowed.call(this, newState)) { - this.emit.call( this, INVALID_STATE, { state: this.state, attemptedState: newState } ); - return false; - } - if ( !this.inExitHandler && newState !== this.state ) { - var oldState; - if ( this.states[newState] ) { - this.targetReplayState = newState; - this.priorState = this.state; - this.state = newState; - oldState = this.priorState; - if ( this.states[oldState] && this.states[oldState]._onExit ) { - this.inExitHandler = true; - this.states[oldState]._onExit.call( this ); - this.inExitHandler = false; - } - this.emit.call( this, TRANSITION, { fromState: oldState, action: this._currentAction, toState: newState } ); - if ( this.states[newState]._onEnter ) { - this.states[newState]._onEnter.call( this ); - } - if ( this.targetReplayState === newState ) { - this.processQueue( NEXT_TRANSITION ); - } - return true; - } - this.emit.call( this, INVALID_STATE, { state: this.state, attemptedState: newState } ); - return false; - } - }, + transition : function () { + var deferred = $.Deferred(); + _transition.call(this, arguments, deferred); + return deferred.promise(); + }, processQueue : function ( type ) { var filterFn = type === NEXT_TRANSITION ? function ( item ) { return item.type === NEXT_TRANSITION && ((!item.untilState) || (item.untilState === this.state)); From fec57d5487f846ba97ae46b5833e8665c80e23e5 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 4 Sep 2013 11:23:03 -0700 Subject: [PATCH 9/9] readme updates --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 02c35a0..98754a2 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,24 @@ var childFsm = new ChildFsm(); ``` +## jQuery Deferreds +I added in jQuery deferreds because I like using .done() and .fail(). Handle() and transition() now return a deferred obj. +Example +```javascript +stateMachine.handle("fooBar") + .done(function(){ + //do something like + stateMachine.transition("nextState") + .done(function(){ + // now we are done with the transition + }) + }) + .fail(function(){ + // Oh no we fails. Probably in the wrong state + )} + +``` + ## The machina.Fsm Prototype Each instance of an machina FSM has the following methods available via it's prototype: