Skip to content

Instantly share code, notes, and snippets.

@coodoo
Created January 5, 2013 04:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save coodoo/4459759 to your computer and use it in GitHub Desktop.
Save coodoo/4459759 to your computer and use it in GitHub Desktop.
Implemented in a way that resembles SMC (http://smc.sourceforge.net/), primarily adding two callbacks a. action() b. guard() Changes I made are marked with "jx" in the comment.
$().on('ready', ready());
function ready(){
var fsm = StateMachine.create({
initial: 'hungry',
/**
* 下面是依 smc 精神設計的 api,主要好處是可每組條件指定 callback,而不用 switch/case
*
* guard: 判斷是否允許執行此 transition,有時條件未滿足,就不允許,效果類似 onBeforeEvent,但可每組條件指定一個 callback
*
* action: 一個 event 觸發時,要執行的工作,效果類似 onAfterEvent(),但可每組條件指定一個 callback
*
* 適用時機:
* 。如果像 eat event 可能會有多組 state 共用,為了避免在 callback 內寫一大堆 switch/case,就可每組明確指定 callback
* 。但如果 event 不會被共用,就可用原版設計的全域 callback,通常這適用於較單純的 state machine
*/
events: [
{ name: 'eat', from: 'hungry', to: 'satisfied', action:'foo', guard:'guardFoo' },
{ name: 'eat', from: 'satisfied', to: 'full', action:'bar' },
{ name: 'eat', from: 'full', to: 'sick', action:'coo' },
{ name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry', action:'doo' },
]});
//========================================================================
//
// guards
/*
Transition Guards
Conditions that must be met in order for transitions to proceed
*/
/**
* @return BOOLEAN false will cancel the transition
*/
fsm.guardFoo = function( name, from, to){
console.log( 'GUARD: ', from, '→', to, ' >by "eat" event ' );
return true;
}
//========================================================================
//
// actions
fsm.foo = function( name, from, to, args ){
console.log( 'ACTION [foo]: ', from, '→', to, ' >for event= ', name );
}
//========================================================================
//
// event - global
//before
fsm.onbeforeeat = function( evt, from, to, args ){
console.log( ' >before event= ', evt, ' > ', from, '→' ,to, ' >args: ', args );
return true;
}
//after
fsm.oneat = function( evt, from, to, args ){
console.log( ' >after event= ', evt, ' > ', to );
};
//========================================================================
//
// state - global
fsm.onleavehungry = function( evt, from, to, args ){
console.log( args, ' leave state= ', evt, from );
};
fsm.onentersatisfied = function( evt, from, to, args ){
console.log( args, ' enter state= ', evt, to );
};
fsm.onleavesatisfied = function( evt, from, to, args ){
console.log( args, ' leave state= ', evt, from );
};
//========================================================================
//
// runner
var result = fsm.eat( '1st' );
console.log( 'result: ', result );
// fsm.eat( '2nd' );
// fsm.eat( '3rd' );
}
(function (window) {
var StateMachine = {
//---------------------------------------------------------------------------
VERSION: "2.2.0",
//---------------------------------------------------------------------------
Result: {
SUCCEEDED: 1, // the event transitioned successfully from one state to another
NOTRANSITION: 2, // the event was successfull but no state transition was necessary
CANCELLED: 3, // the event was cancelled by the caller in a beforeEvent callback or transition guard //jx
ASYNC: 4 // the event is asynchronous and the caller is in control of when the transition occurs
},
Error: {
INVALID_TRANSITION: 100, // caller tried to fire an event that was innapropriate in the current state
PENDING_TRANSITION: 200, // caller tried to fire an event while an async transition was still pending
INVALID_CALLBACK: 300 // caller provided callback function threw an exception
},
WILDCARD: '*',
ASYNC: 'async',
//---------------------------------------------------------------------------
create: function(cfg, target) {
var initial = (typeof cfg.initial == 'string') ? { state: cfg.initial } : cfg.initial; // allow for a simple string, or an object with { state: 'foo', event: 'setup', defer: true|false }
var fsm = target || cfg.target || {};
var events = cfg.events || [];
var callbacks = cfg.callbacks || {};
var map = {};
var add = function(e) {
var from = (e.from instanceof Array) ? e.from : (e.from ? [e.from] : [StateMachine.WILDCARD]); // allow 'wildcard' transition if 'from' is not specified
map[e.name] = map[e.name] || {};
for (var n = 0 ; n < from.length ; n++)
//jx: 加上 action, guard 指令
// allow no-op transition if 'to' is not specified
map[e.name][from[n]] = {
to: e.to || from[n],
action: e.action,
guard: e.guard }; //檢查是否允許執行此 transition,
};
if (initial) {
initial.event = initial.event || 'startup';
add({ name: initial.event, from: 'none', to: initial.state });
}
for(var n = 0 ; n < events.length ; n++)
add(events[n]);
for(var name in map) {
if (map.hasOwnProperty(name))
fsm[name] = StateMachine.buildEvent(name, map[name]);
}
for(var name in callbacks) {
if (callbacks.hasOwnProperty(name))
fsm[name] = callbacks[name]
}
fsm.current = 'none';
fsm.is = function(state) { return this.current == state; };
fsm.can = function(event) { return !this.transition && (map[event].hasOwnProperty(this.current) || map[event].hasOwnProperty(StateMachine.WILDCARD)); }
fsm.cannot = function(event) { return !this.can(event); };
fsm.error = cfg.error || function(name, from, to, args, error, msg, e) { throw e || msg; }; // default behavior when something unexpected happens is to throw an exception, but caller can override this behavior if desired (see github issue #3 and #17)
if (initial && !initial.defer)
fsm[initial.event]();
return fsm;
},//end create() state
//===========================================================================
doCallback: function(fsm, func, name, from, to, args) {
if (func) {
try {
return func.apply(fsm, [name, from, to].concat(args));
}
catch(e) {
return fsm.error(name, from, to, args, StateMachine.Error.INVALID_CALLBACK, "an exception occurred in a caller-provided callback function", e);
}
}
},
beforeEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbefore' + name], name, from, to, args); },
afterEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args); },
leaveState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleave' + from], name, from, to, args); },
enterState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args); },
changeState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onchangestate'], name, from, to, args); },
//jx
defaultAction: function( name, from, to, args ){ console.log( 'no action assigned for event = ', name );},
//jx: 沒提供 guard 的話,無條件允許執行此 transition
defaultGuard: function( name, from, to, args ){ return true; },
buildEvent: function(name, map) {
return function() {
var from = this.current;
var to = map[from].to || map[StateMachine.WILDCARD] || from; //jx
//jx
var action = this[map[from].action];
action = action === undefined ? StateMachine.defaultAction : action;
//jx
var guard = this[map[from].guard];
guard = guard === undefined ? StateMachine.defaultGuard : guard;
//
var args = Array.prototype.slice.call(arguments); // turn arguments into pure array
if (this.transition)
return this.error(name, from, to, args, StateMachine.Error.PENDING_TRANSITION, "event " + name + " inappropriate because previous transition did not complete");
if (this.cannot(name))
return this.error(name, from, to, args, StateMachine.Error.INVALID_TRANSITION, "event " + name + " inappropriate in current state " + this.current);
if (false === StateMachine.beforeEvent(this, name, from, to, args))
return StateMachine.Result.CANCELLED;
//jx - 先檢查 guard condition 是否有通過,才能執行此 transition
if( false === guard.apply(this, [name, from, to].concat(args)) ){
return StateMachine.Result.CANCELLED;
}
//jx - 執行 action callback
//注意:故意讓上面 beforeevent 先跑,那裏面可能有 guardian 會擋掉事件與state 轉換
// action( name, from, to, args.slice() );
action.apply(this, [name, from, to].concat(args));
if (from === to) {
StateMachine.afterEvent(this, name, from, to, args);
return StateMachine.Result.NOTRANSITION;
}
// prepare a transition method for use EITHER lower down,
// or by caller if they want an async transition (indicated by an ASYNC return value from leaveState)
var fsm = this;
this.transition = function() {
fsm.transition = null; // this method should only ever be called once
fsm.current = to;
StateMachine.enterState( fsm, name, from, to, args);
StateMachine.changeState(fsm, name, from, to, args);
StateMachine.afterEvent( fsm, name, from, to, args);
};
this.transition.cancel = function() { // provide a way for caller to cancel async transition if desired (issue #22)
fsm.transition = null;
StateMachine.afterEvent(fsm, name, from, to, args);
}
;
var leave = StateMachine.leaveState(this, name, from, to, args);
if (false === leave) {
this.transition = null;
return StateMachine.Result.CANCELLED;
}
else if ("async" === leave) {
return StateMachine.Result.ASYNC;
}
else {
if (this.transition)
this.transition(); // in case user manually called transition() but forgot to return ASYNC
return StateMachine.Result.SUCCEEDED;
}
};
}
}; // StateMachine
//===========================================================================
if ("function" === typeof define) {
define(function(require) { return StateMachine; });
}
else {
window.StateMachine = StateMachine;
}
}(this));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment