UNPKG

node-red-contrib-xstate-machine

Version:

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

176 lines (162 loc) 27.9 kB
<style>#red-ui-sidebar-smxstate-content,#red-ui-sidebar-smxstate-graph{width:100%;height:100%}#red-ui-sidebar-smxstate-content{overflow:hidden}#red-ui-sidebar-smxstate-context{padding:8px 10px}div.red-ui-sidebar-smxstate-settings{font-size:x-small;display:inline-block}div.red-ui-sidebar-smxstate-settings input,div.red-ui-sidebar-smxstate-settings label,div.red-ui-sidebar-smxstate-settings select{font-size:x-small!important}div.red-ui-sidebar-smxstate-settings label{display:inline;margin-right:6px}div.red-ui-sidebar-smxstate-settings select{width:auto;height:auto;line-height:inherit;margin:0;padding:4px}div.red-ui-sidebar-smxstate-settings input{width:auto;height:auto!important;line-height:inherit!important;margin:0!important;padding:0 0 0 4px!important}</style> <script type="text/x-red" data-template-name="smxstate"> <div class="form-row"> <label for="node-input-name"><i class="icon-tag"></i> Name</label> <input type="text" id="node-input-name" placeholder="Name"> </div> <div class="form-row" style="margin-bottom: 0px;"> <label for="node-input-xstateDefinition" style="width: 200px"><i class="fa fa-wrench"></i> XState State-machine</label> <input type="hidden" id="node-input-xstateDefinition" autofocus="autofocus"> <input type="hidden" id="node-input-noerr"> </div> <div class="form-row node-text-editor-row" style="position:relative"> <div style="position: absolute; right:0; bottom:calc(100% + 3px);"><button id="node-smxstate-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div> <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-xstateDefinition-editor" ></div> </div> </script> <script type="text/javascript">if(!RED)var RED={};let smxstateUtilExports=function(){function e(){let e=$("#red-ui-sidebar-smxstate-display-selected"),t=e.val();return t?{id:t,rootId:e.children("option:selected").attr("data-reveal-id"),aliasId:e.children("option:selected").attr("data-alias-id"),label:e.children("option:selected").text()}:null}var t=[],s=!1,r=[],a=!1;function i(e,t,s,r){$("#red-ui-sidebar-smxstate-display-selected").append($("<option>").attr("value",e).attr("data-reveal-id",s).attr("data-alias-id",r||e).text(t))}return{init:function(){let e=$("<div>").css({display:"flex",flexDirection:"column",height:"100%"}),t=$('<div class="red-ui-sidebar-header" style="text-align: left;">').append($("<form>").css({margin:0,whiteSpace:"normal"}).append($("<label>").attr("for","red-ui-sidebar-smxstate-display-selected").text("State machine to view:")).append($('<select id="red-ui-sidebar-smxstate-display-selected">').change(()=>{RED.smxstate.display()}).css("width","100%").append($("<option disabled selected value=>").text("-- select machine instance --"))).append("<br>",$('<span class="button-group">').append($('<a href="#" id="red-ui-sidebar-smxstate-revealRoot" class="red-ui-sidebar-header-button">').append('<i class="fa fa-search-minus"></i>').click(()=>{RED.smxstate.revealRoot()})),$('<span class="button-group">').append($('<a href="#" id="red-ui-sidebar-smxstate-reveal" class="red-ui-sidebar-header-button">').append('<i class="fa fa-search-plus"></i>').click(()=>{RED.smxstate.reveal()})),$('<span class="button-group">').append($('<a href="#" id="red-ui-sidebar-smxstate-reset" class="red-ui-sidebar-header-button">').append('<i class="fa fa-undo"></i>',"&nbsp;<span>reset</span>").click(()=>{RED.smxstate.reset()})),$('<span class="button-group">').append($('<a href="#" id="red-ui-sidebar-smxstate-refresh" class="red-ui-sidebar-header-button">').append('<i class="fa fa-refresh"></i>',"&nbsp;<span>refresh graph</span>").click(()=>{RED.smxstate.display(!0)})),$('<div class="red-ui-sidebar-smxstate-settings">').css({marginRight:"8px"}).append('<label for="red-ui-sidebar-smxstate-settings-renderer">Renderer:</label>').append($('<select id="red-ui-sidebar-smxstate-settings-renderer">').change(e=>{RED.smxstate.settings.set("renderer",e.target.value)})),$('<div class="red-ui-sidebar-smxstate-settings">').css({marginRight:"8px"}).append('<label for="red-ui-sidebar-smxstate-settings-renderTimeoutMs">Render timeout in ms:</label>').append($('<input type="text" id="red-ui-sidebar-smxstate-settings-renderTimeoutMs">').css("width","40px").change(e=>{try{let t=Number.parseInt(e.target.value);if(Number.isNaN(t)||t<=0)throw"Render timeout must be a strictly positive integer.";RED.smxstate.settings.set("renderTimeoutMs",t)}catch(t){RED.notify(t,"error"),RED.smxstate.settings.get("renderTimeoutMs",t=>{t&&$(e.target).val(t)})}}))));RED.popover.tooltip(t.find("#red-ui-sidebar-smxstate-reset"),"Reset to initial state"),RED.popover.tooltip(t.find("#red-ui-sidebar-smxstate-revealRoot"),"Reveal instance in flow"),RED.popover.tooltip(t.find("#red-ui-sidebar-smxstate-reveal"),"Reveal prototype in flow");let s=$('<div id="red-ui-sidebar-smxstate-context">').append($('<div id="red-ui-sidebar-smxstate-context-header">').text("Context data:")).append('<span id="red-ui-sidebar-smxstate-context-data" class="red-ui-debug-msg-payload">'),r=$("<div>").css({fontSize:"x-small",padding:"8px"}).append("<span><b>Pan:</b> Click+drag / <b>Zoom:</b> Mousewheel</span>"),a=$('<div id="red-ui-sidebar-smxstate-content">').append('<svg id="red-ui-sidebar-smxstate-graph">');t.appendTo(e),s.appendTo(e),r.appendTo(e),a.appendTo(e);var i=this;return setTimeout(()=>{i.refresh()},1e3),{content:e,footer:t}},display:function(t=!1){idObj=e(),$("#red-ui-sidebar-smxstate-graph").empty(),$("#red-ui-sidebar-smxstate-graph").before($('<div id="red-ui-sidebar-smxstate-spinner">').append('<i class="fa fa-circle-o-notch fa-spin fa-5x fa-fw"></i> <span>Loading...</span>').css({textAlign:"center",margin:"10px"})),$.ajax({url:"smxstate/"+idObj.id+"/getgraph",type:"POST",data:{forceRedraw:t},success:function(e){$("#red-ui-sidebar-smxstate-spinner").remove(),RED.notify(`Successfully rendered state-graph for ${idObj.label}`,{type:"success",id:"smxstate"}),$("#red-ui-sidebar-smxstate-graph").replaceWith($(e).attr("id","red-ui-sidebar-smxstate-graph")),function(e,t){var s,r;try{if(s=(s=t.getAttribute("viewBox")).split(/[\n\r\s]+/gi),!Array.isArray(s)||4!=s.length)throw"Invalid viewbox";if((s=s.map(e=>parseFloat(e))).some(e=>!Number.isFinite(e)))throw"Invalid viewbox"}catch(e){s=null}s?r={x:s[0],y:s[1],w:s[2],h:s[3]}:(r={x:0,y:0,w:t.clientWidth,h:t.clientHeight},t.setAttribute("viewBox",`${r.x} ${r.y} ${r.w} ${r.h}`));var a=!1,i={x:0,y:0},n={x:0,y:0},o=1;e.onmousewheel=function(e){e.preventDefault();const s=t.clientWidth,a=t.clientHeight;var i=r.w,n=r.h,d=e.offsetX,l=e.offsetY,c=i*-Math.sign(e.deltaY)*.05,u=n*-Math.sign(e.deltaY)*.05,p=c*d/s,m=u*l/a;r={x:r.x+p,y:r.y+m,w:r.w-c,h:r.h-u},o=s/r.w,t.setAttribute("viewBox",`${r.x} ${r.y} ${r.w} ${r.h}`)},e.onmousedown=function(e){a=!0,i={x:e.x,y:e.y}},e.onmousemove=function(e){if(a){n={x:e.x,y:e.y};var s=(i.x-n.x)/o,d=(i.y-n.y)/o,l={x:r.x+s,y:r.y+d,w:r.w,h:r.h};t.setAttribute("viewBox",`${l.x} ${l.y} ${l.w} ${l.h}`)}},e.onmouseup=function(e){if(a){n={x:e.x,y:e.y};var s=(i.x-n.x)/o,d=(i.y-n.y)/o;r={x:r.x+s,y:r.y+d,w:r.w,h:r.h},t.setAttribute("viewBox",`${r.x} ${r.y} ${r.w} ${r.h}`),a=!1}},e.onmouseleave=function(e){a=!1}}($("#red-ui-sidebar-smxstate-content")[0],$("#red-ui-sidebar-smxstate-graph")[0])},error:function(e,t,s){$("#red-ui-sidebar-smxstate-spinner").remove(),404==e.status?RED.notify(RED._("node-red:common.notification.error",{message:"resource not found"}),"error"):500==e.status?RED.notify("Rendering of the state machine failed.","error"):0==e.status?RED.notify(RED._("node-red:common.notification.error",{message:RED._("node-red:common.notification.errors.no-response")}),"error"):RED.notify(RED._("node-red:common.notification.error",{message:RED._("node-red:common.notification.errors.unexpected",{status:e.status,message:t})}),"error")}})},reset:function(){let t=e();t&&$.ajax({url:"smxstate/"+t.id+"/reset",type:"POST",success:function(e){RED.notify("State machine "+t.id+" was reset.",{type:"success",id:"smxstate"})},error:function(e,t,s){404==e.status?RED.notify(RED._("node-red:common.notification.error",{message:RED._("node-red:common.notification.errors.not-deployed")}),"error"):500==e.status?RED.notify("Error during reset. See logs for more info.","error"):0==e.status?RED.notify(RED._("node-red:common.notification.error",{message:RED._("node-red:common.notification.errors.no-response")}),"error"):RED.notify(RED._("node-red:common.notification.error",{message:RED._("node-red:common.notification.errors.unexpected",{status:e.status,message:t})}),"error")}})},refresh:function(){RED.nodes.filterNodes({type:"smxstate"}),$("#red-ui-sidebar-smxstate-display-selected option:not([disabled])").remove(),$("#red-ui-sidebar-smxstate-settings-renderer option").remove(),$.ajax({url:"smxstate/getnodes",type:"GET",success:function(e){Array.isArray(e)||(e=[e]);for(let t of e)i(t.id,t.path.labels.join("/"),t.rootId,t.alias)},error:function(e,t,s){404==e.status?RED.notify(RED._("node-red:common.notification.error",{message:"resource not found"}),"error"):500==e.status?RED.notify("Retrieval of available state machines failed.","error"):0==e.status?RED.notify(RED._("node-red:common.notification.error",{message:RED._("node-red:common.notification.errors.no-response")}),"error"):RED.notify(RED._("node-red:common.notification.error",{message:RED._("node-red:common.notification.errors.unexpected",{status:e.status,message:t})}),"error")}}),RED.smxstate.settings.get("availableRenderers",e=>{let t=$("#red-ui-sidebar-smxstate-settings-renderer");if(e){Array.isArray(e)||(e=[e]);for(let s of e)t.append('<option value="'+s+'">'+s+"</option>")}RED.smxstate.settings.get("renderer",e=>{e&&t.val(e)})}),RED.smxstate.settings.get("renderTimeoutMs",e=>{e&&$("#red-ui-sidebar-smxstate-settings-renderTimeoutMs").val(e)})},addStatemachineToSidebar:i,deleteStatemachineFromSidebar:function(e){$("#red-ui-sidebar-smxstate-display-selected").children('[value="'+e+'"]').remove()},animate:function e(t){if(t&&t.state&&(!0===t.state.changed||void 0===t.state.changed)&&r.push(t),!a&&r.length>0){a=!0;let t=r.shift(),s=function e(t,s){if("string"==typeof t)return[s?s+"."+t:t];if(!t)return s;let r=Object.keys(t),a=[];for(let i of r){let r=s?s+"."+i:i;t[i]&&a.push(r),a=a.concat(e(t[i],r))}return a}(t.state.state),i=$('#red-ui-sidebar-smxstate-content svg g.graph g:not([class="edge"])');i.children('*[stroke][stroke!="transparent"][stroke!="none"]').attr("stroke","#000000");for(let e of s)i.has("title:contains("+t.machineId+"."+e+"/)").has("title:not(:contains(/initial))").children('*[stroke][stroke!="transparent"][stroke!="none"]').attr("stroke","#FF0000");setTimeout(()=>{a=!1,e()},100),r.length>5&&(r=r.splice(-5))}},updateContext:function r(a){if(a&&a.id&&a.id===e().id&&t.push(a.context),!s&&t.length>0){s=!0;let e=t.shift(),a=RED.utils.createObjectElement(e,{key:null,typeHint:"Object",hideKey:!1});$("#red-ui-sidebar-smxstate-context-data").html(a),setTimeout(()=>{s=!1,r()},100),t.length>5&&(t=t.splice(-5))}},revealRoot:function(){let t=e();t&&RED.view.reveal(t.rootId)},reveal:function(){let t=e();t&&RED.view.reveal(t.aliasId)},updateSettings:function(e){if(e.hasOwnProperty("renderer")&&$("#red-ui-sidebar-smxstate-settings-renderer").val(e.renderer),e.hasOwnProperty("availableRenderers")){let t;for(t of($("#red-ui-sidebar-smxstate-settings-renderer").children("option").attr("disabled","disabled"),e.availableRenderers))$("#red-ui-sidebar-smxstate-settings-renderer").children('option[value="'+t+'"]').removeAttr("disabled")}e.hasOwnProperty("renderTimeoutMs")&&$("#red-ui-sidebar-smxstate-settings-renderTimeoutMs").val(e.renderTimeoutMs)}}}();RED.smxstate?Object.assign(RED.smxstate,smxstateUtilExports):RED.smxstate=smxstateUtilExports,delete smxstateUtilExports;</script> <script type="text/javascript">if(!RED)var RED={};RED.smxstate||(RED.smxstate={}),RED.smxstate.settings={set:function(o,e,t){return $.ajax({url:"smxstate/settings",type:"POST",data:{property:o,value:e},success:t,error:function(e,t,r){404==e.status?RED.notify(RED._("node-red:common.notification.error",{message:"resource not found"}),"error"):500==e.status?RED.notify("smxstate: Unable to set property "+o+".","error"):0==e.status?RED.notify(RED._("node-red:common.notification.error",{message:RED._("node-red:common.notification.errors.no-response")}),"error"):RED.notify(RED._("node-red:common.notification.error",{message:RED._("node-red:common.notification.errors.unexpected",{status:e.status,message:t})}),"error")}})},get:function(o,e){return $.ajax({url:"smxstate/settings",type:"GET",data:{property:o},success:t=>{t=t&&"object"==typeof t&&t.hasOwnProperty(o)?t[o]:null,e&&"function"==typeof e&&e(t)},error:function(e,t,r){404==e.status?RED.notify(RED._("node-red:common.notification.error",{message:"resource not found"}),"error"):500==e.status?RED.notify("smxstate: Retrieval of property "+o+" failed.","error"):0==e.status?RED.notify(RED._("node-red:common.notification.error",{message:RED._("node-red:common.notification.errors.no-response")}),"error"):RED.notify(RED._("node-red:common.notification.error",{message:RED._("node-red:common.notification.errors.unexpected",{status:e.status,message:t})}),"error")}})}};</script> <script type="text/javascript">var defaultStateMachineCode="// Available variables/objects/functions:\r\n// xstate\r\n// - .Machine\r\n// - .interpret\r\n// - .assign\r\n// - .send\r\n// - .sendParent\r\n// - .spawn\r\n// - .raise\r\n// - .actions\r\n//\r\n// Common\r\n// - setInterval, setTimeout, clearInterval, clearTimeout\r\n// - node.send, node.warn, node.log, node.error\r\n// - context.get, context.set\r\n// - flow.get, flow.set\r\n// - env.get\r\n// - util\r\n\r\nconst { assign } = xstate;\r\n\r\n// First define names guards, actions, ...\r\n\r\n/**\r\n * Guards\r\n */\r\nconst maxValueReached = (context, event) => {\r\n return context.counter >= 10;\r\n};\r\n\r\n/**\r\n * Actions\r\n */\r\nconst incrementCounter = assign({\r\n counter: (context, event) => context.counter + 1\r\n});\r\n\r\nconst resetCounter = assign({\r\n counter: (context, event) => {\r\n // Can send log messages via\r\n // - node.log\r\n // - node.warn\r\n // - node.error\r\n //node.warn(\"RESET\");\r\n\r\n // Can send messages to the second outport\r\n // Specify an array to send multiple messages\r\n // at once\r\n // - node.send(msg)\r\n node.send({ payload: \"resetCounter\" });\r\n \r\n return 0;\r\n }\r\n});\r\n\r\n/**\r\n * Activities\r\n */\r\nconst doStuff = () => {\r\n // See https://xstate.js.org/docs/guides/activities.html\r\n const interval = setInterval(() => {\r\n node.send({ payload: 'BEEP' });\r\n }, 1000);\r\n return () => clearInterval(interval);\r\n};\r\n\r\n/***************************\r\n * Main machine definition * \r\n ***************************/\r\nreturn {\r\n machine: {\r\n context: {\r\n counter: 0\r\n },\r\n initial: 'run',\r\n states: {\r\n run: {\r\n initial: 'count',\r\n states: {\r\n count: {\r\n on: {\r\n '': { target: 'reset', cond: 'maxValueReached' }\r\n },\r\n after: {\r\n 1000: { target: 'count', actions: 'incrementCounter' }\r\n }\r\n },\r\n reset: {\r\n exit: 'resetCounter',\r\n after: {\r\n 5000: { target: 'count' }\r\n },\r\n activities: 'doStuff'\r\n }\r\n },\r\n on: {\r\n PAUSE: 'pause'\r\n }\r\n },\r\n pause: {\r\n on: {\r\n RESUME: 'run'\r\n }\r\n }\r\n }\r\n },\r\n // Configuration containing guards, actions, activities, ...\r\n // see above\r\n config: {\r\n guards: { maxValueReached },\r\n actions: { incrementCounter, resetCounter },\r\n activities: { doStuff }\r\n },\r\n // Define listeners (can be an array of functions)\r\n // Functions get called on every state/context update\r\n listeners: (data) => {\r\n //node.warn(data.state + \":\" + data.context.counter);\r\n }\r\n};";RED.nodes.registerType("smxstate",{category:"function",color:"#C7E9C0",defaults:{name:{value:""},xstateDefinition:{value:defaultStateMachineCode},noerr:{value:0,required:!0,validate:e=>!e}},inputs:1,outputs:2,icon:"font-awesome/fa-dot-circle-o",label:function(){return this.name||"smxstate"},inputLabels:"trigger",outputLabels:["stateChanged","msgOutput"],oneditprepare:function(){var e=this;this.editor=RED.editor.createEditor({extraLibs:[{var:"xstate",module:"xstate"},{var:"util",module:"util"}],id:"node-input-xstateDefinition-editor",mode:"ace/mode/nrjavascript",value:$("#node-input-xstateDefinition").val(),globals:{msg:!0,context:!0,RED:!0,util:!0,flow:!0,global:!0,console:!0,Buffer:!0,setTimeout:!0,clearTimeout:!0,setInterval:!0,clearInterval:!0}}),this.editor.focus(),RED.popover.tooltip($("#node-smxstate-expand-js"),RED._("node-red:common.label.expand")),$("#node-smxstate-expand-js").on("click",function(t){t.preventDefault();var n=e.editor.getValue();RED.editor.editJavaScript({value:n,width:"Infinity",cursor:e.editor.getCursorPosition(),mode:"ace/mode/nrjavascript",complete:function(t,n){e.editor.setValue(t,-1),e.editor.gotoLine(n.row+1,n.column,!1),setTimeout(function(){e.editor.focus()},300)}})})},oneditsave:function(){var e=this.editor.getSession().getAnnotations();this.noerr=0,$("#node-input-noerr").val(0);for(var t=0;t<e.length;t++)"error"===e[t].type&&($("#node-input-noerr").val(e.length),this.noerr=e.length);$("#node-input-xstateDefinition").val(this.editor.getValue()),this.editor.destroy(),delete this.editor},oneditcancel:function(){this.editor.destroy(),delete this.editor},oneditresize:function(e){for(var t=$("#dialog-form>div:not(.node-text-editor-row)"),n=$("#dialog-form").height(),r=0;r<t.length;r++)n-=$(t[r]).outerHeight(!0);var a=$("#dialog-form>div.node-text-editor-row");n-=parseInt(a.css("marginTop"))+parseInt(a.css("marginBottom")),$(".node-text-editor").css("height",n+"px"),this.editor.resize()},onpaletteadd:function(){var e=this;let t=RED.smxstate.init();var n=new MutationObserver(function(t){$(t[0].target).is(":visible")?(RED.comms.unsubscribe("smxstate_transition",e.handleStateMachineTransition),RED.comms.subscribe("smxstate_transition",e.handleStateMachineTransition)):RED.comms.unsubscribe("smxstate_transition",e.handleStateMachineTransition)});RED.sidebar.addTab({id:"smxstate",label:"state-machines",name:"State-machines",content:t.content,toolbar:$('<div><span class="button-group"><a id="red-ui-sidebar-smxstate-open" class="red-ui-footer-button" href="#"><i class="fa fa-desktop"></i></a></span></div>'),enableOnEdit:!0,pinned:!1,iconClass:"fa fa-dot-circle-o",action:"contrib:show-smxstate-tab"}),RED.actions.add("contrib:show-smxstate-tab",function(){RED.sidebar.show("smxstate")}),RED.events.on("nodes:add",function(e){e.hasOwnProperty("type")&&e.type});let r=document.querySelector("#red-ui-sidebar-smxstate-content").parentNode.parentNode;n.observe(r,{attributes:!0}),RED.popover.tooltip($("#red-ui-sidebar-smxstate-open"),RED._("node-red:debug.sidebar.openWindow")),$("#red-ui-sidebar-smxstate-open").on("click",function(e){e.preventDefault(),alert("NOT YET IMPLEMENTED")}),this.handleStateMachineTransition=function(e,t){if("object"==typeof t&&"type"in t&&"string"==typeof t.type)switch(t.type){case"transition":RED.smxstate.animate(t);break;case"context":RED.smxstate.updateContext(t);break;default:return}},this.handleStateMachineDisplay=function(e,t){if("object"==typeof t&&"type"in t)switch(t.type.toLowerCase()){case"add":Array.isArray(t.data)||(t.data=[t.data]);for(let e of t.data)RED.smxstate.addStatemachineToSidebar(e.id,e.path.labels.join("/"),e.rootId,e.alias);break;case"delete":Array.isArray(t.data)||(t.data=[t.data]);for(let e of t.data)RED.smxstate.deleteStatemachineFromSidebar(e.id)}},RED.comms.subscribe("smxstate",this.handleStateMachineDisplay)},onpaletteremove:function(){RED.comms.unsubscribe("smxstate",this.handleStateMachineDisplay),RED.comms.unsubscribe("smxstate_transition",this.handleStateMachineTransition),RED.sidebar.removeTab("smxstate"),RED.actions.remove("contrib:show-smxstate-tab"),delete RED.smxstate}});</script> <script type="text/x-red" data-help-name="smxstate"> <p>Provides a runtime environment for state machines using <a href="https://xstate.js.org/docs/" target="_blank">XSTATE</a>.</p> <style> #red-ui-smxstate-help-container pre { overflow-x: auto; white-space: pre; } </style> <div id="red-ui-smxstate-help-container"> <h3>Properties</h3> <dl class="message-properties"> <dt>name<span class="property-type">string</span></dt> <dd>The name of the node as displayed in the editor</dd> <dt>xstateDefinition<span class="property-type">string/javascript</span></dt> <dd> This contains the xstate-compatible code to setup the state-machine. The code has to end with a statement that returns an object of the form <pre>{ &lt;<a href="https://xstate.js.org/docs/guides/machines.html#configuration" target="_blank">xstate machine definition</a>&gt; }</pre> or <pre>{ machine: &lt;<a href="https://xstate.js.org/docs/guides/machines.html#configuration" target="_blank">xstate machine definition</a>&gt;, config: &lt;<a href="https://xstate.js.org/docs/guides/machines.html#options" target="_blank">xstate machine options</a>&gt; }</pre> Anywhere in the code you may use the same functions as in the function node such as e.g. <code>node.send()</code> or <code>setTimeout()</code>. Additionally you can use all the exports of the <em>xstate</em> library via the <code>xstate</code> object. For example to import the <code>assign</code>, <code>raise</code> and <code>log</code> functions type: <pre>const { actions, assign } = xstate; const { raise, log } = actions;</pre> </dd> </dl> <h3>Inputs</h3> <dl class="message-properties"> <dt>topic <span class="property-type">string</span></dt> <dd> - <code>"reset"</code> to reset machine to initial state<br/> - <code>&lt;name of event&gt;</code> to trigger a transition<br/> </dd> <dt>payload <span class="property-type">object</span> </dt> <dd> The data which comes with the event. It can then be used via the <code>.payload</code> property of the event object within action/activity/guard/service callbacks. </dd> </dl> <h3>Outputs</h3> <p>The two outports output messages of the following specifications:</p> <ol class="node-ports"> <li>On occurring event/transition/change of context <dl class="message-properties"> <dt>topic <span class="property-type">string</span></dt> <dd>Equals to <code>"state"</code> if an event or transition occurred. If the data changed it equals to <code>"context"</code>.</dd> <dl class="message-properties"> <dt>payload <span class="property-type">object</span></dt> <dd> Contains an object that represents the current state of the machine if the topic is <code>"state"</code>. See details below for more information. In case of a changed context this contains an object with the new context value. </dd> </dl> </li> <li>Message sent internally from the machine <dl class="message-properties"> <dd>Analogous to the function-node all messages sent via <code>node.send([msg1, msg2, ...]);</code> from within the machine are output through this outport. </dd> </dl> </li> </ol> <h3>Details</h3> <p> See the default node for an example implementation. Also please refer to the excellent <a href="https://xstate.js.org/docs/guides/machines.html" target="_blank">xstate documentation</a> for further details about how to model your use-case as a xstate machine. </p> <p> The <code>payload</code> objects for messages with a topic of <code>"state"</code> output from the first outport have the following properties:</p> <p> <dl class="message-properties"> <dt>state <span class="property-type">string or object</span></dt> <dd> the path of the currently active states as object. If only a top-level state is active then this is a string with the name of the active state. If multiple states are active this is an object where each key is a parent state and each leaf is a string property value, e.g. <pre>{ parentstate1: "childstate1", parentstate2: { childparentstate1: "childstate211", childstate21: {} } }</pre> Here the active state <code>parentstate2.childstate21</code> does not contain any childstate, so the property value is an empty object <code>{}</code>. </dd> <dt>changed <span class="property-type">boolean</span></dt> <dd>boolean flag that is true if the state or context was changed</dd> <dt>done <span class="property-type">boolean</span></dt> <dd> boolean flag that is true if the machine contains a final state which has been reached. You can use it to e.g. trigger a <code>"reset"</code> event. </dd> <dt>activities <span class="property-type">object</span></dt> <dd>object containing all activities with a boolean flag indicating if they are running</dd> <dt>actions <span class="property-type">object</span></dt> <dd>object containing all actions which are currently active</dd> <dt>event <span class="property-type">object</span></dt> <dd> the event object (including the <code>.payload</code> property containing event data) that triggered this message e.g. <pre>{ type: "TRIGGER", // The event name payload: 5 // The event payload (may be an object itself) }</pre> </dd> <dt>context <span class="property-type">object</span></dt> <dd>an object containing the current payload data context of the machine</dd> </dl> </p> <h3>The sidebar</h3> <p> Open the <a href="#" onclick="RED.sidebar.show('smxstate')">sidebar</a> in the node-red editor UI to get a visual graph representation of your machine and its current state and context data. The graph is drawn using <a href="https://www.npmjs.com/package/state-machine-cat">state-machine-cat</a>. </p> <p> On the sidebar you will find a dropdown box containing all running state-machine instances. Upon selection of an instance the graph below gets redrawn and current context is shown. Also the current state is highlighted in red within the graph. </p> <p> The sidebar offers buttons to control various things of the viewed machine: <ul> <li><i class="fa fa-search-minus"></i> <span>Reveal the node containing the state machine instance</span></li> <li><i class="fa fa-search-plus"></i> <span>Reveal the prototype node within a subflow if it is defined within one</span></li> <li><i class="fa fa-undo"></i> <span>Reset the machine to its initial state and context</span></li> <li><i class="fa fa-refresh"></i> <span>Reload the state-machine graph</span></li> </ul> </p> </div> </script>