Command design pattern

Hi All,

I've been using node red for a while now and just like others (see: Guidelines and design principles for Node-RED users ) I also pondered about some design principles according to which I could organize my flows to make them look better. As I have my roots in c++ I tried to find an existing design pattern that could fit the bill and as far as I can see the command pattern may do the job.

[{"id":"7a384da5.74c504","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"c8e83223.f69aa8","type":"subflow","name":"calculate chartdata","info":"","category":"","in":[{"x":60,"y":100,"wires":[{"id":"9b3ebf5.9c0c0c"}]}],"out":[{"x":420,"y":100,"wires":[{"id":"89f14f30.ebc718","port":0}]}],"env":[],"color":"#DDAA99"},{"id":"e564dbf8.d027","type":"ui_base","theme":{"name":"theme-light","lightTheme":{"default":"#0094CE","baseColor":"#0094CE","baseFont":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif","edited":true,"reset":false},"darkTheme":{"default":"#097479","baseColor":"#097479","baseFont":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif","edited":false},"customTheme":{"name":"Untitled Theme 1","default":"#4B7930","baseColor":"#4B7930","baseFont":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"},"themeState":{"base-color":{"default":"#0094CE","value":"#0094CE","edited":false},"page-titlebar-backgroundColor":{"value":"#0094CE","edited":false},"page-backgroundColor":{"value":"#fafafa","edited":false},"page-sidebar-backgroundColor":{"value":"#ffffff","edited":false},"group-textColor":{"value":"#1bbfff","edited":false},"group-borderColor":{"value":"#ffffff","edited":false},"group-backgroundColor":{"value":"#ffffff","edited":false},"widget-textColor":{"value":"#111111","edited":false},"widget-backgroundColor":{"value":"#0094ce","edited":false},"widget-borderColor":{"value":"#ffffff","edited":false},"base-font":{"value":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"}},"angularTheme":{"primary":"indigo","accents":"blue","warn":"red","background":"grey"}},"site":{"name":"Node-RED Dashboard","hideToolbar":"false","allowSwipe":"false","lockMenu":"false","allowTempTheme":"true","dateFormat":"DD/MM/YYYY","sizes":{"sx":48,"sy":48,"gx":6,"gy":6,"cx":6,"cy":6,"px":0,"py":0}}},{"id":"5e1e8043.811fc8","type":"ui_tab","z":"","name":"Home","icon":"dashboard","disabled":false,"hidden":false},{"id":"7bae444b.8c2554","type":"ui_group","z":"","name":"Default","tab":"5e1e8043.811fc8","order":1,"disp":true,"width":"6","collapse":false},{"id":"b4be77e8.0503d8","type":"function","z":"7a384da5.74c504","name":"dispatcher","func":"if(msg.payload===\"reset\"){\n    let msgQueue=new Map();\n    context.set(\"msgQueue\",msgQueue);\n}\nlet msgQueue=context.get(\"msgQueue\");\nif(msgQueue===undefined) msgQueue=new Map();\nif(typeof msg.cmdObj!==\"undefined\"){\n    if(typeof msg.cmdObj.timestamp!==\"undefined\"){\n        //node.warn(\"old:\"+msg.cmdObj.timestamp);\n        //node.warn(\"old size:\"+msgQueue.size);\n        let keys=msgQueue.keys();\n        let returnKey=keys.next();\n        if(returnKey.done===false&&returnKey.value===msg.cmdObj.timestamp){\n            let values=msgQueue.values();\n            let returnMsg=values.next().value;\n            //node.warn(\"pop:\"+returnMsg.cmdObj.timestamp);\n            for(let i=0;i<returnMsg.cmdObj.linkOut.length;++i){\n                let returnAsyncMsg=RED.util.cloneMessage(returnMsg);\n                returnAsyncMsg.cmdObj.linkOut=returnMsg.cmdObj.linkOut[i];\n                if(msg.payload[i]===null) returnAsyncMsg=null;\n                else{\n                    returnAsyncMsg.payload=msg.payload[i];\n                    if(typeof msg.cmdObj.error!==\"undefined\") returnAsyncMsg.cmdObj.error=msg.cmdObj.error;\n                }\n                node.send([null, returnAsyncMsg]);\n            }\n            msgQueue.delete(returnKey.value);\n            context.set(\"msgQueue\",msgQueue);\n            //node.warn(\"re-size:\"+msgQueue.size);\n            let nextMsg=null;\n            let nextKey=keys.next();\n            if(nextKey.done===false){\n                nextMsg=msgQueue.values().next().value;\n            }\n            //node.warn(nextMsg);\n            return [nextMsg, null];\n        }\n        else return [null, null];\n    }\n    else if(typeof msg.cmdObj.priority!==\"undefined\" &&\n            typeof msg.cmdObj.command!==\"undefined\" &&\n            typeof msg.cmdObj.linkIn!==\"undefined\" &&\n            typeof msg.cmdObj.linkOut!==\"undefined\"){\n            if(msg.cmdObj.priority===0){\n                return [msg, null];\n            }\n            else{\n                if(msgQueue.size===0){\n                    let timestamp=Date.now();\n                    msg.cmdObj.timestamp=timestamp;\n//                    node.warn(\"new:\"+timestamp);\n                    msgQueue.set(timestamp,RED.util.cloneMessage(msg));\n//                    node.warn(\"new size:\"+msgQueue.size);\n                    context.set(\"msgQueue\",msgQueue);\n                    return [msg, null];\n                }\n                else{\n                    let timestamp=Date.now();\n                    msg.cmdObj.timestamp=timestamp;\n//                    node.warn(\"new:\"+timestamp);\n                    msgQueue.set(timestamp,RED.util.cloneMessage(msg));\n//                    node.warn(\"new size:\"+msgQueue.size);\n                    context.set(\"msgQueue\",msgQueue);\n                    return [null, null];\n                }\n            }\n    }\n}\nelse return [null, null];\n","outputs":2,"noerr":0,"initialize":"","finalize":"","x":730,"y":260,"wires":[["d5439702.35eb18","f33e7ada.543688"],["d5a5750d.309ca8","18e54399.b75aac"]],"outputLabels":["nextMsg","returnMsg"]},{"id":"7f1fa8fe.fa7e08","type":"link in","z":"7a384da5.74c504","name":"dispacther in","links":["2ff6a7ba.0828a8","449ff944.672e7","48a370e5.9fe878","5358c263.3e2084","9c44a41b.c83978","d0cf51a3.7bfcf8","adfaf092.ec8d38","c58aba91.cc3c3"],"x":595,"y":260,"wires":[["b4be77e8.0503d8","b8788b8b.cfed18"]]},{"id":"d5a5750d.309ca8","type":"switch","z":"7a384da5.74c504","name":"returnMsg","property":"cmdObj.linkOut","propertyType":"msg","rules":[{"t":"eq","v":"chartData out","vt":"str"},{"t":"eq","v":"chartData done","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":3,"x":910,"y":360,"wires":[["1280b712.bf3459"],["23a89831.80c81"],[]]},{"id":"d5439702.35eb18","type":"switch","z":"7a384da5.74c504","name":"nextMsg","property":"cmdObj.command","propertyType":"msg","rules":[{"t":"eq","v":"chartData","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":900,"y":240,"wires":[["9fae5584.3e6f8"],[]]},{"id":"8794205a.d8f3d","type":"inject","z":"7a384da5.74c504","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"0","topic":"","payload":"reset","payloadType":"str","x":530,"y":400,"wires":[["15215577.1e1ffb"]]},{"id":"2cce6153.247656","type":"catch","z":"7a384da5.74c504","name":"Catch dispatcher subflow errors","scope":["274e3fba.764b","8aa6f22a.d5d6b8","60efc591.296fe4"],"uncaught":false,"x":450,"y":360,"wires":[["15215577.1e1ffb"]]},{"id":"15215577.1e1ffb","type":"function","z":"7a384da5.74c504","name":"reset","func":"msg.payload=\"reset\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":650,"y":360,"wires":[["b4be77e8.0503d8"]]},{"id":"f33e7ada.543688","type":"debug","z":"7a384da5.74c504","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":890,"y":180,"wires":[]},{"id":"18e54399.b75aac","type":"debug","z":"7a384da5.74c504","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":890,"y":300,"wires":[]},{"id":"2da86ecb.a6c122","type":"function","z":"7a384da5.74c504","name":"cmdSend","func":"let uiMsg={enabled: true, payload: msg.payload};\nmsg.cmdObj={\n    priority: 1,\n    linkIn: \"chartData\",\n    command: \"chartData\",\n    linkOut: [\"chartData out\", \"chartData done\"]\n}\nreturn [uiMsg, msg];\n","outputs":2,"noerr":0,"initialize":"","finalize":"","x":440,"y":160,"wires":[["531e488e.b0b11"],["adfaf092.ec8d38"]]},{"id":"adfaf092.ec8d38","type":"link out","z":"7a384da5.74c504","name":"","links":["7f1fa8fe.fa7e08"],"x":555,"y":160,"wires":[]},{"id":"8db0395.960c9c8","type":"ui_gauge","z":"7a384da5.74c504","name":"","group":"7bae444b.8c2554","order":1,"width":0,"height":0,"gtype":"gage","title":"gauge","label":"units","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":1250,"y":160,"wires":[]},{"id":"89f14f30.ebc718","type":"function","z":"c8e83223.f69aa8","name":"calculate","func":"msg.payload=[msg.payload*2, msg.payload];\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":320,"y":100,"wires":[[]]},{"id":"35d06192.b61c2e","type":"subflow:c8e83223.f69aa8","z":"7a384da5.74c504","name":"","env":[],"x":670,"y":540,"wires":[["c58aba91.cc3c3"]]},{"id":"de5a955e.627ec8","type":"link in","z":"7a384da5.74c504","name":"calculate chartdata in","links":["9fae5584.3e6f8"],"x":515,"y":540,"wires":[["35d06192.b61c2e"]]},{"id":"c58aba91.cc3c3","type":"link out","z":"7a384da5.74c504","name":"calculate chartdata out","links":["7f1fa8fe.fa7e08"],"x":825,"y":540,"wires":[]},{"id":"9fae5584.3e6f8","type":"link out","z":"7a384da5.74c504","name":"calculate chartdata","links":["de5a955e.627ec8"],"x":1015,"y":220,"wires":[]},{"id":"1280b712.bf3459","type":"link out","z":"7a384da5.74c504","name":"chartData out","links":["ef728d4b.c994e"],"x":1045,"y":340,"wires":[]},{"id":"23a89831.80c81","type":"link out","z":"7a384da5.74c504","name":"","links":["f240c267.f5ed2"],"x":1045,"y":380,"wires":[]},{"id":"ef728d4b.c994e","type":"link in","z":"7a384da5.74c504","name":"gauge in","links":["1280b712.bf3459"],"x":1155,"y":160,"wires":[["8db0395.960c9c8"]]},{"id":"f240c267.f5ed2","type":"link in","z":"7a384da5.74c504","name":"","links":["23a89831.80c81"],"x":55,"y":160,"wires":[["9f7a3904.6f7ba"]]},{"id":"531e488e.b0b11","type":"ui_numeric","z":"7a384da5.74c504","name":"","label":"numeric","tooltip":"","group":"7bae444b.8c2554","order":1,"width":0,"height":0,"wrap":false,"passthru":false,"topic":"","format":"","min":0,"max":"100","step":1,"x":300,"y":160,"wires":[["2da86ecb.a6c122"]]},{"id":"9f7a3904.6f7ba","type":"function","z":"7a384da5.74c504","name":"enable","func":"msg.enabled=true;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":150,"y":160,"wires":[["531e488e.b0b11"]]},{"id":"9b3ebf5.9c0c0c","type":"delay","z":"c8e83223.f69aa8","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":180,"y":100,"wires":[["89f14f30.ebc718"]]},{"id":"b8788b8b.cfed18","type":"debug","z":"7a384da5.74c504","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":680,"y":200,"wires":[]}]

The reason why I'm posting it is to get some opinions on it as it may have some flaws either in the dispatcher implementation or it may later hit some unforseen limitations (like number of links a function node or switch can handle), etc.

The advantages I expect from this:
-each UI input communicates only with the dispatcher
-each UI input has a dedicated subflow within which the corresponding logic is implemented
-each dedicated subflow is assigned to the same catch block that handles the unhandled errors in the subflow (and that of the embedded ones)
-the dispatcher builds a queue of the incoming UI input (this is the current implementation at least)
-debugging gets easier as all messages pass through the dispatcher
-editor flows get more transparent

How it works:
Such a dispatcher logic needs to be put on each editor tab that corresponds to a ui tab. I think it could be implemented globally as well but I'm not sure about the limitations of nodes concerning the number of links they can handle and anyway the user sees only one ui tab at a time.

The message sent to the dispatcher must have a cmdObj json object property like the one in the cmdSend node:

msg.cmdObj={
priority: 1,
linkIn: "chartData",
command: "chartData",
linkOut: ["chartData out", "chartData done"]
}

priority: 1 is normal and 0 is high (actually anything that is not 0 is interpreted as high). High prio messages get through the dispatcher without being put in the queue.

linkIn: identifies the sender

command: the command identifier (used in the nextMsg switch for branching)

linkOut: identifies the receivers (used in the returnMsg switch for branching). Subflows must return the payload as an array containing the payloads that need to be distributed to the linkOut branches.

The process is pretty straightforward:

-the message generated by an input on the ui needs to be passed to the dispatcher with the cmdObj property
-the dispatcher either puts it in the queue if there's another message already being processed or releases it immediately
-when a message is released it is sent to the nextMsg switch which directs it to the corresponding subflow based on its cmdObj.command property
-the subflow gets executed
-when the subflow returns its message it is directed to the dispatcher
-the dispatcher identifies the returning message, copies the msg payloads according to the linkOut property to separate messages and sends them to the returnMsg switch
-if there was an error in the subflow and was indicated in the returned message cmdObj.error property, it is copied over to each message cmdObj.error during copying
-the dispatcher deletes the message from the queue, takes the next one and sends it to the nextMsg switch

As always, there's room for improvement like adding sanity checks, etc. But my main question would be if this whole stuff is viable as a concept or not?

Best regards,
r0ller