Skip to content

Commit 5462141

Browse files
committed
ensure can() returns false during asynchronous state transitions
1 parent 9d71f62 commit 5462141

File tree

5 files changed

+91
-30
lines changed

5 files changed

+91
-30
lines changed

README.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,10 @@ Callbacks
130130

131131
4 callbacks are available if your state machine has methods using the following naming conventions:
132132

133-
* onbefore**event** - fired before an event
134-
* onafter**event** - fired after an event
135-
* onenter**state** - fired when entering a state
136-
* onleave**state** - fired when leaving a state
133+
* onbefore**event** - fired before the event
134+
* onleave**state** - fired when leaving the old state
135+
* onenter**state** - fired when entering the new state
136+
* onafter**event** - fired after the event
137137

138138
For convenience, the 2 most useful callbacks can be shortened:
139139

@@ -179,6 +179,16 @@ Additionally, they can be added and removed from the state machine at any time:
179179
fsm.onred = null;
180180
fsm.onchangestate = function(event, from, to) { document.body.className = to; };
181181

182+
**NOTES:**
183+
184+
* If you return `false` from an `onbeforeevent` handler then you can cancel the event.
185+
* If you return `false` from an `onleavestate` handler then you can perform an asynchronous state transition (see next section)
186+
187+
Asynchronous State Transitions
188+
==============================
189+
190+
* **TODO**
191+
182192
State Machine Classes
183193
=====================
184194

@@ -213,11 +223,6 @@ instances:
213223

214224
This should be easy to adjust to fit your appropriate mechanism for object construction.
215225

216-
Asynchronous State Transitions
217-
==============================
218-
219-
* **TODO**
220-
221226
Initialization Options
222227
======================
223228

demo/demo.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#demo { width: 400px; margin: 0 auto; }
1+
#demo { width: 400px; margin: 0 auto; text-align: center; }
22

33
#controls { text-align: center; }
44

index.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
<h1> Finite State Machine </h1>
1414

1515
<div id="controls">
16-
<button id="clear" onclick="Demo.clear();">all clear</button>
17-
<button id="calm" onclick="Demo.calm();">calm down</button>
16+
<button id="clear" onclick="Demo.clear();">clear</button>
17+
<button id="calm" onclick="Demo.calm();">calm</button>
1818
<button id="warn" onclick="Demo.warn();">warn</button>
1919
<button id="panic" onclick="Demo.panic();">panic!</button>
2020
</div>
@@ -23,7 +23,7 @@ <h1> Finite State Machine </h1>
2323
</div>
2424

2525
<div id="notes">
26-
<i>dashed lines are async events that take 3 seconds</i>
26+
<i>dashed lines are asynchronous state transitions (3 seconds)</i>
2727
</div>
2828

2929
<textarea id="output">

state-machine.js

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ StateMachine = {
1818
var from = (e.from instanceof Array) ? e.from : [e.from];
1919
map[e.name] = map[e.name] || {};
2020
for (var n = 0 ; n < from.length ; n++)
21-
map[e.name][from[n]] = e;
21+
map[e.name][from[n]] = e.to;
2222
};
2323

