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
22 changed files with 1576 additions and 560 deletions

View File

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

View File

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

View File

@@ -17,8 +17,9 @@
var clone = require("clone");
var redUtil = require("@node-red/util").util;
var flowUtil = require("./util");
var events = require("../../events");
const context = require('../context');
var events = require("../events");
const context = require('../nodes/context');
const hooks = require("../hooks");
var Subflow;
var Log;
@@ -539,8 +540,25 @@ class Flow {
}
}
get asyncMessageDelivery() {
return asyncMessageDelivery
send(sendEvents) {
// 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() {
@@ -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 = {
init: function(runtime) {
nodeCloseTimeout = runtime.settings.nodeCloseTimeout || 15000;

View File

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

View File

@@ -15,21 +15,19 @@
**/
var clone = require("clone");
var when = require("when");
var Flow = require('./Flow');
var typeRegistry = require("@node-red/registry");
var deprecated = typeRegistry.deprecated;
var context = require("../context")
var credentials = require("../credentials");
var context = require("../nodes/context")
var credentials = require("../nodes/credentials");
var flowUtil = require("./util");
var log;
var events = require("../../events");
var events = require("../events");
var redUtil = require("@node-red/util").util;
const hooks = require("../hooks");
var storage = 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;
if (type === "full") {
// 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");
if (credentialsPendingReset === true) {
@@ -401,6 +403,8 @@ function stop(type,diff,muteLog) {
stopList = diff.changed.concat(diff.removed).concat(diff.linked);
}
events.emit("flows:stopping",{config: activeConfig, type: type, diff: diff})
for (var id in activeFlows) {
if (activeFlows.hasOwnProperty(id)) {
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"));
}
}
events.emit("flows:stopped",{config: activeConfig, type: type, diff: diff});
// Deprecated event
events.emit("nodes-stopped");
});
}
@@ -481,7 +487,7 @@ function updateMissingTypes() {
}
}
function addFlow(flow, user) {
async function addFlow(flow, user) {
var i,node;
if (!flow.hasOwnProperty('nodes')) {
throw new Error('missing nodes property');
@@ -506,10 +512,10 @@ function addFlow(flow, user) {
node = flow.nodes[i];
if (activeFlowConfig.allNodes[node.id]) {
// TODO nls
return when.reject(new Error('duplicate id'));
throw new Error('duplicate id');
}
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;
nodes.push(node);
@@ -519,10 +525,10 @@ function addFlow(flow, user) {
node = flow.configs[i];
if (activeFlowConfig.allNodes[node.id]) {
// TODO nls
return when.reject(new Error('duplicate id'));
throw new Error('duplicate id');
}
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;
nodes.push(node);
@@ -607,7 +613,7 @@ function getFlow(id) {
return result;
}
function updateFlow(id,newFlow, user) {
async function updateFlow(id,newFlow, user) {
var label = id;
if (id !== 'global') {
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') {
// TODO: nls + error code
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 redNodes = require("./nodes");
var flows = require("./flows");
var storage = require("./storage");
var library = require("./library");
var events = require("./events");
var hooks = require("./hooks");
var settings = require("./settings");
var exec = require("./exec");
@@ -272,7 +274,9 @@ var runtime = {
settings: settings,
storage: storage,
events: events,
hooks: hooks,
nodes: redNodes,
flows: flows,
library: library,
exec: exec,
util: require("@node-red/util").util,
@@ -355,6 +359,7 @@ module.exports = {
storage: storage,
events: events,
hooks: hooks,
util: require("@node-red/util").util,
get httpNode() { return nodeApp },
get httpAdmin() { return adminApp },

View File

@@ -20,7 +20,9 @@ var EventEmitter = require("events").EventEmitter;
var redUtil = require("@node-red/util").util;
var Log = require("@node-red/util").log;
var context = require("./context");
var flows = require("./flows");
var flows = require("../flows");
const hooks = require("../hooks");
const NOOP_SEND = function() {}
@@ -55,10 +57,6 @@ function Node(n) {
// as part of its constructor - config._flow will overwrite this._flow
// which we can tolerate as they are the same object.
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);
}
@@ -125,6 +123,11 @@ Node.prototype.context = function() {
*/
Node.prototype._complete = function(msg,error) {
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) {
// For now, delegate this to this.error
// 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) {
var node = this;
if (event === "input") {
// When Pluggable Message Routing arrives, this will be called from
// 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);
}
this._emitInput.apply(this,args);
} else {
this._emit.apply(this,arguments);
}
@@ -195,42 +190,53 @@ Node.prototype.emit = function(event, ...args) {
Node.prototype._emitInput = function(arg) {
var node = this;
this.metric("receive", arg);
if (node._inputCallback) {
// Just one callback registered.
try {
node._inputCallback(
arg,
function() { node.send.apply(node,arguments) },
function(err) { node._complete(arg,err); }
);
} catch(err) {
node.error(err,arg);
}
} else if (node._inputCallbacks) {
// Multiple callbacks registered. Call each one, tracking eventual completion
var c = node._inputCallbacks.length;
for (var i=0;i<c;i++) {
var cb = node._inputCallbacks[i];
if (cb.length === 2) {
c++;
}
try {
cb.call(
node,
arg,
function() { node.send.apply(node,arguments) },
function(err) {
c--;
if (c === 0) {
node._complete(arg,err);
}
let receiveEvent = { msg:arg, destination: { id: this.id, node: this } }
// onReceive - a node is about to receive a message
hooks.trigger("onReceive",receiveEvent,(err) => {
if (err) {
node.error(err);
return
} else if (err !== false) {
if (node._inputCallback) {
// Just one callback registered.
try {
node._inputCallback(
arg,
function() { node.send.apply(node,arguments) },
function(err) { node._complete(arg,err); }
);
} catch(err) {
node.error(err,arg);
}
} else if (node._inputCallbacks) {
// Multiple callbacks registered. Call each one, tracking eventual completion
var c = node._inputCallbacks.length;
for (var i=0;i<c;i++) {
var cb = node._inputCallbacks[i];
if (cb.length === 2) {
c++;
}
);
} catch(err) {
node.error(err,arg);
try {
cb.call(
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();
}
this.metric("send",msg);
node = this._flow.getNode(this._wire);
/* istanbul ignore else */
if (node) {
node.receive(msg);
}
this._flow.send([{
msg: msg,
source: {
id: this.id,
node: this,
port: 0
},
destination: {
id: this._wire,
node: undefined
},
cloneMessage: false
}]);
return;
} else {
msg = [msg];
@@ -384,7 +398,7 @@ Node.prototype.send = function(msg) {
var sendEvents = [];
var sentMessageId = null;
var hasMissingIds = false;
// for each output of node eg. [msgs to output 0, msgs to output 1, ...]
for (var i = 0; i < numOutputs; i++) {
var wires = this.wires[i]; // wires leaving output i
@@ -398,24 +412,31 @@ Node.prototype.send = function(msg) {
var k = 0;
// for each recipent node of that output
for (var j = 0; j < wires.length; j++) {
node = this._flow.getNode(wires[j]); // node at end of wire j
if (node) {
// for each msg to send eg. [[m1, m2, ...], ...]
for (k = 0; k < msgs.length; k++) {
var m = msgs[k];
if (m !== null && m !== undefined) {
/* 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;
}
// for each msg to send eg. [[m1, m2, ...], ...]
for (k = 0; k < msgs.length; k++) {
var m = msgs[k];
if (m !== null && m !== undefined) {
if (!m._msgid) {
hasMissingIds = 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});
for (i=0;i<sendEvents.length;i++) {
var ev = sendEvents[i];
/* istanbul ignore else */
if (!ev.m._msgid) {
ev.m._msgid = sentMessageId;
if (hasMissingIds) {
for (i=0;i<sendEvents.length;i++) {
var ev = sendEvents[i];
/* istanbul ignore else */
if (!ev.msg._msgid) {
ev.msg._msgid = sentMessageId;
}
}
ev.n.receive(ev.m);
}
this._flow.send(sendEvents);
};
/**
* Receive a 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) {
if (!msg) {

View File

@@ -18,7 +18,6 @@ var clone = require("clone");
var log = require("@node-red/util").log;
var util = require("@node-red/util").util;
var memory = require("./memory");
var flows;
var settings;
@@ -48,7 +47,6 @@ function logUnknownStore(name) {
}
function init(_settings) {
flows = require("../flows");
settings = _settings;
contexts = {};
stores = {};
@@ -513,39 +511,6 @@ function getContext(nodeId, flowId) {
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) {
if(!hasConfiguredStore){
// 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 credentials = require("./credentials");
var flows = require("./flows");
var flowUtil = require("./flows/util")
var flows = require("../flows");
var flowUtil = require("../flows/util")
var context = require("./context");
var Node = require("./Node");
var log;

View File

@@ -145,6 +145,14 @@ module.exports = {
*/
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
* runtime.