diff --git a/packages/node_modules/@node-red/nodes/core/common/60-link.js b/packages/node_modules/@node-red/nodes/core/common/60-link.js index 2b0618d30..24572f8b1 100644 --- a/packages/node_modules/@node-red/nodes/core/common/60-link.js +++ b/packages/node_modules/@node-red/nodes/core/common/60-link.js @@ -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); } diff --git a/test/nodes/core/common/60-link_spec.js b/test/nodes/core/common/60-link_spec.js index 540f6cd48..63733455c 100644 --- a/test/nodes/core/common/60-link_spec.js +++ b/test/nodes/core/common/60-link_spec.js @@ -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) {