2424
if (initial) {
@@ -41,7 +41,7 @@ StateMachine = {
4141

4242
fsm.current = 'none';
4343
fsm.is = function(state) { return this.current == state; };
44-
fsm.can = function(event) { return !!map[event][this.current]; };
44+
fsm.can = function(event) { return !!map[event][this.current] && !this.transition; };
4545
fsm.cannot = function(event) { return !this.can(event); };
4646

4747
if (initial && !initial.defer)
@@ -83,36 +83,32 @@ StateMachine = {
8383
return func.apply(this, [name, from, to].concat(args));
8484
},
8585

86-
transition: function(name, from, to, args) {
87-
this.current = to;
88-
StateMachine.enterState.call(this, name, from, to, args);
89-
StateMachine.changeState.call(this, name, from, to, args);
90-
StateMachine.afterEvent.call(this, name, from, to, args);
91-
},
92-
9386
buildEvent: function(name, map) {
9487
return function() {
9588

9689
if (this.transition)
97-
throw "event " + name + " innapropriate because previous transition (" + this.transition.event + ") from " + this.transition.from + " to " + this.transition.to + " did not complete"
90+
throw "event " + name + " innapropriate because previous transition did not complete"
9891

9992
if (this.cannot(name))
10093
throw "event " + name + " innapropriate in current state " + this.current;
10194

10295
var from = this.current;
103-
var to = map[from].to;
104-
var self = this;
96+
var to = map[from];
10597
var args = Array.prototype.slice.call(arguments); // turn arguments into pure array
10698

10799
if (this.current != to) {
108100

109101
if (false === StateMachine.beforeEvent.call(this, name, from, to, args))
110102
return;
111103

112-
this.transition = function() { StateMachine.transition.call(self, name, from, to, args); self.transition = null; };
113-
this.transition.event = name;
114-
this.transition.from = from;
115-
this.transition.to = to;
104+
var self = this;
105+
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)
106+
self.transition = null; // this method should only ever be called once
107+
self.current = to;
108+
StateMachine.enterState.call( self, name, from, to, args);
109+
StateMachine.changeState.call(self, name, from, to, args);
110+
StateMachine.afterEvent.call( self, name, from, to, args);
111+
};
116112

117113
if (false !== StateMachine.leaveState.call(this, name, from, to, args)) {
118114
if (this.transition) // in case user manually called it but forgot to return false

test/test_async.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ test("state transition fired without completing previous transition", function()
240240
fsm.transition(); equals(fsm.current, 'yellow', "warn event should transition from green to yellow");
241241
fsm.panic(); equals(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet");
242242

243-
raises(fsm.calm.bind(fsm), /event calm innapropriate because previous transition \(panic\) from yellow to red did not complete/);
243+
raises(fsm.calm.bind(fsm), /event calm innapropriate because previous transition did not complete/);
244244

245245
});
246246

@@ -299,3 +299,63 @@ test("callbacks are ordered correctly", function() {
299299

300300
//-----------------------------------------------------------------------------
301301

302+
test("cannot fire event during existing transition", function() {
303+
304+
var fsm = StateMachine.create({
305+
initial: 'green',
306+
events: [
307+
{ name: 'warn', from: 'green', to: 'yellow' },
308+
{ name: 'panic', from: 'yellow', to: 'red' },
309+
{ name: 'calm', from: 'red', to: 'yellow' },
310+
{ name: 'clear', from: 'yellow', to: 'green' }
311+
],
312+
callbacks: {
313+
onleavegreen: function() { return false; },
314+
onleaveyellow: function() { return false; },
315+
onleavered: function() { return false; }
316+
}
317+
});
318+
319+
equals(fsm.current, 'green', "initial state should be green");
320+
equals(fsm.can('warn'), true, "should be able to warn");
321+
equals(fsm.can('panic'), false, "should NOT be able to panic");
322+
equals(fsm.can('calm'), false, "should NOT be able to calm");
323+
equals(fsm.can('clear'), false, "should NOT be able to clear");
324+
325+
fsm.warn();
326+
327+
equals(fsm.current, 'green', "should still be green because we haven't transitioned yet");
328+
equals(fsm.can('warn'), false, "should NOT be able to warn - during transition");
329+
equals(fsm.can('panic'), false, "should NOT be able to panic - during transition");
330+
equals(fsm.can('calm'), false, "should NOT be able to calm - during transition");
331+
equals(fsm.can('clear'), false, "should NOT be able to clear - during transition");
332+
333+
fsm.transition();
334+
335+
equals(fsm.current, 'yellow', "warn event should transition from green to yellow");
336+
equals(fsm.can('warn'), false, "should NOT be able to warn");
337+
equals(fsm.can('panic'), true, "should be able to panic");
338+
equals(fsm.can('calm'), false, "should NOT be able to calm");
339+
equals(fsm.can('clear'), true, "should be able to clear");
340+
341+
fsm.panic();
342+
343+
equals(fsm.current, 'yellow', "should still be yellow because we haven't transitioned yet");
344+
equals(fsm.can('warn'), false, "should NOT be able to warn - during transition");
345+
equals(fsm.can('panic'), false, "should NOT be able to panic - during transition");
346+
equals(fsm.can('calm'), false, "should NOT be able to calm - during transition");
347+
equals(fsm.can('clear'), false, "should NOT be able to clear - during transition");
348+
349+
fsm.transition();
350+
351+
equals(fsm.current, 'red', "panic event should transition from yellow to red");
352+
equals(fsm.can('warn'), false, "should NOT be able to warn");
353+
equals(fsm.can('panic'), false, "should NOT be able to panic");
354+
equals(fsm.can('calm'), true, "should be able to calm");
355+
equals(fsm.can('clear'), false, "should NOT be able to clear");
356+
357+
});
358+
359+
//-----------------------------------------------------------------------------
360+
361+

0 commit comments

Comments
 (0)