Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added state transition limitations #22

Closed
wants to merge 10 commits into from
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ var storageFsm = new machina.Fsm({
_onEnter: function() {
this.handle("sync.customer");
},
allowedTransitions: [
"offline"
],

"save.customer" : function( payload ) {
if( verifyState() ) {
Expand Down Expand Up @@ -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.
Expand All @@ -104,6 +109,18 @@ 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
Expand Down Expand Up @@ -192,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:

Expand Down
19 changes: 18 additions & 1 deletion lib/machina.js
Original file line number Diff line number Diff line change
Expand Up @@ -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; i<allowedTransitions.length; ++i) {
if (newState === allowedTransitions[i]) {
return true;
}
}
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] ) {
Expand All @@ -189,9 +205,10 @@
if ( this.targetReplayState === newState ) {
this.processQueue( NEXT_TRANSITION );
}
return;
return true;
}
this.emit.call( this, INVALID_STATE, { state: this.state, attemptedState: newState } );
return false;
}
},
processQueue : function ( type ) {
Expand Down
2 changes: 1 addition & 1 deletion lib/machina.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

90 changes: 90 additions & 0 deletions spec/machina.fsm.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -943,4 +943,94 @@ 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",
"allStates"
]
},
"started" : {
finish : function () {
this.transition( "finished" );
},
allowedTransitions: [
"finished"
]
},
"finished" : {
_onEnter : function () {

},
allowedTransitions: [
// Final state
]
},
"allStates" :{
_onEnter : function (){

}
}
},
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 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");
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);
});
})
} );
Loading