mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	dynamic link target 1st draft
This commit is contained in:
		@@ -32,8 +32,17 @@
 | 
				
			|||||||
        <label for="node-input-timeout"><span data-i18n="exec.label.timeout"></span></label>
 | 
					        <label for="node-input-timeout"><span data-i18n="exec.label.timeout"></span></label>
 | 
				
			||||||
        <input type="text" id="node-input-timeout" placeholder="30" style="width: 70px; margin-right: 5px;"><span data-i18n="inject.seconds"></span>
 | 
					        <input type="text" id="node-input-timeout" placeholder="30" style="width: 70px; margin-right: 5px;"><span data-i18n="inject.seconds"></span>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div style="position:relative; height: 30px; text-align: right;"><div style="display:inline-block"><input type="text" id="node-input-link-target-filter"></div></div>
 | 
					    <div class="form-row">
 | 
				
			||||||
    <div class="form-row node-input-link-row"></div>
 | 
					        <label for="node-input-linkType" data-i18n="link.linkCallType"></label>
 | 
				
			||||||
 | 
					        <select id="node-input-linkType" style="width: 70%">
 | 
				
			||||||
 | 
					            <option value="static" data-i18n="link.staticLinkCall"></option>
 | 
				
			||||||
 | 
					            <option value="dynamic" data-i18n="link.dynamicLinkCall"></option>
 | 
				
			||||||
 | 
					        </select>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="link-call-target-tree" style="position:relative; height: 30px; text-align: right;">
 | 
				
			||||||
 | 
					        <div style="display:inline-block"><input type="text" id="node-input-link-target-filter"></div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="form-row node-input-link-row link-call-target-tree"></div>
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script type="text/javascript">
 | 
					<script type="text/javascript">
 | 
				
			||||||
