automata
Version:
Automata is a Deterministic Finite State Machine automata framework featuring: a JSON based automata definition, timed transitions, sub-states, guards, FSM registry, etc.
688 lines • 24.1 kB
JavaScript
/**
* Created by ibon on 2/8/16.
*/
"use strict";
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var SessionConsumeMessagePromise = (function () {
function SessionConsumeMessagePromise() {
}
SessionConsumeMessagePromise.prototype.then = function (ok, error) {
this._success = ok;
this._error = error;
return this;
};
SessionConsumeMessagePromise.prototype.__success = function (s, m) {
this._success && this._success(s, m);
};
SessionConsumeMessagePromise.prototype.__error = function (s, message) {
this._error && this._error(s, message);
};
return SessionConsumeMessagePromise;
}());
exports.SessionConsumeMessagePromise = SessionConsumeMessagePromise;
var FSMRegistry = (function () {
function FSMRegistry() {
}
FSMRegistry.FSMFromId = function (id) {
return FSMRegistry._fsm[id];
};
FSMRegistry.register = function (fsm_json) {
var ret = null;
try {
var fsm = new FSM(fsm_json);
FSMRegistry._fsm[fsm.name] = fsm;
console.log("Registered Automata '" + fsm.name + "'");
ret = fsm;
}
catch (e) {
console.error(e);
}
return ret;
};
FSMRegistry.createSession = function (session_controller, fsm_id, o) {
var promise = new SessionConsumeMessagePromise();
var fsm = FSMRegistry._fsm[fsm_id];
if (fsm) {
var session = new Session(session_controller);
if (o) {
session.addObserver(o);
}
session.__initialize(fsm).then(function (session, m) {
promise.__success(session, m);
}, function (session, m) {
promise.__error(session, m);
});
}
else {
setImmediate(function () {
promise.__error(null, new Error("Unkonwn automata: '" + fsm_id + "'"));
});
}
return promise;
};
FSMRegistry._fsm = {};
return FSMRegistry;
}());
exports.FSMRegistry = FSMRegistry;
var State = (function () {
function State(name) {
this._name = name;
this._exit_transitions = {};
this._exit_transitions_count = 0;
this._enter_action = null;
this._exit_action = null;
this._auto_transition = [];
}
State.prototype.transitionForMessage = function (m) {
var tr = this._exit_transitions[m.msgId];
return tr || null;
};
State.prototype.addExitTransition = function (t) {
this._exit_transitions[t.event] = t;
this._exit_transitions_count += 1;
};
Object.defineProperty(State.prototype, "name", {
get: function () {
return this._name;
},
enumerable: true,
configurable: true
});
State.prototype.__onExit = function (s, m) {
if (this._exit_action !== null) {
this._exit_action(s, m);
}
this.__stopTimeoutTransitionElements();
return this._exit_action !== null;
};
State.prototype.__onEnter = function (s, m) {
if (this._enter_action !== null) {
this._enter_action(s, m);
}
this.__startTimeoutTransitionElements(s);
return this._enter_action !== null;
};
State.prototype.__startTimeoutTransitionElements = function (s) {
var _this = this;
this._auto_transition.forEach(function (sate) {
sate.timer_id = setTimeout(_this.__notifyTimeoutEvent.bind(_this, s, sate.message), sate.millis);
});
};
State.prototype.__stopTimeoutTransitionElements = function () {
this._auto_transition.forEach(function (sate) {
if (sate.timer_id !== -1) {
clearTimeout(sate.timer_id);
sate.timer_id = -1;
}
});
};
State.prototype.__notifyTimeoutEvent = function (s, m) {
this.__stopTimeoutTransitionElements();
s.dispatchMessage(m);
};
State.prototype.__setTimeoutTransitionInfo = function (millis, message) {
this._auto_transition.push({
millis: millis,
message: message,
timer_id: -1
});
};
State.prototype.isFinal = function () {
return this._exit_transitions_count === 0;
};
State.prototype.toString = function () {
return this._name;
};
return State;
}());
exports.State = State;
var FSM = (function (_super) {
__extends(FSM, _super);
function FSM(fsm) {
_super.call(this, fsm.name);
this._states = [];
this._transitions = [];
this._initial_state = null;
this.__createStates(fsm.state, fsm.initial_state);
this.__createTransitions(fsm.transition);
}
Object.defineProperty(FSM.prototype, "initial_state", {
get: function () {
return this._initial_state;
},
enumerable: true,
configurable: true
});
FSM.prototype.serialize = function () {
return {
name: this._name,
state: this._states.map(function (st) { return st._name; }),
initial_state: this._initial_state._name,
transition: this._transitions.map(function (tr) {
return {
event: tr._event,
from: tr._initial_state._name,
to: tr._final_state._name
};
})
};
};
FSM.prototype.__createStates = function (states, initial) {
for (var _i = 0, states_1 = states; _i < states_1.length; _i++) {
var name_1 = states_1[_i];
var st = void 0;
if (name_1.lastIndexOf("FSM:") === -1) {
st = new State(name_1);
}
else {
var fsmname = name_1.substring(4);
st = FSMRegistry._fsm[fsmname];
if (!st) {
throw "Automata '" + this._name + "' referencing other non existent automata: '" + name_1 + "'";
}
}
this._states.push(st);
if (st.name === initial) {
this.__setInitialState(st);
}
}
};
FSM.prototype.__setInitialState = function (st) {
this._initial_state = st;
this.__createInitialTransition();
this.__createEnterAction();
};
FSM.prototype.__createInitialTransition = function () {
this.addExitTransition(new Transition(this, this._initial_state, Transition.__InitialTransitionEvent));
};
FSM.prototype.__createEnterAction = function () {
this._enter_action = function (session, message) {
session.postMessage(Transition.__InitialTransitionMessage);
};
};
FSM.prototype.__findStateByName = function (n) {
for (var _i = 0, _a = this._states; _i < _a.length; _i++) {
var s = _a[_i];
if (s.name === n) {
return s;
}
}
return null;
};
FSM.prototype.__createTransitions = function (transitions) {
var _this = this;
transitions.forEach(function (v /*, index:number, arr:TransitionJson[] */) {
var f = _this.__findStateByName(v.from);
var t = _this.__findStateByName(v.to);
var e = v.event;
if (!f || !t) {
throw "Wrongly defined Automata '" + _this.name + "'. Transition '" + v.event + "' refers unknown state:'" + (!f ? v.from : v.to) + "'";
}
_this._transitions.push(new Transition(f, t, e));
// auto transition behavior.
if (typeof v.timeout !== "undefined") {
f.__setTimeoutTransitionInfo(v.timeout.millis, {
msgId: e,
data: v.timeout.data
});
}
});
};
return FSM;
}(State));
exports.FSM = FSM;
var Transition = (function () {
function Transition(from, to, event) {
this._event = event;
this._initial_state = from;
this._final_state = to;
if (from) {
from.addExitTransition(this);
}
}
Object.defineProperty(Transition.prototype, "event", {
get: function () {
return this._event;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Transition.prototype, "final_state", {
get: function () {
return this._final_state;
},
enumerable: true,
configurable: true
});
Transition.prototype.toString = function () {
return this._event;
};
Transition.__InitialTransitionEvent = "__INITIAL_EVENT";
Transition.__InitialTransitionMessage = { msgId: Transition.__InitialTransitionEvent };
return Transition;
}());
exports.Transition = Transition;
var SessionContext = (function () {
function SessionContext(c, p) {
this._current_state = c;
this._prev_state = p;
}
SessionContext.prototype.serialize = function () {
return {
current_state: this._current_state._name,
prev_state: this._prev_state ? this._prev_state._name : "",
};
};
Object.defineProperty(SessionContext.prototype, "current_state", {
get: function () {
return this._current_state;
},
enumerable: true,
configurable: true
});
Object.defineProperty(SessionContext.prototype, "prev_state", {
get: function () {
return this._prev_state;
},
enumerable: true,
configurable: true
});
SessionContext.prototype.currentStateName = function () {
return this._current_state && this._current_state.name;
};
SessionContext.prototype.prevStateName = function () {
return this._prev_state && this._prev_state.name;
};
SessionContext.prototype.printStackTrace = function () {
console.log(" " + this._current_state.name);
};
return SessionContext;
}());
exports.SessionContext = SessionContext;
var Session = (function () {
function Session(session_controller) {
this._states = [];
this._session_controller = session_controller;
this._messages_controller = new SessionMessagesController(this);
this._observers = [];
this._fsm = null;
this._ended = false;
this._sessionEndPromise = null;
}
Session.prototype.__initialize = function (fsm) {
this._fsm = fsm;
this._states.push(new SessionContext(fsm, null));
this.__invoke(fsm.name + "_enter", Transition.__InitialTransitionMessage);
var promise = this.dispatchMessage(Transition.__InitialTransitionMessage);
this._sessionEndPromise = promise;
return promise;
};
Session.prototype.__serializeController = function () {
var sc = this._session_controller;
if (sc.serialize && typeof sc.serialize === "function") {
return sc.serialize();
}
return {};
};
Session.prototype.serialize = function (from_registry) {
var serializedController = this.__serializeController();
return {
ended: this._ended,
fsm: from_registry ? this._fsm.name : this._fsm.serialize(),
states: this._states.map(function (st) { return st.serialize(); }),
controller: serializedController
};
};
Session.deserialize = function (s, deserializer, from_registry) {
var controller = deserializer(s.controller);
var session = new Session(controller);
session.__deserialize(s, from_registry);
return session;
};
Session.prototype.__deserialize = function (s, from_registry) {
var _this = this;
if (!from_registry) {
this._fsm = FSMRegistry.register(s.fsm);
}
else {
if (typeof s.fsm === 'string') {
// try automata saved as string.
this._fsm = FSMRegistry.FSMFromId(s.fsm);
}
else {
// if not, assume it was saved as fully serialized automata, but loading it was saved as automata name.
this._fsm = FSMRegistry.FSMFromId(s.fsm.name);
}
}
this._ended = s.ended;
this._states = s.states.map(function (e) {
var c = e.current_state === _this._fsm.name ?
_this._fsm :
_this._fsm._states.filter(function (s) { return s._name === e.current_state; })[0];
var p = e.prev_state === "" ?
null :
_this._fsm._states.filter(function (s) { return s._name === e.prev_state; })[0];
return new SessionContext(c, p);
});
};
Session.prototype.addObserver = function (o) {
this._observers.push(o);
};
/**
* User side message.
*/
Session.prototype.dispatchMessage = function (m) {
var _this = this;
var c = new SessionConsumeMessagePromise();
if (this._ended) {
setTimeout(function () {
c._error(_this, new Error('Session ended'));
}, 0);
}
else {
this._messages_controller.dispatchMessage(m, c);
}
return c;
};
/**
* From SessionController internals.
*/
Session.prototype.postMessage = function (m) {
this._messages_controller.postMessage(m);
};
Session.prototype.__messageImpl = function (m) {
if (m === Transition.__InitialTransitionMessage) {
this.__consumeMessageForFSM(m);
}
else {
this.__consumeMessageForState(m);
}
};
Object.defineProperty(Session.prototype, "current_state", {
get: function () {
return this._states.length ?
this._states[this._states.length - 1].current_state :
null;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Session.prototype, "prev_state", {
get: function () {
return this._states.length ?
this._states[this._states.length - 1].prev_state :
null;
},
enumerable: true,
configurable: true
});
Session.prototype.__onEnter = function (m) {
var cs = this.current_state;
if (cs !== null && !cs.__onEnter(this, m)) {
this.__invoke(cs.name + "_enter", m);
}
};
Session.prototype.__onExit = function (m) {
var cs = this.current_state;
if (cs !== null && !cs.__onExit(this, m)) {
this.__invoke(cs.name + "_exit", m);
}
};
Session.prototype.__invoke = function (method, m) {
return this._session_controller[method] && this._session_controller[method](this, this.current_state_name, m);
};
Session.prototype.__consumeMessageForFSM = function (m) {
var cs = this.current_state;
var fsm = cs;
var new_current_state = fsm.initial_state;
this._states.push(new SessionContext(new_current_state, this.current_state));
this.__notifyContextCreated(m);
this.__onEnter(m);
};
Session.prototype.__findStateWithTransitionForMessage = function (m) {
var sc = this._states;
var state = null;
for (var i = sc.length - 1; i >= 0; i--) {
var current_state = sc[i].current_state;
var tr = current_state.transitionForMessage(m);
if (tr !== null) {
state = current_state;
break;
}
}
return state;
};
Session.prototype.__exitAllStatesUpToStateWithTransitionForMessage = function (stateWitTransition, m) {
while (this._states.length) {
var cs = this._states[this._states.length - 1];
this.__onExit(m);
if (cs.current_state !== stateWitTransition) {
this._states.pop();
this.__notifyContextDestroyed(m);
}
else {
break;
}
}
};
Session.prototype.__popAllStates = function (m) {
while (this._states.length) {
this.__onExit(m);
this._states.pop();
this.__notifyContextDestroyed(m);
}
};
Session.prototype.__setCurrentState = function (s, m) {
var prev = null;
if (this._states.length) {
prev = this._states.pop().current_state;
}
this._states.push(new SessionContext(s, prev));
this.__notifyStateChange(m);
this.__onEnter(m);
};
Session.prototype.__endSession = function (m) {
this._ended = true;
this.__notifySessionEnded(m);
};
Object.defineProperty(Session.prototype, "current_state_name", {
get: function () {
return this._states.length ?
this._states[this._states.length - 1].currentStateName() :
"<No current state>";
},
enumerable: true,
configurable: true
});
Object.defineProperty(Session.prototype, "prev_state_name", {
get: function () {
return this._states.length ?
this._states[this._states.length - 1].prevStateName() :
"<No prev state>";
},
enumerable: true,
configurable: true
});
Session.prototype.__consumeMessageForState = function (m) {
if (!this._ended) {
var state_for_message = this.__findStateWithTransitionForMessage(m);
if (null !== state_for_message) {
this.__processMessage(state_for_message, m);
}
else {
throw new Error("No message: '" + m.msgId + "' for state: '" + this.current_state_name + "'");
}
}
else {
throw new Error("Session is ended. Message " + m.msgId + " is discarded.");
}
};
Session.prototype.__processMessage = function (state_for_message, m) {
var tr = state_for_message.transitionForMessage(m);
var transition_event = tr.event;
if (!this.__invoke(transition_event + "_preGuard", m)) {
this.__exitAllStatesUpToStateWithTransitionForMessage(state_for_message, m);
this.__invoke(transition_event + "_transition", m);
var next = void 0;
if (!this.__invoke(transition_event + "_postGuard", m)) {
next = tr.final_state;
}
else {
next = state_for_message;
}
this.__setCurrentState(next, m);
if (next.isFinal()) {
// this.__popAllStates(m);
this.__endSession(m);
}
}
};
Session.prototype.fireCustomEvent = function (message) {
for (var _i = 0, _a = this._observers; _i < _a.length; _i++) {
var o = _a[_i];
o.customEvent({
session: this,
message: null,
current_state_name: this.current_state_name,
prev_state_name: this.prev_state.name,
custom_message: message
});
}
};
Session.prototype.__notifySessionEnded = function (m) {
this.__notify(m, "sessionEnded");
};
Session.prototype.__notifyContextCreated = function (m) {
this.__notify(m, "contextCreated");
};
Session.prototype.__notifyContextDestroyed = function (m) {
this.__notify(m, "contextDestroyed");
};
Session.prototype.__notifyStateChange = function (m) {
this.__notify(m, "stateChanged");
};
Session.prototype.__notify = function (m, method) {
for (var _i = 0, _a = this._observers; _i < _a.length; _i++) {
var o = _a[_i];
o[method] && o[method]({
session: this,
message: m,
current_state_name: this.current_state_name,
prev_state_name: this.prev_state_name
});
}
};
Object.defineProperty(Session.prototype, "controller", {
get: function () {
return this._session_controller;
},
enumerable: true,
configurable: true
});
Session.prototype.printStackTrace = function () {
if (this._states.length === 0) {
console.log("session empty");
}
else {
console.log("session stack trace:");
this._states.forEach(function (s) {
s.printStackTrace();
});
}
};
return Session;
}());
exports.Session = Session;
var SessionMessageControllerMessageQueue = (function () {
function SessionMessageControllerMessageQueue(session, m, callback) {
this._session = session;
this._callback = typeof callback !== "undefined" ? callback : null;
this._triggering_message = m;
this._messages_queue = [m];
}
SessionMessageControllerMessageQueue.prototype.postMessage = function (m) {
this._messages_queue.push(m);
};
SessionMessageControllerMessageQueue.prototype.__consumeMessage = function () {
var ret;
if (this._messages_queue.length) {
var m = this._messages_queue.shift();
try {
this._session.__messageImpl(m);
ret = false;
}
catch (e) {
// console.error(`consume for message '${m.msgId}' got exception: `, e);
this._messages_queue = [];
this._callback.__error(this._session, e);
ret = true;
}
}
else {
ret = true;
if (this._callback) {
this._callback.__success(this._session, this._triggering_message);
}
}
return ret;
};
return SessionMessageControllerMessageQueue;
}());
exports.SessionMessageControllerMessageQueue = SessionMessageControllerMessageQueue;
var SessionMessagesController = (function () {
function SessionMessagesController(session) {
this._message_queues = [];
this._session = session;
this._consuming = false;
}
SessionMessagesController.prototype.dispatchMessage = function (m, callback) {
this._message_queues.push(new SessionMessageControllerMessageQueue(this._session, m, callback));
this.__consumeMessage();
};
SessionMessagesController.prototype.postMessage = function (m) {
this._message_queues[0].postMessage(m);
this.__consumeMessage();
};
SessionMessagesController.prototype.__consumeMessage = function () {
if (!this._consuming) {
this._consuming = true;
setImmediate(this.__consumeOne.bind(this));
}
};
SessionMessagesController.prototype.__consumeOne = function () {
if (this._message_queues.length) {
if (this._message_queues[0].__consumeMessage()) {
this._message_queues.shift();
}
}
if (this._message_queues.length) {
setImmediate(this.__consumeOne.bind(this));
}
else {
this._consuming = false;
}
};
return SessionMessagesController;
}());
exports.SessionMessagesController = SessionMessagesController;
var Automata = (function () {
function Automata() {
}
Automata.RegisterFSM = function (file) {
if (typeof file === "string") {
}
else {
FSMRegistry.register(file);
}
};
Automata.CreateSession = function (controller, fsm_name, o) {
return FSMRegistry.createSession(controller, fsm_name, o);
};
return Automata;
}());
exports.Automata = Automata;
//# sourceMappingURL=automata.js.map