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/api/*.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/editor-api/lib/index.js',
|
||||
'packages/node_modules/@node-red/editor-api/lib/auth/index.js'
|
||||
|
@ -57,6 +57,7 @@ function createNodeApi(node) {
|
||||
log: {},
|
||||
settings: {},
|
||||
events: runtime.events,
|
||||
hooks: runtime.hooks,
|
||||
util: runtime.util,
|
||||
version: runtime.version,
|
||||
require: requireModule,
|
||||
|
@ -24,7 +24,6 @@ var deprecated = typeRegistry.deprecated;
|
||||
|
||||
var context = require("../nodes/context")
|
||||
var credentials = require("../nodes/credentials");
|
||||
var router = require("./router");
|
||||
var flowUtil = require("./util");
|
||||
var log;
|
||||
var events = require("../events");
|
||||
@ -70,7 +69,6 @@ function init(runtime) {
|
||||
}
|
||||
Flow.init(runtime);
|
||||
flowUtil.init(runtime);
|
||||
router.init(runtime);
|
||||
}
|
||||
|
||||
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 library = require("./library");
|
||||
var events = require("./events");
|
||||
var hooks = require("./hooks");
|
||||
var settings = require("./settings");
|
||||
var exec = require("./exec");
|
||||
|
||||
@ -273,6 +274,7 @@ var runtime = {
|
||||
settings: settings,
|
||||
storage: storage,
|
||||
events: events,
|
||||
hooks: hooks,
|
||||
nodes: redNodes,
|
||||
flows: flows,
|
||||
library: library,
|
||||
@ -357,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 },
|
||||
|
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,
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
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