Hi,
I'm new with Impact and was looking for a FSM solution. The code from drailing didn't quite work (yes, it wasn't tested) so I had some fixes.
Here's a working FSM port from jakesgordon's javascript-state-machine:
ig.module
(
'plugin.statemachine'
)
.requires
(
'impact.impact'
)
.defines(function()
{
ig.StateMachine = ig.Class.extend(
{
fsm: {},
//---------------------------------------------------------------------------
init: function(cfg, target)
{
this.fsm = this.create(cfg, target);
},
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] : [ig.StateMachine.WILDCARD]);
// allow 'wildcard' transition if 'from' is not specified
map[e.name] = map[e.name] || {};
for (var n = 0; n < from.length; n++)
map[e.name][from[n]] = e.to || from[n];
// allow no-op transition if 'to' is not specified
};
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] = this.buildEvent(name, map[name]);
console.log("BUILT EVENT, name: " + name + ", fsm: " + JSON.stringify(fsm));
}
}
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) { throw msg; };
// default behavior when something unexpected happens is to throw an exception, but caller can override this behavior if desired (see github issue #3)
if (initial && !initial.defer)
fsm[initial.event]();
console.log("INITIAL: " + initial.event + ", fsm: " + JSON.stringify(fsm));
return fsm;
},
//===========================================================================
doCallback: function(fsm, func, name, from, to, args)
{
if (func)
{
try
{
console.log("DO CALLBACK, name: " + name + ", from: " + from + ", to: " + to + ", args: " + args);
return func.apply(fsm, [name, from, to].concat(args));
}
catch(e)
{
return fsm.error(name, from, to, args, ig.StateMachine.Error.INVALID_CALLBACK, "an exception occurred in a caller-provided callback function");
}
}
},
beforeEvent: function(fsm, name, from, to, args)
{
return this.doCallback(fsm, fsm['onbefore' + name], name, from, to, args);
},
afterEvent: function(fsm, name, from, to, args)
{
return this.doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args);
},
leaveState: function(fsm, name, from, to, args)
{
return this.doCallback(fsm, fsm['onleave' + from], name, from, to, args);
},
enterState: function(fsm, name, from, to, args)
{
return this.doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args);
},
changeState: function(fsm, name, from, to, args)
{
return this.doCallback(fsm, fsm['onchangestate'], name, from, to, args);
},
buildEvent: function(name, map)
{
console.log("BUILDING EVENT, name: " + name + ", map: " + JSON.stringify(map));
var self = this;
return function()
{
var from = this.current;
var to = map[from] || map[ig.StateMachine.WILDCARD] || from;
var args = Array.prototype.slice.call(arguments);
// turn arguments into pure array
if (this.transition)
return this.error(name, from, to, args, ig.StateMachine.Error.PENDING_TRANSITION, "event " + name + " inappropriate because previous transition did not complete");
if (this.cannot(name))
return this.error(name, from, to, args, ig.StateMachine.Error.INVALID_TRANSITION, "event " + name + " inappropriate in current state " + this.current);
if (false === self.beforeEvent(this, name, from, to, args))
return ig.StateMachine.Result.CANCELLED;
if (from === to)
{
self.afterEvent(this, name, from, to, args);
return ig.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;
self.enterState(fsm, name, from, to, args);
self.changeState(fsm, name, from, to, args);
self.afterEvent(fsm, name, from, to, args);
};
var leave = self.leaveState(this, name, from, to, args);
if (false === leave)
{
this.transition = null;
return ig.StateMachine.Result.CANCELLED;
}
else if ("async" === leave)
{
return ig.StateMachine.ASYNC;
}
else
{
if (this.transition)
this.transition();
// in case user manually called transition() but forgot to return ASYNC
return ig.StateMachine.Result.SUCCEEDED;
}
};
}
});
//---------------------------------------------------------------------------
ig.StateMachine.VERSION = "2.1.0";
//---------------------------------------------------------------------------
ig.StateMachine.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
ASYNC: 4,
// the event is asynchronous and the caller is in control of when the transition occurs
};
ig.StateMachine.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
};
ig.StateMachine.WILDCARD = '*';
ig.StateMachine.ASYNC = 'async';
});