Permalink
Browse files

ensure `can()` returns false during asynchronous state transitions

  • Loading branch information...
1 parent 9d71f62 commit 5462141f5b85df041d12cb8da6ca8f077562105b @jakesgordon committed Aug 19, 2011
Showing with 91 additions and 30 deletions.
  1. +14 −9 README.md
  2. +1 −1 demo/demo.css
  3. +3 −3 index.html
  4. +12 −16 state-machine.js
  5. +61 −1 test/test_async.js
View
23 README.md
@@ -130,10 +130,10 @@ Callbacks
4 callbacks are available if your state machine has methods using the following naming conventions:
- * onbefore**event** - fired before an event
- * onafter**event** - fired after an event
- * onenter**state** - fired when entering a state
- * onleave**state** - fired when leaving a state
+ * onbefore**event** - fired before the event
+ * onleave**state** - fired when leaving the old state
+ * onenter**state** - fired when entering the new state
+ * onafter**event** - fired after the event
For convenience, the 2 most useful callbacks can be shortened:
@@ -179,6 +179,16 @@ Additionally, they can be added and removed from the state machine at any time:
fsm.onred = null;
fsm.onchangestate = function(event, from, to) { document.body.className = to; };
+**NOTES:**
+
+ * If you return `false` from an `onbeforeevent` handler then you can cancel the event.
+ * If you return `false` from an `onleavestate` handler then you can perform an asynchronous state transition (see next section)
+
+Asynchronous State Transitions
+==============================
+
+ * **TODO**
+
State Machine Classes
=====================
@@ -213,11 +223,6 @@ instances:
This should be easy to adjust to fit your appropriate mechanism for object construction.
-Asynchronous State Transitions
-==============================
-
- * **TODO**
-
Initialization Options
======================
View
2 demo/demo.css
@@ -1,4 +1,4 @@
-#demo { width: 400px; margin: 0 auto; }
+#demo { width: 400px; margin: 0 auto; text-align: center; }
#controls { text-align: center; }
View
6 index.html
@@ -13,8 +13,8 @@
<h1> Finite State Machine </h1>
<div id="controls">
- <button id="clear" onclick="Demo.clear();">all clear</button>
- <button id="calm" onclick="Demo.calm();">calm down</button>
+ <button id="clear" onclick="Demo.clear();">clear</button>
+ <button id="calm" onclick="Demo.calm();">calm</button>
<button id="warn" onclick="Demo.warn();">warn</button>
<button id="panic" onclick="Demo.panic();">panic!</button>
</div>
@@ -23,7 +23,7 @@
</div>
<div id="notes">
- <i>dashed lines are async events that take 3 seconds</i>
+ <i>dashed lines are asynchronous state transitions (3 seconds)</i>
</div>
<textarea id="output">
View
28 state-machine.js
@@ -18,7 +18,7 @@ StateMachine = {
var from = (e.from instanceof Array) ? e.from : [e.from];
map[e.name] = map[e.name] || {};
for (var n = 0 ; n < from.length ; n++)
- map[e.name][from[n]] = e;
+ map[e.name][from[n]] = e.to;
};
if (initial) {
@@ -41,7 +41,7 @@ StateMachine = {
fsm.current = 'none';
fsm.is = function(state) { return this.current == state; };
- fsm.can = function(event) { return !!map[event][this.current]; };
+ fsm.can = function(event) { return !!map[event][this.current] && !this.transition; };
fsm.cannot = function(event) { return !this.can(event); };
if (initial && !initial.defer)
@@ -83,36 +83,32 @@ StateMachine = {
return func.apply(this, [name, from, to].concat(args));
},
- transition: function(name, from, to, args) {
- this.current = to;
- StateMachine.enterState.call(this, name, from, to, args);
- StateMachine.changeState.call(this, name, from, to, args);
- StateMachine.afterEvent.call(this, name, from, to, args);
- },
-
buildEvent: function(name, map) {
return function() {
if (this.transition)
- throw "event " + name + " innapropriate because previous transition (" + this.transition.event + ") from " + this.transition.from + " to " + this.transition.to + " did not complete"
+ throw "event " + name + " innapropriate because previous transition did not complete"
if (this.cannot(name))
throw "event " + name + " innapropriate in current state " + this.current;
var from = this.current;
- var to = map[from].to;
- var self = this;
+ var to = map[from];
var args = Array.prototype.slice.call(arguments); // turn arguments into pure array
if (this.current != to) {
if (false === StateMachine.beforeEvent.call(this, name, from, to, args))
return;
- this.transition = function() { StateMachine.transition.call(self, name, from, to, args); self.transition = null; };
- this.transition.event = name;
- this.transition.from = from;
- this.transition.to = to;
+ var self = this;
+ this.transition = function() { // prepare transition method for use either lower down, or by caller if they want an async transition (indicated by a false return value from leaveState)
+ self.transition = null; // this method should only ever be called once
+ self.current = to;
+ StateMachine.enterState.call( self, name, from, to, args);
+ StateMachine.changeState.call(self, name, from, to, args);
+ StateMachine.afterEvent.call( self, name, from, to, args);
+ };
if (false !== StateMachine.leaveState.call(this, name, from, to, args)) {
if (this.transition) // in case user manually called it but forgot to return false
View
62 test/test_async.js
@@ -240,7 +240,7 @@ test("state transition fired without completing previous transition", function()
fsm.transition(); equals(fsm.current, 'yellow', "warn event should transition from green to yellow");
fsm.panic(); equals(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet");
- raises(fsm.calm.bind(fsm), /event calm innapropriate because previous transition \(panic\) from yellow to red did not complete/);
+ raises(fsm.calm.bind(fsm), /event calm innapropriate because previous transition did not complete/);
});
@@ -299,3 +299,63 @@ test("callbacks are ordered correctly", function() {
//-----------------------------------------------------------------------------
+test("cannot fire event during existing transition", function() {
+
+ var fsm = StateMachine.create({
+ initial: 'green',
+ events: [
+ { name: 'warn', from: 'green', to: 'yellow' },
+ { name: 'panic', from: 'yellow', to: 'red' },
+ { name: 'calm', from: 'red', to: 'yellow' },
+ { name: 'clear', from: 'yellow', to: 'green' }
+ ],
+ callbacks: {
+ onleavegreen: function() { return false; },
+ onleaveyellow: function() { return false; },
+ onleavered: function() { return false; }
+ }
+ });
+
+ equals(fsm.current, 'green', "initial state should be green");
+ equals(fsm.can('warn'), true, "should be able to warn");
+ equals(fsm.can('panic'), false, "should NOT be able to panic");
+ equals(fsm.can('calm'), false, "should NOT be able to calm");
+ equals(fsm.can('clear'), false, "should NOT be able to clear");
+
+ fsm.warn();
+
+ equals(fsm.current, 'green', "should still be green because we haven't transitioned yet");
+ equals(fsm.can('warn'), false, "should NOT be able to warn - during transition");
+ equals(fsm.can('panic'), false, "should NOT be able to panic - during transition");
+ equals(fsm.can('calm'), false, "should NOT be able to calm - during transition");
+ equals(fsm.can('clear'), false, "should NOT be able to clear - during transition");
+
+ fsm.transition();
+
+ equals(fsm.current, 'yellow', "warn event should transition from green to yellow");
+ equals(fsm.can('warn'), false, "should NOT be able to warn");
+ equals(fsm.can('panic'), true, "should be able to panic");
+ equals(fsm.can('calm'), false, "should NOT be able to calm");
+ equals(fsm.can('clear'), true, "should be able to clear");
+
+ fsm.panic();
+
+ equals(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet");
+ equals(fsm.can('warn'), false, "should NOT be able to warn - during transition");
+ equals(fsm.can('panic'), false, "should NOT be able to panic - during transition");
+ equals(fsm.can('calm'), false, "should NOT be able to calm - during transition");
+ equals(fsm.can('clear'), false, "should NOT be able to clear - during transition");
+
+ fsm.transition();
+
+ equals(fsm.current, 'red', "panic event should transition from yellow to red");
+ equals(fsm.can('warn'), false, "should NOT be able to warn");
+ equals(fsm.can('panic'), false, "should NOT be able to panic");
+ equals(fsm.can('calm'), true, "should be able to calm");
+ equals(fsm.can('clear'), false, "should NOT be able to clear");
+
+});
+
+//-----------------------------------------------------------------------------
+
+

0 comments on commit 5462141

Please sign in to comment.