@@ -259,6 +268,7 @@
 | 
				
			|||||||
        defaults: {
 | 
					        defaults: {
 | 
				
			||||||
            name: {value:""},
 | 
					            name: {value:""},
 | 
				
			||||||
            links: { value: [], type:"link in[]"},
 | 
					            links: { value: [], type:"link in[]"},
 | 
				
			||||||
 | 
					            linkType: {value:"static"},
 | 
				
			||||||
            timeout: { value: "30", validate:RED.validators.number(true) }
 | 
					            timeout: { value: "30", validate:RED.validators.number(true) }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        inputs: 1,
 | 
					        inputs: 1,
 | 
				
			||||||
@@ -271,7 +281,9 @@
 | 
				
			|||||||
            if (this.name) {
 | 
					            if (this.name) {
 | 
				
			||||||
                return this.name;
 | 
					                return this.name;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if (this.links.length > 0) {
 | 
					            if (this.linkType === "dynamic") {
 | 
				
			||||||
 | 
					                return this._("link.dynamicLinkLabel");
 | 
				
			||||||
 | 
					            } else if (this.links.length > 0) {
 | 
				
			||||||
                var targetNode = RED.nodes.node(this.links[0]);
 | 
					                var targetNode = RED.nodes.node(this.links[0]);
 | 
				
			||||||
                return targetNode && (targetNode.name || this._("link.linkCall"));
 | 
					                return targetNode && (targetNode.name || this._("link.linkCall"));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -281,6 +293,19 @@
 | 
				
			|||||||
            return this.name?"node_label_italic":"";
 | 
					            return this.name?"node_label_italic":"";
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        oneditprepare: function() {
 | 
					        oneditprepare: function() {
 | 
				
			||||||
 | 
					            console.log("link call oneditprepare")
 | 
				
			||||||
 | 
					            const updateVisibility = function() {
 | 
				
			||||||
 | 
					                const static = $('#node-input-linkType').val() !== "dynamic";
 | 
				
			||||||
 | 
					                if(static) {
 | 
				
			||||||
 | 
					                    $("div.link-call-target-tree").show();
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    $("div.link-call-target-tree").hide();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            $("#node-input-linkType").on("change",function(d){
 | 
				
			||||||
 | 
					                updateVisibility();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            updateVisibility();
 | 
				
			||||||
            onEditPrepare(this,"link in");
 | 
					            onEditPrepare(this,"link in");
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        oneditsave: function() {
 | 
					        oneditsave: function() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,8 +16,107 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
module.exports = function(RED) {
 | 
					module.exports = function(RED) {
 | 
				
			||||||
    "use strict";
 | 
					    "use strict";
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const crypto = require("crypto");
 | 
					    const crypto = require("crypto");
 | 
				
			||||||
 | 
					    const targetCache = (function() {
 | 
				
			||||||
 | 
					        const name2id = {};
 | 
				
			||||||
 | 
					        let id2target = {};
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            /** @type {((node: Node, [flowName]: string) => string) & ((nodeName: string, flowName: string) => string))} */
 | 
				
			||||||
 | 
					            generateLookupName(node, flowName) {
 | 
				
			||||||
 | 
					                if(!flowName) {
 | 
				
			||||||
 | 
					                    flowName = node._flow.flow.label
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if(node instanceof LinkInNode) {
 | 
				
			||||||
 | 
					                    return `${flowName}/${node.name}`;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return `${flowName}/${node}`;
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            add(node) {
 | 
				
			||||||
 | 
					                const id = node.id;
 | 
				
			||||||
 | 
					                const nodeName = node.name;
 | 
				
			||||||
 | 
					                if(!nodeName){ return null;} //node must be named
 | 
				
			||||||
 | 
					                const flowName = node._flow.flow.label;
 | 
				
			||||||
 | 
					                const lookupName = targetCache.generateLookupName(nodeName, flowName);
 | 
				
			||||||
 | 
					                console.log(`Adding node '${lookupName}' (${id}) to cache`)
 | 
				
			||||||
 | 
					                if(name2id[lookupName] && name2id[lookupName].id !== id) {
 | 
				
			||||||
 | 
					                    //TODO: reassignment? duplate lookupName?  throw error or warning?
 | 
				
			||||||
 | 
					                    console.warn(`💣 '${lookupName}' is already assigned to node with ID: ${name2id[lookupName]} `)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                name2id[lookupName] = id;
 | 
				
			||||||
 | 
					                id2target[id] = {
 | 
				
			||||||
 | 
					                    lookupName,
 | 
				
			||||||
 | 
					                    nodeName,
 | 
				
			||||||
 | 
					                    flowName,
 | 
				
			||||||
 | 
					                    id
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return id2target[id];
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            remove(node) {
 | 
				
			||||||
 | 
					                if(node.name) console.log(`Removing node '${targetCache.generateLookupName(node)}' (${node.id})`)
 | 
				
			||||||
 | 
					                const target = id2target[node.id];
 | 
				
			||||||
 | 
					                targetCache._removeById(node.id);
 | 
				
			||||||
 | 
					                if(target) {
 | 
				
			||||||
 | 
					                    targetCache._removeById(target.id);
 | 
				
			||||||
 | 
					                    targetCache._removeByName(target.lookupName);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            getTarget(lookupName) {
 | 
				
			||||||
 | 
					                const id = name2id[lookupName];
 | 
				
			||||||
 | 
					                const target = id2target[id];
 | 
				
			||||||
 | 
					                return target;
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            verify(cachedTarget, targetNode, nodeName, flowName) {
 | 
				
			||||||
 | 
					                cachedTarget = cachedTarget || {};
 | 
				
			||||||
 | 
					                targetNode = targetNode || {};
 | 
				
			||||||
 | 
					                const _lookupName = targetCache.generateLookupName(nodeName, flowName);
 | 
				
			||||||
 | 
					                const cachedId = name2id[_lookupName];
 | 
				
			||||||
 | 
					   
 | 
				
			||||||
 | 
					                const idOK = cachedTarget.id === targetNode.id && cachedId === targetNode.id;
 | 
				
			||||||
 | 
					                const nodeNameOK = nodeName === cachedTarget.nodeName && cachedTarget.nodeName == targetNode.name;
 | 
				
			||||||
 | 
					                const flowNameOK = flowName === cachedTarget.flowName && cachedTarget.flowName == targetNode._flow.flow.label;
 | 
				
			||||||
 | 
					                const lookupNameOK = _lookupName === cachedTarget.lookupName;
 | 
				
			||||||
 | 
					                if(idOK && nodeNameOK && flowNameOK && lookupNameOK) {
 | 
				
			||||||
 | 
					                    return true;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                console.warn(`verify(cachedTarget, targetNode, nodeName, flowName) failed: `, {cachedTarget, targetNode, nodeName, flowName})
 | 
				
			||||||
 | 
					                console.warn(`> idOK:${idOK}, nodeNameOK:${nodeNameOK}, flowNameOK:${flowNameOK}, lookupNameOK:${lookupNameOK}`)
 | 
				
			||||||
 | 
					                targetCache._removeById(targetNode.id);
 | 
				
			||||||
 | 
					                targetCache._removeById(cachedId);
 | 
				
			||||||
 | 
					                targetCache._removeByName(_lookupName);
 | 
				
			||||||
 | 
					                targetCache._removeByName(cachedTarget.lookupName);
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            _removeById(id) {
 | 
				
			||||||
 | 
					                if(!id) {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                const target = id2target[id];
 | 
				
			||||||
 | 
					                if(target && target.lookupName) {
 | 
				
			||||||
 | 
					                    delete name2id[target.lookupName];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                delete id2target[id];
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            _removeByName(lookupName) {
 | 
				
			||||||
 | 
					                if(!lookupName) {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                var id = name2id[lookupName];
 | 
				
			||||||
 | 
					                var target = id2target[id];
 | 
				
			||||||
 | 
					                if(id) {
 | 
				
			||||||
 | 
					                    delete id2target[id];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if(target && target.lookupName) {
 | 
				
			||||||
 | 
					                    delete name2id[target.lookupName];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                delete name2id[lookupName];
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            clear() {
 | 
				
			||||||
 | 
					                name2id = {};
 | 
				
			||||||
 | 
					                id2target = {};
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    })();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function LinkInNode(n) {
 | 
					    function LinkInNode(n) {
 | 
				
			||||||
        RED.nodes.createNode(this,n);
 | 
					        RED.nodes.createNode(this,n);
 | 
				
			||||||
@@ -27,12 +126,27 @@ module.exports = function(RED) {
 | 
				
			|||||||
            msg._event = n.event;
 | 
					            msg._event = n.event;
 | 
				
			||||||
            node.receive(msg);
 | 
					            node.receive(msg);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        //update target cache for link calls
 | 
				
			||||||
 | 
					        function updateCache(o) {
 | 
				
			||||||
 | 
					            console.log(node.id, node.name, o.config.rev, o)
 | 
				
			||||||
 | 
					            const changed = o.diff.changed.includes(node.id);
 | 
				
			||||||
 | 
					            const added = o.diff.added.includes(node.id);
 | 
				
			||||||
 | 
					            if(changed) {
 | 
				
			||||||
 | 
					                targetCache.remove(node);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if((changed || added) && node.name) {
 | 
				
			||||||
 | 
					                targetCache.add(node);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        RED.events.on("flows:started", updateCache);
 | 
				
			||||||
        RED.events.on(event,handler);
 | 
					        RED.events.on(event,handler);
 | 
				
			||||||
        this.on("input", function(msg, send, done) {
 | 
					        this.on("input", function(msg, send, done) {
 | 
				
			||||||
            send(msg);
 | 
					            send(msg);
 | 
				
			||||||
            done();
 | 
					            done();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        this.on("close",function() {
 | 
					        this.on("close",function() {
 | 
				
			||||||
 | 
					            targetCache.remove(node);
 | 
				
			||||||
            RED.events.removeListener(event,handler);
 | 
					            RED.events.removeListener(event,handler);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -74,14 +188,55 @@ module.exports = function(RED) {
 | 
				
			|||||||
    function LinkCallNode(n) {
 | 
					    function LinkCallNode(n) {
 | 
				
			||||||
        RED.nodes.createNode(this,n);
 | 
					        RED.nodes.createNode(this,n);
 | 
				
			||||||
        const node = this;
 | 
					        const node = this;
 | 
				
			||||||
        const target = n.links[0];
 | 
					        const staticTarget = n.links[0];
 | 
				
			||||||
 | 
					        const linkType = n.linkType;
 | 
				
			||||||
        const messageEvents = {};
 | 
					        const messageEvents = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let timeout = parseFloat(n.timeout || "30") * 1000;
 | 
					        let timeout = parseFloat(n.timeout || "30") * 1000;
 | 
				
			||||||
        if (isNaN(timeout)) {
 | 
					        if (isNaN(timeout)) {
 | 
				
			||||||
            timeout = 30000;
 | 
					            timeout = 30000;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        function findNode(target) {
 | 
				
			||||||
 | 
					            let foundNode = null;
 | 
				
			||||||
 | 
					            switch (typeof target) {
 | 
				
			||||||
 | 
					                case "string":
 | 
				
			||||||
 | 
					                    const nameParts = target.split("/");
 | 
				
			||||||
 | 
					                    switch (nameParts.length) {
 | 
				
			||||||
 | 
					                        case 1:
 | 
				
			||||||
 | 
					                            target = { nodeName: nameParts[0], flowName: node._flow.flow.label }
 | 
				
			||||||
 | 
					                            break;
 | 
				
			||||||
 | 
					                        case 2:
 | 
				
			||||||
 | 
					                            target = { nodeName: nameParts[1], flowName: nameParts[0] }
 | 
				
			||||||
 | 
					                            break;
 | 
				
			||||||
 | 
					                        default:
 | 
				
			||||||
 | 
					                            target = { nodeName: target, flowName: node._flow.flow.label }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                case "object":
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                default:
 | 
				
			||||||
 | 
					                    throw new Error("Invalid target");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if(!target.nodeName || typeof target.nodeName !== "string") {
 | 
				
			||||||
 | 
					                throw new Error("Link call target name invalid")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if(!target.flowName || typeof target.flowName !== "string") {
 | 
				
			||||||
 | 
					                throw new Error("Link call target flow name invalid")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const lookupName = targetCache.generateLookupName(target.nodeName, target.flowName);
 | 
				
			||||||
 | 
					            //const targetNode = targetCache.getNode(lookupName);
 | 
				
			||||||
 | 
					            const cachedTarget = targetCache.getTarget(lookupName);
 | 
				
			||||||
 | 
					            if (cachedTarget) {
 | 
				
			||||||
 | 
					                foundNode = RED.nodes.getNode(cachedTarget.id);
 | 
				
			||||||
 | 
					                if (targetCache.verify(cachedTarget, foundNode, target.nodeName, target.flowName)) {
 | 
				
			||||||
 | 
					                    return foundNode;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            throw new Error(`Link in node not found: ${lookupName}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        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) {
 | 
				
			||||||
                    msg._linkSource = msg._linkSource || [];
 | 
					                    msg._linkSource = msg._linkSource || [];
 | 
				
			||||||
                    const messageEvent = {
 | 
					                    const messageEvent = {
 | 
				
			||||||
                        id: crypto.randomBytes(14).toString('hex'),
 | 
					                        id: crypto.randomBytes(14).toString('hex'),
 | 
				
			||||||
@@ -96,10 +251,11 @@ module.exports = function(RED) {
 | 
				
			|||||||
                        }, timeout )
 | 
					                        }, timeout )
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
                    msg._linkSource.push(messageEvent);
 | 
					                    msg._linkSource.push(messageEvent);
 | 
				
			||||||
            var targetNode = RED.nodes.getNode(target);
 | 
					 | 
				
			||||||
            if (targetNode) {
 | 
					 | 
				
			||||||
                    targetNode.receive(msg);
 | 
					                    targetNode.receive(msg);
 | 
				
			||||||
                }  
 | 
					                }  
 | 
				
			||||||
 | 
					            } catch (error) {
 | 
				
			||||||
 | 
					                node.error(error, msg);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.returnLinkMessage = function(eventId, msg) {
 | 
					        this.returnLinkMessage = function(eventId, msg) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -170,6 +170,10 @@
 | 
				
			|||||||
        "outMode": "Mode",
 | 
					        "outMode": "Mode",
 | 
				
			||||||
        "sendToAll": "Send to all connected link nodes",
 | 
					        "sendToAll": "Send to all connected link nodes",
 | 
				
			||||||
        "returnToCaller": "Return to calling link node",
 | 
					        "returnToCaller": "Return to calling link node",
 | 
				
			||||||
 | 
					        "linkCallType": "Link Type",
 | 
				
			||||||
 | 
					        "staticLinkCall": "Fixed target",
 | 
				
			||||||
 | 
					        "dynamicLinkCall": "Dynamic target (msg.target)",
 | 
				
			||||||
 | 
					        "dynamicLinkLabel": "Dynamic",
 | 
				
			||||||
        "error": {
 | 
					        "error": {
 | 
				
			||||||
            "missingReturn": "Missing return node information"
 | 
					            "missingReturn": "Missing return node information"
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user