/* 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 . * * 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=[""]; for(var s in this.smStates) { table.push(""); } table.push(""); for(var s1 in this.smStates) { table.push(""); 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(""); } else { table.push(""); } } table.push(""); } table.push("
"+s+"
"+s1+""); for(var e in cells[cellname]) { table.push("
"); table.push("
"+e+"
"); for(var m in cells[cellname][e]) { table.push("
"+cells[cellname][e][m]+"
"); } table.push("
"); } table.push("
"); return table.join(""); } JHStateMachine.prototype.smGetHistoryTable=function() { var table=[""]; for(var i in this.smHistory) { var hEntry=this.smHistory[i]; table.push(""); var date=new Date(hEntry.date); var timestamp=date.getHours()+":"+date.getMinutes()+":"+date.getSeconds()+"."+date.getMilliseconds(); table.push(""); table.push(""); table.push(""); table.push(""); table.push(""); table.push(""); } table.push("
DateToEventMethodsFrom
"+timestamp+""+hEntry.toState+"
"+hEntry.event+"
("+hEntry.args+")
"); for(var j in hEntry.methods) { table.push(hEntry.methods[j]+"
"); } table.push("
"+hEntry.fromState+"
"); 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; }