mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Link Call should not call into subflow...
- includes missing jsdoc - improves speed (no searching, only lookups) - code formatting consistency - improve tests
This commit is contained in:
		| @@ -21,14 +21,15 @@ | ||||
|  * @property {string} name - Name of target Node | ||||
|  * @property {number} flowId - ID of flow where the target node exists | ||||
|  * @property {string} flowName - Name of flow where the target node exists | ||||
|  * @property {boolean} isSubFlow - True if the link-in node exists in a subflow instance | ||||
|  */ | ||||
|  | ||||
|  | ||||
| module.exports = function(RED) { | ||||
|     "use strict"; | ||||
|     const crypto = require("crypto"); | ||||
|     const targetCache = (function() { | ||||
|         const registry = { ids: {}, named: {}}; | ||||
|     const targetCache = (function () { | ||||
|         const registry = { id: {}, name: {} }; | ||||
|         function getIndex(/** @type {[LinkTarget]}*/ targets, id) { | ||||
|             for (let index = 0; index < (targets || []).length; index++) { | ||||
|                 const element = targets[index]; | ||||
| @@ -46,21 +47,26 @@ module.exports = function(RED) { | ||||
|         function generateTarget(node) { | ||||
|             const isSubFlow = node._flow.TYPE === "subflow"; | ||||
|             return { | ||||
|                 id: node.id,  | ||||
|                 name: node.name || node.id,  | ||||
|                 id: node.id, | ||||
|                 name: node.name || node.id, | ||||
|                 flowId: node._flow.flow.id, | ||||
|                 flowName: isSubFlow ?  node._flow.subflowDef.name : node._flow.flow.label, | ||||
|                 flowName: isSubFlow ? node._flow.subflowDef.name : node._flow.flow.label, | ||||
|                 isSubFlow: isSubFlow | ||||
|             } | ||||
|         } | ||||
|         return { | ||||
|             /** | ||||
|              * Get a list of targets registerd to this name | ||||
|              * @param {string} name  | ||||
|              * @param {string} name Name of the target | ||||
|              * @param {boolean} [excludeSubflows] set `true` to exclude  | ||||
|              * @returns {[LinkTarget]} Targets registerd to this name. | ||||
|              */ | ||||
|             getTargets(name) { | ||||
|                 return registry.named[name] || []; | ||||
|             getTargets(name, excludeSubflows) { | ||||
|                 const targets = registry.name[name] || []; | ||||
|                 if (excludeSubflows) { | ||||
|                     return targets.filter(e => e.isSubFlow != true); | ||||
|                 } | ||||
|                 return targets; | ||||
|             }, | ||||
|             /** | ||||
|              * Get a single target by registered name. | ||||
| @@ -70,17 +76,17 @@ module.exports = function(RED) { | ||||
|              * @param {string} [flowId]  | ||||
|              * @returns {LinkTarget} target | ||||
|              */ | ||||
|              getTarget(name, flowId) { | ||||
|             getTarget(name, flowId) { | ||||
|                 /** @type {[LinkTarget]}*/ | ||||
|                 let possibleTargets = this.getTargets(name); | ||||
|                 /** @type {LinkTarget}*/ let target; | ||||
|                 if(possibleTargets.length) { | ||||
|                     if(flowId) { | ||||
|                         possibleTargets = possibleTargets.filter(e => e.flowId == flowId); | ||||
|                     } | ||||
|                 /** @type {LinkTarget}*/  | ||||
|                 let target; | ||||
|                 if (possibleTargets.length && flowId) { | ||||
|                     possibleTargets = possibleTargets.filter(e => e.flowId == flowId); | ||||
|                 } | ||||
|                 if(possibleTargets.length === 1) { | ||||
|                 if (possibleTargets.length === 1) { | ||||
|                     target = possibleTargets[0]; | ||||
|                 }  | ||||
|                 } | ||||
|                 return target; | ||||
|             }, | ||||
|             /** | ||||
| @@ -89,35 +95,35 @@ module.exports = function(RED) { | ||||
|              * @returns {LinkTarget} target | ||||
|              */ | ||||
|             getTargetById(nodeId) { | ||||
|                 return registry.ids[nodeId]; | ||||
|             },             | ||||
|                 return registry.id[nodeId]; | ||||
|             }, | ||||
|             register(/** @type {LinkInNode} */ node) { | ||||
|                 const target = generateTarget(node); | ||||
|                 const tByName = this.getTarget(target.name, target.flowId); | ||||
|                 if(!tByName) { | ||||
|                     registry.named[target.name] = registry.named[target.name] || []; | ||||
|                     registry.named[target.name].push(target) | ||||
|                 if (!tByName || tByName.id !== target.id) { | ||||
|                     registry.name[target.name] = registry.name[target.name] || []; | ||||
|                     registry.name[target.name].push(target) | ||||
|                 } | ||||
|                 registry.ids[target.id] = target; | ||||
|                 registry.id[target.id] = target; | ||||
|                 return target; | ||||
|             }, | ||||
|             remove(node) { | ||||
|                 const target = generateTarget(node); | ||||
|                 const tn = this.getTarget(target.name, target.flowId); | ||||
|                 if(tn) { | ||||
|                 if (tn) { | ||||
|                     const targs = this.getTargets(tn.name); | ||||
|                     const idx = getIndex(targs, tn.id); | ||||
|                     if(idx > -1) { | ||||
|                         targs.splice(idx,1); | ||||
|                     if (idx > -1) { | ||||
|                         targs.splice(idx, 1); | ||||
|                     } | ||||
|                     if(targs.length === 0) { | ||||
|                         delete registry.named[tn.name]; | ||||
|                     if (targs.length === 0) { | ||||
|                         delete registry.name[tn.name]; | ||||
|                     } | ||||
|                 } | ||||
|                 delete registry.ids[target.id]; | ||||
|                 delete registry.id[target.id]; | ||||
|             }, | ||||
|             clear() { | ||||
|                 registry = { ids: {}, named: {}}; | ||||
|                 registry = { id: {}, name: {} }; | ||||
|             } | ||||
|         } | ||||
|     })(); | ||||
| @@ -187,17 +193,27 @@ module.exports = function(RED) { | ||||
|         if (isNaN(timeout)) { | ||||
|             timeout = 30000; | ||||
|         } | ||||
|         function findNode(target) { | ||||
|             let foundNode = RED.nodes.getNode(target); //1st see if the target is a direct node id | ||||
|             if(!foundNode) { | ||||
|                 //first look in this flow for the node | ||||
|         function getTargetNode(msg) { | ||||
|             const dynamicMode = linkType === "dynamic"; | ||||
|             const target = dynamicMode ? msg.target : staticTarget | ||||
|  | ||||
|             ////1st see if the target is a direct node id | ||||
|             let foundNode; | ||||
|             if (targetCache.getTargetById(target)) { | ||||
|                 foundNode = RED.nodes.getNode(target) | ||||
|             } | ||||
|             if (target && !foundNode && dynamicMode) { | ||||
|                 //next, look in **this flow only** for the node | ||||
|                 let cachedTarget = targetCache.getTarget(target, node._flow.flow.id); | ||||
|                 if(!cachedTarget) { | ||||
|                     //single target node not found in registry! get all possible targets | ||||
|                     const possibleTargets = targetCache.getTargets(target); | ||||
|                     if(possibleTargets.length === 1) { | ||||
|                 if (!cachedTarget) { | ||||
|                     //single target node not found in registry!  | ||||
|                     //get all possible targets from regular flows (exclude subflow instances) | ||||
|                     const possibleTargets = targetCache.getTargets(target, true); | ||||
|                     if (possibleTargets.length === 1) { | ||||
|                         //only 1 link-in found with this name - good, lets use it | ||||
|                         cachedTarget = possibleTargets[0]; | ||||
|                     } else if (possibleTargets.length > 1) { | ||||
|                         //more than 1 link-in has this name, raise an error | ||||
|                         throw new Error(`Multiple link-in nodes named '${target}' found`); | ||||
|                     } | ||||
|                 } | ||||
| @@ -205,31 +221,31 @@ module.exports = function(RED) { | ||||
|                     foundNode = RED.nodes.getNode(cachedTarget.id); | ||||
|                 } | ||||
|             } | ||||
|             if(foundNode instanceof LinkInNode) { | ||||
|             if (foundNode instanceof LinkInNode) { | ||||
|                 return foundNode; | ||||
|             } | ||||
|             throw new Error(`link-in node '${target}' not found`); | ||||
|             throw new Error(`target link-in node '${target || ""}' not found`); | ||||
|         } | ||||
|         this.on("input", function(msg, send, done) { | ||||
|         this.on("input", function (msg, send, done) { | ||||
|             try { | ||||
|                 let targetNode = linkType == "dynamic" ? findNode(msg.target) : RED.nodes.getNode(staticTarget); | ||||
|                 if (targetNode && targetNode instanceof LinkInNode) { | ||||
|                 const targetNode = getTargetNode(msg); | ||||
|                 if (targetNode instanceof LinkInNode) { | ||||
|                     msg._linkSource = msg._linkSource || []; | ||||
|                     const messageEvent = { | ||||
|                         id: crypto.randomBytes(14).toString('hex'), | ||||
|                         node: node.id, | ||||
|                     }         | ||||
|                     } | ||||
|                     messageEvents[messageEvent.id] = { | ||||
|                         msg: RED.util.cloneMessage(msg), | ||||
|                         send, | ||||
|                         done, | ||||
|                         ts: setTimeout(function() { | ||||
|                         ts: setTimeout(function () { | ||||
|                             timeoutMessage(messageEvent.id) | ||||
|                         }, timeout ) | ||||
|                         }, timeout) | ||||
|                     }; | ||||
|                     msg._linkSource.push(messageEvent); | ||||
|                     targetNode.receive(msg); | ||||
|                 }   | ||||
|                 } | ||||
|             } catch (error) { | ||||
|                 node.error(error, msg); | ||||
|             } | ||||
|   | ||||
| @@ -120,63 +120,103 @@ describe('link Node', function() { | ||||
|     }); | ||||
|  | ||||
|     describe("link-call node", function() { | ||||
|         it('should call static link-in node and get response', function(done) { | ||||
|             var flow = [{id:"link-in-1", type:"link in", wires: [[ "func"]]}, | ||||
|                         {id:"func", type:"helper", wires: [["link-out-1"]]}, | ||||
|                         {id:"link-out-1", type:"link out", mode: "return"}, | ||||
|                         {id:"link-call", type:"link call", links:["link-in-1"], wires:[["n4"]]}, | ||||
|                         {id:"n4", type:"helper"} ]; | ||||
|             helper.load(linkNode, flow, function() { | ||||
|         it('should call static link-in node and get response', function (done) { | ||||
|             var flow = [{ id: "link-in-1", type: "link in", wires: [["func"]] }, | ||||
|             { id: "func", type: "helper", wires: [["link-out-1"]] }, | ||||
|             { id: "link-out-1", type: "link out", mode: "return" }, | ||||
|             { id: "link-call", type: "link call", links: ["link-in-1"], wires: [["n4"]] }, | ||||
|             { id: "n4", type: "helper" }]; | ||||
|             helper.load(linkNode, flow, function () { | ||||
|                 var func = helper.getNode("func"); | ||||
|                 func.on("input", function(msg, send, done) { | ||||
|                 func.on("input", function (msg, send, done) { | ||||
|                     msg.payload = "123"; | ||||
|                     send(msg); | ||||
|                     done(); | ||||
|                 }) | ||||
|                 var n1 = helper.getNode("link-call"); | ||||
|                 var n4 = helper.getNode("n4"); | ||||
|                 n4.on("input", function(msg) { | ||||
|                 n4.on("input", function (msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('payload', '123'); | ||||
|                         done(); | ||||
|                     } catch(err) { | ||||
|                     } catch (err) { | ||||
|                         done(err); | ||||
|                     } | ||||
|                 }); | ||||
|                 n1.receive({payload:"hello"}); | ||||
|                 n1.receive({ payload: "hello" }); | ||||
|             }); | ||||
|         }) | ||||
|      | ||||
|         it('should call link-in node by name and get response', function(done) { | ||||
|  | ||||
|         it('should call link-in node by name and get response', function (done) { | ||||
|             this.timeout(500); | ||||
|             var payload = Date.now(); | ||||
|             var flow = [{id:"link-in-1", type:"link in", name:"double payload", wires: [[ "func"]]}, | ||||
|                         {id:"func", type:"helper", wires: [["link-out-1"]]}, | ||||
|                         {id:"link-out-1", type:"link out", mode: "return"}, | ||||
|                         {id:"link-call", type:"link call", linkType:"dynamic", links:[], wires:[["n4"]]}, | ||||
|                         {id:"n4", type:"helper"} ]; | ||||
|             helper.load(linkNode, flow, function() { | ||||
|             var flow = [ | ||||
|                 { id: "tab-flow-1", type: "tab", label: "Flow 1" }, | ||||
|                 { id: "tab-flow-2", type: "tab", label: "Flow 2" }, | ||||
|                 { id: "link-in-1", z: "tab-flow-1", type: "link in", name: "double payload", wires: [["func"]] }, | ||||
|                 { id: "link-in-2", z: "tab-flow-2", type: "link in", name: "double payload", wires: [["func"]] }, | ||||
|                 { id: "func", z: "tab-flow-1", type: "helper", wires: [["link-out-1"]] }, | ||||
|                 { id: "link-out-1", z: "tab-flow-1", type: "link out", mode: "return" }, | ||||
|                 { id: "link-call", z: "tab-flow-1", type: "link call", linkType: "dynamic", links: [], wires: [["n4"]] }, | ||||
|                 { id: "n4", z: "tab-flow-1", type: "helper" } | ||||
|             ]; | ||||
|             helper.load(linkNode, flow, function () { | ||||
|                 var func = helper.getNode("func"); | ||||
|                 func.on("input", function(msg, send, done) { | ||||
|                 func.on("input", function (msg, send, done) { | ||||
|                     msg.payload += msg.payload; | ||||
|                     send(msg); | ||||
|                     done(); | ||||
|                 }) | ||||
|                 var n1 = helper.getNode("link-call"); | ||||
|                 var n4 = helper.getNode("n4"); | ||||
|                 n4.on("input", function(msg) { | ||||
|                 n4.on("input", function (msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property('payload'); | ||||
|                         msg.payload.should.eql(payload + payload); | ||||
|                         done(); | ||||
|                     } catch(err) { | ||||
|                     } catch (err) { | ||||
|                         done(err); | ||||
|                     } | ||||
|                 }); | ||||
|                 n1.receive({payload:payload, target:"double payload" }); | ||||
|                 n1.receive({ payload: payload, target: "double payload" }); | ||||
|             }); | ||||
|         }) | ||||
|         it('should timeout waiting for link return', function(done) { | ||||
|             this.timeout(1000);  | ||||
|         // //TODO: This test is DISABLED - helper.load() calls callback but none of the nodes are available (issue loading a flow with a subflow) | ||||
|         // it('should call link-in node by name in subflow', function (done) { | ||||
|         //     this.timeout(9999500); | ||||
|         //     var payload = Date.now(); | ||||
|         //     var flow = [ | ||||
|         //         {"id":"sub-flow-template","type":"subflow","name":"Subflow","info":"","category":"","in":[{"wires":[{"id":"link-call-1"}]}],"out":[{"wires":[{"id":"link-call-1","port":0}]}]}, | ||||
|         //         {"id":"link-call-1","type":"link call","z":"sub-flow-template","name":"","links":[],"linkType":"dynamic","timeout":"5","wires":[[]]}, | ||||
|         //         {"id":"link-in-1","type":"link in","z":"sub-flow-template","name":"double payload","links":[],"wires":[["func"]]}, | ||||
|         //         {"id":"func","type":"function","z":"sub-flow-template","name":"payload.a  x  payload.b","func":"msg.payload += msg.payload\nreturn msg;\n","outputs":1,"wires":[["link-out-1"]]}, | ||||
|         //         {"id":"link-out-1","type":"link out","z":"sub-flow-template","name":"","mode":"return","links":[],"wires":[]}, | ||||
|         //         {"id":"sub-flow-1","type":"subflow:sub-flow-template","z":"tab-flow-1","name":"","wires":[["n4"]]}, | ||||
|         //         { id: "n4", z: "tab-flow-1", type: "helper" } | ||||
|         //     ]; | ||||
|         //     helper.load(linkNode, flow, function () { | ||||
|         //         var sf = helper.getNode("sub-flow-1"); | ||||
|         //         var func = helper.getNode("func"); | ||||
|         //         var n4 = helper.getNode("n4"); | ||||
|         //         func.on("input", function (msg, send, done) { | ||||
|         //             msg.payload += msg.payload; | ||||
|         //             send(msg); | ||||
|         //             done(); | ||||
|         //         }) | ||||
|         //         n4.on("input", function (msg) { | ||||
|         //             try { | ||||
|         //                 msg.should.have.property('payload'); | ||||
|         //                 msg.payload.should.eql(payload + payload); | ||||
|         //                 done(); | ||||
|         //             } catch (err) { | ||||
|         //                 done(err); | ||||
|         //             } | ||||
|         //         }); | ||||
|         //         sf.receive({ payload: payload, target: "double payload" }); | ||||
|         //     }); | ||||
|         // }) | ||||
|         it('should timeout waiting for link return', function (done) { | ||||
|             this.timeout(1000); | ||||
|             const flow = [ | ||||
|                 { id: "tab-flow-1", type: "tab", label: "Flow 1" }, | ||||
|                 { id: "link-in-1", z: "tab-flow-1", type: "link in", name: "double payload", wires: [["func"]] }, | ||||
| @@ -184,32 +224,66 @@ describe('link Node', function() { | ||||
|                 { id: "link-out-1", z: "tab-flow-1", type: "link out", mode: "" }, //not return mode, cause link-call timeout | ||||
|                 { id: "link-call", z: "tab-flow-1", type: "link call", linkType: "static", "timeout": "0.5", links: ["link-in-1"], wires: [["n4"]] }, | ||||
|                 { id: "catch-all", z: "tab-flow-1", type: "catch", scope: ["link-call"], uncaught: true, wires: [["n4"]] }, | ||||
|                 { id: "n4", z: "tab-flow-1", type: "helper" }  | ||||
|                 { id: "n4", z: "tab-flow-1", type: "helper" } | ||||
|             ]; | ||||
|             helper.load(linkNode, flow, function() { | ||||
|             helper.load(linkNode, flow, function () { | ||||
|                 const funcNode = helper.getNode("func"); | ||||
|                 const linkCallNode = helper.getNode("link-call"); | ||||
|                 const helperNode = helper.getNode("n4"); | ||||
|                 funcNode.on("input", function(msg, send, done) { | ||||
|                 funcNode.on("input", function (msg, send, done) { | ||||
|                     msg.payload += msg.payload; | ||||
|                     send(msg); | ||||
|                     done(); | ||||
|                 }) | ||||
|                 helperNode.on("input", function(msg) { | ||||
|                 helperNode.on("input", function (msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property("target", "double payload"); | ||||
|                         msg.should.have.property("error"); | ||||
|                         msg.error.should.have.property("message", "timeout"); | ||||
|                         msg.error.should.have.property("source"); | ||||
|                         done(); | ||||
|                     } catch(err) { | ||||
|                     } catch (err) { | ||||
|                         done(err); | ||||
|                     } | ||||
|                 }); | ||||
|                 linkCallNode.receive({payload:"hello", target:"double payload" }); | ||||
|                 linkCallNode.receive({ payload: "hello", target: "double payload" }); | ||||
|             }); | ||||
|         }) | ||||
|         it('should raise error due to multiple targets', function(done) { | ||||
|         it('should raise error due to multiple targets on same tab', function (done) { | ||||
|             this.timeout(55500); | ||||
|             const flow = [ | ||||
|                 { id: "tab-flow-1", type: "tab", label: "Flow 1" }, | ||||
|                 { id: "link-in-1", z: "tab-flow-1", type: "link in", name: "double payload", wires: [["func"]] }, | ||||
|                 { id: "link-in-2", z: "tab-flow-1", type: "link in", name: "double payload", wires: [["func"]] }, | ||||
|                 { id: "func", z: "tab-flow-1", type: "helper", wires: [["link-out-1"]] }, | ||||
|                 { id: "link-out-1", z: "tab-flow-1", type: "link out", mode: "return" }, | ||||
|                 { id: "link-call", z: "tab-flow-1", type: "link call", linkType: "dynamic", links: [], wires: [["n4"]] }, | ||||
|                 { id: "catch-all", z: "tab-flow-1", type: "catch", scope: ["link-call"], uncaught: true, wires: [["n4"]] }, | ||||
|                 { id: "n4", z: "tab-flow-1", type: "helper" } | ||||
|             ]; | ||||
|             helper.load(linkNode, flow, function () { | ||||
|                 const funcNode = helper.getNode("func"); | ||||
|                 const linkCall = helper.getNode("link-call"); | ||||
|                 const helperNode = helper.getNode("n4"); | ||||
|                 funcNode.on("input", function (msg, send, _done) { | ||||
|                     done(new Error("Function should not be called")) | ||||
|                 }) | ||||
|                 helperNode.on("input", function (msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property("target", "double payload"); | ||||
|                         msg.should.have.property("error"); | ||||
|                         msg.error.should.have.property("message"); | ||||
|                         msg.error.message.should.match(/.*Multiple link-in nodes.*/) | ||||
|                         msg.error.should.have.property("source"); | ||||
|                         done(); | ||||
|                     } catch (err) { | ||||
|                         done(err); | ||||
|                     } | ||||
|                 }); | ||||
|                 linkCall.receive({ payload: "hello", target: "double payload" }); | ||||
|             }); | ||||
|         }) | ||||
|         it('should raise error due to multiple targets on different tabs', function (done) { | ||||
|             this.timeout(500); | ||||
|             const flow = [ | ||||
|                 { id: "tab-flow-1", type: "tab", label: "Flow 1" }, | ||||
| @@ -221,28 +295,28 @@ describe('link Node', function() { | ||||
|                 { id: "link-out-1", z: "tab-flow-1", type: "link out", mode: "return" }, | ||||
|                 { id: "link-call", z: "tab-flow-1", type: "link call", linkType: "dynamic", links: [], wires: [["n4"]] }, | ||||
|                 { id: "catch-all", z: "tab-flow-1", type: "catch", scope: ["link-call"], uncaught: true, wires: [["n4"]] }, | ||||
|                 { id: "n4", z: "tab-flow-1", type: "helper" }  | ||||
|                 { id: "n4", z: "tab-flow-1", type: "helper" } | ||||
|             ]; | ||||
|             helper.load(linkNode, flow, function() { | ||||
|             helper.load(linkNode, flow, function () { | ||||
|                 const funcNode = helper.getNode("func"); | ||||
|                 const linkCall = helper.getNode("link-call"); | ||||
|                 const helperNode = helper.getNode("n4"); | ||||
|                 funcNode.on("input", function(msg, send, _done) { | ||||
|                 funcNode.on("input", function (msg, send, _done) { | ||||
|                     done(new Error("Function should not be called")) | ||||
|                 }) | ||||
|                 helperNode.on("input", function(msg) { | ||||
|                     try{ | ||||
|                 helperNode.on("input", function (msg) { | ||||
|                     try { | ||||
|                         msg.should.have.property("target", "double payload"); | ||||
|                         msg.should.have.property("error"); | ||||
|                         msg.error.should.have.property("message"); | ||||
|                         msg.error.message.should.match(/.*Multiple link-in nodes.*/) | ||||
|                         msg.error.should.have.property("source"); | ||||
|                         done(); | ||||
|                     } catch(err) { | ||||
|                     } catch (err) { | ||||
|                         done(err); | ||||
|                     } | ||||
|                 }); | ||||
|                 linkCall.receive({payload:"hello", target:"double payload" }); | ||||
|                 linkCall.receive({ payload: "hello", target: "double payload" }); | ||||
|             }); | ||||
|         }) | ||||
|         it('should allow nested link-call flows', function(done) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user