Permalink
Browse files

check transitions are valid, and handle sync events

  • Loading branch information...
1 parent a2f9c16 commit 1d1a330d21c65bee15ab8b1107b422ab9fbc3a53 @dominictarr committed Jun 15, 2011
Showing with 175 additions and 26 deletions.
  1. +70 −25 fsm.js
  2. +1 −1 package.json
  3. +89 −0 test/fsm.asynct.js
  4. +15 −0 test/fsm.synct.js
View
95 fsm.js
@@ -8,26 +8,43 @@ function first(obj){
return i
}
}
+/*
+TODO:
+
+ namespaced events
+ (specificially, name spaced errors)
+
+ process sync events FIFO no LIFO.
+
+ check graph is fully connected and no dead ends.
+
+*/
+
function FSM (schema){
if(!(this instanceof FSM)) return new FSM (schema)
var state = first(schema) //name it start, OR ELSE
, self = this
+ , events = []
+ , changing = false
, callback
this.transitions = []
+ //create default states.
if(!schema.end)
schema.end = {
- _in: function (){callback.apply(null,[null].concat([].slice.call(arguments)))}
+ _in: function (){
+ callback.apply(null,[].slice.call(arguments))}
}
+ //create default states.
if(!schema.fatal)
schema.fatal = {
_in: function (err){
if('function' !== typeof callback)
- throw arguments
+ throw err || new Error ('FSM in fatal state')
callback.apply(null,[].slice.call(arguments))
}
}
@@ -54,66 +71,94 @@ function FSM (schema){
return this
}
+ //get events, and check that the transtions are valid
+ function isState(e){
+ return schema[e] ? e : false
+ }
+ var isArray = Array.isArray
+
this.getEvents = function (){
var s = []
for(var i in schema){
- for(var j in schema[i])
+ for(var j in schema[i])//events
if(!~s.indexOf(j) && j[0] != '_')
s.push(j)
+ if(j !== '_in' && 'function' !== typeof schema[i][j]){ //repeating
+ var trans = schema[i][j]
+ trans = isArray(trans) ? trans[0] : trans
+ if(!isState(trans))
+ throw new Error(['transition:', j, ':', i, '->', trans, 'is not to a valid state'].join(' ') )
+ }
}
return s
}
- function isState(e){
- return schema[e] ? e : false
- }
- var isArray = Array.isArray
-
- function applyAll(list,ignore,args){
+ function applyTo(args,funx){
args = args || []
try {
- if('function' == typeof list) {
- console.log("ARGS",args)
- return list.apply(self,args)
+ if('function' == typeof funx) {
+ return funx.apply(self,args)
}
- list.forEach(function (e){
- e.apply(self,args)
+ funx.forEach(function (e){
+ e.apply(self,args)
})
} catch (err) {
- //action thru an exception, generate throw event. (will transition to fatal by default)
+ //action threw an exception, generate throw event. (will transition to fatal by default)
//unless the FSM is complete, then throw it again and let someone else handle it.
if(state == 'fatal' || state == 'end')
throw err
self.event('throw', [err].concat(args))
}
}
+
this.callback = function (eventname){ //add options to apply timeout
return function () {
var args = [].slice.call(arguments)
self.event(args[0] ? 'error' : eventname, args)
}
}
+ /*
+ rewrite this function:
+
+ push event onto a list
+
+ then if FSM isn't already between states
+ (because many events could be generated syncronously)
+
+ start processing events, on a first come first served basis.
+ */
this.event = function (e,args){
+ events.push([e,args])
+
+ while(!changing && events.length){
+ changing = true
+ changeState.apply(this,events.shift())
+ changing = false
+ }
+ }
+ function changeState (e,args) {
var oldState = state
, trans = schema[state][e] || (e === 'error' || e === 'throw' ? 'fatal' : null)
args = args || []
if('string' === typeof trans && isState(trans)){
state = trans
- this.transitions .push(e)
+ self.transitions.push(e)
} else if (isArray(trans) && isState(trans[0])){
state = trans[0]
- this.transitions .push(e)
- applyAll(trans.splice(1),this,args)
- }
+ self.transitions .push(e)
+ applyTo(args,trans.splice(1))
+ } else if('function' == typeof trans)
+ throw new Error('transition cannot be function:' + trans)
- console.log( e,':',oldState,'->',state)
+ console.log(e, ':', oldState, '->', state)
- if(schema[state]._in && oldState != state)
- applyAll(schema[state]._in,this,args)
-
- return this
+ if(schema[state]._in && oldState != state){
+ console.log(schema[state]._in.toString())
+ applyTo(args,schema[state]._in)
+ }
+ return self
}
this.getEvents().forEach(function (e){
@@ -124,6 +169,6 @@ function FSM (schema){
args = [].slice.call(arguments)
if('function' === typeof args[args.length - 1])
callback = args.pop()
- applyAll(schema.start._in,self,args)
+ applyTo(args,schema.start._in)
}
}
View
@@ -2,7 +2,7 @@
"author": "Dominic Tarr <dominic.tarr@gmail.com> (http://bit.ly/dominictarr)",
"name": "fsm",
"description": "Finite State Machine - Separate Control Flow from IO",
- "version": "0.0.0",
+ "version": "0.0.1",
"homepage": "https://github.com/dominictarr/fsm",
"repository": {
"type": "git",
View
@@ -139,4 +139,93 @@ exports ['fsm can give log of transitions'] = function (test){
test.done()
})
+}
+
+exports ['set up next tick while transferring to next state'] = function (test){
+
+ var fsm = new FSM({
+ start: {
+ _in: function (){
+ setTimeout(this.callback('next'),10)
+ },
+ next: ['middle', function (){
+ setTimeout(this.callback('done'),10)
+ }]
+ },
+ middle: {
+ done: 'end'
+ }
+ })
+
+ fsm.call(function (err){
+ it(fsm.getState()).equal('end')
+ it(fsm.transitions).deepEqual(['next','done'])
+ it(err).equal(null)
+ test.done()
+ })
+
+}
+
+exports ['set up next tick while transferring to next state 2'] = function (test){
+
+ var fsm = new FSM({
+ start: {
+ _in: function (){
+ setTimeout(this.callback('next'),10)
+ },
+ next: ['middle', function (){
+ setTimeout(this.callback('next'),10)
+ }]
+ },
+ middle: {
+ done: 'end', //accidentially had this outside the middle state.
+ /*
+ 1. checking for non blocking would have detected this problem,
+
+ */
+ next: ['middle',function (){
+ setTimeout(this.callback('done'),10)
+ }]
+ }
+ })
+
+ fsm.call(function (err){
+ it(fsm.getState()).equal('end')
+ it(fsm.transitions).deepEqual(['next','next','done'])
+ it(err).equal(null)
+ test.done()
+ })
+
+}
+
+exports ['if an action generates a event syncly, it should be defured untill the transition is complete'] = function (test){
+
+ var fsm = new FSM({
+ start: {
+ _in: function (){
+ setTimeout(this.callback('next'),10)
+ },
+ next: ['middle', function (){
+ setTimeout(this.callback('next'),10)
+ }]
+ },
+ middle: {
+ done: 'end', //accidentially had this outside the middle state.
+ /*
+ 1. checking for non blocking would have detected this problem,
+
+ */
+ next: ['middle',function (){
+ this.callback('done')()
+ }]
+ }
+ })
+
+ fsm.call(function (err){
+ it(fsm.getState()).equal('end')
+ it(fsm.transitions).deepEqual(['next','next','done'])
+ it(err).equal(null)
+ test.done()
+ })
+
}
View
@@ -147,4 +147,19 @@ exports ['fsm should not catch errors thrown in final callback'] = function (){
}
it(caught).ok()
it(called).equal(1)
+}
+
+exports ['throw if an invalid transition is defined'] = function (){
+
+ it(function (){
+ new FSM({
+ start:{
+ next: 'middle'
+ },
+ middle: {
+ next: 'nowhere'
+ }
+ })
+ }).throws()
+
}

0 comments on commit 1d1a330

Please sign in to comment.