Open SimonGAndrews opened 2 years ago
While working issue #14 (determining initial states on compound state nodes) it was concluded that the stateMap was an optimal place to store the initial state for any compound state nodes. In this way each compound states initial is evaluated just one time when the machine is created. Thus reducing the run time effort on transitions. So getStateMap now contains the calls to getInitial().
/* fsmPlus ref issue 15 - returns a map of all state nodes in a machine
* in the form of an object with a key set to ID of each state node (custom ID or full def ID).
* the keys value is an object with optional properties:
* - pathID: full state ID if state node ID is a custom ID
* - initial: id of the state nodes initial state/substate when node is compound
* - type: M machine, C Compound, A Atomic,
* a recursive function , Called with arg obj set to machine config object, other args are empty on initial call
* see functions: validateStateID(), for usage
*/
function getStateMap(obj,sofar,stateMap){ // recursive function to generate stateMap from fsmConfig
if (!obj) throw new Error('(getStateMap) - called without a configuration object');
if (!sofar) { //assume calling at top of config (machine)
stateMap = {};
if (! ('id' in obj)) {throw new Error ("(getStateMap) - Machine config requires 'id' at top level");}
sofar = '#' + obj.id;
if (!('initial' in obj) && 'states' in obj ) {throw new Error ("(getStateMap) - Machine " + sofar + " requires 'initial' state at top level");}
stateMap[sofar]={type:'M',initial:getInitial(sofar,obj)};
}
if ('states' in obj) {
Object.keys(obj.states).forEach((k) => {
let node="";
if ('id' in obj.states[k]) { // add node using Custom ID as key with a pathID property
node = '#'+ obj.states[k].id;
stateMap[node] = Object.assign({},{pathID: sofar +'.'+ k});
} else { // add node using full state ID as key no pathID
node = sofar +'.'+ k;
stateMap[node]= {};
}
if ('states' in obj.states[k]) { //Compound state node - go down to substates
stateMap[node].type = 'C';
stateMap[node].initial = getInitial(sofar +'.'+ k,obj.states[k]);
getStateMap(obj.states[k],sofar +'.'+ k,stateMap);
}else { //Atomic state node
stateMap[node].type = 'A';
return stateMap
}
});
}
return stateMap;
}
the following function, validateStateID, uses the resulting stateMap to validate a given statenode exists in the config/stateMap, in addition the function converts custom IDs to sull state IDs and returns the initial state of a statenode when called with mapInitial = true.
/** fsmPlus - validates/converts and returns full state ID from a state reference
* using lookups into stateMap. expects arg stateRef is in form of a full state id or a custom id
* - in all cases returns null if resolved state does not exist in stateMap
* - returns stateRef as full ID if exists as key in stateMap
* - returns full ID lookup in stateMap if stateRef is a custom ID
* - returns stateRef as full ID if stateRef is pathID of a custom ID
* - returns lookup of initial in stateMap when mapInitial is true (used validating target paths)
*/
function validateStateID(stateRef,stateMap,mapInitial) {
if (!stateRef || !stateMap) return null;
if (!(stateMap[stateRef])){
/* stateRef not a full state ID - maybe a custom id */
let id = '';
if (id = Object.keys(stateMap).find( k => stateMap[k].pathID === stateRef)){
/* return key or initial of a custom id */
if (mapInitial && stateMap[id].initial) return stateMap[id].initial;
return stateMap[id].pathID;
}
else return null ;
}
else if (mapInitial && stateMap[stateRef].initial) return stateMap[stateRef].initial;
return (stateMap[stateRef].pathID) ? stateMap[stateRef].pathID : stateRef;
}
example of stateMap for this machine
carBuildMachine1 = { // baseline case
id: 'buildCar',
initial: 'chassie',
states: {
chassie: {
initial: 'axle',
on:{SCRAP: '#buildCar.scrap'},
states:{
axle: { on:{DONE: '#buildCar.body', RESET:'#buildCar.chassie.engine'}},
engine: {
initial: 'transmission',
on:{SCRAP: '#buildCar.chassie.engine.scrapEngine'},
states:{
block:{id:'engineBlock', on:{DONE: 'transmission'}},
transmission:{ on:{DONE: '#buildCar.chassie.axle'}},
scrapEngine:{}
}
}
},
},
body: {
id:'body',
type:'atomic',
initial:'aType',
states: {aType:{},bType:{}}
},
scrap: {}
}
};
stateMap is
{
'#buildCar': { type: 'M', initial: '#buildCar.chassie.axle' },
'#buildCar.chassie': { type: 'C', initial: '#buildCar.chassie.axle' },
'#buildCar.chassie.axle': { type: 'A' },
'#buildCar.chassie.engine': { type: 'C', initial: '#buildCar.chassie.engine.transmission' },
'#engineBlock': { pathID: '#buildCar.chassie.engine.block', type: 'A' },
'#buildCar.chassie.engine.transmission': { type: 'A' },
'#buildCar.chassie.engine.scrapEngine': { type: 'A' },
'#body': {
pathID: '#buildCar.body',
type: 'C',
initial: '#buildCar.body.aType'
},
'#buildCar.body.aType': { type: 'A' },
'#buildCar.body.bType': { type: 'A' },
'#buildCar.scrap': { type: 'A' }
}
unit test stateID.test.js is used to test this functionality.
Functionality is required to:
A solution approach was chosen using an initial pass thru the machine config to store state values and custom ids with thier corresponding paths (full state value Ids). A recursive function getStateMap() creates a state mapping object with keys identifying the state ids (inc custom ids) with values containing the paths to those ids. The resulting state mapping object stateMap is built upon machine creation. using getStateMap(), and attached to machine.initial and all subsequent transitioned states as an additional property.
An additional runtime function validateStateID() validates/converts and returns full state value from varying types of state reference (eg custom id) using lookups into stateMap