Created
May 7, 2010 03:19
-
-
Save noonat/393002 to your computer and use it in GitHub Desktop.
JavaScript state machine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Javascript state machine. Allows you to specify a number of routes | |
// and set a target state, and it will call through the routes to | |
// get there. | |
function Machine(defaultState, routes) { | |
this.target = undefined; | |
this.transition = undefined; | |
this.routes = typeof routes === 'function' ? routes() : routes; | |
// Set the state last, so the route will be triggered. | |
this.state = defaultState; | |
} | |
Object.defineProperties(Machine.prototype, { | |
// The current state of the machine. When changed, calls onExit() for the | |
// old state, and onEnter() for the new state, then calls next() to see | |
// if we need more transitions. | |
state: { | |
get: function() { | |
return this._state; | |
}, | |
set: function(state) { | |
if (this._state !== state) { | |
var oldRoute = this.routes[this._state] | |
, newRoute = this.routes[state]; | |
if (oldRoute && oldRoute.onExit) { | |
oldRoute.onExit(); | |
} | |
this._state = state; | |
if (newRoute && newRoute.onEnter) { | |
newRoute.onEnter(); | |
} | |
this.next(); | |
} | |
} | |
}, | |
// The state we're trying to get to. There may be many transitions | |
// before the machine actually reaches this state. Calls next() when | |
// set, to see if we need to start transitioning. | |
target: { | |
get: function() { | |
return this._target; | |
}, | |
set: function(target) { | |
this._target = target; | |
this.next(); | |
} | |
} | |
}); | |
// If the machine is trying to get to a different target state, this finds | |
// the next transition and runs it, unless there is already a transition | |
// in progress. Throws TransitionError if it reaches a state where it can't | |
// find a route to get to the target state. | |
Machine.prototype.next = function() { | |
if (this.transition) { | |
// Don't do anything, transition in progress. | |
return; | |
} else if (this.target && this.target !== this.state) { | |
// We've got somewhere to go. Find and run the next transition. | |
var route = this.routes[this.state]; | |
var handler = route[this.target]; | |
if (handler) { | |
this.transition = new Transition(this, this.state, handler); | |
this.transition.run(); | |
} else { | |
throw new TransitionError('Invalid state transition from "' + | |
this.state + '" to "' + this.target + '"'); | |
} | |
} else { | |
// We're where we want to be. | |
this.target = undefined; | |
} | |
}; | |
// transition = new Transition(machine, 'foo', function() { | |
// if (!ready) { | |
// this.cancel(); | |
// } else if (doit()) { | |
// this.ok('bar'); | |
// } else { | |
// this.fail('doit() failed'); | |
// } | |
// }); | |
function Transition(machine, from, handler) { | |
this.machine = machine; | |
this.done = false; | |
this.from = from; | |
this.handler = handler; | |
} | |
// Used internally to make sure the transition is still valid. | |
Transition.prototype.guard = function() { | |
if (this.done) { | |
return false; | |
} else { | |
this.done = true; | |
return this.machine.transition === this; | |
} | |
}; | |
// transition.run(); | |
// | |
// Invokes the function attached to the transition. Passing the transition as | |
// an argument, and as the this scope for the function. | |
Transition.prototype.run = function() { | |
if (this.guard()) { | |
this.handler.call(this, this); | |
} | |
}; | |
// transition.ok(); | |
// | |
// Set machine.state to the target state of this transition. | |
Transition.prototype.ok = function(to) { | |
if (typeof to === 'undefined') { | |
throw new TransitionError('transition.ok() called but "to" is undefined'); | |
} else if (this.guard()) { | |
this.machine.state = this.to; | |
this.machine.transition = undefined; | |
} | |
}; | |
// transition.fail('ohnoes!'); | |
// transition.fail(new Error('oh my')); | |
// | |
// Rollback the state machine and throw a TransitionError. | |
Transition.prototype.fail = function(err) { | |
if (this.guard()) { | |
this.machine.state = this.from; | |
this.machine.target = undefined; | |
this.machine.transition = undefined; | |
throw (typeof err === 'string' ? new Error(err) : err); | |
} | |
}; | |
// transition.cancel(); | |
// | |
// Cancel this transition and allow the machine to continue, but don't modify | |
// machine.state or machine.target. Can be used when the transition can't do | |
// anything now, but wants to be run later, as it will be triggered again when | |
// machine.next() is called. | |
Transition.prototype.cancel = function() { | |
if (this.guard()) { | |
this.machine.transition = undefined; | |
} | |
}; | |
// Thrown when machine has a problem transitioning from one state to another. | |
// This can be caused by requesting an impossible state transition, or a | |
// transition calling transition.fail(). | |
function TransitionError(transition, err) { | |
this.transition = transition; | |
this.err = err; | |
} | |
TransitionError.prototype.toString = function() { | |
return 'TransitionError: ' + this.err; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment