938 lines
34 KiB
JavaScript
938 lines
34 KiB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.jiboStateMachine = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
|
"use strict";
|
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
});
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const Transition_1 = require("./Transition");
|
|
const TimeoutTransition_1 = require("./TimeoutTransition");
|
|
const Utils_1 = require("./Utils");
|
|
const log_1 = require("./log");
|
|
const log = log_1.default.createChild("State");
|
|
class State {
|
|
constructor(sm, name, transitions) {
|
|
this.sm = sm;
|
|
this.name = name;
|
|
this.transitions = [];
|
|
this._isCurrent = false;
|
|
this._entryCounter = 0;
|
|
this._exitCounter = 0;
|
|
this._isExiting = false;
|
|
this._internals = [];
|
|
this._events = new Map();
|
|
this._typedEvents = new Map();
|
|
sm._addState(this);
|
|
if (transitions) {
|
|
this.addTransition(transitions);
|
|
}
|
|
}
|
|
addTransition(transition) {
|
|
if (Array.isArray(transition)) {
|
|
transition.forEach(trans => {
|
|
this._addTransition(trans);
|
|
});
|
|
}
|
|
else {
|
|
this._addTransition(transition);
|
|
}
|
|
return this;
|
|
}
|
|
_addTransition(transition) {
|
|
if (transition.getSourceState()) {
|
|
this.sm._rejectCurrentPromise(new Error(`This transition has already been assigned to a source state`));
|
|
}
|
|
transition._setSourceState(this);
|
|
this.transitions.push(transition);
|
|
}
|
|
addInternalTransition(name, destState) {
|
|
const exists = this._internals.some(i => (i.name === name && i.destState === destState));
|
|
if (exists) {
|
|
throw new Error(`State already has internal transition of name '${name}' to state '${destState.name}'`);
|
|
}
|
|
let internal = {
|
|
name,
|
|
destState,
|
|
transition: new Transition_1.Transition(name, destState)
|
|
};
|
|
this._internals.push(internal);
|
|
this.addTransition(internal.transition);
|
|
return this;
|
|
}
|
|
addEventTransition(event, destState) {
|
|
return this._addEventTransition(event, destState, false);
|
|
}
|
|
_addEventTransition(event, destState, global = false) {
|
|
if (this._events.has(event)) {
|
|
throw new Error(`Duplicate event transition. State: '${this.name}', event: '${event}'`);
|
|
}
|
|
let trans = new Transition_1.Transition(Utils_1.createEventTransitionName(event), destState);
|
|
trans._isGlobal = global;
|
|
const handler = (result) => {
|
|
if (this._isCurrent) {
|
|
trans.trigger(result);
|
|
}
|
|
};
|
|
this._events.set(event, { event, handler });
|
|
this.addTransition(trans);
|
|
return this;
|
|
}
|
|
addTypedEventTransition(event, destState) {
|
|
return this._addTypedEventTransition(event, destState, false);
|
|
}
|
|
_addTypedEventTransition(event, destState, global = false) {
|
|
let trans = new Transition_1.Transition(Utils_1.createTypedEventTransitionName(event), destState);
|
|
trans._isGlobal = global;
|
|
const handler = (result) => {
|
|
if (this._isCurrent) {
|
|
trans.trigger(result);
|
|
}
|
|
};
|
|
this._typedEvents.set(event, { event, handler });
|
|
this.addTransition(trans);
|
|
return this;
|
|
}
|
|
addDoneTransition(destState) {
|
|
if (this._doneTransition) {
|
|
this.sm._rejectCurrentPromise(new Error(`Done transition for state '${this.name}' already registered`));
|
|
}
|
|
this._doneTransition = new Transition_1.Transition('Done', destState);
|
|
this.addTransition(this._doneTransition);
|
|
}
|
|
_enter(transition, result) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
this._entryCounter++;
|
|
this._isCurrent = true;
|
|
this._lastIncomingResults = result;
|
|
// Here we subscribe to all events
|
|
this._events.forEach(eventCont => {
|
|
this.sm.on(eventCont.event, eventCont.handler);
|
|
});
|
|
this._typedEvents.forEach(eventCont => {
|
|
eventCont.event.on(eventCont.handler);
|
|
});
|
|
// Here we emit the sm transition event since all the state is appropriate at this time
|
|
this.sm.stateChanged.emit(transition);
|
|
// We evaluate this state's onEntry method
|
|
if (this.onEntry) {
|
|
try {
|
|
const ret = this.onEntry(transition, result);
|
|
if (ret instanceof Promise) {
|
|
// If we have a done transition then we wait on the result from the promise
|
|
// and pass it into the next state
|
|
if (this._doneTransition) {
|
|
const entryCounterThisTime = this._entryCounter;
|
|
const retFromPromise = yield ret;
|
|
if (this._isCurrent && this._entryCounter === entryCounterThisTime) {
|
|
this._doneTransition.trigger(retFromPromise);
|
|
}
|
|
}
|
|
else {
|
|
ret.catch(error => {
|
|
this.sm._rejectCurrentPromise(error);
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
if (this._doneTransition) {
|
|
this._doneTransition.trigger(ret);
|
|
}
|
|
}
|
|
}
|
|
catch (e) {
|
|
this.sm._rejectCurrentPromise(e);
|
|
}
|
|
}
|
|
else {
|
|
if (this._doneTransition) {
|
|
this._doneTransition.trigger(null);
|
|
}
|
|
}
|
|
// We evaluate this state's transitions' onEntry methods
|
|
// There is a chance that we are not current anymore
|
|
if (this._isCurrent) {
|
|
for (let trans of this.transitions) {
|
|
trans._enter();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
_exit(transition) {
|
|
this._isExiting = true;
|
|
// Here we unsubscribe from all events
|
|
this._events.forEach(eventCont => {
|
|
this.sm.removeListener(eventCont.event, eventCont.handler);
|
|
});
|
|
this._typedEvents.forEach(eventCont => {
|
|
eventCont.event.removeListener(eventCont.handler);
|
|
});
|
|
let retPromise;
|
|
if (this.onExit) {
|
|
try {
|
|
const ret = this.onExit(transition);
|
|
if (ret instanceof Promise) {
|
|
retPromise = ret;
|
|
}
|
|
}
|
|
catch (e) {
|
|
this.sm._rejectCurrentPromise(e);
|
|
}
|
|
}
|
|
for (let trans of this.transitions) {
|
|
trans._exit();
|
|
}
|
|
let exitCleanup = () => {
|
|
this._exitCounter++;
|
|
this._isExiting = false;
|
|
this._isCurrent = false;
|
|
};
|
|
if (retPromise) {
|
|
return retPromise
|
|
.then(exitCleanup)
|
|
.catch(e => this.sm._rejectCurrentPromise(e));
|
|
}
|
|
else {
|
|
exitCleanup();
|
|
}
|
|
}
|
|
_stop() {
|
|
let retPromise;
|
|
this.transitions.forEach(trans => {
|
|
if (trans instanceof TimeoutTransition_1.TimeoutTransition) {
|
|
trans.stop();
|
|
}
|
|
});
|
|
// First we call onStop if it is defined
|
|
if (this.onStop) {
|
|
try {
|
|
const ret = this.onStop();
|
|
if (ret instanceof Promise) {
|
|
retPromise = ret;
|
|
}
|
|
}
|
|
catch (e) {
|
|
this.sm._rejectCurrentPromise(e);
|
|
}
|
|
}
|
|
if (!this._isExiting && this._exitCounter !== this._entryCounter) {
|
|
// We also call exit
|
|
const finalTransition = new Transition_1.Transition('FINAL TRANSITON', null);
|
|
finalTransition._setSourceState(this);
|
|
const callExit = () => {
|
|
let exitRet;
|
|
try {
|
|
exitRet = this._exit(finalTransition);
|
|
}
|
|
catch (e) {
|
|
this.sm._rejectCurrentPromise(e);
|
|
}
|
|
return exitRet;
|
|
};
|
|
// If we have a stop promise we chain this to that
|
|
if (retPromise) {
|
|
retPromise = retPromise.then(() => callExit());
|
|
}
|
|
else {
|
|
// Otherwise we just call Exit
|
|
let exitRet = callExit();
|
|
if (exitRet instanceof Promise) {
|
|
retPromise = exitRet;
|
|
}
|
|
}
|
|
}
|
|
if (retPromise instanceof Promise) {
|
|
return retPromise.then(() => undefined);
|
|
}
|
|
}
|
|
/**
|
|
* Retrieves the last result that was provided to this state upon entry
|
|
* @returns {Object}
|
|
*/
|
|
getLastIncomingResult() {
|
|
return this._lastIncomingResults;
|
|
}
|
|
/**
|
|
* Returns whether this is the current state of the state machine
|
|
* @return {boolean}
|
|
*/
|
|
isCurrent() {
|
|
return this._isCurrent;
|
|
}
|
|
transitionTo(arg1, arg2, arg3) {
|
|
let destState;
|
|
let name;
|
|
let result;
|
|
// If we are being given a transition name
|
|
if (typeof arg1 === 'string') {
|
|
name = arg1;
|
|
if (arg2 instanceof State) {
|
|
destState = arg2;
|
|
result = arg3;
|
|
}
|
|
else {
|
|
result = arg2;
|
|
}
|
|
}
|
|
else if (arg1 instanceof State) {
|
|
destState = arg1;
|
|
result = arg2;
|
|
}
|
|
// Here we find the internal transition that matches
|
|
const results = this._internals.filter(i => {
|
|
return ((name === undefined) || (name === i.name)) &&
|
|
((destState === undefined) || (destState === i.destState));
|
|
});
|
|
const destStateName = destState ? destState.name : 'unknown';
|
|
if (results.length === 0) {
|
|
throw new Error(`State '${this.name}': no internal transition of name '${name}' to state '${destStateName}' found`);
|
|
}
|
|
else if (results.length > 1) {
|
|
throw new Error(`State '${this.name}': has more than one matching internal transition: '${name}' to state '${destStateName}'`);
|
|
}
|
|
else {
|
|
const internal = results[0];
|
|
const internalDestStateName = internal.destState ? internal.destState.name : 'unknown';
|
|
const internalTransName = internal.transition ? internal.transition.name : 'unknown';
|
|
if (!this._isCurrent) {
|
|
let warn = `Attempting internal transition out of a non-current state. ` +
|
|
`This state: '${this.name}', target state: '${internalDestStateName}', ` +
|
|
`transition name: '${internalTransName}'. `;
|
|
if (this.sm.current) {
|
|
warn += `Current state: '${this.sm.current.name}'.`;
|
|
}
|
|
else {
|
|
warn += `The state machine is currently not running.`;
|
|
}
|
|
log.warn(warn);
|
|
}
|
|
else {
|
|
internal.transition.trigger(result);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Destroys the state. Should be called when state machine is getting discarded
|
|
*/
|
|
destroy() {
|
|
// this._isCurrent = false;
|
|
this._events.forEach(eventContainer => {
|
|
this.sm.removeListener(eventContainer.event, eventContainer.handler);
|
|
});
|
|
this._events.clear();
|
|
this._typedEvents.forEach(eventContainer => {
|
|
eventContainer.event.removeListener(eventContainer.handler);
|
|
});
|
|
this._typedEvents.clear();
|
|
this.transitions.forEach(trans => trans.destroy());
|
|
// this.transitions = [];
|
|
// this._internalTransitions.clear();
|
|
this._lastIncomingResults = null;
|
|
this.onEntry = null;
|
|
this.onExit = null;
|
|
this.onUpdate = null;
|
|
this.onStop = null;
|
|
}
|
|
toString() {
|
|
return 'State: ' + this.name;
|
|
}
|
|
}
|
|
exports.State = State;
|
|
// For unit tests
|
|
State.prototype.log = log;
|
|
|
|
},{"./TimeoutTransition":3,"./Transition":4,"./Utils":5,"./log":6}],2:[function(require,module,exports){
|
|
"use strict";
|
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
});
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const events = require("events");
|
|
const fs = require("fs");
|
|
const jibo_typed_events_1 = require("jibo-typed-events");
|
|
const Transition_1 = require("./Transition");
|
|
const Utils_1 = require("./Utils");
|
|
const log_1 = require("./log");
|
|
const log = log_1.default.createChild("StateMachine");
|
|
class StateMachine extends events.EventEmitter {
|
|
constructor() {
|
|
super();
|
|
this.states = new Map();
|
|
this.stateChanged = new jibo_typed_events_1.Event(`State transitioned`);
|
|
this._globalEvents = new Map();
|
|
this._globalTypedEvents = new Map();
|
|
this._inTransition = false;
|
|
this._loopDetection = new Map();
|
|
this._isStopped = false;
|
|
this._traceIndex = 0;
|
|
this._trace = [];
|
|
this._cleanupAll();
|
|
}
|
|
/**
|
|
* Adds a global event that will transition out of any state into a destination state
|
|
* @param {string} event - Name of event
|
|
* @param {State} destState - State to transition to
|
|
* @param {State[]} [exceptionStates] - States that this global transition does NOT apply to
|
|
* @returns {this}
|
|
* @memberof StateMachine
|
|
*/
|
|
addGlobalEventTransition(event, destState, exceptionStates = []) {
|
|
if (this._globalEvents.has(event)) {
|
|
throw new Error(`Duplicate global event transition. Event: '${event}'`);
|
|
}
|
|
this._globalEvents.set(event, {
|
|
event: event,
|
|
destination: destState,
|
|
exceptions: exceptionStates
|
|
});
|
|
// Add to all existing states
|
|
this.states.forEach(state => {
|
|
if (state !== destState && exceptionStates.indexOf(state) === -1) {
|
|
state._addEventTransition(event, destState, true);
|
|
}
|
|
});
|
|
return this;
|
|
}
|
|
/**
|
|
* Adds a global event that will transition out of any state into a destination state
|
|
* @param {Event} event - Typed event
|
|
* @param {State} destState - State to transition to
|
|
* @param {State[]} [exceptionStates] - States that this global transition does NOT apply to
|
|
* @returns {this}
|
|
* @memberof StateMachine
|
|
*/
|
|
addGlobalTypedEventTransition(event, destState, exceptionStates = []) {
|
|
if (this._globalTypedEvents.has(event)) {
|
|
throw new Error(`Duplicate global typed event transition. Event: '${event.name}'`);
|
|
}
|
|
this._globalTypedEvents.set(event, {
|
|
event: event,
|
|
destination: destState,
|
|
exceptions: exceptionStates
|
|
});
|
|
// Add to all existing states
|
|
this.states.forEach(state => {
|
|
if (state !== destState && exceptionStates.indexOf(state) === -1) {
|
|
state._addTypedEventTransition(event, destState, true);
|
|
}
|
|
});
|
|
return this;
|
|
}
|
|
/**
|
|
* Rejects currently running start promise
|
|
* @param {string|Error} error
|
|
* @private
|
|
*/
|
|
_rejectCurrentPromise(error) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (this._promiseReject) {
|
|
this._promiseReject(error);
|
|
}
|
|
else {
|
|
log.warn(`Reject being called when SM isn't running, with error: `, error);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Resoles currently running start promise
|
|
* @param {object} data
|
|
* @private
|
|
*/
|
|
_resolveCurrentPromise(data) {
|
|
if (this._promiseResolve) {
|
|
this._promiseResolve(data);
|
|
}
|
|
else {
|
|
log.warn(`Resolve called when SM isn't running, with data: `, data);
|
|
}
|
|
}
|
|
/**
|
|
* Destroys the state machine and all of its states.
|
|
* Should be called when state machine is getting discarded
|
|
*/
|
|
destroy() {
|
|
const destroy = () => {
|
|
this._globalEvents.clear();
|
|
this._globalTypedEvents.clear();
|
|
this.states.forEach(state => {
|
|
state.destroy();
|
|
});
|
|
this.states.clear();
|
|
this._cleanupAll();
|
|
};
|
|
if (this._currentRunPromise) {
|
|
return this.stop().then(destroy);
|
|
}
|
|
else {
|
|
return Promise.resolve().then(destroy);
|
|
}
|
|
}
|
|
/**
|
|
* Returns current state of state machine
|
|
* @returns {State}
|
|
*/
|
|
getCurrentState() {
|
|
return this.current;
|
|
}
|
|
/**
|
|
* Sets the initial state of the state machine
|
|
* @param state
|
|
* @returns {StateMachine}
|
|
*/
|
|
setInitial(state) {
|
|
this.initial = state;
|
|
return this;
|
|
}
|
|
/**
|
|
* Starts the state machine. This is required to be called once.
|
|
* @param {Object} [input] - Optional input data
|
|
* @returns {Promise} resolved when any state throws an error or calls this.exit();
|
|
*/
|
|
start(input) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (!this.initial) {
|
|
return Promise.reject(new Error(`State machine does not yet have an initial state`));
|
|
}
|
|
else if (this._currentRunPromise) {
|
|
return Promise.reject(new Error(`State machine already running, please call stop first`));
|
|
}
|
|
this._currentRunPromise = new Promise((resolve, reject) => {
|
|
this._promiseResolve = resolve;
|
|
this._promiseReject = reject;
|
|
}).then((data) => {
|
|
this._cleanupAfterStart();
|
|
return data;
|
|
}).catch((e) => __awaiter(this, void 0, void 0, function* () {
|
|
try {
|
|
yield this.stop();
|
|
}
|
|
catch (stopError) {
|
|
log.error(`Error while stopping state machine because of an earlier error`);
|
|
log.error(stopError);
|
|
}
|
|
finally {
|
|
this._cleanupAfterStart();
|
|
throw e;
|
|
}
|
|
}));
|
|
this._cleanupBeforeStart();
|
|
this.current = this.initial;
|
|
this.current._isCurrent = true;
|
|
let initialTransition = new Transition_1.Transition('INITIAL TRANSITON', this.initial);
|
|
this._addToTrace(initialTransition);
|
|
this.current._enter(initialTransition, input);
|
|
return this._currentRunPromise;
|
|
});
|
|
}
|
|
/**
|
|
* Informs whether the state machine is currently running
|
|
* @return {boolean} True if the state machine is currently running
|
|
*/
|
|
isRunning() {
|
|
return (!!this._currentRunPromise);
|
|
}
|
|
/**
|
|
* Waits until state machine is finished. If it isn't running, it resolves promise immediately
|
|
* @return {Promise<any>}
|
|
*/
|
|
waitUntilFinished() {
|
|
if (this._currentRunPromise) {
|
|
return this._currentRunPromise;
|
|
}
|
|
else {
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
_addState(state) {
|
|
if (this.states.has(state.name)) {
|
|
this._rejectCurrentPromise(new Error(`State with name '${state.name}' already exists`));
|
|
return;
|
|
}
|
|
if (!this.initial) {
|
|
this.setInitial(state);
|
|
}
|
|
// Here we add all current global transitions to this state
|
|
this._globalEvents.forEach(event => {
|
|
if (event.destination !== state && event.exceptions.indexOf(state) === -1) {
|
|
state._addEventTransition(event.event, event.destination, true);
|
|
}
|
|
});
|
|
this._globalTypedEvents.forEach(event => {
|
|
if (event.destination !== state && event.exceptions.indexOf(state) === -1) {
|
|
state._addTypedEventTransition(event.event, event.destination, true);
|
|
}
|
|
});
|
|
this.states.set(state.name, state);
|
|
return this;
|
|
}
|
|
/**
|
|
* Get state by name
|
|
* @param {string} name
|
|
* @returns {State}
|
|
*/
|
|
getState(name) {
|
|
return this.states.get(name);
|
|
}
|
|
_addToTrace(transition) {
|
|
const te = {
|
|
transition: transition,
|
|
timestamp: Date.now(),
|
|
};
|
|
if (this._trace.length < StateMachine.TRACE_MAX_LENGTH) {
|
|
this._trace.push(te);
|
|
}
|
|
else {
|
|
this._trace[this._traceIndex] = te;
|
|
this._traceIndex = (this._traceIndex + 1) % StateMachine.TRACE_MAX_LENGTH;
|
|
}
|
|
}
|
|
/**
|
|
* Updates the current state, if it has an onUpdate callback.
|
|
* Normal functionality of the StateMachine does not require that you call this, only if your
|
|
* state should be connected to an update loop.
|
|
*/
|
|
update() {
|
|
if (this.current && this.current.onUpdate) {
|
|
this.current.onUpdate();
|
|
}
|
|
}
|
|
/**
|
|
* Stops the current state (calls its onStop callback).
|
|
* @returns {Promise} resolved when stop is complete
|
|
*/
|
|
stop(data) {
|
|
// If we have a current run instance
|
|
if (this._currentRunPromise) {
|
|
this._isStopped = true;
|
|
if (this.current) {
|
|
this._isStopped = true;
|
|
const stopRet = this.current._stop();
|
|
// We wait for the exit promise to complete if it exists
|
|
const exitWait = this._currentStateExitPromise || Promise.resolve();
|
|
if (stopRet instanceof Promise) {
|
|
return exitWait.then(() => stopRet).then(() => {
|
|
this.current = null;
|
|
this._resolveCurrentPromise(data);
|
|
});
|
|
}
|
|
else {
|
|
return exitWait.then(() => {
|
|
this.current = null;
|
|
this._resolveCurrentPromise(data);
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
else {
|
|
// Stop being called but SM wasn't running
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
/**
|
|
* Returns the trace of transitions leading up to the current one (limited
|
|
* by StateMachine.TRACE_MAX_LENGTH)
|
|
* @returns {TraceElement[]}
|
|
*/
|
|
getTrace() {
|
|
let out = [];
|
|
if (this._trace.length < StateMachine.TRACE_MAX_LENGTH) {
|
|
for (let i = 0; i < this._trace.length; i++) {
|
|
out.push(this._trace[i]);
|
|
}
|
|
}
|
|
else {
|
|
for (let i = 0; i < this._trace.length; i++) {
|
|
let ind = (this._traceIndex + i) % StateMachine.TRACE_MAX_LENGTH;
|
|
out.push(this._trace[ind]);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
/**
|
|
* Provides a convenient string representation of getTrace()
|
|
* @returns {string}
|
|
*/
|
|
traceToString() {
|
|
let trace = this.getTrace();
|
|
let str = '';
|
|
trace.forEach(t => {
|
|
let sName = t.transition.getSourceState() ? t.transition.getSourceState().name : 'none';
|
|
str += `Source: '${sName}' -> Transition: '${t.transition.name}' @ ${t.timestamp} ` +
|
|
`-> Destination: '${t.transition.getDestinationState().name}' \n`;
|
|
});
|
|
return str;
|
|
}
|
|
/**
|
|
* Transitions using a particular transition
|
|
* @param transition
|
|
* @param result
|
|
* @private
|
|
*/
|
|
_transitionTo(transition, result) {
|
|
if (this._isStopped) {
|
|
return;
|
|
}
|
|
// if the state machine is stopped, do nothing
|
|
if (!this.current) {
|
|
return;
|
|
}
|
|
// See if this is the first in sequence of continuous transitions
|
|
let isThisFirstTransition = !this._inTransition;
|
|
if (isThisFirstTransition) {
|
|
this._inTransition = true;
|
|
}
|
|
// Here we look for loops
|
|
let destName = transition.getDestinationState().name;
|
|
let loopCounter = this._loopDetection.get(destName);
|
|
if (!loopCounter) {
|
|
this._loopDetection.set(destName, 1);
|
|
}
|
|
else if (loopCounter > StateMachine.MAX_LOOPS) {
|
|
return this._rejectCurrentPromise(new Error(`State machine has looped '${loopCounter}' times in ` +
|
|
`one transition. Next state: '${destName}'`));
|
|
}
|
|
else {
|
|
this._loopDetection.set(destName, loopCounter + 1);
|
|
}
|
|
this._addToTrace(transition);
|
|
if (transition.getSourceState() !== this.current) {
|
|
this._rejectCurrentPromise(new Error(`Can't apply transition '${transition}' since the `
|
|
+ `current state is '${this.current}'`));
|
|
}
|
|
// Notify current state and transitions of exit
|
|
const exitDone = () => {
|
|
this._currentStateExitPromise = null;
|
|
if (!this._isStopped) {
|
|
// Notify next state and transitions of entry
|
|
this.current = transition.getDestinationState();
|
|
this.current._enter(transition, result);
|
|
// When leaving the first transition in a sequence, we clear our loop
|
|
// detection mechanism
|
|
if (isThisFirstTransition) {
|
|
this._inTransition = false;
|
|
this._loopDetection.clear();
|
|
}
|
|
}
|
|
};
|
|
const exitRet = this.current._exit(transition);
|
|
if (exitRet instanceof Promise) {
|
|
this._currentStateExitPromise = exitRet;
|
|
return exitRet.then(exitDone).catch(e => this._rejectCurrentPromise(e));
|
|
}
|
|
else {
|
|
exitDone();
|
|
}
|
|
}
|
|
/**
|
|
* Produces a .dot file from current state graph
|
|
* Writes dotfile to disk at filepath
|
|
* @param {string} filePath
|
|
* @returns {string}
|
|
*/
|
|
toDotFile(filePath) {
|
|
const regBgColor = "lightgrey";
|
|
const globalBgColor = "#a0a0a0";
|
|
let lines = ['digraph graphname {'];
|
|
this.states.forEach(state => {
|
|
let name = state.name.replace(/\"/ig, '\\"');
|
|
let lineColor = (state === this.initial) ? 'black' : regBgColor;
|
|
lines.push(`"${name}" [style=filled,fillcolor="${regBgColor}",color="${lineColor}"];`);
|
|
});
|
|
// Here we handle all global transitions
|
|
if (this._globalEvents.size > 0 || this._globalTypedEvents.size > 0) {
|
|
let globalName = "Any state";
|
|
lines.push(`"${globalName}" [shape=box,style=filled,fillcolor="${globalBgColor}",color=black];`);
|
|
this._globalEvents.forEach(event => {
|
|
let targetName = event.destination.name.replace(/\"/ig, '\\"');
|
|
let transitionName = Utils_1.createEventTransitionName(event.event).replace(/\"/ig, '\\"');
|
|
lines.push(` "${globalName}" -> "${targetName}" [label = "${transitionName}"];`);
|
|
});
|
|
this._globalTypedEvents.forEach(event => {
|
|
let targetName = event.destination.name.replace(/\"/ig, '\\"');
|
|
let transitionName = Utils_1.createTypedEventTransitionName(event.event).replace(/\"/ig, '\\"');
|
|
lines.push(` "${globalName}" -> "${targetName}" [label = "${transitionName}"];`);
|
|
});
|
|
}
|
|
// Here we handle all non-global transitions
|
|
this.states.forEach(state => {
|
|
state.transitions.forEach((transition) => {
|
|
if (!transition._isGlobal) {
|
|
let sourceStateName = state.name.replace(/\"/ig, '\\"');
|
|
let targetName = transition.getDestinationState().name.replace(/\"/ig, '\\"');
|
|
let transitionName = transition.name.replace(/\"/ig, '\\"');
|
|
lines.push(` "${sourceStateName}" -> "${targetName}" [label = "${transitionName}"];`);
|
|
}
|
|
});
|
|
});
|
|
lines.push('}');
|
|
const str = lines.join('\n');
|
|
if (filePath) {
|
|
fs.writeFileSync(filePath, str, { encoding: 'utf8' });
|
|
}
|
|
return str;
|
|
}
|
|
_cleanupAll() {
|
|
this._cleanupBeforeStart();
|
|
this._cleanupAfterStart();
|
|
}
|
|
_cleanupAfterStart() {
|
|
this._isStopped = false;
|
|
this._currentRunPromise = null;
|
|
this._currentStateExitPromise = null;
|
|
this._promiseResolve = null;
|
|
this._promiseReject = null;
|
|
this._inTransition = false;
|
|
}
|
|
_cleanupBeforeStart() {
|
|
this._isStopped = false;
|
|
this._inTransition = false;
|
|
this._traceIndex = 0;
|
|
this._trace = [];
|
|
this._loopDetection.clear();
|
|
}
|
|
}
|
|
StateMachine.TRACE_MAX_LENGTH = 20;
|
|
StateMachine.MAX_LOOPS = 5;
|
|
exports.StateMachine = StateMachine;
|
|
|
|
},{"./Transition":4,"./Utils":5,"./log":6,"events":undefined,"fs":undefined,"jibo-typed-events":undefined}],3:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const Transition_1 = require("./Transition");
|
|
class TimeoutTransition extends Transition_1.Transition {
|
|
constructor(name, destState, timeMs) {
|
|
super(name, destState);
|
|
this.onEntry = () => {
|
|
this._timeoutHandle = setTimeout(() => {
|
|
this._timeoutHandle = null;
|
|
this.trigger();
|
|
}, timeMs);
|
|
};
|
|
this.onExit = () => {
|
|
if (this._timeoutHandle) {
|
|
clearTimeout(this._timeoutHandle);
|
|
this._timeoutHandle = null;
|
|
}
|
|
};
|
|
}
|
|
stop() {
|
|
this.onExit();
|
|
}
|
|
}
|
|
exports.TimeoutTransition = TimeoutTransition;
|
|
|
|
},{"./Transition":4}],4:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
class Transition {
|
|
constructor(name, _destState) {
|
|
this.name = name;
|
|
this._destState = _destState;
|
|
// Is only used for global states and only matters for dot visualization
|
|
this._isGlobal = false;
|
|
}
|
|
/**
|
|
* Called to trigger the current transition
|
|
* @param {object} [result] to pass to next state
|
|
*/
|
|
trigger(result) {
|
|
if (this._sourceState) {
|
|
this._sourceState.sm._transitionTo(this, result);
|
|
}
|
|
}
|
|
_enter() {
|
|
if (this.onEntry) {
|
|
this.onEntry();
|
|
}
|
|
}
|
|
_exit() {
|
|
if (this.onExit) {
|
|
this.onExit();
|
|
}
|
|
}
|
|
/**
|
|
* The source state for this transition
|
|
* @returns {State}
|
|
*/
|
|
getSourceState() {
|
|
return this._sourceState;
|
|
}
|
|
/**
|
|
* The destination for this transition
|
|
* @returns {State}
|
|
*/
|
|
getDestinationState() {
|
|
return this._destState;
|
|
}
|
|
/**
|
|
* Sets the source state, should only be called when adding transition to
|
|
* the source state.
|
|
* Can only be called once per transition
|
|
* @param sourceState
|
|
* @private
|
|
*/
|
|
_setSourceState(sourceState) {
|
|
if (this._sourceState) {
|
|
this._sourceState.sm._rejectCurrentPromise(new Error(`Transition '${this}' already has source state`));
|
|
}
|
|
this._sourceState = sourceState;
|
|
}
|
|
/**
|
|
* Destroys the transition. Should be called when state machine is getting discarded
|
|
*/
|
|
destroy() {
|
|
this.onEntry = null;
|
|
this.onExit = null;
|
|
this._sourceState = null;
|
|
}
|
|
toString() {
|
|
return this.name;
|
|
}
|
|
}
|
|
exports.Transition = Transition;
|
|
|
|
},{}],5:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
function createTypedEventTransitionName(event) {
|
|
return 'TEvent: ' + event.name;
|
|
}
|
|
exports.createTypedEventTransitionName = createTypedEventTransitionName;
|
|
function createEventTransitionName(event) {
|
|
return 'Event: ' + event;
|
|
}
|
|
exports.createEventTransitionName = createEventTransitionName;
|
|
|
|
},{}],6:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const jibo_log_1 = require("jibo-log");
|
|
exports.default = new jibo_log_1.Log('StateMachine');
|
|
|
|
},{"jibo-log":undefined}],7:[function(require,module,exports){
|
|
"use strict";
|
|
function __export(m) {
|
|
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
|
|
}
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
__export(require("./StateMachine"));
|
|
__export(require("./Transition"));
|
|
__export(require("./State"));
|
|
__export(require("./TimeoutTransition"));
|
|
|
|
},{"./State":1,"./StateMachine":2,"./TimeoutTransition":3,"./Transition":4}]},{},[7])(7)
|
|
});
|
|
|
|
//# sourceMappingURL=jibo-state-machine.js.map
|