Skip to content

Instantly share code, notes, and snippets.

@noonat
Created May 7, 2010 03:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save noonat/393002 to your computer and use it in GitHub Desktop.
Save noonat/393002 to your computer and use it in GitHub Desktop.
JavaScript state machine
// 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