UNPKG

node-red-contrib-xstate-machine

Version:

Xstate-based state machine implementation using state-machine-cat visualization for node red.

225 lines (170 loc) 7.4 kB
// Converter for XSTATE machines to SMCAT format for visualization // It does not support all features const indentString = require('indent-string'); const typeSep = '/'; const finalNodeSuffix = "Node"; function sanitizeStateId(state) { let sanitizeStateId = String(state); sanitizeStateId = sanitizeStateId.replaceAll('"', "'"); return sanitizeStateId; } function getStateId(state, type) { return "\"" + sanitizeStateId(state.id) + typeSep + (type === undefined ? state.type : type) + "\""; } function getStateAction(actionTrigger) { if(!actionTrigger || actionTrigger.length == 0 ) return null; actionTrigger = actionTrigger .filter( e => !e.hasOwnProperty('id') || !e.id.startsWith("xstate.after(") ) .filter( e => !e.hasOwnProperty('sendId') || !e.sendId.startsWith("xstate.after(") ) .filter( e => ( e.type ? true : false) ); let actionNames = []; actionNames = actionTrigger.map( e => { if( e.hasOwnProperty('type') && e.type === 'xstate.invoke') { return "invoke::" + (e.src ? e.src : e.id); } return e.id ? e.id : e.type; }); return actionNames .map( e => e.replace(/['";,{}\[\]]\s/sgi,'') + '()' ) .join(' '); } function getStateCode(state) { let code = []; if( state.onEntry && state.onEntry.length > 0 ) { let actionDef = getStateAction( state.onEntry ); if( actionDef ) code.push( "entry/ " + actionDef ); } if( state.activities && state.activities.length > 0 ) { let actionDef = getStateAction( state.activities ); if( actionDef ) code.push( "do/ " + actionDef ); } if( state.onExit && state.onExit.length > 0 ) { let actionDef = getStateAction( state.onExit ); if( actionDef ) code.push( "exit/ " + actionDef ); } return code.join('\n'); } function typeMap(intype) { const mapping = { atomic: "regular", compound: "regular", final: "regular", } if( mapping[intype] ) return mapping[intype]; else return intype; } function createChildrenStates(parentState,level) { if( !level ) level = 0; let transitionsDef = createTransitions(parentState).join(";\n"); let statesDef = []; if( transitionsDef ) transitionsDef += ';'; for( let [state,stateObj] of Object.entries(parentState.states) ){ let stateDef = getStateId(stateObj) + ` [label="${state}" type=${typeMap(stateObj.type)}]`; let transitionDef = ''; let code = getStateCode(stateObj); if( code ) stateDef += " :\n" + indentString(code, 4); // Recurse let childDefs = createChildrenStates(stateObj, level+1); if( childDefs.states ) stateDef += ' {\n' + indentString(childDefs.states, 4*(level+1)) + '\n}'; if( childDefs.transitions ) transitionsDef += "\n" + childDefs.transitions; statesDef.push(stateDef); // Add additional final state in the diagram for "the" final state if( stateObj.type === "final" ) { statesDef.push(getStateId(stateObj) + finalNodeSuffix + ' [label="" type=final]'); } } // Initial state for compound/parallel state if( statesDef.length > 0 && parentState.type !== "parallel" ) { let rootNode = getClosestChildState( parentState, parentState.initialStateNodes[0] ); if( rootNode ) { statesDef.unshift(getStateId(parentState, "initial")); transitionsDef = statesDef[0] + " => " + getStateId(rootNode) + ";" + transitionsDef; } } return { states: statesDef.length > 0 ? statesDef.join(',\n') + ";" : null, transitions: transitionsDef.length > 0 ? transitionsDef : null }; } function getClosestChildState(parent, child) { if( !(child.parent) ) return null; if( child.parent === parent ) // Found it return child; return getClosestChildState(parent, child.parent); } function convertMilliseconds(ms) { // Try seconds, minutes, hours, days, weeks let s = ms/1000; if( s != Math.round(s) ) return `${ms} ms`; let min = s/60; if( min != Math.round(min) ) return `${s} s`; let h = min/60; if( h != Math.round(h) ) return `${min} min`; let d = h/24; if( d != Math.round(d) ) return `${h} h`; let w = d/7; if( w != Math.round(w) ) return `${d} d`; return `${w} w`; } function getTransitionEvent(transition) { if( transition.hasOwnProperty("delay") ) { return 'after ' + convertMilliseconds(transition.delay); } else { if( transition.event.startsWith('done.') ) { return 'done'; } else { return transition.event; } } } function createTransitions(parentState) { let transitionsDef = []; // If this state is a final state, create another "always" transition from this target state to the real final state node if( parentState.type === "final" ) transitionsDef.push( getStateId(parentState) + " => " + getStateId(parentState) + finalNodeSuffix ); for( let transition of parentState.transitions ) { let transitionDefs = []; if( !transition.source ) { console.warn(`State "${parentState.id}" has got a transition for event ${transition.event} with no source state.`); continue; } if( !transition.target ) { // Self transition transitionDefs.push( getStateId( transition.source ) + " => " + getStateId( transition.source ) ); } else { for( let targetState of transition.target ) { transitionDefs.push( getStateId( transition.source ) + " => " + getStateId( targetState ) ); } } if( transition.internal ) transitionDefs = transitionDefs.map( (e) => e + ' [type=internal]' ); let transitionLabel = ''; if( transition.event ) transitionLabel += getTransitionEvent(transition); if( transition.cond ) { if( Array.isArray(transition.cond) ) { if( transition.cond.length > 0 ) transitionLabel += " [" + transition.cond.reduce( (a,e) => (!a?e.name:a + " && " + e.name), "" ) + "]"; } else transitionLabel += " [" + transition.cond.name + "]"; } if( transition.actions ) { if( Array.isArray(transition.actions) ) { if( transition.actions.length > 0 ) transitionLabel += " / " + transition.actions.reduce( (a,e) => (!a? (e.type + "()"):(a + ", " + e.type + "()")), "" ); } else transitionLabel += " / " + transition.actions.type + "()"; } if( transitionLabel ) transitionDefs = transitionDefs.map( (e) => e + " : " + transitionLabel.trim()); transitionsDef = transitionsDef.concat(transitionDefs); } return transitionsDef; } exports.toSmcat = function(machine) { let smcat = createChildrenStates(machine); return smcat.states + "\n\n" + smcat.transitions; }