mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
Add RED.hooks engine
This commit is contained in:
parent
d57ec0cd53
commit
bdd736315a
@ -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'
|
||||||
|
@ -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,
|
||||||
|
@ -24,7 +24,6 @@ var deprecated = typeRegistry.deprecated;
|
|||||||
|
|
||||||
var context = require("../nodes/context")
|
var context = require("../nodes/context")
|
||||||
var credentials = require("../nodes/credentials");
|
var credentials = require("../nodes/credentials");
|
||||||
var router = require("./router");
|
|
||||||
var flowUtil = require("./util");
|
var flowUtil = require("./util");
|
||||||
var log;
|
var log;
|
||||||
var events = require("../events");
|
var events = require("../events");
|
||||||
@ -70,7 +69,6 @@ function init(runtime) {
|
|||||||
}
|
}
|
||||||
Flow.init(runtime);
|
Flow.init(runtime);
|
||||||
flowUtil.init(runtime);
|
flowUtil.init(runtime);
|
||||||
router.init(runtime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadFlows() {
|
function loadFlows() {
|
||||||
|
165
packages/node_modules/@node-red/runtime/lib/hooks.js
vendored
Normal file
165
packages/node_modules/@node-red/runtime/lib/hooks.js
vendored
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
const Log = require("@node-red/util").log;
|
||||||
|
|
||||||
|
// Flags for what hooks have handlers registered
|
||||||
|
let states = { }
|
||||||
|
|
||||||
|
// Hooks by id
|
||||||
|
let hooks = { }
|
||||||
|
|
||||||
|
// Hooks by label
|
||||||
|
let labelledHooks = { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime hooks engine
|
||||||
|
* @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 (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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add("preSend", function(sendEvents) {
|
||||||
|
// console.log("preSend",JSON.stringify(sendEvents));
|
||||||
|
// })
|
||||||
|
// add("preRoute", function(sendEvent) {
|
||||||
|
// console.log("preRoute",JSON.stringify(sendEvent.msg));
|
||||||
|
// })
|
||||||
|
// add("onSend", function(sendEvent) {
|
||||||
|
// console.log("onSend",JSON.stringify(sendEvent.msg));
|
||||||
|
// })
|
||||||
|
// add("postSend", function(sendEvent) {
|
||||||
|
// console.log("postSend",JSON.stringify(sendEvent.msg));
|
||||||
|
// })
|
||||||
|
// add("onReceive", function(recEvent) {
|
||||||
|
// console.log("onReceive",recEvent.destination.id,JSON.stringify(recEvent.msg))
|
||||||
|
// })
|
||||||
|
// add("postReceive", function(recEvent) {
|
||||||
|
// console.log("postReceive",recEvent.destination.id,JSON.stringify(recEvent.msg))
|
||||||
|
// })
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
hooks = {}
|
||||||
|
labelledHooks = {}
|
||||||
|
states = {}
|
||||||
|
}
|
||||||
|
module.exports = {
|
||||||
|
get states() { return states },
|
||||||
|
clear,
|
||||||
|
add,
|
||||||
|
remove,
|
||||||
|
trigger
|
||||||
|
}
|
@ -23,6 +23,7 @@ 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");
|
||||||
|
|
||||||
@ -273,6 +274,7 @@ var runtime = {
|
|||||||
settings: settings,
|
settings: settings,
|
||||||
storage: storage,
|
storage: storage,
|
||||||
events: events,
|
events: events,
|
||||||
|
hooks: hooks,
|
||||||
nodes: redNodes,
|
nodes: redNodes,
|
||||||
flows: flows,
|
flows: flows,
|
||||||
library: library,
|
library: library,
|
||||||
@ -357,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 },
|
||||||
|
8
packages/node_modules/node-red/lib/red.js
vendored
8
packages/node_modules/node-red/lib/red.js
vendored
@ -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.
|
||||||
|
221
test/unit/@node-red/runtime/lib/hooks_spec.js
Normal file
221
test/unit/@node-red/runtime/lib/hooks_spec.js
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
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;
|
||||||
|
should.not.exist(hooks.states.foo);
|
||||||
|
hooks.add("foo", function(payload) { calledWith = payload } )
|
||||||
|
hooks.states.foo.should.be.true();
|
||||||
|
let data = { a: 1 };
|
||||||
|
hooks.trigger("foo",data,err => {
|
||||||
|
calledWith.should.equal(data);
|
||||||
|
done(err);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls hooks in the order they were registered", function(done) {
|
||||||
|
hooks.add("foo", function(payload) { payload.order.push("A") } )
|
||||||
|
hooks.add("foo", function(payload) { payload.order.push("B") } )
|
||||||
|
let data = { order:[] };
|
||||||
|
hooks.trigger("foo",data,err => {
|
||||||
|
data.order.should.eql(["A","B"])
|
||||||
|
done(err);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not allow multiple hooks with same id.label", function() {
|
||||||
|
hooks.add("foo.one", function(payload) { payload.order.push("A") } );
|
||||||
|
(function() {
|
||||||
|
hooks.add("foo.one", function(payload) { payload.order.push("B") } )
|
||||||
|
}).should.throw();
|
||||||
|
})
|
||||||
|
|
||||||
|
it("removes labelled hook", function(done) {
|
||||||
|
hooks.add("foo.A", function(payload) { payload.order.push("A") } )
|
||||||
|
hooks.add("foo.B", function(payload) { payload.order.push("B") } )
|
||||||
|
|
||||||
|
hooks.remove("foo.A");
|
||||||
|
hooks.states.foo.should.be.true();
|
||||||
|
|
||||||
|
let data = { order:[] };
|
||||||
|
hooks.trigger("foo",data,err => {
|
||||||
|
try {
|
||||||
|
data.order.should.eql(["B"])
|
||||||
|
|
||||||
|
hooks.remove("foo.B");
|
||||||
|
should.not.exist(hooks.states.foo);
|
||||||
|
|
||||||
|
done(err);
|
||||||
|
} catch(err2) {
|
||||||
|
done(err2);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("cannot remove unlabelled hook", function() {
|
||||||
|
hooks.add("foo", function(payload) { payload.order.push("A") } );
|
||||||
|
(function() {
|
||||||
|
hooks.remove("foo")
|
||||||
|
}).should.throw();
|
||||||
|
})
|
||||||
|
it("removes all hooks with same label", function(done) {
|
||||||
|
hooks.add("foo.A", function(payload) { payload.order.push("A") } )
|
||||||
|
hooks.add("foo.B", function(payload) { payload.order.push("B") } )
|
||||||
|
hooks.add("bar.A", function(payload) { payload.order.push("C") } )
|
||||||
|
hooks.add("bar.B", function(payload) { payload.order.push("D") } )
|
||||||
|
|
||||||
|
let data = { order:[] };
|
||||||
|
hooks.trigger("foo",data,err => {
|
||||||
|
data.order.should.eql(["A","B"])
|
||||||
|
hooks.trigger("bar", data, err => {
|
||||||
|
data.order.should.eql(["A","B","C","D"])
|
||||||
|
|
||||||
|
data.order = [];
|
||||||
|
|
||||||
|
hooks.remove("*.A");
|
||||||
|
|
||||||
|
hooks.trigger("foo",data,err => {
|
||||||
|
data.order.should.eql(["B"])
|
||||||
|
hooks.trigger("bar", data, err => {
|
||||||
|
data.order.should.eql(["B","D"])
|
||||||
|
})
|
||||||
|
done(err);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it("halts execution on return false", function(done) {
|
||||||
|
hooks.add("foo.A", function(payload) { payload.order.push("A"); return false } )
|
||||||
|
hooks.add("foo.B", function(payload) { payload.order.push("B") } )
|
||||||
|
|
||||||
|
let data = { order:[] };
|
||||||
|
hooks.trigger("foo",data,err => {
|
||||||
|
data.order.should.eql(["A"])
|
||||||
|
err.should.be.false();
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it("halts execution on thrown error", function(done) {
|
||||||
|
hooks.add("foo.A", function(payload) { payload.order.push("A"); throw new Error("error") } )
|
||||||
|
hooks.add("foo.B", function(payload) { payload.order.push("B") } )
|
||||||
|
|
||||||
|
let data = { order:[] };
|
||||||
|
hooks.trigger("foo",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("foo.A", function(payload, done) {
|
||||||
|
setTimeout(function() {
|
||||||
|
payload.order.push("A")
|
||||||
|
done()
|
||||||
|
},30)
|
||||||
|
})
|
||||||
|
hooks.add("foo.B", function(payload) { payload.order.push("B") } )
|
||||||
|
|
||||||
|
let data = { order:[] };
|
||||||
|
hooks.trigger("foo",data,err => {
|
||||||
|
data.order.should.eql(["A","B"])
|
||||||
|
done(err);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handler can use callback function - halt execution", function(done) {
|
||||||
|
hooks.add("foo.A", function(payload, done) {
|
||||||
|
setTimeout(function() {
|
||||||
|
payload.order.push("A")
|
||||||
|
done(false)
|
||||||
|
},30)
|
||||||
|
})
|
||||||
|
hooks.add("foo.B", function(payload) { payload.order.push("B") } )
|
||||||
|
|
||||||
|
let data = { order:[] };
|
||||||
|
hooks.trigger("foo",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("foo.A", function(payload, done) {
|
||||||
|
setTimeout(function() {
|
||||||
|
done(new Error("test error"))
|
||||||
|
},30)
|
||||||
|
})
|
||||||
|
hooks.add("foo.B", function(payload) { payload.order.push("B") } )
|
||||||
|
|
||||||
|
let data = { order:[] };
|
||||||
|
hooks.trigger("foo",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("foo.A", async function(payload) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(function() {
|
||||||
|
payload.order.push("A")
|
||||||
|
resolve()
|
||||||
|
},30)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
hooks.add("foo.B", function(payload) { payload.order.push("B") } )
|
||||||
|
|
||||||
|
let data = { order:[] };
|
||||||
|
hooks.trigger("foo",data,err => {
|
||||||
|
data.order.should.eql(["A","B"])
|
||||||
|
done(err);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handler be an async function - halt execution", function(done) {
|
||||||
|
hooks.add("foo.A", async function(payload) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(function() {
|
||||||
|
payload.order.push("A")
|
||||||
|
resolve(false)
|
||||||
|
},30)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
hooks.add("foo.B", function(payload) { payload.order.push("B") } )
|
||||||
|
|
||||||
|
let data = { order:[] };
|
||||||
|
hooks.trigger("foo",data,err => {
|
||||||
|
data.order.should.eql(["A"])
|
||||||
|
done(err);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it("handler be an async function - halt on error", function(done) {
|
||||||
|
hooks.add("foo.A", async function(payload) {
|
||||||
|
return new Promise((resolve,reject) => {
|
||||||
|
setTimeout(function() {
|
||||||
|
reject(new Error("test error"))
|
||||||
|
},30)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
hooks.add("foo.B", function(payload) { payload.order.push("B") } )
|
||||||
|
|
||||||
|
let data = { order:[] };
|
||||||
|
hooks.trigger("foo",data,err => {
|
||||||
|
data.order.should.eql([])
|
||||||
|
should.exist(err);
|
||||||
|
err.should.not.be.false()
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user