1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00
node-red/packages/node_modules/@node-red/util/lib/hooks.js
Nick O'Leary f7210effec
Rework hooks structure to be a linkedlist
Allows for safe removal of hooks whilst they are being invoked
2021-04-26 21:14:42 +01:00

244 lines
6.9 KiB
JavaScript

const Log = require("./log.js");
const VALID_HOOKS = [
// Message Routing Path
"onSend",
"preRoute",
"preDeliver",
"postDeliver",
"onReceive",
"postReceive",
"onComplete",
// Module install hooks
"preInstall",
"postInstall",
"preUninstall",
"postUninstall"
]
// Flags for what hooks have handlers registered
let states = { }
// Doubly-LinkedList of hooks by id.
// - hooks[id] points to head of list
// - each list item looks like:
// {
// cb: the callback function
// location: filename/line of code that added the hook
// previousHook: reference to previous hook in list
// nextHook: reference to next hook in list
// removed: a flag that is set if the item was removed
// }
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/util_hooks
*/
/**
* Register a handler to a named hook
* @memberof @node-red/util_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 && labelledHooks[label] && labelledHooks[label][id]) {
throw new Error("Hook "+hookId+" already registered")
}
// 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}`);
const hookItem = {cb:callback, location: callModule, previousHook: null, nextHook: null }
let tailItem = hooks[id];
if (tailItem === undefined) {
hooks[id] = hookItem;
} else {
while(tailItem.nextHook !== null) {
tailItem = tailItem.nextHook
}
tailItem.nextHook = hookItem;
hookItem.previousHook = tailItem;
}
if (label) {
labelledHooks[label] = labelledHooks[label]||{};
labelledHooks[label][id] = hookItem;
}
// TODO: get rid of this;
states[id] = true;
}
/**
* Remove a handled from a named hook
* @memberof @node-red/util_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,hookItem) {
let previousHook = hookItem.previousHook;
let nextHook = hookItem.nextHook;
if (previousHook) {
previousHook.nextHook = nextHook;
} else {
hooks[id] = nextHook;
}
if (nextHook) {
nextHook.previousHook = previousHook;
}
hookItem.removed = true;
if (!previousHook && !nextHook) {
delete hooks[id];
delete states[id];
}
}
function trigger(hookId, payload, done) {
let hookItem = hooks[hookId];
if (!hookItem) {
if (done) {
done();
return;
} else {
return Promise.resolve();
}
}
if (!done) {
return new Promise((resolve,reject) => {
invokeStack(hookItem,payload,function(err) {
if (err !== undefined && err !== false) {
if (!(err instanceof Error)) {
err = new Error(err);
}
err.hook = hookId
reject(err);
} else {
resolve(err);
}
})
});
} else {
invokeStack(hookItem,payload,done)
}
}
function invokeStack(hookItem,payload,done) {
function callNextHook(err) {
if (!hookItem || err) {
done(err);
return;
}
if (hookItem.removed) {
hookItem = hookItem.nextHook;
callNextHook();
return;
}
const callback = hookItem.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;
}
hookItem = hookItem.nextHook;
callNextHook();
} catch(err) {
done(err);
return;
}
} else {
try {
callback(payload,handleResolve)
} catch(err) {
done(err);
return;
}
}
}
function handleResolve(result) {
if (result === undefined) {
hookItem = hookItem.nextHook;
callNextHook();
} else {
done(result);
}
}
callNextHook();
}
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
}