mirror of https://github.com/node-red/node-red.git
Merge c58d4ae45f into ce5fe9079e
commit
7164a2eba8
|
|
@ -55,6 +55,7 @@ RED.editor.codeEditor.monaco = (function() {
|
|||
const type = "monaco";
|
||||
const monacoThemes = ["vs","vs-dark","hc-black"]; //TODO: consider setting hc-black autmatically based on acessability?
|
||||
let userSelectedTheme;
|
||||
let tsFuncData; //used to cache the original func.d.ts data for later use when updating linkcall targets
|
||||
|
||||
//TODO: get from externalModules.js For now this is enough for feature parity with ACE (and then some).
|
||||
const knownModules = {
|
||||
|
|
@ -143,6 +144,9 @@ RED.editor.codeEditor.monaco = (function() {
|
|||
var typePath = "types/" + libPath;
|
||||
$.get(typePath)
|
||||
.done(function(data) {
|
||||
if (libPath === "node-red/func.d.ts") {
|
||||
tsFuncData = data; //cache the original func.d.ts data for later use when updating linkcall targets
|
||||
}
|
||||
modulesCache[libPath] = data;
|
||||
if(!preloadOnly) {
|
||||
loadedLibs.JS[libModule] = monaco.typescript.javascriptDefaults.addExtraLib(data, "file://types/" + libPackage + "/" + libModule + "/index.d.ts");
|
||||
|
|
@ -929,6 +933,63 @@ RED.editor.codeEditor.monaco = (function() {
|
|||
//check if extraLibs are to be loaded (e.g. fs or os)
|
||||
refreshModuleLibs(editorOptions.extraLibs)
|
||||
|
||||
// update func.d.ts definitions for up-to-date link-in target names in the node.linkcall() definition
|
||||
if (tsFuncData && options.node && options.node.type === 'function') {
|
||||
const getUpdatedLinkcallDefs = () => {
|
||||
const linkInNodes = RED.nodes.filterNodes({type:'link in'})
|
||||
const viableLinkTargets = []
|
||||
for (const target of linkInNodes) {
|
||||
// links on same flow/subflow as caller node are valid targets
|
||||
// however links inside a (different) subflow are not valid targets.
|
||||
if (target.z === options.node.z || !RED.nodes.subflow(target.z)) {
|
||||
viableLinkTargets.push(target)
|
||||
}
|
||||
}
|
||||
const targetsByName = [...new Set(viableLinkTargets.map(node => node.name || '').filter(name => name.length > 0))].map(s => JSON.stringify(s));
|
||||
const targetsById = [...new Set(viableLinkTargets.map(node => node.id))].map(s => JSON.stringify(s));
|
||||
const targets = [...targetsByName, ...targetsById].filter(s => s && s.length > 0).join("|") || 'string';
|
||||
return `
|
||||
// #region:linkcall
|
||||
/**
|
||||
* Utility function to call, a reusable flow defined as a subroutine (link-in ~ <nodes> ~ link-out).
|
||||
* When the \`linkcall\` function resolves, it returns the \`msg\` object returned by the link-out
|
||||
* (return) node of the subroutine.
|
||||
*
|
||||
* @param {string} target - the name or ID of the target link-in subroutine to call
|
||||
* @param {Object} msg - the message object to pass to the subroutine
|
||||
* @param {Object} [options] - call options
|
||||
* @param {number} [options.timeout=5000] - the maximum time to wait for a response (default: 5000ms)
|
||||
* @param {boolean} [options.clone=true] - flag to indicate if the message should be cloned (default: true)
|
||||
* @return {Promise<Object>} - resolves with the returned message
|
||||
*
|
||||
* @example Call "greeting-person" subroutine by name:
|
||||
* \`\`\`javascript
|
||||
* msg.payload = "Joe Bloggs";
|
||||
* const resultMsg = await node.linkcall("greeting-person", msg, {timeout: 10000});
|
||||
* msg.payload = resultMsg.payload; // payload = "Hello Joe Bloggs"
|
||||
* return msg; // return updated msg
|
||||
* \`\`\`
|
||||
*
|
||||
* @example Call "greeting-person" subroutine by node id:
|
||||
* \`\`\`javascript
|
||||
* msg.payload = "John Doe";
|
||||
* const result = await node.linkcall("a1b2c3d4e5f6", msg); // result.payload will be "Hello John Doe"
|
||||
* return result; // return new result object
|
||||
* \`\`\`
|
||||
*/
|
||||
static linkcall(target: ${targets}, msg: object, options?: { timeout?: number; clone?: boolean; [key: string]: any }): Promise<object>;
|
||||
// #endregion:linkcall
|
||||
`
|
||||
}
|
||||
const newDefs = getUpdatedLinkcallDefs();
|
||||
data = tsFuncData.replace(/\/\/ #region:linkcall[\s\S]*?\/\/ #endregion:linkcall/g, newDefs);
|
||||
if (loadedLibs.JS.func) {
|
||||
loadedLibs.JS.func.dispose();
|
||||
loadedLibs.JS.func = null;
|
||||
}
|
||||
loadedLibs.JS.func = monaco.languages.typescript.javascriptDefaults.addExtraLib(data, 'file://types/node-red/func/index.d.ts');
|
||||
}
|
||||
|
||||
function refreshModuleLibs(extraModuleLibs) {
|
||||
var defs = [];
|
||||
var imports = [];
|
||||
|
|
|
|||
|
|
@ -57,6 +57,36 @@ declare class node {
|
|||
* @see Node-RED documentation [writing-functions: adding-status](https://nodered.org/docs/user-guide/writing-functions#adding-status)
|
||||
*/
|
||||
static status(status:string|boolean|number);
|
||||
// #region:linkcall
|
||||
/**
|
||||
* Utility function to call a reusable flow defined as a subroutine (link-in/link-out nodes).
|
||||
*
|
||||
* @param target - the target of the link-in subroutine to call (can be node ID or name of link-in node)
|
||||
* @param {Object} msg - the message object to pass to the subroutine
|
||||
* @param {Object} [options] - call options
|
||||
* @param {number} [options.timeout=5000] - the maximum time to wait for a response (default: 5000ms)
|
||||
* @param {boolean} [options.clone=true] - flag to indicate if the message should be cloned (default: true)
|
||||
*
|
||||
* @return {Promise<Object>} - resolves with the returned message
|
||||
*
|
||||
* @example Call "greeting-person" subroutine by name:
|
||||
* ```javascript
|
||||
* msg.payload = "Joe Bloggs";
|
||||
* const result = await node.linkcall("greeting-person", msg, {timeout: 10000});
|
||||
* msg.payload = result.payload; // payload = "Hello Joe Bloggs"
|
||||
* return msg; // return updated msg
|
||||
* ```
|
||||
*
|
||||
* @example Call subroutine by node id:
|
||||
* ```javascript
|
||||
* msg.payload = "John Doe";
|
||||
* const result = await node.linkcall("a1b2c3d4e5f67890", msg);
|
||||
* // result.payload will be "Hello John Doe"
|
||||
* return result; // return new result object
|
||||
* ```
|
||||
*/
|
||||
static linkcall(target: string, msg: object, options?: { timeout?: number; clone?: boolean; [key: string]: any }): Promise<object>;
|
||||
// #endregion:linkcall
|
||||
/** the id of this node */
|
||||
public static readonly id:string;
|
||||
/** the name of this node */
|
||||
|
|
|
|||
|
|
@ -207,5 +207,16 @@ declare namespace RED {
|
|||
* @memberof @node-red/util_util
|
||||
*/
|
||||
function parseContextStore(key: string): any;
|
||||
/**
|
||||
* Utility function to call a reusable flow defined as a subroutine (link-in/link-out nodes).
|
||||
*
|
||||
* @param {string} target - the target of the link-in subroutine to call (can be the name or ID of a link-in node)
|
||||
* @param {Object} msg - the message object to pass to the subroutine
|
||||
* @param {Object} [options] - call options
|
||||
* @param {number} [options.timeout=5000] - the maximum time to wait for a response (default: 5000ms)
|
||||
* @return {Promise<Object>} - resolves with the returned message
|
||||
* @memberof @node-red/util_util
|
||||
*/
|
||||
function linkcall(target: string, msg: object, options?: object): Promise<object>;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,102 +28,6 @@
|
|||
module.exports = function(RED) {
|
||||
"use strict";
|
||||
const crypto = require("crypto");
|
||||
const targetCache = (function () {
|
||||
let registry = { id: {}, name: {} }
|
||||
function getIndex (/** @type {[LinkTarget]} */ targets, id) {
|
||||
for (let index = 0; index < (targets || []).length; index++) {
|
||||
const element = targets[index]
|
||||
if (element.id === id) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
/**
|
||||
* Generate a target object from a node
|
||||
* @param {LinkInNode} node
|
||||
* @returns {LinkTarget} a link target object
|
||||
*/
|
||||
function generateTarget (node) {
|
||||
const isSubFlow = node._flow.TYPE === 'subflow'
|
||||
return {
|
||||
id: node.id,
|
||||
name: node.name || node.id,
|
||||
flowId: node._flow.flow.id,
|
||||
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 Name of the target
|
||||
* @param {boolean} [excludeSubflows] set `true` to exclude
|
||||
* @returns {[LinkTarget]} Targets registerd to this 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.
|
||||
* To restrict to a single flow, include the `flowId`
|
||||
* If there is no targets OR more than one target, null is returned
|
||||
* @param {string} name Name of the node
|
||||
* @param {string} [flowId]
|
||||
* @returns {LinkTarget} target
|
||||
*/
|
||||
getTarget (name, flowId) {
|
||||
/** @type {[LinkTarget]} */
|
||||
let possibleTargets = this.getTargets(name)
|
||||
/** @type {LinkTarget} */
|
||||
let target
|
||||
if (possibleTargets.length && flowId) {
|
||||
possibleTargets = possibleTargets.filter(e => e.flowId === flowId)
|
||||
}
|
||||
if (possibleTargets.length === 1) {
|
||||
target = possibleTargets[0]
|
||||
}
|
||||
return target
|
||||
},
|
||||
/**
|
||||
* Get a target by node ID
|
||||
* @param {string} nodeId ID of the node
|
||||
* @returns {LinkTarget} target
|
||||
*/
|
||||
getTargetById (nodeId) {
|
||||
return registry.id[nodeId]
|
||||
},
|
||||
register (/** @type {LinkInNode} */ node) {
|
||||
const target = generateTarget(node)
|
||||
const tByName = this.getTarget(target.name, target.flowId)
|
||||
if (!tByName || tByName.id !== target.id) {
|
||||
registry.name[target.name] = registry.name[target.name] || []
|
||||
registry.name[target.name].push(target)
|
||||
}
|
||||
registry.id[target.id] = target
|
||||
return target
|
||||
},
|
||||
remove (node) {
|
||||
const target = generateTarget(node)
|
||||
const targs = this.getTargets(target.name)
|
||||
const idx = getIndex(targs, target.id)
|
||||
if (idx > -1) {
|
||||
targs.splice(idx, 1)
|
||||
}
|
||||
if (targs.length === 0) {
|
||||
delete registry.name[target.name]
|
||||
}
|
||||
delete registry.id[target.id]
|
||||
},
|
||||
clear () {
|
||||
registry = { id: {}, name: {} }
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
function LinkInNode(n) {
|
||||
RED.nodes.createNode(this,n);
|
||||
|
|
@ -133,14 +37,14 @@ module.exports = function(RED) {
|
|||
msg._event = n.event;
|
||||
node.receive(msg);
|
||||
}
|
||||
targetCache.register(node);
|
||||
RED.nodes.linkcallTargets.register(node);
|
||||
RED.events.on(event,handler);
|
||||
this.on("input", function(msg, send, done) {
|
||||
send(msg);
|
||||
done();
|
||||
});
|
||||
this.on("close",function() {
|
||||
targetCache.remove(node);
|
||||
RED.nodes.linkcallTargets.remove(node);
|
||||
RED.events.removeListener(event,handler);
|
||||
});
|
||||
}
|
||||
|
|
@ -190,42 +94,12 @@ module.exports = function(RED) {
|
|||
if (isNaN(timeout)) {
|
||||
timeout = 30000;
|
||||
}
|
||||
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 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`);
|
||||
}
|
||||
}
|
||||
if (cachedTarget) {
|
||||
foundNode = RED.nodes.getNode(cachedTarget.id);
|
||||
}
|
||||
}
|
||||
if (foundNode instanceof LinkInNode) {
|
||||
return foundNode;
|
||||
}
|
||||
throw new Error(`target link-in node '${target || ""}' not found`);
|
||||
}
|
||||
this.on("input", function (msg, send, done) {
|
||||
try {
|
||||
const targetNode = getTargetNode(msg);
|
||||
const dynamicMode = linkType === "dynamic";
|
||||
const target = dynamicMode ? msg.target : staticTarget
|
||||
const targetNode = RED.nodes.linkcallTargets.getTargetNode(node, target, dynamicMode);
|
||||
if (targetNode instanceof LinkInNode) {
|
||||
msg._linkSource = msg._linkSource || [];
|
||||
const messageEvent = {
|
||||
|
|
@ -282,5 +156,4 @@ module.exports = function(RED) {
|
|||
}
|
||||
RED.nodes.registerType("link call",LinkCallNode);
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -527,9 +527,15 @@
|
|||
}
|
||||
});
|
||||
|
||||
var buildEditor = function(id, stateId, focus, value, defaultValue, extraLibs, offset) {
|
||||
var editor = RED.editor.createEditor({
|
||||
const buildEditor = function(id, node, focus, value, defaultValue, extraLibs, offset) {
|
||||
const stateId = `${node.id}/${id}`;
|
||||
const editor = RED.editor.createEditor({
|
||||
id: id,
|
||||
node: {
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
z: node.z
|
||||
},
|
||||
mode: 'ace/mode/nrjavascript',
|
||||
value: value || defaultValue || "",
|
||||
stateId: stateId,
|
||||
|
|
@ -556,9 +562,9 @@
|
|||
editor.__stateId = stateId;
|
||||
return editor;
|
||||
}
|
||||
this.initEditor = buildEditor('node-input-init-editor', this.id + "/" + "initEditor", false, $("#node-input-initialize").val(), RED._("node-red:function.text.initialize"), undefined, 0);
|
||||
this.editor = buildEditor('node-input-func-editor', this.id + "/" + "editor", true, $("#node-input-func").val(), undefined, that.libs || [], undefined, -1);
|
||||
this.finalizeEditor = buildEditor('node-input-finalize-editor', this.id + "/" + "finalizeEditor", false, $("#node-input-finalize").val(), RED._("node-red:function.text.finalize"), undefined, 0);
|
||||
this.initEditor = buildEditor('node-input-init-editor', this, false, $("#node-input-initialize").val(), RED._("node-red:function.text.initialize"), undefined, 0);
|
||||
this.editor = buildEditor('node-input-func-editor', this, true, $("#node-input-func").val(), undefined, that.libs || [], undefined, -1);
|
||||
this.finalizeEditor = buildEditor('node-input-finalize-editor', this, false, $("#node-input-finalize").val(), RED._("node-red:function.text.finalize"), undefined, 0);
|
||||
|
||||
RED.library.create({
|
||||
url:"functions", // where to get the data from
|
||||
|
|
|
|||
|
|
@ -93,6 +93,8 @@ module.exports = function(RED) {
|
|||
function FunctionNode(n) {
|
||||
RED.nodes.createNode(this,n);
|
||||
var node = this;
|
||||
/** @type {vm.Script} */
|
||||
node.script = null;
|
||||
node.name = n.name;
|
||||
node.func = n.func;
|
||||
node.outputs = n.outputs;
|
||||
|
|
@ -127,7 +129,8 @@ module.exports = function(RED) {
|
|||
"on:__node__.on,"+
|
||||
"status:__node__.status,"+
|
||||
"send:function(msgs,cloneMsg){ __node__.send(__send__,__msgid__,msgs,cloneMsg);},"+
|
||||
"done:__done__"+
|
||||
"done:__done__,"+
|
||||
"linkcall: __node__.linkcall"+
|
||||
"};\n"+
|
||||
node.func+"\n"+
|
||||
"})(msg,__send__,__done__);";
|
||||
|
|
@ -158,6 +161,75 @@ module.exports = function(RED) {
|
|||
node.outstandingIntervals = [];
|
||||
node.clearStatus = false;
|
||||
|
||||
const crypto = require("crypto");
|
||||
const messageEvents = {};
|
||||
const timeoutMessage = (eventId) => {
|
||||
const messageEvent = messageEvents[eventId];
|
||||
if (messageEvent) {
|
||||
delete messageEvents[eventId];
|
||||
messageEvent.reject(new Error("timeout"));
|
||||
}
|
||||
};
|
||||
|
||||
node.returnLinkMessage = function(eventId, msg) {
|
||||
if (Array.isArray(msg._linkSource) && msg._linkSource.length === 0) {
|
||||
delete msg._linkSource;
|
||||
}
|
||||
const messageEvent = messageEvents[eventId];
|
||||
if (messageEvent) {
|
||||
clearTimeout(messageEvent.ts);
|
||||
delete messageEvents[eventId];
|
||||
messageEvent.resolve(msg);
|
||||
}
|
||||
}
|
||||
|
||||
node.linkcall = async function (target, msg, options) {
|
||||
try {
|
||||
if (typeof target !== "string" || target.trim() === "") {
|
||||
throw new Error(RED._("function.error.linkcallTargetInvalid"));
|
||||
}
|
||||
if (typeof msg !== "object" || msg === null) {
|
||||
throw new Error(RED._("function.error.linkcallMsgInvalid"));
|
||||
}
|
||||
options = options || {}
|
||||
options.timeout = options.timeout || 5000 // default timeout 5s
|
||||
options.clone = options.clone === false ? false : true // default to cloning the message for safety
|
||||
|
||||
const isDynamic = !RED.nodes.getNode(target) // if target is a node id, then its a static choice (not dynamic)
|
||||
const targetNode = RED.nodes.linkcallTargets.getTargetNode(node, target, isDynamic)
|
||||
if (!targetNode || targetNode.type !== 'link in') {
|
||||
// getTargetNode should have already thrown an error if the target was not found, but in case it didn't for some reason, we throw here too.
|
||||
throw new Error(RED._("function.error.linkcallTargetNotFound"));
|
||||
}
|
||||
|
||||
const m = options.clone === false ? msg : RED.util.cloneMessage(msg)
|
||||
m._linkSource = m._linkSource || []
|
||||
const messageEvent = {
|
||||
id: crypto.randomBytes(14).toString('hex'),
|
||||
node: node.id,
|
||||
}
|
||||
messageEvents[messageEvent.id] = {
|
||||
msg: m,
|
||||
options: options,
|
||||
send: sandbox.__send__,
|
||||
done: sandbox.__done__,
|
||||
ts: setTimeout(function () {
|
||||
timeoutMessage(messageEvent.id)
|
||||
}, options.timeout)
|
||||
}
|
||||
m._linkSource.push(messageEvent)
|
||||
targetNode.receive(m)
|
||||
return new Promise((resolve, reject) => {
|
||||
messageEvents[messageEvent.id].resolve = resolve
|
||||
messageEvents[messageEvent.id].reject = reject
|
||||
})
|
||||
} catch (error) {
|
||||
context
|
||||
node.error(error, msg);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var sandbox = {
|
||||
console:console,
|
||||
util:util,
|
||||
|
|
@ -170,7 +242,6 @@ module.exports = function(RED) {
|
|||
...RED.util,
|
||||
getSetting: function (_node, name, _flow) {
|
||||
// Ensure `node` argument is the Function node and do not allow flow to be overridden.
|
||||
return RED.util.getSetting(node, name);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -206,6 +277,9 @@ module.exports = function(RED) {
|
|||
status: function() {
|
||||
node.clearStatus = true;
|
||||
node.status.apply(node, arguments);
|
||||
},
|
||||
linkcall: async function (msg, target, options) {
|
||||
return node.linkcall.apply(node, arguments)
|
||||
}
|
||||
},
|
||||
context: {
|
||||
|
|
|
|||
|
|
@ -1,59 +1,28 @@
|
|||
[
|
||||
{
|
||||
"id": "62ea32aa.d73aac",
|
||||
"id": "0364d0b105293afc",
|
||||
"type": "comment",
|
||||
"z": "6312c0588348b2d4",
|
||||
"name": "Example: Link Call Node",
|
||||
"info": "Link call node can call link in node then get result from link out node.",
|
||||
"x": 230,
|
||||
"y": 180,
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "Example: Link Call",
|
||||
"info": "### Link call node\nThe `link call` node can call a `link in` node then get result from `link out` node.\nIt can be set to a fixed target or passed a `target` in the `msg` for dynamic calling.\n\n### Function link call\nA `link in` node can be called from a function using `node.linkcall`\n",
|
||||
"x": 170,
|
||||
"y": 60,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "c588bc36.87fec",
|
||||
"id": "f0e17fb6266e3e46",
|
||||
"type": "comment",
|
||||
"z": "6312c0588348b2d4",
|
||||
"name": "↓ call link in node",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "↓ call link (fixed target)",
|
||||
"info": "",
|
||||
"x": 440,
|
||||
"y": 220,
|
||||
"x": 420,
|
||||
"y": 120,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "cd31efb4d2c6967e",
|
||||
"type": "link call",
|
||||
"z": "6312c0588348b2d4",
|
||||
"name": "",
|
||||
"links": [
|
||||
"dbc46892c8d14c37"
|
||||
],
|
||||
"timeout": "30",
|
||||
"x": 420,
|
||||
"y": 260,
|
||||
"wires": [
|
||||
[
|
||||
"c3db64d1d2260340"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dbc46892c8d14c37",
|
||||
"type": "link in",
|
||||
"z": "6312c0588348b2d4",
|
||||
"name": "",
|
||||
"links": [],
|
||||
"x": 315,
|
||||
"y": 340,
|
||||
"wires": [
|
||||
[
|
||||
"e10575d73f2e5352"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "6b61792143b3b0a3",
|
||||
"id": "2869c5a483bad49f",
|
||||
"type": "inject",
|
||||
"z": "6312c0588348b2d4",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "",
|
||||
"props": [
|
||||
{
|
||||
|
|
@ -71,56 +40,18 @@
|
|||
"topic": "",
|
||||
"payload": "",
|
||||
"payloadType": "date",
|
||||
"x": 240,
|
||||
"y": 260,
|
||||
"x": 200,
|
||||
"y": 160,
|
||||
"wires": [
|
||||
[
|
||||
"cd31efb4d2c6967e"
|
||||
"06a8cdd201630b4c"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "e10575d73f2e5352",
|
||||
"type": "change",
|
||||
"z": "6312c0588348b2d4",
|
||||
"name": "",
|
||||
"rules": [
|
||||
{
|
||||
"t": "set",
|
||||
"p": "payload",
|
||||
"pt": "msg",
|
||||
"to": "Hello, World!",
|
||||
"tot": "str"
|
||||
}
|
||||
],
|
||||
"action": "",
|
||||
"property": "",
|
||||
"from": "",
|
||||
"to": "",
|
||||
"reg": false,
|
||||
"x": 450,
|
||||
"y": 340,
|
||||
"wires": [
|
||||
[
|
||||
"cf8438e7137bc0f0"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cf8438e7137bc0f0",
|
||||
"type": "link out",
|
||||
"z": "6312c0588348b2d4",
|
||||
"name": "",
|
||||
"mode": "return",
|
||||
"links": [],
|
||||
"x": 595,
|
||||
"y": 340,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "c3db64d1d2260340",
|
||||
"id": "943afa6102c26e24",
|
||||
"type": "debug",
|
||||
"z": "6312c0588348b2d4",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
|
|
@ -129,28 +60,282 @@
|
|||
"complete": "false",
|
||||
"statusVal": "",
|
||||
"statusType": "auto",
|
||||
"x": 600,
|
||||
"y": 260,
|
||||
"x": 610,
|
||||
"y": 160,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "6d077dfa0987febb",
|
||||
"type": "comment",
|
||||
"z": "6312c0588348b2d4",
|
||||
"name": "↑called from link call node",
|
||||
"info": "",
|
||||
"x": 410,
|
||||
"y": 380,
|
||||
"id": "06a8cdd201630b4c",
|
||||
"type": "link call",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "",
|
||||
"links": [
|
||||
"e1b6b1fbd9b33c6d"
|
||||
],
|
||||
"timeout": "30",
|
||||
"x": 390,
|
||||
"y": 160,
|
||||
"wires": [
|
||||
[
|
||||
"943afa6102c26e24"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "b23d2de4047ac09b",
|
||||
"type": "link call",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "",
|
||||
"links": [
|
||||
"e1b6b1fbd9b33c6d"
|
||||
],
|
||||
"linkType": "dynamic",
|
||||
"timeout": "30",
|
||||
"x": 380,
|
||||
"y": 280,
|
||||
"wires": [
|
||||
[
|
||||
"94c83d101ad168b0"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "2706a2894820982a",
|
||||
"type": "inject",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "",
|
||||
"props": [
|
||||
{
|
||||
"p": "payload"
|
||||
},
|
||||
{
|
||||
"p": "target",
|
||||
"v": "subroutine-1",
|
||||
"vt": "str"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": 0.1,
|
||||
"topic": "",
|
||||
"payload": "",
|
||||
"payloadType": "date",
|
||||
"x": 200,
|
||||
"y": 280,
|
||||
"wires": [
|
||||
[
|
||||
"b23d2de4047ac09b"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "94c83d101ad168b0",
|
||||
"type": "debug",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "false",
|
||||
"statusVal": "",
|
||||
"statusType": "auto",
|
||||
"x": 610,
|
||||
"y": 280,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "53b9a0adfd8c4217",
|
||||
"id": "57355f9fbe74f23c",
|
||||
"type": "comment",
|
||||
"z": "6312c0588348b2d4",
|
||||
"name": "↑return to link call node",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "↓ set msg.target",
|
||||
"info": "",
|
||||
"x": 680,
|
||||
"y": 380,
|
||||
"x": 200,
|
||||
"y": 240,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "558bf6ebcececd37",
|
||||
"type": "inject",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "",
|
||||
"props": [
|
||||
{
|
||||
"p": "payload"
|
||||
},
|
||||
{
|
||||
"p": "target",
|
||||
"v": "subroutine-1",
|
||||
"vt": "str"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": 0.1,
|
||||
"topic": "",
|
||||
"payload": "",
|
||||
"payloadType": "date",
|
||||
"x": 200,
|
||||
"y": 400,
|
||||
"wires": [
|
||||
[
|
||||
"9095bcd27e7c3bd7"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "4dbf26da9c203018",
|
||||
"type": "debug",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "false",
|
||||
"statusVal": "",
|
||||
"statusType": "auto",
|
||||
"x": 610,
|
||||
"y": 400,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "2b032ec174258528",
|
||||
"type": "comment",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "↓ call subroutine-1 from a function",
|
||||
"info": "",
|
||||
"x": 450,
|
||||
"y": 360,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "9095bcd27e7c3bd7",
|
||||
"type": "function",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "call subroutine-1",
|
||||
"func": "\nconst m = await node.linkcall('subroutine-1', msg, { timeout: 1000 })\n\nreturn m;",
|
||||
"outputs": 1,
|
||||
"timeout": 0,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 400,
|
||||
"y": 400,
|
||||
"wires": [
|
||||
[
|
||||
"4dbf26da9c203018"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "4237c6c2ee2e5c03",
|
||||
"type": "comment",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "↓ call link in (target set by msg.target)",
|
||||
"info": "",
|
||||
"x": 470,
|
||||
"y": 240,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "502424230fe03b8e",
|
||||
"type": "group",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"name": "subroutine-1",
|
||||
"style": {
|
||||
"label": true
|
||||
},
|
||||
"nodes": [
|
||||
"e1b6b1fbd9b33c6d",
|
||||
"547ff90d37be5bfa",
|
||||
"5dc4c8eee329ed86",
|
||||
"37c42be69a257586",
|
||||
"f2a44468d6af6018"
|
||||
],
|
||||
"x": 134,
|
||||
"y": 499,
|
||||
"w": 532,
|
||||
"h": 122
|
||||
},
|
||||
{
|
||||
"id": "e1b6b1fbd9b33c6d",
|
||||
"type": "link in",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"g": "502424230fe03b8e",
|
||||
"name": "subroutine-1",
|
||||
"links": [],
|
||||
"x": 175,
|
||||
"y": 540,
|
||||
"wires": [
|
||||
[
|
||||
"547ff90d37be5bfa"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "547ff90d37be5bfa",
|
||||
"type": "change",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"g": "502424230fe03b8e",
|
||||
"name": "",
|
||||
"rules": [
|
||||
{
|
||||
"t": "set",
|
||||
"p": "payload",
|
||||
"pt": "msg",
|
||||
"to": "Hello, from subroutine-1",
|
||||
"tot": "str"
|
||||
}
|
||||
],
|
||||
"action": "",
|
||||
"property": "",
|
||||
"from": "",
|
||||
"to": "",
|
||||
"reg": false,
|
||||
"x": 400,
|
||||
"y": 540,
|
||||
"wires": [
|
||||
[
|
||||
"5dc4c8eee329ed86"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "5dc4c8eee329ed86",
|
||||
"type": "link out",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"g": "502424230fe03b8e",
|
||||
"name": "",
|
||||
"mode": "return",
|
||||
"links": [],
|
||||
"x": 625,
|
||||
"y": 540,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "37c42be69a257586",
|
||||
"type": "comment",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"g": "502424230fe03b8e",
|
||||
"name": "↑ link node \"sunbroutine-1\"",
|
||||
"info": "",
|
||||
"x": 270,
|
||||
"y": 580,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "f2a44468d6af6018",
|
||||
"type": "comment",
|
||||
"z": "2cdc739225d6a4e8",
|
||||
"g": "502424230fe03b8e",
|
||||
"name": "return to caller ↑ ",
|
||||
"info": "",
|
||||
"x": 560,
|
||||
"y": 580,
|
||||
"wires": []
|
||||
}
|
||||
]
|
||||
|
|
@ -195,7 +195,9 @@
|
|||
"dynamicLinkLabel": "Dynamic",
|
||||
"errors": {
|
||||
"missingReturn": "Missing return node information",
|
||||
"linkUndefined": "link undefined"
|
||||
"linkUndefined": "link undefined",
|
||||
"linkCallMultipleTargetsFound": "Multiple 'link in' nodes named '__target__' found",
|
||||
"linkCallTargetNotFound": "Target 'link in' node '__target__' not found"
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
|
|
@ -287,7 +289,10 @@
|
|||
"inputListener": "Cannot add listener to 'input' event within Function",
|
||||
"non-message-returned": "Function tried to send a message of type __type__",
|
||||
"invalid-js": "Error in JavaScript code",
|
||||
"missing-module": "Module __module__ missing"
|
||||
"missing-module": "Module __module__ missing",
|
||||
"linkcallTargetInvalid" : "Error link call target parameter is not valid, expected a string",
|
||||
"linkcallMsgInvalid" : "Error link call msg parameter is not valid, expected an object",
|
||||
"linkcallTargetNotFound": "Error target 'link in' node not found"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ function createNodeApi(node) {
|
|||
}
|
||||
return i18n._.apply(null,args);
|
||||
}
|
||||
red.nodes.linkcallTargets = runtime.nodes.linkcallTargets;
|
||||
return red;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,135 @@ function uninstallModule(module) {
|
|||
}
|
||||
}
|
||||
|
||||
const linkcallTargets = (function linkcallTargets () {
|
||||
let registry = { id: {}, name: {} }
|
||||
function getIndex (/** @type {[LinkTarget]} */ targets, id) {
|
||||
for (let index = 0; index < (targets || []).length; index++) {
|
||||
const element = targets[index]
|
||||
if (element.id === id) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
/**
|
||||
* Generate a target object from a node
|
||||
* @param {LinkInNode} node
|
||||
* @returns {LinkTarget} a link target object
|
||||
*/
|
||||
function generateTarget (node) {
|
||||
const isSubFlow = node._flow.TYPE === 'subflow'
|
||||
return {
|
||||
id: node.id,
|
||||
name: node.name || node.id,
|
||||
flowId: node._flow.flow.id,
|
||||
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 Name of the target
|
||||
* @param {boolean} [excludeSubflows] set `true` to exclude
|
||||
* @returns {[LinkTarget]} Targets registerd to this 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.
|
||||
* To restrict to a single flow, include the `flowId`
|
||||
* If there is no targets OR more than one target, null is returned
|
||||
* @param {string} name Name of the node
|
||||
* @param {string} [flowId]
|
||||
* @returns {LinkTarget} target
|
||||
*/
|
||||
getTarget (name, flowId) {
|
||||
/** @type {[LinkTarget]} */
|
||||
let possibleTargets = this.getTargets(name)
|
||||
/** @type {LinkTarget} */
|
||||
let target
|
||||
if (possibleTargets.length && flowId) {
|
||||
possibleTargets = possibleTargets.filter(e => e.flowId === flowId)
|
||||
}
|
||||
if (possibleTargets.length === 1) {
|
||||
target = possibleTargets[0]
|
||||
}
|
||||
return target
|
||||
},
|
||||
getTargetNode(srcNode, target, isDynamic) {
|
||||
const dynamicMode = isDynamic === true;
|
||||
|
||||
// 1st see if the target is a direct node id
|
||||
let foundNode;
|
||||
if (this.getTargetById(target)) {
|
||||
foundNode = flows.get(target)
|
||||
}
|
||||
if (target && !foundNode && dynamicMode) {
|
||||
//next, look in **this flow only** for the node
|
||||
let cachedTarget = this.getTarget(target, srcNode._flow.flow.id);
|
||||
if (!cachedTarget) {
|
||||
//single target node not found in registry!
|
||||
//get all possible targets from regular flows (exclude subflow instances)
|
||||
const possibleTargets = this.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`);
|
||||
}
|
||||
}
|
||||
if (cachedTarget) {
|
||||
foundNode = flows.get(cachedTarget.id) //RED.nodes.getNode(cachedTarget.id);
|
||||
}
|
||||
}
|
||||
if (foundNode && foundNode.constructor?.name === 'LinkInNode') {
|
||||
return foundNode;
|
||||
}
|
||||
throw new Error(`target link-in node '${target || ""}' not found`);
|
||||
},
|
||||
/**
|
||||
* Get a target by node ID
|
||||
* @param {string} nodeId ID of the node
|
||||
* @returns {LinkTarget} target
|
||||
*/
|
||||
getTargetById (nodeId) {
|
||||
return registry.id[nodeId]
|
||||
},
|
||||
register (/** @type {LinkInNode} */ node) {
|
||||
const target = generateTarget(node)
|
||||
const tByName = this.getTarget(target.name, target.flowId)
|
||||
if (!tByName || tByName.id !== target.id) {
|
||||
registry.name[target.name] = registry.name[target.name] || []
|
||||
registry.name[target.name].push(target)
|
||||
}
|
||||
registry.id[target.id] = target
|
||||
return target
|
||||
},
|
||||
remove (node) {
|
||||
const target = generateTarget(node)
|
||||
const targs = this.getTargets(target.name)
|
||||
const idx = getIndex(targs, target.id)
|
||||
if (idx > -1) {
|
||||
targs.splice(idx, 1)
|
||||
}
|
||||
if (targs.length === 0) {
|
||||
delete registry.name[target.name]
|
||||
}
|
||||
delete registry.id[target.id]
|
||||
},
|
||||
clear () {
|
||||
registry = { id: {}, name: {} }
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
module.exports = {
|
||||
// Lifecycle
|
||||
init: init,
|
||||
|
|
@ -219,6 +348,8 @@ module.exports = {
|
|||
enableNode: enableNode,
|
||||
disableNode: disableNode,
|
||||
|
||||
linkcallTargets: linkcallTargets,
|
||||
|
||||
// Node type registry
|
||||
registerType: registerType,
|
||||
registerSubflow: registerSubflow,
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@
|
|||
* limitations under the License.
|
||||
**/
|
||||
|
||||
var should = require("should");
|
||||
var functionNode = require("nr-test-utils").require("@node-red/nodes/core/function/10-function.js");
|
||||
var Context = require("nr-test-utils").require("@node-red/runtime/lib/nodes/context");
|
||||
var helper = require("node-red-node-test-helper");
|
||||
var RED = require("nr-test-utils").require("node-red/lib/red");
|
||||
const should = require("should");
|
||||
const sinon = require("sinon");
|
||||
const functionNode = require("nr-test-utils").require("@node-red/nodes/core/function/10-function.js");
|
||||
const linkNode = require("nr-test-utils").require("@node-red/nodes/core/common/60-link.js");
|
||||
const Context = require("nr-test-utils").require("@node-red/runtime/lib/nodes/context");
|
||||
const helper = require("node-red-node-test-helper");
|
||||
const RED = require("nr-test-utils").require("node-red/lib/red");
|
||||
describe('function node', function() {
|
||||
|
||||
before(function(done) {
|
||||
|
|
@ -1835,4 +1837,492 @@ describe('function node', function() {
|
|||
});
|
||||
|
||||
});
|
||||
|
||||
describe('link call from function', function () {
|
||||
afterEach(function () {
|
||||
delete RED.settings.functionExternalModules;
|
||||
})
|
||||
it('should call subroutine on same tab by name and get response', async function () {
|
||||
const flow = [
|
||||
// ↓↓ subroutine ↓↓
|
||||
{ id: "li1", type: "link in", name: "subroutine", wires: [["sbn"]] },
|
||||
{ id: "sbn", type: "function", wires: [["lo1"]], func: "msg._payload=msg.payload; msg.payload='set-in-function'; return msg;" },
|
||||
{ id: "lo1", type: "link out", mode: "return" },
|
||||
// ↓↓ main flow ↓↓
|
||||
{ id: "f1", type: "function", wires: [["h1"]], func: "const m = await node.linkcall('subroutine', msg, { timeout: 1000 }); return m;" },
|
||||
{ id: "c1", type: "catch", scope: ["f1"], uncaught: true, wires: [["h1"]] },
|
||||
{ id: "h1", type: "helper" }
|
||||
]
|
||||
|
||||
await helper.load([linkNode, functionNode], flow)
|
||||
const f1 = helper.getNode("f1");
|
||||
const h1 = helper.getNode("h1");
|
||||
const c1 = helper.getNode("c1");
|
||||
const c1SpyReceived = sinon.spy(c1, "receive")
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
h1.on("input", function (msg) {
|
||||
try {
|
||||
c1SpyReceived.called.should.be.false() // should not have been caught as error
|
||||
msg.should.have.property("payload", "set-in-function")
|
||||
msg.should.have.property("_payload", "original")
|
||||
msg.should.not.have.property("error")
|
||||
resolve()
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
f1.receive({ payload: "original", topic: "test" }) // trigger function node which will call the subroutine
|
||||
})
|
||||
})
|
||||
it('should call subroutine on different tab by name and get response', async function () {
|
||||
const flow = [
|
||||
// ↓↓ flow tabs ↓↓
|
||||
{ id: "tab-flow-main", type: "tab", label: "Main Flow" },
|
||||
{ id: "tab-flow-2", type: "tab", label: "Flow 2" },
|
||||
// ↓↓ subroutine ↓↓
|
||||
{ id: "li1", type: "link in", z: "tab-flow-2", name: "subroutine-on-different-tab", wires: [["sbn"]] },
|
||||
{ id: "sbn", type: "function", z: "tab-flow-2", wires: [["lo1"]], func: "msg._payload=msg.payload; msg.payload='set-in-function'; return msg;" },
|
||||
{ id: "lo1", type: "link out", z: "tab-flow-2", mode: "return" },
|
||||
// ↓↓ main flow ↓↓
|
||||
{ id: "f1", type: "function", z: "tab-flow-main", wires: [["h1"]], func: "const m = await node.linkcall('subroutine-on-different-tab', msg, { timeout: 1000 }); return m;" },
|
||||
{ id: "c1", type: "catch", z: "tab-flow-main", scope: ["f1"], uncaught: true, wires: [["h1"]] },
|
||||
{ id: "h1", type: "helper", z: "tab-flow-main" }
|
||||
]
|
||||
|
||||
await helper.load([linkNode, functionNode], flow)
|
||||
const f1 = helper.getNode("f1");
|
||||
const h1 = helper.getNode("h1");
|
||||
const c1 = helper.getNode("c1");
|
||||
const c1SpyReceived = sinon.spy(c1, "receive")
|
||||
const li1 = helper.getNode("li1");
|
||||
const li1SpyReceived = sinon.spy(li1, "receive")
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
h1.on("input", function (msg) {
|
||||
try {
|
||||
li1SpyReceived.called.should.be.true() // subroutine should have been called
|
||||
c1SpyReceived.called.should.be.false() // should not have been caught as error
|
||||
msg.should.have.property("payload", "set-in-function")
|
||||
msg.should.have.property("_payload", "original")
|
||||
msg.should.not.have.property("error")
|
||||
resolve()
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
f1.receive({ payload: "original", topic: "test" }) // trigger function node which will call the subroutine
|
||||
})
|
||||
})
|
||||
it('should call subroutine on same tab by node id and get response', async function () {
|
||||
const flow = [
|
||||
// ↓↓ subroutine ↓↓
|
||||
{ id: "li1", type: "link in", name: "subroutine", wires: [["sbn"]] },
|
||||
{ id: "sbn", type: "function", wires: [["lo1"]], func: "msg._payload=msg.payload; msg.payload='set-in-function'; return msg;" },
|
||||
{ id: "lo1", type: "link out", mode: "return" },
|
||||
// ↓↓ main flow ↓↓
|
||||
{ id: "f1", type: "function", wires: [["h1"]], func: "const m = await node.linkcall('li1', msg, { timeout: 1000 }); return m;" },
|
||||
{ id: "c1", type: "catch", scope: ["f1"], uncaught: true, wires: [["h1"]] },
|
||||
{ id: "h1", type: "helper" }
|
||||
]
|
||||
|
||||
await helper.load([linkNode, functionNode], flow)
|
||||
const f1 = helper.getNode("f1");
|
||||
const h1 = helper.getNode("h1");
|
||||
const c1 = helper.getNode("c1");
|
||||
const c1SpyReceived = sinon.spy(c1, "receive")
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
h1.on("input", function (msg) {
|
||||
try {
|
||||
c1SpyReceived.called.should.be.false() // should not have been caught as error
|
||||
msg.should.have.property("payload", "set-in-function")
|
||||
msg.should.have.property("_payload", "original")
|
||||
msg.should.not.have.property("error")
|
||||
resolve()
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
f1.receive({ payload: "original", topic: "test" }) // trigger function node which will call the subroutine
|
||||
})
|
||||
})
|
||||
it('should call subroutine on different tab by node id and get response', async function () {
|
||||
const flow = [
|
||||
// ↓↓ flow tabs ↓↓
|
||||
{ id: "tab-flow-main", type: "tab", label: "Main Flow" },
|
||||
{ id: "tab-flow-2", type: "tab", label: "Flow 2" },
|
||||
// ↓↓ subroutine on flow 2 ↓↓
|
||||
{ id: "li2", type: "link in", z: "tab-flow-2", name: "subroutine", wires: [["sbn2"]] },
|
||||
{ id: "sbn2", type: "function", z: "tab-flow-2", wires: [["lo2"]], func: "msg._payload=msg.payload; msg.payload='set-in-function'; return msg;" },
|
||||
{ id: "lo2", type: "link out", z: "tab-flow-2", mode: "return" },
|
||||
// ↓↓ main flow ↓↓
|
||||
{ id: "f1", type: "function", z: "tab-flow-main", wires: [["h1"]], func: "const m = await node.linkcall('li2', msg, { timeout: 500 }); return m;" },
|
||||
{ id: "c1", type: "catch", z: "tab-flow-main", scope: ["f1"], uncaught: true, wires: [["h1"]] },
|
||||
{ id: "h1", type: "helper", z: "tab-flow-main" }
|
||||
]
|
||||
|
||||
await helper.load([linkNode, functionNode], flow)
|
||||
const f1 = helper.getNode("f1");
|
||||
const h1 = helper.getNode("h1");
|
||||
const c1 = helper.getNode("c1");
|
||||
const li2 = helper.getNode("li2");
|
||||
const c1SpyReceived = sinon.spy(c1, "receive")
|
||||
const li2SpyReceived = sinon.spy(li2, "receive")
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
h1.on("input", function (msg) {
|
||||
try {
|
||||
li2SpyReceived.called.should.be.true() // subroutine should have been called
|
||||
c1SpyReceived.called.should.be.false() // should not have been caught as error
|
||||
msg.should.have.property("payload", "set-in-function")
|
||||
msg.should.have.property("_payload", "original")
|
||||
msg.should.not.have.property("error")
|
||||
resolve()
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
f1.receive({ payload: "original", topic: "test" }) // trigger function node which will call the subroutine
|
||||
})
|
||||
})
|
||||
it('should clone the message by default', async function () {
|
||||
const flow = [
|
||||
// ↓↓ subroutine ↓↓
|
||||
{ id: "li1", type: "link in", name: "subroutine", wires: [["sbn"]] },
|
||||
{ id: "sbn", type: "function", wires: [["lo1"]], func: "msg.payload.prop='modified'; return msg;" },
|
||||
{ id: "lo1", type: "link out", mode: "return" },
|
||||
// ↓↓ main flow ↓↓
|
||||
{ id: "f1",
|
||||
type: "function",
|
||||
wires: [["h1"]],
|
||||
func: `
|
||||
const payloadPropBefore = msg.payload.prop;
|
||||
const newMsg = await node.linkcall('li1', msg);
|
||||
const payloadPropAfter = msg.payload.prop;
|
||||
const newPayloadProp = newMsg.payload.prop;
|
||||
msg.testData = {payloadPropBefore, payloadPropAfter, newPayloadProp}; // attach data to message to make it easier to assert in test
|
||||
return msg;
|
||||
`
|
||||
},
|
||||
{ id: "c1", type: "catch", scope: ["f1"], uncaught: true, wires: [["h1"]] },
|
||||
{ id: "h1", type: "helper" }
|
||||
]
|
||||
|
||||
await helper.load([linkNode, functionNode], flow)
|
||||
const f1 = helper.getNode("f1");
|
||||
const h1 = helper.getNode("h1");
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
h1.on("input", function (msg) {
|
||||
try {
|
||||
msg.should.have.property('payload').and.have.property('prop', 'original')
|
||||
should(msg.testData).have.property('payloadPropBefore', 'original')
|
||||
should(msg.testData).have.property('payloadPropAfter', 'original')
|
||||
should(msg.testData).have.property('newPayloadProp', 'modified')
|
||||
resolve()
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
f1.receive({ payload: { prop: "original" }, topic: "test" }) // trigger function node which will call the subroutine
|
||||
})
|
||||
})
|
||||
it('should not clone the message when clone option=false', async function () {
|
||||
const flow = [
|
||||
// ↓↓ subroutine ↓↓
|
||||
{ id: "li1", type: "link in", name: "subroutine", wires: [["sbn"]] },
|
||||
{ id: "sbn", type: "function", wires: [["lo1"]], func: "msg.payload.prop='modified'; return msg;" },
|
||||
{ id: "lo1", type: "link out", mode: "return" },
|
||||
// ↓↓ main flow ↓↓
|
||||
{ id: "f1",
|
||||
type: "function",
|
||||
wires: [["h1"]],
|
||||
func: `
|
||||
const payloadPropBefore = msg.payload.prop;
|
||||
const newMsg = await node.linkcall('li1', msg, { clone: false });
|
||||
const payloadPropAfter = msg.payload.prop;
|
||||
const newPayloadProp = newMsg.payload.prop;
|
||||
msg.testData = {payloadPropBefore, payloadPropAfter, newPayloadProp}; // attach data to message to make it easier to assert in test
|
||||
return msg;
|
||||
`
|
||||
},
|
||||
{ id: "c1", type: "catch", scope: ["f1"], uncaught: true, wires: [["h1"]] },
|
||||
{ id: "h1", type: "helper" }
|
||||
]
|
||||
|
||||
await helper.load([linkNode, functionNode], flow)
|
||||
const f1 = helper.getNode("f1");
|
||||
const h1 = helper.getNode("h1");
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
h1.on("input", function (msg) {
|
||||
try {
|
||||
msg.should.have.property('payload').and.have.property('prop', 'modified') // original message should have been mutated by subroutine
|
||||
should(msg.testData).have.property('payloadPropBefore', 'original')
|
||||
should(msg.testData).have.property('payloadPropAfter', 'modified')
|
||||
should(msg.testData).have.property('newPayloadProp', 'modified')
|
||||
resolve()
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
f1.receive({ payload: { prop: "original" }, topic: "test" }) // trigger function node which will call the subroutine
|
||||
})
|
||||
})
|
||||
it('should call subroutine on same tab even when there are same named targets on other tabs', async function () {
|
||||
const flow = [
|
||||
// ↓↓ flow tabs ↓↓
|
||||
{ id: "tab-flow-main", type: "tab", label: "Main Flow" },
|
||||
{ id: "tab-flow-2", type: "tab", label: "Flow 2" },
|
||||
// ↓↓ subroutine on main flow ↓↓
|
||||
{ id: "li1", type: "link in", z: "tab-flow-main", name: "subroutine", wires: [["sbn"]] },
|
||||
{ id: "sbn", type: "function", z: "tab-flow-main", wires: [["lo1"]], func: "msg._payload=msg.payload; msg.payload='set-in-function'; return msg;" },
|
||||
{ id: "lo1", type: "link out", z: "tab-flow-main", mode: "return" },
|
||||
// ↓↓ subroutine on flow 2 ↓↓
|
||||
{ id: "li2", type: "link in", z: "tab-flow-2", name: "subroutine", wires: [["sbn2"]] },
|
||||
{ id: "sbn2", type: "function", z: "tab-flow-2", wires: [["lo2"]], func: "msg._payload=msg.payload; msg.payload='set-in-function'; return msg;" },
|
||||
{ id: "lo2", type: "link out", z: "tab-flow-2", mode: "return" },
|
||||
// ↓↓ main flow ↓↓
|
||||
{ id: "f1", type: "function", z: "tab-flow-main", wires: [["h1"]], func: "const m = await node.linkcall('subroutine', msg, { timeout: 500 }); return m;" },
|
||||
{ id: "c1", type: "catch", z: "tab-flow-main", scope: ["f1"], uncaught: true, wires: [["h1"]] },
|
||||
{ id: "h1", type: "helper", z: "tab-flow-main" }
|
||||
]
|
||||
|
||||
await helper.load([linkNode, functionNode], flow)
|
||||
const f1 = helper.getNode("f1");
|
||||
const h1 = helper.getNode("h1");
|
||||
const c1 = helper.getNode("c1");
|
||||
const li1 = helper.getNode("li1");
|
||||
const li2 = helper.getNode("li2");
|
||||
const c1SpyReceived = sinon.spy(c1, "receive")
|
||||
const li1SpyReceived = sinon.spy(li1, "receive")
|
||||
const li2SpyReceived = sinon.spy(li2, "receive")
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
h1.on("input", function (msg) {
|
||||
try {
|
||||
li1SpyReceived.called.should.be.true() // subroutine on same tab should have been called
|
||||
li2SpyReceived.called.should.be.false() // subroutine on different tab should not have been called
|
||||
c1SpyReceived.called.should.be.false() // should not have been caught as error
|
||||
msg.should.have.property("payload", "set-in-function")
|
||||
msg.should.have.property("_payload", "original")
|
||||
msg.should.not.have.property("error")
|
||||
resolve()
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
f1.receive({ payload: "original", topic: "test" }) // trigger function node which will call the subroutine
|
||||
})
|
||||
})
|
||||
it('should call nested subroutines', async function () {
|
||||
const flow = [
|
||||
// ↓↓ flow tabs ↓↓
|
||||
{ id: "tab-flow-main", type: "tab", label: "Main Flow" },
|
||||
{ id: "tab-flow-2", type: "tab", label: "Flow 2" },
|
||||
// ↓↓ subroutine on main flow ↓↓
|
||||
{ id: "li1", type: "link in", z: "tab-flow-main", name: "subroutine", wires: [["sbn"]] },
|
||||
{ id: "sbn", type: "function", z: "tab-flow-main", wires: [["lo1"]], func: "const m = await node.linkcall('subroutine2', msg, { timeout: 500 }); return m;" },
|
||||
{ id: "lo1", type: "link out", z: "tab-flow-main", mode: "return" },
|
||||
// ↑↑ subroutine end ↑↑
|
||||
// ↓↓ subroutine on flow 2 ↓↓
|
||||
{ id: "li2", type: "link in", z: "tab-flow-2", name: "subroutine2", wires: [["sbn2"]] },
|
||||
{ id: "sbn2", type: "function", z: "tab-flow-2", wires: [["lo2"]], func: "msg._payload=msg.payload; msg.payload='set-in-subroutine-2'; return msg;" },
|
||||
{ id: "lo2", type: "link out", z: "tab-flow-2", mode: "return" },
|
||||
// ↑↑ subroutine end ↑↑
|
||||
{ id: "f1", type: "function", z: "tab-flow-main", wires: [["h1"]], func: "const m = await node.linkcall('subroutine', msg, { timeout: 500 }); return m;" },
|
||||
{ id: "c1", type: "catch", z: "tab-flow-main", scope: ["f1"], uncaught: true, wires: [["h1"]] },
|
||||
{ id: "h1", type: "helper", z: "tab-flow-main" }
|
||||
]
|
||||
|
||||
await helper.load([linkNode, functionNode], flow)
|
||||
const f1 = helper.getNode("f1");
|
||||
const h1 = helper.getNode("h1");
|
||||
const c1 = helper.getNode("c1");
|
||||
const c1SpyReceived = sinon.spy(c1, "receive")
|
||||
const li1 = helper.getNode("li1");
|
||||
const li1SpyReceived = sinon.spy(li1, "receive")
|
||||
const li2 = helper.getNode("li2");
|
||||
const li2SpyReceived = sinon.spy(li2, "receive")
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
h1.on("input", function (msg) {
|
||||
try {
|
||||
li1SpyReceived.called.should.be.true() // subroutine on same tab should have been called
|
||||
li2SpyReceived.called.should.be.true() // nested subroutine should have been called
|
||||
c1SpyReceived.called.should.be.false() // should not have been caught as error
|
||||
msg.should.have.property("payload", "set-in-subroutine-2")
|
||||
msg.should.have.property("_payload", "original")
|
||||
msg.should.not.have.property("error")
|
||||
resolve()
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
f1.receive({ payload: "original", topic: "test" }) // trigger function node which will call the subroutine
|
||||
})
|
||||
})
|
||||
it('should timeout waiting for link return', async function () {
|
||||
this.timeout(1000);
|
||||
const flow = [
|
||||
// ↓↓ flow tabs ↓↓
|
||||
{ id: "tab-flow-main", type: "tab", label: "Main Flow" },
|
||||
// ↓↓ subroutine ↓↓
|
||||
{ id: "li1", type: "link in", z: "tab-flow-main", name: "subroutine", wires: [["sbn"]] },
|
||||
{ id: "sbn", type: "function", z: "tab-flow-main", wires: [["lo1"]], func: "msg._payload=msg.payload; msg.payload='set-in-function'; return msg;" },
|
||||
{ id: "lo1", type: "link out", z: "tab-flow-main", mode: "" }, // not return mode, cause link-call timeout
|
||||
// ↓↓ main flow ↓↓
|
||||
{ id: "f1", type: "function", z: "tab-flow-main", wires: [["h1"]], func: "const m = await node.linkcall('subroutine', msg, { timeout: 500 }); return m;" },
|
||||
{ id: "c1", type: "catch", z: "tab-flow-main", scope: ["f1"], uncaught: true, wires: [["h1"]] },
|
||||
{ id: "h1", type: "helper", z: "tab-flow-main" }
|
||||
]
|
||||
|
||||
await helper.load([linkNode, functionNode], flow)
|
||||
const f1 = helper.getNode("f1")
|
||||
const h1 = helper.getNode("h1")
|
||||
const c1 = helper.getNode("c1")
|
||||
const li1 = helper.getNode("li1")
|
||||
const c1SpyReceived = sinon.spy(c1, "receive")
|
||||
const li1SpyReceived = sinon.spy(li1, "receive")
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
h1.on("input", function (msg) {
|
||||
try {
|
||||
li1SpyReceived.called.should.be.true() // subroutine should have been called
|
||||
c1SpyReceived.called.should.be.true() // should have been caught as error
|
||||
msg.error.should.have.property("message").and.match(/timeout/)
|
||||
msg.error.should.have.property("source")
|
||||
msg.error.source.should.have.property("id", "f1")
|
||||
resolve()
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
f1.receive({ payload: "original", topic: "test" })
|
||||
})
|
||||
})
|
||||
it('should raise error for non-existent target subroutine', async function () {
|
||||
this.timeout(1000);
|
||||
const flow = [
|
||||
// ↓↓ flow tabs ↓↓
|
||||
{ id: "tab-flow-main", type: "tab", label: "Main Flow" },
|
||||
// ↓↓ subroutine ↓↓
|
||||
{ id: "li1", type: "link in", z: "tab-flow-main", name: "subroutine", wires: [["sbn"]] },
|
||||
{ id: "sbn", type: "function", z: "tab-flow-main", wires: [["lo1"]], func: "msg._payload=msg.payload; msg.payload='set-in-function'; return msg;" },
|
||||
{ id: "lo1", type: "link out", z: "tab-flow-main", mode: "" }, // not return mode, cause link-call timeout
|
||||
// ↓↓ main flow ↓↓
|
||||
{ id: "f1", type: "function", z: "tab-flow-main", wires: [["h1"]], func: "const m = await node.linkcall('non-existent-subroutine', msg, { timeout: 500 }); return m;" },
|
||||
{ id: "c1", type: "catch", z: "tab-flow-main", scope: ["f1"], uncaught: true, wires: [["h1"]] },
|
||||
{ id: "h1", type: "helper", z: "tab-flow-main" }
|
||||
]
|
||||
|
||||
await helper.load([linkNode, functionNode], flow)
|
||||
const f1 = helper.getNode("f1")
|
||||
const h1 = helper.getNode("h1")
|
||||
const c1 = helper.getNode("c1")
|
||||
const li1 = helper.getNode("li1")
|
||||
const c1SpyReceived = sinon.spy(c1, "receive")
|
||||
const li1SpyReceived = sinon.spy(li1, "receive")
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
h1.on("input", function (msg) {
|
||||
try {
|
||||
li1SpyReceived.called.should.be.false() // subroutine should not have been called
|
||||
c1SpyReceived.called.should.be.true() // should have been caught as error
|
||||
msg.error.should.have.property("message").and.match(/target link-in node \'non-existent-subroutine\' not found/i)
|
||||
msg.error.should.have.property("source")
|
||||
msg.error.source.should.have.property("id", "f1")
|
||||
resolve()
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
f1.receive({ payload: "original", topic: "test" })
|
||||
})
|
||||
})
|
||||
it('should raise error due to multiple targets on same tab', async function () {
|
||||
const flow = [
|
||||
// ↓↓ flow tabs ↓↓
|
||||
{ id: "tab-flow-main", type: "tab", label: "Main Flow" },
|
||||
// ↓↓ subroutine ↓↓
|
||||
{ id: "li1", type: "link in", z: "tab-flow-main", name: "subroutine", wires: [["sbn"]] },
|
||||
{ id: "li2", type: "link in", z: "tab-flow-main", name: "subroutine", wires: [["sbn"]] },
|
||||
{ id: "sbn", type: "function", z: "tab-flow-main", wires: [["lo1"]], func: "msg._payload=msg.payload; msg.payload='set-in-function'; return msg;" },
|
||||
{ id: "lo1", type: "link out", z: "tab-flow-main", mode: "return" },
|
||||
// ↓↓ main flow ↓↓
|
||||
{ id: "f1", type: "function", z: "tab-flow-main", wires: [["h1"]], func: "const m = await node.linkcall('subroutine', msg, { timeout: 500 }); return m;" },
|
||||
{ id: "c1", type: "catch", z: "tab-flow-main", scope: ["f1"], uncaught: true, wires: [["h1"]] },
|
||||
{ id: "h1", type: "helper", z: "tab-flow-main" }
|
||||
]
|
||||
|
||||
await helper.load([linkNode, functionNode], flow)
|
||||
const f1 = helper.getNode("f1")
|
||||
const h1 = helper.getNode("h1")
|
||||
const c1 = helper.getNode("c1")
|
||||
const li1 = helper.getNode("li1")
|
||||
const c1SpyReceived = sinon.spy(c1, "receive")
|
||||
const li1SpyReceived = sinon.spy(li1, "receive")
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
h1.on("input", function (msg) {
|
||||
try {
|
||||
li1SpyReceived.called.should.be.false() // subroutine should not have been called
|
||||
c1SpyReceived.called.should.be.true() // should have been caught as error
|
||||
msg.error.should.have.property("message").and.match(/Multiple link-in nodes named 'subroutine' found/i)
|
||||
msg.error.should.have.property("source")
|
||||
msg.error.source.should.have.property("id", "f1")
|
||||
resolve()
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
f1.receive({ payload: "original", topic: "test" })
|
||||
})
|
||||
})
|
||||
it('should raise error due to multiple targets on different tabs', async function () {
|
||||
const flow = [
|
||||
// ↓↓ flow tabs ↓↓
|
||||
{ id: "tab-flow-main", type: "tab", label: "Main Flow" },
|
||||
{ id: "tab-flow-2", type: "tab", label: "Flow 2" },
|
||||
{ id: "tab-flow-3", type: "tab", label: "Flow 3" },
|
||||
// ↓↓ subroutine on flow 2 ↓↓
|
||||
{ id: "li1", type: "link in", z: "tab-flow-2", name: "subroutine", wires: [["sbn"]] },
|
||||
{ id: "sbn", type: "function", z: "tab-flow-2", wires: [["lo1"]], func: "msg._payload=msg.payload; msg.payload='set-in-function'; return msg;" },
|
||||
{ id: "lo1", type: "link out", z: "tab-flow-2", mode: "return" },
|
||||
// ↓↓ subroutine on flow 3 ↓↓
|
||||
{ id: "li2", type: "link in", z: "tab-flow-3", name: "subroutine", wires: [["sbn2"]] },
|
||||
{ id: "sbn2", type: "function", z: "tab-flow-3", wires: [["lo2"]], func: "msg._payload=msg.payload; msg.payload='set-in-function'; return msg;" },
|
||||
{ id: "lo2", type: "link out", z: "tab-flow-3", mode: "return" },
|
||||
// ↓↓ main flow ↓↓
|
||||
{ id: "f1", type: "function", z: "tab-flow-main", wires: [["h1"]], func: "const m = await node.linkcall('subroutine', msg, { timeout: 500 }); return m;" },
|
||||
{ id: "c1", type: "catch", z: "tab-flow-main", scope: ["f1"], uncaught: true, wires: [["h1"]] },
|
||||
{ id: "h1", type: "helper", z: "tab-flow-main" }
|
||||
]
|
||||
|
||||
await helper.load([linkNode, functionNode], flow)
|
||||
const f1 = helper.getNode("f1")
|
||||
const h1 = helper.getNode("h1")
|
||||
const c1 = helper.getNode("c1")
|
||||
const li1 = helper.getNode("li1")
|
||||
const c1SpyReceived = sinon.spy(c1, "receive")
|
||||
const li1SpyReceived = sinon.spy(li1, "receive")
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
h1.on("input", function (msg) {
|
||||
try {
|
||||
li1SpyReceived.called.should.be.false() // subroutine should not have been called
|
||||
c1SpyReceived.called.should.be.true() // should have been caught as error
|
||||
msg.error.should.have.property("message").and.match(/Multiple link-in nodes named 'subroutine' found/i)
|
||||
msg.error.should.have.property("source")
|
||||
msg.error.source.should.have.property("id", "f1")
|
||||
resolve()
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
f1.receive({ payload: "original", topic: "test" })
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue