jocly/src/browser/jocly.state-machine.js
2017-03-29 17:30:37 +02:00

476 lines
13 KiB
JavaScript

/* Copyright 2017 Jocly
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the GNU Affero General Public License in all respects
* for all of the code used other than as permitted herein. If you modify
* file(s) with this exception, you may extend this exception to your
* version of the file(s), but you are not obligated to do so. If you do not
* wish to do so, delete this exception statement from your version. If you
* delete this exception statement from all source files in the program,
* then also delete it in the license file.
*/
function JHStateMachine() {
}
JHStateMachine.prototype={}
JHStateMachine.prototype.init=function() {
this.smState=null;
this.smStates={};
this.smEventQueue=[];
this.smScheduled=false;
this.smPauseNotified=false;
this.smPaused=true;
this.smHistory=[];
this.smGroups={};
}
JHStateMachine.prototype.smDebug=function() {}
JHStateMachine.prototype.smWarning=function() {}
JHStateMachine.prototype.smError=function() {}
JHStateMachine.prototype.smTransition=function(states,events,newState,methods) {
states=this.smSolveStates(states);
if(typeof(events)=="string") {
events=[events];
}
if(typeof(methods)=="string") {
methods=[methods];
}
for(var s in states) {
var stateName=states[s];
if(typeof(this.smStates[stateName])=="undefined") {
this.smStates[stateName]={
transitions: {},
enteringMethods: [],
leavingMethods: []
}
}
for(var e in events) {
var eventName=events[e];
if(typeof(this.smStates[stateName].transitions[eventName])=="undefined") {
this.smStates[stateName].transitions[eventName]={
state: (newState!=null)?newState:stateName,
methods: []
};
}
for(var m in methods) {
var methodName=methods[m];
this.smStates[stateName].transitions[eventName].methods.push(methodName);
}
}
}
if(newState!=null && typeof(this.smStates[newState])=="undefined") {
this.smStates[newState]={
transitions: {},
enteringMethods: [],
leavingMethods: []
}
}
}
JHStateMachine.prototype.smEntering=function(states,methods) {
if(typeof(states)=="string") {
states=[states];
}
if(typeof(methods)=="string") {
methods=[methods];
}
for(var s in states) {
var stateName=states[s];
if(typeof(this.smStates[stateName])=="undefined") {
this.smStates[stateName]={
transitions: {},
enteringMethods: [],
leavingMethods: []
}
}
for(var m in methods) {
var methodName=methods[m];
this.smStates[stateName].enteringMethods.push(methodName);
}
}
}
JHStateMachine.prototype.smLeaving=function(states,methods) {
if(typeof(states)=="string") {
states=[states];
}
if(typeof(methods)=="string") {
methods=[methods];
}
for(var s in states) {
var stateName=states[s];
if(typeof(this.smStates[stateName])=="undefined") {
this.smStates[stateName]={
transitions: {},
enteringMethods: [],
leavingMethods: []
}
}
for(var m in methods) {
var methodName=methods[m];
this.smStates[stateName].leavingMethods.push(methodName);
}
}
}
JHStateMachine.prototype.smStateGroup=function(group,states) {
if(typeof(states)=="string")
states=[states];
if(typeof(this.smGroups[group])=="undefined")
this.smGroups[group]=[];
states=this.smSolveStates(states);
for(var i in states) {
var state=states[i];
if(!this.smContained(state,this.smGroups[group]))
this.smGroups[group].push(state);
}
}
JHStateMachine.prototype.smSetInitialState=function(state) {
this.smState=state;
}
JHStateMachine.prototype.smGetState=function() {
return this.smState;
}
JHStateMachine.prototype.smHandleEvent=function(event,args) {
if(typeof(this.smStates[this.smState])=="undefined") {
console.error("Unknown state '",this.smState,"'");
return;
}
var hEntry={
date: new Date().getTime(),
fromState: this.smState,
event: event,
methods: []
}
try {
hEntry.args=JSON.stringify(args);
} catch(e) {
//console.error("handleEvent(event,...) JSON.stringify(args): ",e);
}
var transition=this.smStates[this.smState].transitions[event];
if(typeof(transition)=="undefined") {
console.warn("JHStateMachine: Event '",event,"' not handled in state '",this.smState,"'");
return;
}
this.smCurrentEvent=event;
var stateChanged=(this.smState!=transition.state);
if(stateChanged) {
var leavingMethods=this.smStates[this.smState].leavingMethods;
for(var i in leavingMethods) {
try {
hEntry.methods.push(leavingMethods[i]);
if(typeof leavingMethods[i]=="function")
leavingMethods[i].call(this,args);
else
this['$'+leavingMethods[i]](args);
} catch(e) {
console.error("Exception in leaving [",this.smState,"] --> "+
(typeof leavingMethods[i]=="function"?leavingMethods[i].name:leavingMethods[i])
+"(",args,"): ",e);
throw e;
}
}
}
for(var i in transition.methods) {
try {
hEntry.methods.push(transition.methods[i]);
if(typeof transition.methods[i]=="function")
transition.methods[i].call(this,args);
else
this['$'+transition.methods[i]](args);
} catch(e) {
console.error("Exception in ["+this.smState+"] -- "+event+" --> "+
(typeof transition.methods[i]=="function"?transition.methods[i].name:transition.methods[i])
+"(",args,"): ",
e);
throw e;
}
}
this.smJHStateMachineLeavingState(this.smState,event,args);
this.smDebug("{",this.smState,"} == [",event,"] ==> {",transition.state,"}");
this.smState=transition.state;
if(stateChanged) {
var enteringMethods=this.smStates[this.smState].enteringMethods;
for(var i in enteringMethods) {
try {
hEntry.methods.push(enteringMethods[i]);
if(typeof enteringMethods[i]=="function")
enteringMethods[i].call(this,args);
else
this['$'+enteringMethods[i]](args);
} catch(e) {
console.error("Exception in entering ["+this.smState+"] --> "+
(typeof enteringMethods[i]=="function"?enteringMethods[i].name:enteringMethods[i])
+"(",args,"): ",e);
throw e;
}
}
}
this.smCurrentEvent=null;
this.smJHStateMachineEnteringState(this.smState,event,args);
hEntry.toState=this.smState;
this.smHistory.splice(0,0,hEntry);
while(this.smHistory.length>50)
this.smHistory.pop();
}
JHStateMachine.prototype.smPlay=function() {
var $this=this;
if(this.smPaused) {
this.smPaused=false;
setTimeout(function() {
$this.smRun();
},0);
}
}
JHStateMachine.prototype.smPause=function() {
this.smPaused=true;
}
JHStateMachine.prototype.smStep=function() {
this.smPauseNotified=false;
if(this.smEventQueue.length>0) {
var eventItem=this.smEventQueue.shift();
this.smHandleEvent(eventItem.event,eventItem.args);
}
this.smNotifyPause();
}
JHStateMachine.prototype.smRun=function() {
this.smScheduled=false;
var stepCount=0;
while(this.smEventQueue.length>0) {
if(this.smPaused) {
this.smRunEnd(stepCount);
return;
} else {
stepCount++;
this.smStep();
}
}
while(this.smPaused==false && this.smEventQueue.length>0) {
stepCount++;
this.smStep();
}
this.smRunEnd(stepCount);
}
JHStateMachine.prototype.smRunEnd=function() {
}
JHStateMachine.prototype.smQueueEvent=function(event,args) {
var self=this;
this.smEventQueue.push({event: event, args: args});
this.smNotifyPause();
if(!this.smScheduled) {
this.smScheduled=true;
setTimeout(function() {
self.smRun();
},0);
}
}
JHStateMachine.prototype.smNotifyPause=function() {
if(this.smEventQueue.length>0 && this.smPaused==true) {
var item=this.smEventQueue[0];
this.smJHStateMachinePaused(item.event,item.args);
}
}
JHStateMachine.prototype.smJHStateMachineEnteringState=function(state,event,args) {
}
JHStateMachine.prototype.smJHStateMachineLeavingState=function(state,event,args) {
}
JHStateMachine.prototype.smJHStateMachinePaused=function(state,event,args) {
}
JHStateMachine.prototype.smGetTable=function() {
var cells={}
for(var s in this.smStates) {
var state=this.smStates[s];
for(var e in state.transitions) {
var toState=state.transitions[e].state;
var cellname=s+"/"+toState;
if(typeof(cells[cellname])=="undefined") {
cells[cellname]={};
}
cells[cellname][e]=[];
if(s!=toState) {
for(var m in state.leavingMethods) {
cells[cellname][e].push(state.leavingMethods[m]);
}
}
for(var m in state.transitions[e].methods) {
cells[cellname][e].push(state.transitions[e].methods[m]);
}
if(s!=toState) {
for(var m in this.smStates[toState].enteringMethods) {
cells[cellname][e].push(this.smStates[toState].enteringMethods[m]);
}
}
}
}
var table=["<table><tr><td></td>"];
for(var s in this.smStates) {
table.push("<td class='state'>"+s+"</td>");
}
table.push("</tr>");
for(var s1 in this.smStates) {
table.push("<tr><td class='state'>"+s1+"</td>");
var state1=this.smStates[s1];
for(var s2 in this.smStates) {
var state2=this.smStates[s2];
var cellname=s1+"/"+s2;
if(typeof(cells[cellname])=="undefined") {
table.push("<td class='empty'></td>");
} else {
table.push("<td class='transition'>");
for(var e in cells[cellname]) {
table.push("<div class='event'>");
table.push("<div class='eventname'>"+e+"</div>");
for(var m in cells[cellname][e]) {
table.push("<div class='method'>"+cells[cellname][e][m]+"</div>");
}
table.push("</div>");
}
table.push("</td>");
}
}
table.push("</tr>");
}
table.push("</table>");
return table.join("");
}
JHStateMachine.prototype.smGetHistoryTable=function() {
var table=["<table><tr><th>Date</th><th>To</th><th>Event</th><th>Methods</th><th>From</th></tr>"];
for(var i in this.smHistory) {
var hEntry=this.smHistory[i];
table.push("<tr>");
var date=new Date(hEntry.date);
var timestamp=date.getHours()+":"+date.getMinutes()+":"+date.getSeconds()+"."+date.getMilliseconds();
table.push("<td class='timestamp'>"+timestamp+"</td>");
table.push("<td class='to'>"+hEntry.toState+"</td>");
table.push("<td><div class='event'>"+hEntry.event+"</div><div class='args'>("+hEntry.args+")</div></td>");
table.push("<td class='methods'>");
for(var j in hEntry.methods) {
table.push(hEntry.methods[j]+"<br/>");
}
table.push("</td>");
table.push("<td class='from'>"+hEntry.fromState+"</td>");
table.push("</tr>");
}
table.push("</table>");
return table.join("");
}
JHStateMachine.prototype.smSolveStates=function(states) {
var states0=[];
if(typeof(states)=="string") {
states=[states];
}
for(var s in states) {
var state=states[s];
if(typeof(this.smGroups[state])=="undefined") {
if(!this.smContained(state,states0))
states0.push(state);
} else {
for(var s0 in this.smGroups[state])
if(!this.smContained(this.smGroups[state][s0]),states0)
states0.push(this.smGroups[state][s0]);
}
}
return states0;
}
JHStateMachine.prototype.smContained=function(state,group) {
for(var i in group) {
if(state==group[i])
return true;
}
return false;
}
JHStateMachine.prototype.smCheck=function() {
var result={
missing: [],
unused: []
}
var existingFnt=[];
for(var s in this.smStates) {
for(var i in this.smStates[s].enteringMethods) {
var fnt=this.smStates[s].enteringMethods[i];
existingFnt[fnt]=true;
}
for(var i in this.smStates[s].leavingMethods) {
var fnt=this.smStates[s].leavingMethods[i];
existingFnt[fnt]=true;
}
for(var e in this.smStates[s].transitions) {
var event=this.smStates[s].transitions[e];
for(var i in event.methods) {
var fnt=event.methods[i];
existingFnt[fnt]=true;
}
}
}
for(var fnt in existingFnt) {
if(typeof(this['$'+fnt])!="function") {
result.missing.push(fnt);
console.error("JHStateMachine: missing function $",fnt);
}
}
for(var k in this) {
try {
if(k[0]=='$' && typeof(this[k])=="function") {
var fnt=k.substr(1);
if(typeof(existingFnt[fnt])=="undefined") {
//this.warning("JHStateMachine.check "+this.target.name+": unused function "+k);
result.unused.push(fnt);
}
}
} catch(e) {}
}
return result;
}