1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Merge pull request #2665 from node-red/msg-router

Pluggable Message Routing
This commit is contained in:
Nick O'Leary 2020-09-29 20:36:23 +01:00 committed by GitHub
commit 101e96dcb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1576 additions and 560 deletions

View File

@ -457,6 +457,7 @@ module.exports = function(grunt) {
'packages/node_modules/@node-red/runtime/lib/index.js', 'packages/node_modules/@node-red/runtime/lib/index.js',
'packages/node_modules/@node-red/runtime/lib/api/*.js', 'packages/node_modules/@node-red/runtime/lib/api/*.js',
'packages/node_modules/@node-red/runtime/lib/events.js', 'packages/node_modules/@node-red/runtime/lib/events.js',
'packages/node_modules/@node-red/runtime/lib/hooks.js',
'packages/node_modules/@node-red/util/**/*.js', 'packages/node_modules/@node-red/util/**/*.js',
'packages/node_modules/@node-red/editor-api/lib/index.js', 'packages/node_modules/@node-red/editor-api/lib/index.js',
'packages/node_modules/@node-red/editor-api/lib/auth/index.js' 'packages/node_modules/@node-red/editor-api/lib/auth/index.js'

View File

@ -57,6 +57,7 @@ function createNodeApi(node) {
log: {}, log: {},
settings: {}, settings: {},
events: runtime.events, events: runtime.events,
hooks: runtime.hooks,
util: runtime.util, util: runtime.util,
version: runtime.version, version: runtime.version,
require: requireModule, require: requireModule,

View File

@ -49,11 +49,9 @@ var api = module.exports = {
* @return {Promise<Flows>} - the active flow configuration * @return {Promise<Flows>} - the active flow configuration
* @memberof @node-red/runtime_flows * @memberof @node-red/runtime_flows
*/ */
getFlows: function(opts) { getFlows: async function(opts) {
return new Promise(function(resolve,reject) { runtime.log.audit({event: "flows.get"}, opts.req);
runtime.log.audit({event: "flows.get"}, opts.req); return runtime.flows.getFlows();
return resolve(runtime.nodes.getFlows());
});
}, },
/** /**
* Sets the current flow configuration * Sets the current flow configuration
@ -65,38 +63,35 @@ var api = module.exports = {
* @return {Promise<Flows>} - the active flow configuration * @return {Promise<Flows>} - the active flow configuration
* @memberof @node-red/runtime_flows * @memberof @node-red/runtime_flows
*/ */
setFlows: function(opts) { setFlows: async function(opts) {
return mutex.runExclusive(function() { return mutex.runExclusive(async function() {
return new Promise(function(resolve,reject) { var flows = opts.flows;
var deploymentType = opts.deploymentType||"full";
runtime.log.audit({event: "flows.set",type:deploymentType}, opts.req);
var flows = opts.flows; var apiPromise;
var deploymentType = opts.deploymentType||"full"; if (deploymentType === 'reload') {
runtime.log.audit({event: "flows.set",type:deploymentType}, opts.req); apiPromise = runtime.flows.loadFlows(true);
} else {
var apiPromise; if (flows.hasOwnProperty('rev')) {
if (deploymentType === 'reload') { var currentVersion = runtime.flows.getFlows().rev;
apiPromise = runtime.nodes.loadFlows(true); if (currentVersion !== flows.rev) {
} else { var err;
if (flows.hasOwnProperty('rev')) { err = new Error();
var currentVersion = runtime.nodes.getFlows().rev; err.code = "version_mismatch";
if (currentVersion !== flows.rev) { err.status = 409;
var err; //TODO: log warning
err = new Error(); throw err;
err.code = "version_mismatch";
err.status = 409;
//TODO: log warning
return reject(err);
}
} }
apiPromise = runtime.nodes.setFlows(flows.flows,flows.credentials,deploymentType,null,null,opts.user);
} }
apiPromise.then(function(flowId) { apiPromise = runtime.flows.setFlows(flows.flows,flows.credentials,deploymentType,null,null,opts.user);
return resolve({rev:flowId}); }
}).catch(function(err) { return apiPromise.then(function(flowId) {
runtime.log.warn(runtime.log._("api.flows.error-"+(deploymentType === 'reload'?'reload':'save'),{message:err.message})); return {rev:flowId};
runtime.log.warn(err.stack); }).catch(function(err) {
return reject(err); runtime.log.warn(runtime.log._("api.flows.error-"+(deploymentType === 'reload'?'reload':'save'),{message:err.message}));
}); runtime.log.warn(err.stack);
throw err
}); });
}); });
}, },
@ -110,22 +105,20 @@ var api = module.exports = {
* @return {Promise<String>} - the id of the added flow * @return {Promise<String>} - the id of the added flow
* @memberof @node-red/runtime_flows * @memberof @node-red/runtime_flows
*/ */
addFlow: function(opts) { addFlow: async function(opts) {
return mutex.runExclusive(function() { return mutex.runExclusive(async function() {
return new Promise(function (resolve, reject) { var flow = opts.flow;
var flow = opts.flow; return runtime.flows.addFlow(flow, opts.user).then(function (id) {
runtime.nodes.addFlow(flow,opts.user).then(function (id) { runtime.log.audit({event: "flow.add", id: id}, opts.req);
runtime.log.audit({event: "flow.add", id: id}, opts.req); return id;
return resolve(id); }).catch(function (err) {
}).catch(function (err) { runtime.log.audit({
runtime.log.audit({ event: "flow.add",
event: "flow.add", error: err.code || "unexpected_error",
error: err.code || "unexpected_error", message: err.toString()
message: err.toString() }, opts.req);
}, opts.req); err.status = 400;
err.status = 400; throw err;
return reject(err);
})
}) })
}); });
}, },
@ -139,20 +132,18 @@ var api = module.exports = {
* @return {Promise<Flow>} - the active flow configuration * @return {Promise<Flow>} - the active flow configuration
* @memberof @node-red/runtime_flows * @memberof @node-red/runtime_flows
*/ */
getFlow: function(opts) { getFlow: async function(opts) {
return new Promise(function (resolve,reject) { var flow = runtime.flows.getFlow(opts.id);
var flow = runtime.nodes.getFlow(opts.id); if (flow) {
if (flow) { runtime.log.audit({event: "flow.get",id:opts.id}, opts.req);
runtime.log.audit({event: "flow.get",id:opts.id}, opts.req); return flow;
return resolve(flow); } else {
} else { runtime.log.audit({event: "flow.get",id:opts.id,error:"not_found"}, opts.req);
runtime.log.audit({event: "flow.get",id:opts.id,error:"not_found"}, opts.req); var err = new Error();
var err = new Error(); err.code = "not_found";
err.code = "not_found"; err.status = 404;
err.status = 404; throw err;
return reject(err); }
}
})
}, },
/** /**
* Updates an existing flow configuration * Updates an existing flow configuration
@ -164,42 +155,29 @@ var api = module.exports = {
* @return {Promise<String>} - the id of the updated flow * @return {Promise<String>} - the id of the updated flow
* @memberof @node-red/runtime_flows * @memberof @node-red/runtime_flows
*/ */
updateFlow: function(opts) { updateFlow: async function(opts) {
return mutex.runExclusive(function() { return mutex.runExclusive(async function() {
return new Promise(function (resolve, reject) { var flow = opts.flow;
var flow = opts.flow; var id = opts.id;
var id = opts.id; return runtime.flows.updateFlow(id, flow, opts.user).then(function () {
try { runtime.log.audit({event: "flow.update", id: id}, opts.req);
runtime.nodes.updateFlow(id, flow, opts.user).then(function () { return id;
runtime.log.audit({event: "flow.update", id: id}, opts.req); }).catch(function (err) {
return resolve(id); if (err.code === 404) {
}).catch(function (err) { runtime.log.audit({event: "flow.update", id: id, error: "not_found"}, opts.req);
runtime.log.audit({ // TODO: this swap around of .code and .status isn't ideal
event: "flow.update", err.status = 404;
error: err.code || "unexpected_error", err.code = "not_found";
message: err.toString() } else {
}, opts.req); runtime.log.audit({
err.status = 400; event: "flow.update",
return reject(err); error: err.code || "unexpected_error",
}) message: err.toString()
} catch (err) { }, opts.req);
if (err.code === 404) { err.status = 400;
runtime.log.audit({event: "flow.update", id: id, error: "not_found"}, opts.req);
// TODO: this swap around of .code and .status isn't ideal
err.status = 404;
err.code = "not_found";
return reject(err);
} else {
runtime.log.audit({
event: "flow.update",
error: err.code || "unexpected_error",
message: err.toString()
}, opts.req);
err.status = 400;
return reject(err);
}
} }
}); throw err;
})
}); });
}, },
/** /**
@ -211,42 +189,28 @@ var api = module.exports = {
* @return {Promise} - resolves if successful * @return {Promise} - resolves if successful
* @memberof @node-red/runtime_flows * @memberof @node-red/runtime_flows
*/ */
deleteFlow: function(opts) { deleteFlow: async function(opts) {
return mutex.runExclusive(function() { return mutex.runExclusive(function() {
return new Promise(function (resolve, reject) { var id = opts.id;
var id = opts.id; return runtime.flows.removeFlow(id, opts.user).then(function () {
try { runtime.log.audit({event: "flow.remove", id: id}, opts.req);
runtime.nodes.removeFlow(id, opts.user).then(function () { return;
runtime.log.audit({event: "flow.remove", id: id}, opts.req); }).catch(function (err) {
return resolve(); if (err.code === 404) {
}).catch(function (err) { runtime.log.audit({event: "flow.remove", id: id, error: "not_found"}, opts.req);
runtime.log.audit({ // TODO: this swap around of .code and .status isn't ideal
event: "flow.remove", err.status = 404;
id: id, err.code = "not_found";
error: err.code || "unexpected_error", } else {
message: err.toString() runtime.log.audit({
}, opts.req); event: "flow.remove",
err.status = 400; id: id,
return reject(err); error: err.code || "unexpected_error",
}); message: err.toString()
} catch (err) { }, opts.req);
if (err.code === 404) { err.status = 400;
runtime.log.audit({event: "flow.remove", id: id, error: "not_found"}, opts.req);
// TODO: this swap around of .code and .status isn't ideal
err.status = 404;
err.code = "not_found";
return reject(err);
} else {
runtime.log.audit({
event: "flow.remove",
id: id,
error: err.code || "unexpected_error",
message: err.toString()
}, opts.req);
err.status = 400;
return reject(err);
}
} }
throw err;
}); });
}); });
}, },
@ -261,35 +225,33 @@ var api = module.exports = {
* @return {Promise<Object>} - the safe credentials * @return {Promise<Object>} - the safe credentials
* @memberof @node-red/runtime_flows * @memberof @node-red/runtime_flows
*/ */
getNodeCredentials: function(opts) { getNodeCredentials: async function(opts) {
return new Promise(function(resolve,reject) { runtime.log.audit({event: "credentials.get",type:opts.type,id:opts.id}, opts.req);
runtime.log.audit({event: "credentials.get",type:opts.type,id:opts.id}, opts.req); var credentials = runtime.nodes.getCredentials(opts.id);
var credentials = runtime.nodes.getCredentials(opts.id); if (!credentials) {
if (!credentials) { return {};
return resolve({}); }
} var sendCredentials = {};
var sendCredentials = {}; var cred;
var cred; if (/^subflow(:|$)/.test(opts.type)) {
if (/^subflow(:|$)/.test(opts.type)) { for (cred in credentials) {
for (cred in credentials) { if (credentials.hasOwnProperty(cred)) {
if (credentials.hasOwnProperty(cred)) { sendCredentials['has_'+cred] = credentials[cred] != null && credentials[cred] !== '';
sendCredentials['has_'+cred] = credentials[cred] != null && credentials[cred] !== '';
}
}
} else {
var definition = runtime.nodes.getCredentialDefinition(opts.type) || {};
for (cred in definition) {
if (definition.hasOwnProperty(cred)) {
if (definition[cred].type == "password") {
var key = 'has_' + cred;
sendCredentials[key] = credentials[cred] != null && credentials[cred] !== '';
continue;
}
sendCredentials[cred] = credentials[cred] || '';
}
} }
} }
resolve(sendCredentials); } else {
}) var definition = runtime.nodes.getCredentialDefinition(opts.type) || {};
for (cred in definition) {
if (definition.hasOwnProperty(cred)) {
if (definition[cred].type == "password") {
var key = 'has_' + cred;
sendCredentials[key] = credentials[cred] != null && credentials[cred] !== '';
continue;
}
sendCredentials[cred] = credentials[cred] || '';
}
}
}
return sendCredentials;
} }
} }

View File

@ -14,9 +14,34 @@
* limitations under the License. * limitations under the License.
**/ **/
var events = require("events"); const events = new (require("events")).EventEmitter();
module.exports = new events.EventEmitter();
const deprecatedEvents = {
"nodes-stopped": "flows:stopped",
"nodes-started": "flows:started"
}
function wrapEventFunction(obj,func) {
events["_"+func] = events[func];
return function(eventName, listener) {
if (deprecatedEvents.hasOwnProperty(eventName)) {
const log = require("@node-red/util").log;
const stack = (new Error().stack).split("\n")[2].split("(")[1].slice(0,-1);
log.warn(`[RED.events] Deprecated use of "${eventName}" event from "${stack}". Use "${deprecatedEvents[eventName]}" instead.`)
}
return events["_"+func].call(events,eventName,listener)
}
}
events.on = wrapEventFunction(events,"on");
events.once = wrapEventFunction(events,"once");
events.addListener = events.on;
module.exports = events;
/** /**
* Runtime events emitter * Runtime events emitter

View File

@ -17,8 +17,9 @@
var clone = require("clone"); var clone = require("clone");
var redUtil = require("@node-red/util").util; var redUtil = require("@node-red/util").util;
var flowUtil = require("./util"); var flowUtil = require("./util");
var events = require("../../events"); var events = require("../events");
const context = require('../context'); const context = require('../nodes/context');
const hooks = require("../hooks");
var Subflow; var Subflow;
var Log; var Log;
@ -539,8 +540,25 @@ class Flow {
} }
} }
get asyncMessageDelivery() { send(sendEvents) {
return asyncMessageDelivery // onSend - passed an array of SendEvent objects. The messages inside these objects are exactly what the node has passed to node.send - meaning there could be duplicate references to the same message object.
// preRoute - called once for each SendEvent object in turn
// preDeliver - the local router has identified the node it is going to send to. At this point, the message has been cloned if needed.
// postDeliver - the message has been dispatched to be delivered asynchronously (unless the sync delivery flag is set, in which case it would be continue as synchronous delivery)
// onReceive - a node is about to receive a message
// postReceive - the message has been passed to the node's input handler
// onDone, onError - the node has completed with a message or logged an error
handleOnSend(this,sendEvents, (err, eventData) => {
if (err) {
let srcNode;
if (Array.isArray(eventData)) {
srcNode = eventData[0].source.node;
} else {
srcNode = eventData.source.node;
}
srcNode.error(err);
}
});
} }
dump() { dump() {
@ -589,6 +607,67 @@ function stopNode(node,removed) {
} }
function handleOnSend(flow,sendEvents, reportError) {
// onSend - passed an array of SendEvent objects. The messages inside these objects are exactly what the node has passed to node.send - meaning there could be duplicate references to the same message object.
hooks.trigger("onSend",sendEvents,(err) => {
if (err) {
reportError(err,sendEvents);
return
} else if (err !== false) {
for (var i=0;i<sendEvents.length;i++) {
handlePreRoute(flow,sendEvents[i],reportError)
}
}
});
}
function handlePreRoute(flow, sendEvent, reportError) {
// preRoute - called once for each SendEvent object in turn
hooks.trigger("preRoute",sendEvent,(err) => {
if (err) {
reportError(err,sendEvent);
return;
} else if (err !== false) {
sendEvent.destination.node = flow.getNode(sendEvent.destination.id);
if (sendEvent.destination.node) {
if (sendEvent.cloneMessage) {
sendEvent.msg = redUtil.cloneMessage(sendEvent.msg);
}
handlePreDeliver(flow,sendEvent,reportError);
}
}
})
}
function handlePreDeliver(flow,sendEvent, reportError) {
// preDeliver - the local router has identified the node it is going to send to. At this point, the message has been cloned if needed.
hooks.trigger("preDeliver",sendEvent,(err) => {
if (err) {
reportError(err,sendEvent);
return;
} else if (err !== false) {
if (asyncMessageDelivery) {
setImmediate(function() {
if (sendEvent.destination.node) {
sendEvent.destination.node.receive(sendEvent.msg);
}
})
} else {
if (sendEvent.destination.node) {
sendEvent.destination.node.receive(sendEvent.msg);
}
}
// postDeliver - the message has been dispatched to be delivered asynchronously (unless the sync delivery flag is set, in which case it would be continue as synchronous delivery)
hooks.trigger("postDeliver", sendEvent, function(err) {
if (err) {
reportError(err,sendEvent);
}
})
}
})
}
module.exports = { module.exports = {
init: function(runtime) { init: function(runtime) {
nodeCloseTimeout = runtime.settings.nodeCloseTimeout || 15000; nodeCloseTimeout = runtime.settings.nodeCloseTimeout || 15000;

View File

@ -16,14 +16,14 @@
const clone = require("clone"); const clone = require("clone");
const Flow = require('./Flow').Flow; const Flow = require('./Flow').Flow;
const context = require('../context'); const context = require('../nodes/context');
const util = require("util"); const util = require("util");
const redUtil = require("@node-red/util").util; const redUtil = require("@node-red/util").util;
const flowUtil = require("./util"); const flowUtil = require("./util");
const credentials = require("../credentials"); const credentials = require("../nodes/credentials");
var Log; var Log;
@ -159,7 +159,7 @@ class Subflow extends Flow {
start(diff) { start(diff) {
var self = this; var self = this;
// Create a subflow node to accept inbound messages and route appropriately // Create a subflow node to accept inbound messages and route appropriately
var Node = require("../Node"); var Node = require("../nodes/Node");
if (this.subflowDef.status) { if (this.subflowDef.status) {
var subflowStatusConfig = { var subflowStatusConfig = {

View File

@ -15,21 +15,19 @@
**/ **/
var clone = require("clone"); var clone = require("clone");
var when = require("when");
var Flow = require('./Flow'); var Flow = require('./Flow');
var typeRegistry = require("@node-red/registry"); var typeRegistry = require("@node-red/registry");
var deprecated = typeRegistry.deprecated; var deprecated = typeRegistry.deprecated;
var context = require("../nodes/context")
var context = require("../context") var credentials = require("../nodes/credentials");
var credentials = require("../credentials");
var flowUtil = require("./util"); var flowUtil = require("./util");
var log; var log;
var events = require("../../events"); var events = require("../events");
var redUtil = require("@node-red/util").util; var redUtil = require("@node-red/util").util;
const hooks = require("../hooks");
var storage = null; var storage = null;
var settings = null; var settings = null;
@ -294,6 +292,8 @@ function start(type,diff,muteLog) {
} }
} }
events.emit("flows:starting", {config: activeConfig, type: type, diff: diff})
var id; var id;
if (type === "full") { if (type === "full") {
// A full start means everything should // A full start means everything should
@ -354,6 +354,8 @@ function start(type,diff,muteLog) {
} }
} }
} }
events.emit("flows:started", {config: activeConfig, type: type, diff: diff});
// Deprecated event
events.emit("nodes-started"); events.emit("nodes-started");
if (credentialsPendingReset === true) { if (credentialsPendingReset === true) {
@ -401,6 +403,8 @@ function stop(type,diff,muteLog) {
stopList = diff.changed.concat(diff.removed).concat(diff.linked); stopList = diff.changed.concat(diff.removed).concat(diff.linked);
} }
events.emit("flows:stopping",{config: activeConfig, type: type, diff: diff})
for (var id in activeFlows) { for (var id in activeFlows) {
if (activeFlows.hasOwnProperty(id)) { if (activeFlows.hasOwnProperty(id)) {
var flowStateChanged = diff && (diff.added.indexOf(id) !== -1 || diff.removed.indexOf(id) !== -1); var flowStateChanged = diff && (diff.added.indexOf(id) !== -1 || diff.removed.indexOf(id) !== -1);
@ -432,6 +436,8 @@ function stop(type,diff,muteLog) {
log.info(log._("nodes.flows.stopped-flows")); log.info(log._("nodes.flows.stopped-flows"));
} }
} }
events.emit("flows:stopped",{config: activeConfig, type: type, diff: diff});
// Deprecated event
events.emit("nodes-stopped"); events.emit("nodes-stopped");
}); });
} }
@ -481,7 +487,7 @@ function updateMissingTypes() {
} }
} }
function addFlow(flow, user) { async function addFlow(flow, user) {
var i,node; var i,node;
if (!flow.hasOwnProperty('nodes')) { if (!flow.hasOwnProperty('nodes')) {
throw new Error('missing nodes property'); throw new Error('missing nodes property');
@ -506,10 +512,10 @@ function addFlow(flow, user) {
node = flow.nodes[i]; node = flow.nodes[i];
if (activeFlowConfig.allNodes[node.id]) { if (activeFlowConfig.allNodes[node.id]) {
// TODO nls // TODO nls
return when.reject(new Error('duplicate id')); throw new Error('duplicate id');
} }
if (node.type === 'tab' || node.type === 'subflow') { if (node.type === 'tab' || node.type === 'subflow') {
return when.reject(new Error('invalid node type: '+node.type)); throw new Error('invalid node type: '+node.type);
} }
node.z = flow.id; node.z = flow.id;
nodes.push(node); nodes.push(node);
@ -519,10 +525,10 @@ function addFlow(flow, user) {
node = flow.configs[i]; node = flow.configs[i];
if (activeFlowConfig.allNodes[node.id]) { if (activeFlowConfig.allNodes[node.id]) {
// TODO nls // TODO nls
return when.reject(new Error('duplicate id')); throw new Error('duplicate id');
} }
if (node.type === 'tab' || node.type === 'subflow') { if (node.type === 'tab' || node.type === 'subflow') {
return when.reject(new Error('invalid node type: '+node.type)); throw new Error('invalid node type: '+node.type);
} }
node.z = flow.id; node.z = flow.id;
nodes.push(node); nodes.push(node);
@ -607,7 +613,7 @@ function getFlow(id) {
return result; return result;
} }
function updateFlow(id,newFlow, user) { async function updateFlow(id,newFlow, user) {
var label = id; var label = id;
if (id !== 'global') { if (id !== 'global') {
if (!activeFlowConfig.flows[id]) { if (!activeFlowConfig.flows[id]) {
@ -667,7 +673,7 @@ function updateFlow(id,newFlow, user) {
}) })
} }
function removeFlow(id, user) { async function removeFlow(id, user) {
if (id === 'global') { if (id === 'global') {
// TODO: nls + error code // TODO: nls + error code
throw new Error('not allowed to remove global'); throw new Error('not allowed to remove global');

View File

@ -0,0 +1,182 @@
const Log = require("@node-red/util").log;
const VALID_HOOKS = [
// Message Routing Path
"onSend",
"preRoute",
"preDeliver",
"postDeliver",
"onReceive",
"postReceive",
"onComplete"
]
// Flags for what hooks have handlers registered
let states = { }
// Hooks by id
let hooks = { }
// Hooks by label
let labelledHooks = { }
/**
* Runtime hooks engine
*
* The following hooks can be used:
*
* Message sending
* - `onSend` - passed an array of `SendEvent` objects. The messages inside these objects are exactly what the node has passed to `node.send` - meaning there could be duplicate references to the same message object.
* - `preRoute` - passed a `SendEvent`
* - `preDeliver` - passed a `SendEvent`. The local router has identified the node it is going to send to. At this point, the message has been cloned if needed.
* - `postDeliver` - passed a `SendEvent`. The message has been dispatched to be delivered asynchronously (unless the sync delivery flag is set, in which case it would be continue as synchronous delivery)
* - `onReceive` - passed a `ReceiveEvent` when a node is about to receive a message
* - `postReceive` - passed a `ReceiveEvent` when the message has been given to the node's `input` handler(s)
* - `onComplete` - passed a `CompleteEvent` when the node has completed with a message or logged an error
*
* @mixin @node-red/runtime_hooks
*/
/**
* Register a handler to a named hook
* @memberof @node-red/runtime_hooks
* @param {String} hookId - the name of the hook to attach to
* @param {Function} callback - the callback function for the hook
*/
function add(hookId, callback) {
let [id, label] = hookId.split(".");
if (VALID_HOOKS.indexOf(id) === -1) {
throw new Error(`Invalid hook '${id}'`);
}
if (label) {
if (labelledHooks[label] && labelledHooks[label][id]) {
throw new Error("Hook "+hookId+" already registered")
}
labelledHooks[label] = labelledHooks[label]||{};
labelledHooks[label][id] = callback;
}
// Get location of calling code
const stack = new Error().stack;
const callModule = stack.split("\n")[2].split("(")[1].slice(0,-1);
Log.debug(`Adding hook '${hookId}' from ${callModule}`);
hooks[id] = hooks[id] || [];
hooks[id].push({cb:callback,location:callModule});
states[id] = true;
}
/**
* Remove a handled from a named hook
* @memberof @node-red/runtime_hooks
* @param {String} hookId - the name of the hook event to remove - must be `name.label`
*/
function remove(hookId) {
let [id,label] = hookId.split(".");
if ( !label) {
throw new Error("Cannot remove hook without label: ",hookId)
}
Log.debug(`Removing hook '${hookId}'`);
if (labelledHooks[label]) {
if (id === "*") {
// Remove all hooks for this label
let hookList = Object.keys(labelledHooks[label]);
for (let i=0;i<hookList.length;i++) {
removeHook(hookList[i],labelledHooks[label][hookList[i]])
}
delete labelledHooks[label];
} else if (labelledHooks[label][id]) {
removeHook(id,labelledHooks[label][id])
delete labelledHooks[label][id];
if (Object.keys(labelledHooks[label]).length === 0){
delete labelledHooks[label];
}
}
}
}
function removeHook(id,callback) {
let i = hooks[id].findIndex(hook => hook.cb === callback);
if (i !== -1) {
hooks[id].splice(i,1);
if (hooks[id].length === 0) {
delete hooks[id];
delete states[id];
}
}
}
function trigger(hookId, payload, done) {
const hookStack = hooks[hookId];
if (!hookStack || hookStack.length === 0) {
done();
return;
}
let i = 0;
function callNextHook(err) {
if (i === hookStack.length || err) {
done(err);
return;
}
const hook = hookStack[i++];
const callback = hook.cb;
if (callback.length === 1) {
try {
let result = callback(payload);
if (result === false) {
// Halting the flow
done(false);
return
}
if (result && typeof result.then === 'function') {
result.then(handleResolve, callNextHook)
return;
}
callNextHook();
} catch(err) {
done(err);
return;
}
} else {
try {
callback(payload,handleResolve)
} catch(err) {
done(err);
return;
}
}
}
callNextHook();
function handleResolve(result) {
if (result === undefined) {
callNextHook();
} else {
done(result);
}
}
}
function clear() {
hooks = {}
labelledHooks = {}
states = {}
}
function has(hookId) {
let [id, label] = hookId.split(".");
if (label) {
return !!(labelledHooks[label] && labelledHooks[label][id])
}
return !!states[id]
}
module.exports = {
has,
clear,
add,
remove,
trigger
}

View File

@ -19,9 +19,11 @@ var when = require('when');
var externalAPI = require("./api"); var externalAPI = require("./api");
var redNodes = require("./nodes"); var redNodes = require("./nodes");
var flows = require("./flows");
var storage = require("./storage"); var storage = require("./storage");
var library = require("./library"); var library = require("./library");
var events = require("./events"); var events = require("./events");
var hooks = require("./hooks");
var settings = require("./settings"); var settings = require("./settings");
var exec = require("./exec"); var exec = require("./exec");
@ -272,7 +274,9 @@ var runtime = {
settings: settings, settings: settings,
storage: storage, storage: storage,
events: events, events: events,
hooks: hooks,
nodes: redNodes, nodes: redNodes,
flows: flows,
library: library, library: library,
exec: exec, exec: exec,
util: require("@node-red/util").util, util: require("@node-red/util").util,
@ -355,6 +359,7 @@ module.exports = {
storage: storage, storage: storage,
events: events, events: events,
hooks: hooks,
util: require("@node-red/util").util, util: require("@node-red/util").util,
get httpNode() { return nodeApp }, get httpNode() { return nodeApp },
get httpAdmin() { return adminApp }, get httpAdmin() { return adminApp },

View File

@ -20,7 +20,9 @@ var EventEmitter = require("events").EventEmitter;
var redUtil = require("@node-red/util").util; var redUtil = require("@node-red/util").util;
var Log = require("@node-red/util").log; var Log = require("@node-red/util").log;
var context = require("./context"); var context = require("./context");
var flows = require("./flows"); var flows = require("../flows");
const hooks = require("../hooks");
const NOOP_SEND = function() {} const NOOP_SEND = function() {}
@ -55,10 +57,6 @@ function Node(n) {
// as part of its constructor - config._flow will overwrite this._flow // as part of its constructor - config._flow will overwrite this._flow
// which we can tolerate as they are the same object. // which we can tolerate as they are the same object.
Object.defineProperty(this,'_flow', {value: n._flow, enumerable: false, writable: true }) Object.defineProperty(this,'_flow', {value: n._flow, enumerable: false, writable: true })
this._asyncDelivery = n._flow.asyncMessageDelivery;
}
if (this._asyncDelivery === undefined) {
this._asyncDelivery = true;
} }
this.updateWires(n.wires); this.updateWires(n.wires);
} }
@ -125,6 +123,11 @@ Node.prototype.context = function() {
*/ */
Node.prototype._complete = function(msg,error) { Node.prototype._complete = function(msg,error) {
this.metric("done",msg); this.metric("done",msg);
hooks.trigger("onComplete",{ msg: msg, error: error, node: { id: this.id, node: this }}, err => {
if (err) {
this.error(err);
}
})
if (error) { if (error) {
// For now, delegate this to this.error // For now, delegate this to this.error
// But at some point, the timeout handling will need to know about // But at some point, the timeout handling will need to know about
@ -173,15 +176,7 @@ Node.prototype._emit = Node.prototype.emit;
Node.prototype.emit = function(event, ...args) { Node.prototype.emit = function(event, ...args) {
var node = this; var node = this;
if (event === "input") { if (event === "input") {
// When Pluggable Message Routing arrives, this will be called from this._emitInput.apply(this,args);
// that and will already be sync/async depending on the router.
if (this._asyncDelivery) {
setImmediate(function() {
node._emitInput.apply(node,args);
});
} else {
this._emitInput.apply(this,args);
}
} else { } else {
this._emit.apply(this,arguments); this._emit.apply(this,arguments);
} }
@ -195,42 +190,53 @@ Node.prototype.emit = function(event, ...args) {
Node.prototype._emitInput = function(arg) { Node.prototype._emitInput = function(arg) {
var node = this; var node = this;
this.metric("receive", arg); this.metric("receive", arg);
if (node._inputCallback) { let receiveEvent = { msg:arg, destination: { id: this.id, node: this } }
// Just one callback registered. // onReceive - a node is about to receive a message
try { hooks.trigger("onReceive",receiveEvent,(err) => {
node._inputCallback( if (err) {
arg, node.error(err);
function() { node.send.apply(node,arguments) }, return
function(err) { node._complete(arg,err); } } else if (err !== false) {
); if (node._inputCallback) {
} catch(err) { // Just one callback registered.
node.error(err,arg); try {
} node._inputCallback(
} else if (node._inputCallbacks) { arg,
// Multiple callbacks registered. Call each one, tracking eventual completion function() { node.send.apply(node,arguments) },
var c = node._inputCallbacks.length; function(err) { node._complete(arg,err); }
for (var i=0;i<c;i++) { );
var cb = node._inputCallbacks[i]; } catch(err) {
if (cb.length === 2) { node.error(err,arg);
c++; }
} } else if (node._inputCallbacks) {
try { // Multiple callbacks registered. Call each one, tracking eventual completion
cb.call( var c = node._inputCallbacks.length;
node, for (var i=0;i<c;i++) {
arg, var cb = node._inputCallbacks[i];
function() { node.send.apply(node,arguments) }, if (cb.length === 2) {
function(err) { c++;
c--;
if (c === 0) {
node._complete(arg,err);
}
} }
); try {
} catch(err) { cb.call(
node.error(err,arg); node,
arg,
function() { node.send.apply(node,arguments) },
function(err) {
c--;
if (c === 0) {
node._complete(arg,err);
}
}
);
} catch(err) {
node.error(err,arg);
}
}
} }
// postReceive - the message has been passed to the node's input handler
hooks.trigger("postReceive",receiveEvent,(err) => {if (err) { node.error(err) }});
} }
} });
} }
/** /**
@ -366,11 +372,19 @@ Node.prototype.send = function(msg) {
msg._msgid = redUtil.generateId(); msg._msgid = redUtil.generateId();
} }
this.metric("send",msg); this.metric("send",msg);
node = this._flow.getNode(this._wire); this._flow.send([{
/* istanbul ignore else */ msg: msg,
if (node) { source: {
node.receive(msg); id: this.id,
} node: this,
port: 0
},
destination: {
id: this._wire,
node: undefined
},
cloneMessage: false
}]);
return; return;
} else { } else {
msg = [msg]; msg = [msg];
@ -384,7 +398,7 @@ Node.prototype.send = function(msg) {
var sendEvents = []; var sendEvents = [];
var sentMessageId = null; var sentMessageId = null;
var hasMissingIds = false;
// for each output of node eg. [msgs to output 0, msgs to output 1, ...] // for each output of node eg. [msgs to output 0, msgs to output 1, ...]
for (var i = 0; i < numOutputs; i++) { for (var i = 0; i < numOutputs; i++) {
var wires = this.wires[i]; // wires leaving output i var wires = this.wires[i]; // wires leaving output i
@ -398,24 +412,31 @@ Node.prototype.send = function(msg) {
var k = 0; var k = 0;
// for each recipent node of that output // for each recipent node of that output
for (var j = 0; j < wires.length; j++) { for (var j = 0; j < wires.length; j++) {
node = this._flow.getNode(wires[j]); // node at end of wire j // for each msg to send eg. [[m1, m2, ...], ...]
if (node) { for (k = 0; k < msgs.length; k++) {
// for each msg to send eg. [[m1, m2, ...], ...] var m = msgs[k];
for (k = 0; k < msgs.length; k++) { if (m !== null && m !== undefined) {
var m = msgs[k]; if (!m._msgid) {
if (m !== null && m !== undefined) { hasMissingIds = true;
/* istanbul ignore else */
if (!sentMessageId) {
sentMessageId = m._msgid;
}
if (msgSent) {
var clonedmsg = redUtil.cloneMessage(m);
sendEvents.push({n:node,m:clonedmsg});
} else {
sendEvents.push({n:node,m:m});
msgSent = true;
}
} }
/* istanbul ignore else */
if (!sentMessageId) {
sentMessageId = m._msgid;
}
sendEvents.push({
msg: m,
source: {
id: this.id,
node: this,
port: i
},
destination: {
id: wires[j],
node: undefined
},
cloneMessage: msgSent
});
msgSent = true;
} }
} }
} }
@ -428,21 +449,22 @@ Node.prototype.send = function(msg) {
} }
this.metric("send",{_msgid:sentMessageId}); this.metric("send",{_msgid:sentMessageId});
for (i=0;i<sendEvents.length;i++) { if (hasMissingIds) {
var ev = sendEvents[i]; for (i=0;i<sendEvents.length;i++) {
/* istanbul ignore else */ var ev = sendEvents[i];
if (!ev.m._msgid) { /* istanbul ignore else */
ev.m._msgid = sentMessageId; if (!ev.msg._msgid) {
ev.msg._msgid = sentMessageId;
}
} }
ev.n.receive(ev.m);
} }
this._flow.send(sendEvents);
}; };
/** /**
* Receive a message. * Receive a message.
* *
* This will emit the `input` event with the provided message. * This will emit the `input` event with the provided message.
* As of 1.0, this will return *before* any 'input' callback handler is invoked.
*/ */
Node.prototype.receive = function(msg) { Node.prototype.receive = function(msg) {
if (!msg) { if (!msg) {

View File

@ -18,7 +18,6 @@ var clone = require("clone");
var log = require("@node-red/util").log; var log = require("@node-red/util").log;
var util = require("@node-red/util").util; var util = require("@node-red/util").util;
var memory = require("./memory"); var memory = require("./memory");
var flows;
var settings; var settings;
@ -48,7 +47,6 @@ function logUnknownStore(name) {
} }
function init(_settings) { function init(_settings) {
flows = require("../flows");
settings = _settings; settings = _settings;
contexts = {}; contexts = {};
stores = {}; stores = {};
@ -513,39 +511,6 @@ function getContext(nodeId, flowId) {
return newContext; return newContext;
} }
//
// function getContext(localId,flowId,parent) {
// var contextId = localId;
// if (flowId) {
// contextId = localId+":"+flowId;
// }
// console.log("getContext",localId,flowId,"known?",contexts.hasOwnProperty(contextId));
// if (contexts.hasOwnProperty(contextId)) {
// return contexts[contextId];
// }
// var newContext = createContext(contextId,undefined,parent);
// if (flowId) {
// var node = flows.get(flowId);
// console.log("flows,get",flowId,node&&node.type)
// var parent = undefined;
// if (node && node.type.startsWith("subflow:")) {
// parent = node.context().flow;
// }
// else {
// parent = createRootContext();
// }
// var flowContext = getContext(flowId,undefined,parent);
// Object.defineProperty(newContext, 'flow', {
// value: flowContext
// });
// }
// Object.defineProperty(newContext, 'global', {
// value: contexts['global']
// })
// contexts[contextId] = newContext;
// return newContext;
// }
function deleteContext(id,flowId) { function deleteContext(id,flowId) {
if(!hasConfiguredStore){ if(!hasConfiguredStore){
// only delete context if there's no configured storage. // only delete context if there's no configured storage.

View File

@ -23,8 +23,8 @@ var util = require("util");
var registry = require("@node-red/registry"); var registry = require("@node-red/registry");
var credentials = require("./credentials"); var credentials = require("./credentials");
var flows = require("./flows"); var flows = require("../flows");
var flowUtil = require("./flows/util") var flowUtil = require("../flows/util")
var context = require("./context"); var context = require("./context");
var Node = require("./Node"); var Node = require("./Node");
var log; var log;

View File

@ -145,6 +145,14 @@ module.exports = {
*/ */
events: runtime.events, events: runtime.events,
/**
* Runtime hooks engine
* @see @node-red/runtime_hooks
* @memberof node-red
*/
hooks: runtime.hooks,
/** /**
* This provides access to the internal settings module of the * This provides access to the internal settings module of the
* runtime. * runtime.

View File

@ -37,7 +37,7 @@ describe("runtime-api/flows", function() {
it("returns the current flow configuration", function(done) { it("returns the current flow configuration", function(done) {
flows.init({ flows.init({
log: mockLog(), log: mockLog(),
nodes: { flows: {
getFlows: function() { return [1,2,3] } getFlows: function() { return [1,2,3] }
} }
}); });
@ -76,7 +76,7 @@ describe("runtime-api/flows", function() {
}) })
flows.init({ flows.init({
log: mockLog(), log: mockLog(),
nodes: { flows: {
getFlows: function() { return {rev:"currentRev",flows:[]} }, getFlows: function() { return {rev:"currentRev",flows:[]} },
setFlows: setFlows, setFlows: setFlows,
loadFlows: loadFlows loadFlows: loadFlows
@ -192,7 +192,7 @@ describe("runtime-api/flows", function() {
}); });
flows.init({ flows.init({
log: mockLog(), log: mockLog(),
nodes: { flows: {
addFlow: addFlow addFlow: addFlow
} }
}); });
@ -225,7 +225,7 @@ describe("runtime-api/flows", function() {
}); });
flows.init({ flows.init({
log: mockLog(), log: mockLog(),
nodes: { flows: {
getFlow: getFlow getFlow: getFlow
} }
}); });
@ -255,7 +255,9 @@ describe("runtime-api/flows", function() {
var err = new Error(); var err = new Error();
// TODO: quirk of internal api - uses .code for .status // TODO: quirk of internal api - uses .code for .status
err.code = 404; err.code = 404;
throw err; var p = Promise.reject(err);
p.catch(()=>{});
return p;
} else if (id === "error") { } else if (id === "error") {
var err = new Error(); var err = new Error();
// TODO: quirk of internal api - uses .code for .status // TODO: quirk of internal api - uses .code for .status
@ -268,7 +270,7 @@ describe("runtime-api/flows", function() {
}); });
flows.init({ flows.init({
log: mockLog(), log: mockLog(),
nodes: { flows: {
updateFlow: updateFlow updateFlow: updateFlow
} }
}); });
@ -311,7 +313,9 @@ describe("runtime-api/flows", function() {
var err = new Error(); var err = new Error();
// TODO: quirk of internal api - uses .code for .status // TODO: quirk of internal api - uses .code for .status
err.code = 404; err.code = 404;
throw err; var p = Promise.reject(err);
p.catch(()=>{});
return p;
} else if (flow === "error") { } else if (flow === "error") {
var err = new Error(); var err = new Error();
// TODO: quirk of internal api - uses .code for .status // TODO: quirk of internal api - uses .code for .status
@ -324,7 +328,7 @@ describe("runtime-api/flows", function() {
}); });
flows.init({ flows.init({
log: mockLog(), log: mockLog(),
nodes: { flows: {
removeFlow: removeFlow removeFlow: removeFlow
} }
}); });

View File

@ -22,10 +22,11 @@ var util = require("util");
var NR_TEST_UTILS = require("nr-test-utils"); var NR_TEST_UTILS = require("nr-test-utils");
var flowUtils = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows/util"); var flowUtils = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/util");
var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows/Flow"); var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Flow");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows"); var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node"); var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
var hooks = NR_TEST_UTILS.require("@node-red/runtime/lib/hooks");
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry"); var typeRegistry = NR_TEST_UTILS.require("@node-red/registry");
@ -826,4 +827,371 @@ describe('Flow', function() {
}); });
describe("#send", function() {
it("sends a message - no cloning", function(done) {
var shutdownTest = function(err) {
hooks.clear();
flow.stop().then(() => { done(err) });
}
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
Object.keys(flow.getActiveNodes()).should.have.length(2);
var n1 = flow.getNode('1');
var n2 = flow.getNode('2');
var messageReceived = false;
n2.receive = function(msg) {
messageReceived = true;
try {
msg.should.be.exactly(message);
shutdownTest();
} catch(err) {
shutdownTest(err);
}
}
var message = {payload:"hello"}
flow.send([{
msg: message,
source: { id:"1", node: n1 },
destination: { id:"2", node: undefined },
cloneMessage: false
}])
messageReceived.should.be.false()
})
it("sends a message - cloning", function(done) {
var shutdownTest = function(err) {
hooks.clear();
flow.stop().then(() => { done(err) });
}
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
Object.keys(flow.getActiveNodes()).should.have.length(2);
var n1 = flow.getNode('1');
var n2 = flow.getNode('2');
n2.receive = function(msg) {
try {
// Message should be cloned
msg.should.be.eql(message);
msg.should.not.be.exactly(message);
shutdownTest();
} catch(err) {
shutdownTest(err);
}
}
var message = {payload:"hello"}
flow.send([{
msg: message,
source: { id:"1", node: n1 },
destination: { id:"2", node: undefined },
cloneMessage: true
}])
})
it("sends multiple messages", function(done) {
var shutdownTest = function(err) {
hooks.clear();
flow.stop().then(() => { done(err) });
}
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
Object.keys(flow.getActiveNodes()).should.have.length(2);
var n1 = flow.getNode('1');
var n2 = flow.getNode('2');
var messageCount = 0;
n2.receive = function(msg) {
try {
msg.should.be.exactly(messages[messageCount++]);
if (messageCount === 2) {
shutdownTest();
}
} catch(err) {
shutdownTest(err);
}
}
var messages = [{payload:"hello"},{payload:"world"}];
flow.send([{
msg: messages[0],
source: { id:"1", node: n1 },
destination: { id:"2", node: undefined }
},{
msg: messages[1],
source: { id:"1", node: n1 },
destination: { id:"2", node: undefined }
}])
})
it("sends a message - triggers hooks", function(done) {
var hookErrors = [];
var messageReceived = false;
var hooksCalled = [];
hooks.add("onSend", function(sendEvents) {
hooksCalled.push("onSend")
try {
messageReceived.should.be.false()
sendEvents.should.have.length(1);
sendEvents[0].msg.should.be.exactly(message);
} catch(err) {
hookErrors.push(err);
}
})
hooks.add("preRoute", function(sendEvent) {
hooksCalled.push("preRoute")
try {
messageReceived.should.be.false()
sendEvent.msg.should.be.exactly(message);
should.not.exist(sendEvent.destination.node)
} catch(err) {
hookErrors.push(err);
}
})
hooks.add("preDeliver", function(sendEvent) {
hooksCalled.push("preDeliver")
try {
messageReceived.should.be.false()
// Cloning should have happened
sendEvent.msg.should.not.be.exactly(message);
// Destinatino node populated
should.exist(sendEvent.destination.node)
} catch(err) {
hookErrors.push(err);
}
})
hooks.add("postDeliver", function(sendEvent) {
hooksCalled.push("postDeliver")
try {
messageReceived.should.be.false()
} catch(err) {
hookErrors.push(err);
}
})
var shutdownTest = function(err) {
hooks.clear();
flow.stop().then(() => { done(err) });
}
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
Object.keys(flow.getActiveNodes()).should.have.length(2);
var n1 = flow.getNode('1');
var n2 = flow.getNode('2');
n2.receive = function(msg) {
messageReceived = true;
try {
msg.should.be.eql(message);
msg.should.not.be.exactly(message);
hooksCalled.should.eql(["onSend","preRoute","preDeliver","postDeliver"])
if (hookErrors.length > 0) {
shutdownTest(hookErrors[0])
} else {
shutdownTest();
}
} catch(err) {
shutdownTest(err);
}
}
var message = {payload:"hello"}
flow.send([{
msg: message,
source: { id:"1", node: n1 },
destination: { id:"2", node: undefined },
cloneMessage: true
}])
})
describe("errors thrown by hooks are reported to the sending node", function() {
var flow;
var n1,n2;
var messageReceived = false;
var errorReceived = null;
before(function() {
hooks.add("onSend", function(sendEvents) {
if (sendEvents[0].msg.payload === "trigger-onSend") {
throw new Error("onSend Error");
}
})
hooks.add("preRoute", function(sendEvent) {
if (sendEvent.msg.payload === "trigger-preRoute") {
throw new Error("preRoute Error");
}
})
hooks.add("preDeliver", function(sendEvent) {
if (sendEvent.msg.payload === "trigger-preDeliver") {
throw new Error("preDeliver Error");
}
})
hooks.add("postDeliver", function(sendEvent) {
if (sendEvent.msg.payload === "trigger-postDeliver") {
throw new Error("postDeliver Error");
}
})
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}
]);
flow = Flow.create({},config,config.flows["t1"]);
flow.start();
n1 = flow.getNode('1');
n2 = flow.getNode('2');
n2.receive = function(msg) {
messageReceived = true;
}
n1.error = function(err) {
errorReceived = err;
}
})
after(function(done) {
hooks.clear();
flow.stop().then(() => { done() });
})
beforeEach(function() {
messageReceived = false;
errorReceived = null;
})
function testHook(hook, msgExpected, done) {
var message = {payload:"trigger-"+hook}
flow.send([{
msg: message,
source: { id:"1", node: n1 },
destination: { id:"2", node: undefined },
cloneMessage: true
}])
setTimeout(function() {
try {
messageReceived.should.equal(msgExpected);
should.exist(errorReceived)
errorReceived.toString().should.containEql(hook);
done();
} catch(err) {
done(err);
}
},10)
}
it("onSend", function(done) { testHook("onSend", false, done) })
it("preRoute", function(done) { testHook("preRoute", false, done) })
it("preDeliver", function(done) { testHook("preDeliver", false, done) })
it("postDeliver", function(done) { testHook("postDeliver", true, done) })
})
describe("hooks can stop the sending of messages", function() {
var flow;
var n1,n2;
var messageReceived = false;
var errorReceived = false;
before(function() {
hooks.add("onSend", function(sendEvents) {
if (sendEvents[0].msg.payload === "trigger-onSend") {
return false
}
})
hooks.add("preRoute", function(sendEvent) {
if (sendEvent.msg.payload === "trigger-preRoute") {
return false
}
})
hooks.add("preDeliver", function(sendEvent) {
if (sendEvent.msg.payload === "trigger-preDeliver") {
return false
}
})
hooks.add("postDeliver", function(sendEvent) {
if (sendEvent.msg.payload === "trigger-postDeliver") {
return false
}
})
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}
]);
flow = Flow.create({},config,config.flows["t1"]);
flow.start();
n1 = flow.getNode('1');
n2 = flow.getNode('2');
n2.receive = function(msg) {
messageReceived = true;
}
n1.error = function(err) {
errorReceived = true;
}
})
after(function(done) {
hooks.clear();
flow.stop().then(() => { done() });
})
function testSend(payload,messageReceivedExpected,errorReceivedExpected,done) {
messageReceived = false;
errorReceived = false;
flow.send([{
msg: {payload: payload},
source: { id:"1", node: n1 },
destination: { id:"2", node: undefined },
cloneMessage: true
}])
setTimeout(function() {
try {
messageReceived.should.eql(messageReceivedExpected)
errorReceived.should.eql(errorReceivedExpected)
done();
} catch(err) {
done(err);
}
},10)
}
function testHook(hook, done) {
testSend("pass",true,false,err => {
if (err) {
done(err)
} else {
testSend("trigger-"+hook,false,false,done);
}
})
}
it("onSend", function(done) { testHook("onSend", done) })
it("preRoute", function(done) { testHook("preRoute", done) })
it("preDeliver", function(done) { testHook("preDeliver", done) })
// postDeliver happens after delivery is scheduled so cannot stop it
// it("postDeliver", function(done) { testHook("postDeliver", done) })
})
})
}); });

View File

@ -22,11 +22,11 @@ var util = require("util");
var NR_TEST_UTILS = require("nr-test-utils"); var NR_TEST_UTILS = require("nr-test-utils");
var Subflow = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows/Subflow"); var Subflow = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Subflow");
var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows/Flow"); var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Flow");
var flowUtils = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows/util"); var flowUtils = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/util");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows"); var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node"); var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry"); var typeRegistry = NR_TEST_UTILS.require("@node-red/registry");

View File

@ -20,13 +20,13 @@ var when = require("when");
var clone = require("clone"); var clone = require("clone");
var NR_TEST_UTILS = require("nr-test-utils"); var NR_TEST_UTILS = require("nr-test-utils");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows"); var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
var RedNode = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node"); var RedNode = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
var RED = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes"); var RED = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes");
var events = NR_TEST_UTILS.require("@node-red/runtime/lib/events"); var events = NR_TEST_UTILS.require("@node-red/runtime/lib/events");
var credentials = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/credentials"); var credentials = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/credentials");
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry") var typeRegistry = NR_TEST_UTILS.require("@node-red/registry")
var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows/Flow"); var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Flow");
describe('flows/index', function() { describe('flows/index', function() {
@ -209,8 +209,7 @@ describe('flows/index', function() {
storage.getFlows = function() { storage.getFlows = function() {
return when.resolve({flows:originalConfig}); return when.resolve({flows:originalConfig});
} }
events.once('flows:started',function() {
events.once('nodes-started',function() {
flows.setFlows(newConfig,"nodes").then(function() { flows.setFlows(newConfig,"nodes").then(function() {
flows.getFlows().flows.should.eql(newConfig); flows.getFlows().flows.should.eql(newConfig);
flowCreate.flows['t1'].update.called.should.be.true(); flowCreate.flows['t1'].update.called.should.be.true();
@ -239,7 +238,7 @@ describe('flows/index', function() {
return when.resolve({flows:originalConfig}); return when.resolve({flows:originalConfig});
} }
events.once('nodes-started',function() { events.once('flows:started',function() {
flows.setFlows(newConfig,"nodes").then(function() { flows.setFlows(newConfig,"nodes").then(function() {
flows.getFlows().flows.should.eql(newConfig); flows.getFlows().flows.should.eql(newConfig);
flowCreate.flows['t1'].update.called.should.be.true(); flowCreate.flows['t1'].update.called.should.be.true();
@ -301,7 +300,7 @@ describe('flows/index', function() {
return when.resolve({flows:originalConfig}); return when.resolve({flows:originalConfig});
} }
events.once('nodes-started',function() { events.once('flows:started',function() {
Object.keys(flowCreate.flows).should.eql(['_GLOBAL_','t1']); Object.keys(flowCreate.flows).should.eql(['_GLOBAL_','t1']);
done(); done();
}); });
@ -398,7 +397,7 @@ describe('flows/index', function() {
// return when.resolve({flows:originalConfig}); // return when.resolve({flows:originalConfig});
// } // }
// //
// events.once('nodes-started',function() { // events.once('flows:started',function() {
// flows.handleError(originalConfig[0],"message",{}); // flows.handleError(originalConfig[0],"message",{});
// flowCreate.flows['t1'].handleError.called.should.be.true(); // flowCreate.flows['t1'].handleError.called.should.be.true();
// done(); // done();
@ -423,7 +422,7 @@ describe('flows/index', function() {
// return when.resolve({flows:originalConfig}); // return when.resolve({flows:originalConfig});
// } // }
// //
// events.once('nodes-started',function() { // events.once('flows:started',function() {
// flows.handleError(originalConfig[0],"message",{}); // flows.handleError(originalConfig[0],"message",{});
// try { // try {
// flowCreate.flows['t1'].handleError.called.should.be.true(); // flowCreate.flows['t1'].handleError.called.should.be.true();
@ -451,7 +450,7 @@ describe('flows/index', function() {
// return when.resolve({flows:originalConfig}); // return when.resolve({flows:originalConfig});
// } // }
// //
// events.once('nodes-started',function() { // events.once('flows:started',function() {
// flows.handleStatus(originalConfig[0],"message"); // flows.handleStatus(originalConfig[0],"message");
// flowCreate.flows['t1'].handleStatus.called.should.be.true(); // flowCreate.flows['t1'].handleStatus.called.should.be.true();
// done(); // done();
@ -477,7 +476,7 @@ describe('flows/index', function() {
// return when.resolve({flows:originalConfig}); // return when.resolve({flows:originalConfig});
// } // }
// //
// events.once('nodes-started',function() { // events.once('flows:started',function() {
// flows.handleStatus(originalConfig[0],"message"); // flows.handleStatus(originalConfig[0],"message");
// try { // try {
// flowCreate.flows['t1'].handleStatus.called.should.be.true(); // flowCreate.flows['t1'].handleStatus.called.should.be.true();

View File

@ -19,7 +19,7 @@ var sinon = require("sinon");
var when = require("when"); var when = require("when");
var clone = require("clone"); var clone = require("clone");
var NR_TEST_UTILS = require("nr-test-utils"); var NR_TEST_UTILS = require("nr-test-utils");
var flowUtil = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows/util"); var flowUtil = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/util");
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry"); var typeRegistry = NR_TEST_UTILS.require("@node-red/registry");
var redUtil = NR_TEST_UTILS.require("@node-red/util").util; var redUtil = NR_TEST_UTILS.require("@node-red/util").util;

View File

@ -0,0 +1,252 @@
const should = require("should");
const NR_TEST_UTILS = require("nr-test-utils");
const hooks = NR_TEST_UTILS.require("@node-red/runtime/lib/hooks");
describe("runtime/hooks", function() {
afterEach(function() {
hooks.clear();
})
it("allows a hook to be registered", function(done) {
let calledWith = null;
hooks.has("onSend").should.be.false();
hooks.add("onSend", function(payload) { calledWith = payload } )
hooks.has("onSend").should.be.true();
let data = { a: 1 };
hooks.trigger("onSend",data,err => {
calledWith.should.equal(data);
done(err);
})
})
it("rejects invalid hook id", function(done) {
try {
hooks.add("foo", function(payload) {})
done(new Error("Invalid hook accepted"))
} catch(err) {
done();
}
})
it("calls hooks in the order they were registered", function(done) {
hooks.add("onSend", function(payload) { payload.order.push("A") } )
hooks.add("onSend", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A","B"])
done(err);
})
})
it("does not allow multiple hooks with same id.label", function() {
hooks.has("onSend.one").should.be.false();
hooks.has("onSend").should.be.false();
hooks.add("onSend.one", function(payload) { payload.order.push("A") } );
hooks.has("onSend.one").should.be.true();
hooks.has("onSend").should.be.true();
(function() {
hooks.add("onSend.one", function(payload) { payload.order.push("B") } )
}).should.throw();
})
it("removes labelled hook", function(done) {
hooks.has("onSend.A").should.be.false();
hooks.has("onSend.B").should.be.false();
hooks.has("onSend").should.be.false();
hooks.add("onSend.A", function(payload) { payload.order.push("A") } )
hooks.has("onSend.A").should.be.true();
hooks.has("onSend.B").should.be.false();
hooks.has("onSend").should.be.true();
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
hooks.has("onSend.A").should.be.true();
hooks.has("onSend.B").should.be.true();
hooks.has("onSend").should.be.true();
hooks.remove("onSend.A");
hooks.has("onSend.A").should.be.false();
hooks.has("onSend.B").should.be.true();
hooks.has("onSend").should.be.true();
let data = { order:[] };
hooks.trigger("onSend",data,err => {
try {
data.order.should.eql(["B"])
hooks.remove("onSend.B");
hooks.has("onSend.A").should.be.false();
hooks.has("onSend.B").should.be.false();
hooks.has("onSend").should.be.false();
done(err);
} catch(err2) {
done(err2);
}
})
})
it("cannot remove unlabelled hook", function() {
hooks.add("onSend", function(payload) { payload.order.push("A") } );
(function() {
hooks.remove("onSend")
}).should.throw();
})
it("removes all hooks with same label", function(done) {
hooks.add("onSend.A", function(payload) { payload.order.push("A") } )
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
hooks.add("preRoute.A", function(payload) { payload.order.push("C") } )
hooks.add("preRoute.B", function(payload) { payload.order.push("D") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A","B"])
hooks.trigger("preRoute", data, err => {
data.order.should.eql(["A","B","C","D"])
data.order = [];
hooks.remove("*.A");
hooks.trigger("onSend",data,err => {
data.order.should.eql(["B"])
hooks.trigger("preRoute", data, err => {
data.order.should.eql(["B","D"])
})
done(err);
})
})
})
})
it("halts execution on return false", function(done) {
hooks.add("onSend.A", function(payload) { payload.order.push("A"); return false } )
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A"])
err.should.be.false();
done();
})
})
it("halts execution on thrown error", function(done) {
hooks.add("onSend.A", function(payload) { payload.order.push("A"); throw new Error("error") } )
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A"])
should.exist(err);
err.should.not.be.false()
done();
})
})
it("handler can use callback function", function(done) {
hooks.add("onSend.A", function(payload, done) {
setTimeout(function() {
payload.order.push("A")
done()
},30)
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A","B"])
done(err);
})
})
it("handler can use callback function - halt execution", function(done) {
hooks.add("onSend.A", function(payload, done) {
setTimeout(function() {
payload.order.push("A")
done(false)
},30)
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A"])
err.should.be.false()
done();
})
})
it("handler can use callback function - halt on error", function(done) {
hooks.add("onSend.A", function(payload, done) {
setTimeout(function() {
done(new Error("test error"))
},30)
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql([])
should.exist(err);
err.should.not.be.false()
done();
})
})
it("handler be an async function", function(done) {
hooks.add("onSend.A", async function(payload) {
return new Promise(resolve => {
setTimeout(function() {
payload.order.push("A")
resolve()
},30)
});
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A","B"])
done(err);
})
})
it("handler be an async function - halt execution", function(done) {
hooks.add("onSend.A", async function(payload) {
return new Promise(resolve => {
setTimeout(function() {
payload.order.push("A")
resolve(false)
},30)
});
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A"])
done(err);
})
})
it("handler be an async function - halt on error", function(done) {
hooks.add("onSend.A", async function(payload) {
return new Promise((resolve,reject) => {
setTimeout(function() {
reject(new Error("test error"))
},30)
});
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql([])
should.exist(err);
err.should.not.be.false()
done();
})
})
});

View File

@ -19,7 +19,8 @@ var sinon = require('sinon');
var NR_TEST_UTILS = require("nr-test-utils"); var NR_TEST_UTILS = require("nr-test-utils");
var RedNode = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node"); var RedNode = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
var Log = NR_TEST_UTILS.require("@node-red/util").log; var Log = NR_TEST_UTILS.require("@node-red/util").log;
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows"); var hooks = NR_TEST_UTILS.require("@node-red/runtime/lib/hooks");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
describe('Node', function() { describe('Node', function() {
describe('#constructor',function() { describe('#constructor',function() {
@ -181,6 +182,93 @@ describe('Node', function() {
}); });
n.receive(message); n.receive(message);
}); });
it('triggers onComplete hook when done callback provided', function(done) {
var handleCompleteCalled = false;
var hookCalled = false;
var n = new RedNode({id:'123',type:'abc', _flow: {
handleComplete: function(node,msg) {
handleCompleteCalled = true;
}
}});
var hookError;
hooks.add("onComplete",function(completeEvent) {
hookCalled = true;
try {
handleCompleteCalled.should.be.false("onComplete should be called before handleComplete")
should.not.exist(completeEvent.error);
completeEvent.msg.should.deepEqual(message);
completeEvent.node.id.should.eql("123");
completeEvent.node.node.should.equal(n);
} catch(err) {
hookError = err;
}
})
var message = {payload:"hello world"};
n.on('input',function(msg, nodeSend, nodeDone) {
nodeDone();
});
n.receive(message);
setTimeout(function() {
if (hookError) {
done(hookError);
return
}
try {
hookCalled.should.be.true("onComplete hook should be called");
handleCompleteCalled.should.be.true("handleComplete should be called");
done();
} catch(err) {
done(err);
}
})
});
it('triggers onComplete hook when done callback provided - with error', function(done) {
var handleCompleteCalled = false;
var hookCalled = false;
var errorReported = false;
var n = new RedNode({id:'123',type:'abc', _flow: {
handleComplete: function(node,msg) {
handleCompleteCalled = true;
}
}});
var hookError;
hooks.add("onComplete",function(completeEvent) {
hookCalled = true;
try {
handleCompleteCalled.should.be.false("onComplete should be called before handleComplete")
should.exist(completeEvent.error);
completeEvent.error.toString().should.equal("Error: test error")
completeEvent.msg.should.deepEqual(message);
completeEvent.node.id.should.eql("123");
completeEvent.node.node.should.equal(n);
} catch(err) {
hookError = err;
}
})
var message = {payload:"hello world"};
n.on('input',function(msg, nodeSend, nodeDone) {
nodeDone(new Error("test error"));
});
n.error = function(err,msg) {
errorReported = true;
}
n.receive(message);
setTimeout(function() {
if (hookError) {
done(hookError);
return
}
try {
hookCalled.should.be.true("onComplete hook should be called");
handleCompleteCalled.should.be.false("handleComplete should not be called");
done();
} catch(err) {
done(err);
}
})
});
it('logs error if callback provides error', function(done) { it('logs error if callback provides error', function(done) {
var n = new RedNode({id:'123',type:'abc'}); var n = new RedNode({id:'123',type:'abc'});
sinon.stub(n,"error",function(err,msg) {}); sinon.stub(n,"error",function(err,msg) {});
@ -201,151 +289,256 @@ describe('Node', function() {
}); });
n.receive(message); n.receive(message);
}); });
it("triggers hooks when receiving a message", function(done) {
var hookErrors = [];
var messageReceived = false;
var hooksCalled = [];
hooks.add("onReceive", function(receiveEvent) {
hooksCalled.push("onReceive")
try {
messageReceived.should.be.false("Message should not have been received before onReceive")
receiveEvent.msg.should.be.exactly(message);
receiveEvent.destination.id.should.equal("123")
receiveEvent.destination.node.should.equal(n)
} catch(err) {
hookErrors.push(err);
}
})
hooks.add("postReceive", function(receiveEvent) {
hooksCalled.push("postReceive")
try {
messageReceived.should.be.true("Message should have been received before postReceive")
receiveEvent.msg.should.be.exactly(message);
receiveEvent.destination.id.should.equal("123")
receiveEvent.destination.node.should.equal(n)
} catch(err) {
hookErrors.push(err);
}
})
var n = new RedNode({id:'123',type:'abc'});
var message = {payload:"hello world"};
n.on('input',function(msg) {
messageReceived = true;
try {
should.strictEqual(this,n);
hooksCalled.should.eql(["onReceive"])
should.deepEqual(msg,message);
} catch(err) {
hookErrors.push(err)
}
});
n.receive(message);
setTimeout(function() {
hooks.clear();
if (hookErrors.length > 0) {
done(hookErrors[0])
} else {
done();
}
},10);
});
describe("errors thrown by hooks are reported", function() {
before(function() {
hooks.add("onReceive",function(recEvent) {
if (recEvent.msg.payload === "trigger-onReceive") {
throw new Error("onReceive Error")
}
})
hooks.add("postReceive",function(recEvent) {
if (recEvent.msg.payload === "trigger-postReceive") {
throw new Error("postReceive Error")
}
})
})
after(function() {
hooks.clear();
})
function testHook(hook, msgExpected, done) {
var messageReceived = false;
var errorReceived;
var n = new RedNode({id:'123',type:'abc'});
var message = {payload:"trigger-"+hook};
n.on('input',function(msg) {
messageReceived = true;
});
n.error = function (err) {
errorReceived = err;
}
n.receive(message);
setTimeout(function() {
try {
messageReceived.should.equal(msgExpected,`Hook ${hook} messageReceived expected ${msgExpected} actual ${messageReceived}`);
should.exist(errorReceived);
errorReceived.toString().should.containEql(hook)
done()
} catch(err) {
done(err);
}
},10);
}
it("onReceive", function(done) { testHook("onReceive", false, done)})
it("postReceive", function(done) { testHook("postReceive", true, done)})
})
}); });
describe("hooks can halt receive", function() {
before(function() {
hooks.add("onReceive",function(recEvent) {
if (recEvent.msg.payload === "trigger-onReceive") {
return false;
}
})
})
after(function() {
hooks.clear();
})
function testHook(hook, msgExpected, done) {
var messageReceived = false;
var errorReceived;
var n = new RedNode({id:'123',type:'abc'});
var message = {payload:"trigger-"+hook};
n.on('input',function(msg) {
messageReceived = true;
});
n.error = function (err) {
errorReceived = err;
}
n.receive(message);
setTimeout(function() {
try {
messageReceived.should.equal(msgExpected,`Hook ${hook} messageReceived expected ${msgExpected} actual ${messageReceived}`);
should.not.exist(errorReceived);
done()
} catch(err) {
done(err);
}
},10);
}
it("onReceive", function(done) { testHook("onReceive", false, done)})
})
describe('#send', function() { describe('#send', function() {
it('emits a single message', function(done) { it('emits a single message', function(done) {
var flow = { var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]}, send: (sendEvents) => {
try {
sendEvents.should.have.length(1);
sendEvents[0].msg.should.equal(message);
sendEvents[0].destination.should.eql({id:"n2", node: undefined});
sendEvents[0].source.should.eql({id:"n1", node: n1, port: 0})
done();
} catch(err) {
done(err);
}
},
}; };
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]}); var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var message = {payload:"hello world"}; var message = {payload:"hello world"};
var messageReceived = false;
n2.on('input',function(msg) {
// msg equals message, and is not a new copy
messageReceived = true;
should.strictEqual(this,n2);
should.deepEqual(msg,message);
should.strictEqual(msg,message);
done();
});
n1.send(message); n1.send(message);
messageReceived.should.be.false();
});
it('emits a single message - multiple input event listeners', function(done) {
var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var message = {payload:"hello world"};
var messageReceived = 0;
n2.on('input',function(msg) {
// msg equals message, and is not a new copy
messageReceived++;
messageReceived.should.be.exactly(1);
should.strictEqual(this,n2);
should.deepEqual(msg,message);
should.strictEqual(msg,message);
});
n2.on('input',function(msg) {
messageReceived++;
messageReceived.should.be.exactly(2);
should.strictEqual(this,n2);
should.deepEqual(msg,message);
should.strictEqual(msg,message);
done();
});
n1.send(message);
messageReceived.should.be.exactly(0);
});
it('emits a single message - synchronous mode', function(done) {
var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
asyncMessageDelivery: false
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var message = {payload:"hello world"};
var messageReceived = false;
var notSyncErr;
n2.on('input',function(msg) {
try {
// msg equals message, and is not a new copy
messageReceived = true;
should.deepEqual(msg,message);
should.strictEqual(msg,message);
done(notSyncErr);
} catch(err) {
done(err);
}
});
n1.send(message);
try {
messageReceived.should.be.true();
} catch(err) {
notSyncErr = err;
}
}); });
it('emits a message with callback provided send', function(done) { it('emits a message with callback provided send', function(done) {
var flow = { var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]}, handleError: (node,logMessage,msg,reportingNode) => {done(logMessage)},
handleComplete: (node,msg) => {} handleComplete: (node,msg) => {},
send: (sendEvents) => {
try {
sendEvents.should.have.length(1);
sendEvents[0].msg.should.equal(message);
sendEvents[0].destination.should.eql({id:"n2", node: undefined});
sendEvents[0].source.should.eql({id:"n1", node: n1, port: 0});
sendEvents[0].cloneMessage.should.be.false();
done();
} catch(err) {
done(err);
}
},
}; };
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]}); var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var message = {payload:"hello world"}; var message = {payload:"hello world"};
var messageReceived = false;
n1.on('input',function(msg,nodeSend,nodeDone) { n1.on('input',function(msg,nodeSend,nodeDone) {
nodeSend(msg); nodeSend(msg);
nodeDone(); nodeDone();
}); });
n2.on('input',function(msg) {
// msg equals message, and is not a new copy
messageReceived = true;
should.deepEqual(msg,message);
should.strictEqual(msg,message);
done();
});
n1.receive(message); n1.receive(message);
messageReceived.should.be.false();
}); });
it('emits multiple messages on a single output', function(done) { it('emits multiple messages on a single output', function(done) {
var flow = { var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]}, handleError: (node,logMessage,msg,reportingNode) => {done(logMessage)},
send: (sendEvents) => {
try {
sendEvents.should.have.length(2);
sendEvents[0].msg.should.equal(messages[0]);
sendEvents[0].destination.should.eql({id:"n2", node: undefined});
sendEvents[0].source.should.eql({id:"n1", node: n1, port: 0});
sendEvents[0].cloneMessage.should.be.false();
sendEvents[1].msg.should.equal(messages[1]);
sendEvents[1].destination.should.eql({id:"n2", node: undefined});
sendEvents[1].source.should.eql({id:"n1", node: n1, port: 0});
sendEvents[1].cloneMessage.should.be.true();
done();
} catch(err) {
done(err);
}
},
}; };
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]}); var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var messages = [ var messages = [
{payload:"hello world"}, {payload:"hello world"},
{payload:"hello world again"} {payload:"hello world again"}
]; ];
var rcvdCount = 0;
n2.on('input',function(msg) {
if (rcvdCount === 0) {
// first msg sent, don't clone
should.deepEqual(msg,messages[rcvdCount]);
should.strictEqual(msg,messages[rcvdCount]);
rcvdCount += 1;
} else {
// second msg sent, clone
msg.payload.should.equal(messages[rcvdCount].payload);
should.notStrictEqual(msg,messages[rcvdCount]);
done();
}
});
n1.send([messages]); n1.send([messages]);
}); });
it('emits messages to multiple outputs', function(done) { it('emits messages to multiple outputs', function(done) {
var flow = { var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2,'n3':n3,'n4':n4,'n5':n5}[id]}, handleError: (node,logMessage,msg,reportingNode) => {done(logMessage)},
send: (sendEvents) => {
try {
sendEvents.should.have.length(3);
sendEvents[0].msg.should.equal(messages[0]);
sendEvents[0].destination.should.eql({id:"n2", node: undefined});
sendEvents[0].source.should.eql({id:"n1", node: n1, port: 0});
sendEvents[0].cloneMessage.should.be.false();
should.exist(sendEvents[0].msg._msgid);
sendEvents[1].msg.should.equal(messages[2]);
sendEvents[1].destination.should.eql({id:"n4", node: undefined});
sendEvents[1].source.should.eql({id:"n1", node: n1, port: 2})
sendEvents[1].cloneMessage.should.be.true();
should.exist(sendEvents[1].msg._msgid);
sendEvents[2].msg.should.equal(messages[2]);
sendEvents[2].destination.should.eql({id:"n5", node: undefined});
sendEvents[2].source.should.eql({id:"n1", node: n1, port: 2})
sendEvents[2].cloneMessage.should.be.true();
should.exist(sendEvents[2].msg._msgid);
sendEvents[0].msg._msgid.should.eql(sendEvents[1].msg._msgid)
sendEvents[1].msg._msgid.should.eql(sendEvents[2].msg._msgid)
done();
} catch(err) {
done(err);
}
}
}; };
var n1 = new RedNode({_flow:flow, id:'n1',type:'abc',wires:[['n2'],['n3'],['n4','n5']]}); var n1 = new RedNode({_flow:flow, id:'n1',type:'abc',wires:[['n2'],['n3'],['n4','n5']]});
var n2 = new RedNode({_flow:flow, id:'n2',type:'abc'}); var n2 = new RedNode({_flow:flow, id:'n2',type:'abc'});
var n3 = new RedNode({_flow:flow, id:'n3',type:'abc'}); var n3 = new RedNode({_flow:flow, id:'n3',type:'abc'});
var n4 = new RedNode({_flow:flow, id:'n4',type:'abc'}); var n4 = new RedNode({_flow:flow, id:'n4',type:'abc'});
var n5 = new RedNode({_flow:flow, id:'n5',type:'abc'}); var n5 = new RedNode({_flow:flow, id:'n5',type:'abc'});
var messages = [ var messages = [
{payload:"hello world"}, {payload:"hello world"},
null, null,
@ -354,48 +547,12 @@ describe('Node', function() {
var rcvdCount = 0; var rcvdCount = 0;
// first message sent, don't clone
// message uuids should match
n2.on('input',function(msg) {
should.deepEqual(msg,messages[0]);
should.strictEqual(msg,messages[0]);
rcvdCount += 1;
if (rcvdCount == 3) {
done();
}
});
n3.on('input',function(msg) {
should.fail(null,null,"unexpected message");
});
// second message sent, clone
// message uuids wont match since we've cloned
n4.on('input',function(msg) {
msg.payload.should.equal(messages[2].payload);
should.notStrictEqual(msg,messages[2]);
rcvdCount += 1;
if (rcvdCount == 3) {
done();
}
});
// third message sent, clone
// message uuids wont match since we've cloned
n5.on('input',function(msg) {
msg.payload.should.equal(messages[2].payload);
should.notStrictEqual(msg,messages[2]);
rcvdCount += 1;
if (rcvdCount == 3) {
done();
}
});
n1.send(messages); n1.send(messages);
}); });
it('emits no messages', function(done) { it('emits no messages', function(done) {
var flow = { var flow = {
handleError: (node,logMessage,msg,reportingNode) => {done(logMessage)},
getNode: (id) => { return {'n1':n1,'n2':n2}[id]}, getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
}; };
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]}); var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
@ -412,105 +569,85 @@ describe('Node', function() {
n1.send(); n1.send();
}); });
it('emits messages ignoring non-existent nodes', function(done) { // it('emits messages without cloning req or res', function(done) {
var flow = { // var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]}, // getNode: (id) => { return {'n1':n1,'n2':n2,'n3':n3}[id]},
}; // send: (node,dst,msg) => { setImmediate(function() { flow.getNode(dst).receive(msg) })}
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n9'],['n2']]}); // };
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'}); // var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[[['n2'],['n3']]]});
// var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
// var n3 = new RedNode({_flow:flow,id:'n3',type:'abc'});
//
// var req = {};
// var res = {};
// var cloned = {};
// var message = {payload: "foo", cloned: cloned, req: req, res: res};
//
// var rcvdCount = 0;
//
// // first message to be sent, so should not be cloned
// n2.on('input',function(msg) {
// should.deepEqual(msg, message);
// msg.cloned.should.be.exactly(message.cloned);
// msg.req.should.be.exactly(message.req);
// msg.res.should.be.exactly(message.res);
// rcvdCount += 1;
// if (rcvdCount == 2) {
// done();
// }
// });
//
// // second message to be sent, so should be cloned
// // message uuids wont match since we've cloned
// n3.on('input',function(msg) {
// msg.payload.should.equal(message.payload);
// msg.cloned.should.not.be.exactly(message.cloned);
// msg.req.should.be.exactly(message.req);
// msg.res.should.be.exactly(message.res);
// rcvdCount += 1;
// if (rcvdCount == 2) {
// done();
// }
// });
//
// n1.send(message);
// });
var messages = [ // it("logs the uuid for all messages sent", function(done) {
{payload:"hello world"}, // var logHandler = {
{payload:"hello world again"} // msgIds:[],
]; // messagesSent: 0,
// emit: function(event, msg) {
// only one message sent, so no copy needed // if (msg.event == "node.abc.send" && msg.level == Log.METRIC) {
n2.on('input',function(msg) { // this.messagesSent++;
should.deepEqual(msg,messages[1]); // this.msgIds.push(msg.msgid);
should.strictEqual(msg,messages[1]); // (typeof msg.msgid).should.not.be.equal("undefined");
done(); // }
}); // }
// };
n1.send(messages); //
}); // Log.addHandler(logHandler);
// var flow = {
it('emits messages without cloning req or res', function(done) { // getNode: (id) => { return {'n1':sender,'n2':receiver1,'n3':receiver2}[id]},
var flow = { // send: (node,dst,msg) => { setImmediate(function() { flow.getNode(dst).receive(msg) })}
getNode: (id) => { return {'n1':n1,'n2':n2,'n3':n3}[id]}, // };
}; //
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[[['n2'],['n3']]]}); // var sender = new RedNode({_flow:flow,id:'n1',type:'abc', wires:[['n2', 'n3']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'}); // var receiver1 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var n3 = new RedNode({_flow:flow,id:'n3',type:'abc'}); // var receiver2 = new RedNode({_flow:flow,id:'n3',type:'abc'});
// sender.send({"some": "message"});
var req = {}; // setTimeout(function() {
var res = {}; // try {
var cloned = {}; // logHandler.messagesSent.should.equal(1);
var message = {payload: "foo", cloned: cloned, req: req, res: res}; // should.exist(logHandler.msgIds[0])
// Log.removeHandler(logHandler);
var rcvdCount = 0; // done();
// } catch(err) {
// first message to be sent, so should not be cloned // Log.removeHandler(logHandler);
n2.on('input',function(msg) { // done(err);
should.deepEqual(msg, message); // }
msg.cloned.should.be.exactly(message.cloned); // },50)
msg.req.should.be.exactly(message.req); // })
msg.res.should.be.exactly(message.res);
rcvdCount += 1;
if (rcvdCount == 2) {
done();
}
});
// second message to be sent, so should be cloned
// message uuids wont match since we've cloned
n3.on('input',function(msg) {
msg.payload.should.equal(message.payload);
msg.cloned.should.not.be.exactly(message.cloned);
msg.req.should.be.exactly(message.req);
msg.res.should.be.exactly(message.res);
rcvdCount += 1;
if (rcvdCount == 2) {
done();
}
});
n1.send(message);
});
it("logs the uuid for all messages sent", function(done) {
var logHandler = {
msgIds:[],
messagesSent: 0,
emit: function(event, msg) {
if (msg.event == "node.abc.send" && msg.level == Log.METRIC) {
this.messagesSent++;
this.msgIds.push(msg.msgid);
(typeof msg.msgid).should.not.be.equal("undefined");
}
}
};
Log.addHandler(logHandler);
var flow = {
getNode: (id) => { return {'n1':sender,'n2':receiver1,'n3':receiver2}[id]},
};
var sender = new RedNode({_flow:flow,id:'n1',type:'abc', wires:[['n2', 'n3']]});
var receiver1 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var receiver2 = new RedNode({_flow:flow,id:'n3',type:'abc'});
sender.send({"some": "message"});
setTimeout(function() {
try {
logHandler.messagesSent.should.equal(1);
should.exist(logHandler.msgIds[0])
Log.removeHandler(logHandler);
done();
} catch(err) {
Log.removeHandler(logHandler);
done(err);
}
},50)
})
}); });

View File

@ -23,7 +23,7 @@ var inherits = require("util").inherits;
var NR_TEST_UTILS = require("nr-test-utils"); var NR_TEST_UTILS = require("nr-test-utils");
var index = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/index"); var index = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/index");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows"); var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
var registry = NR_TEST_UTILS.require("@node-red/registry") var registry = NR_TEST_UTILS.require("@node-red/registry")
var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node"); var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");