diff --git a/packages/node_modules/@node-red/editor-client/src/js/hooks.js b/packages/node_modules/@node-red/editor-client/src/js/hooks.js index 096c8e5b5..d7a3c1a97 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/hooks.js +++ b/packages/node_modules/@node-red/editor-client/src/js/hooks.js @@ -1,135 +1,206 @@ -RED.hooks = (function() { +RED.hooks = (function () { + // At the time of writing this PR, VALID_HOOKS were not enforced. There may be a good reason for this + // so the below flag has been added to permit this behaviour. If desired, this can be set to false to + // enforce that only known hooks can be added/triggered. + const knownHooksOnly = false - var VALID_HOOKS = [ + const VALID_HOOKS = Object.freeze({ + viewRemoveNode: true, + viewAddNode: true, + viewRemovePort: true, + viewAddPort: true, + viewRedrawNode: true, + debugPreProcessMessage: true, + debugPostProcessMessage: true + }) - ] + /** + * @typedef {keyof typeof VALID_HOOKS} HookId - A string literal type representing a hook identifier (sans label). + * + * @typedef {Object} HookItem - An item in the linked list of hooks for a given HookId + * @property {function} cb - The callback function to be called when the hook is triggered + * @property {HookItem|null} previousHook - The previous hook in the linked list + * @property {HookItem|null} nextHook - The next hook in the linked list + * @property {boolean} removed - Flag indicating if the hook has been removed + * + * @typedef {Record} Hooks - A mapping of HookIds to the head of their linked list of HookItems + */ - var hooks = { } - var labelledHooks = { } + + /** @type {Hooks} - A mapping of HookIds to the head of their linked list of HookItems */ + let hooks = {} + + /** @type {Record>} - A mapping of labels to their hooks */ + let labelledHooks = {} function add(hookId, callback) { - var parts = hookId.split("."); - var id = parts[0], label = parts[1]; + const { label, id } = parseLabelledHook(hookId) - // 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") + if (knownHooksOnly && !isKnownHook(id)) { + throw new Error("Invalid hook '" + id + "'") + } + if (label && labelledHooks[label] && labelledHooks[label][id]) { + throw new Error("Hook " + hookId + " already registered") + } + if (typeof callback !== "function") { + throw new Error("Invalid hook '" + hookId + "'. Callback must be a function") } - var hookItem = {cb:callback, previousHook: null, nextHook: null } - var tailItem = hooks[id]; + /** @type {HookItem} */ + const hookItem = { cb: callback, previousHook: null, nextHook: null } + + let tailItem = hooks[id] if (tailItem === undefined) { - hooks[id] = hookItem; + hooks[id] = hookItem } else { - while(tailItem.nextHook !== null) { + while (tailItem.nextHook !== null) { tailItem = tailItem.nextHook } - tailItem.nextHook = hookItem; - hookItem.previousHook = tailItem; + tailItem.nextHook = hookItem + hookItem.previousHook = tailItem } if (label) { - labelledHooks[label] = labelledHooks[label]||{}; - labelledHooks[label][id] = hookItem; + labelledHooks[label] = labelledHooks[label] || {} + labelledHooks[label][id] = hookItem } } + function remove(hookId) { - var parts = hookId.split("."); - var id = parts[0], label = parts[1]; - if ( !label) { - throw new Error("Cannot remove hook without label: "+hookId) + const { label, id } = parseLabelledHook(hookId) + if (!label) { + throw new Error("Cannot remove hook without label: " + hookId) } if (labelledHooks[label]) { if (id === "*") { // Remove all hooks for this label - var hookList = Object.keys(labelledHooks[label]); - for (var i=0;i { + invokeStack(hookItem, payload, function (err) { + if (err !== undefined && err !== false) { + if (!(err instanceof Error)) { + err = new Error(err) + } + err.hook = id + reject(err) + } else { + resolve(err) + } + }) + }) + } else { + invokeStack(hookItem, payload, done) + } + } + + /** + * @private + */ + function invokeStack(hookItem, payload, done) { function callNextHook(err) { if (!hookItem || err) { - if (done) { done(err) } - return err; + done(err) + return } if (hookItem.removed) { - hookItem = hookItem.nextHook; - return callNextHook(); + hookItem = hookItem.nextHook + callNextHook() + return } - var callback = hookItem.cb; + const callback = hookItem.cb if (callback.length === 1) { try { - let result = callback(payload); + let result = callback(payload) if (result === false) { // Halting the flow - if (done) { done(false) } - return result; + done(false) + return } - hookItem = hookItem.nextHook; - return callNextHook(); - } catch(e) { - console.warn(e); - if (done) { done(e);} - return e; + if (result && typeof result.then === 'function') { + result.then(handleResolve, callNextHook) + return + } + hookItem = hookItem.nextHook + callNextHook() + } catch (e) { + done(e) + return } } else { - // There is a done callback try { - callback(payload,function(result) { - if (result === undefined) { - hookItem = hookItem.nextHook; - callNextHook(); - } else { - if (done) { done(result)} - } - }) - } catch(e) { - console.warn(e); - if (done) { done(e) } - return e; + callback(payload, handleResolve) + } catch (e) { + done(e) + return } } } - - return callNextHook(); + function handleResolve(result) { + if (result === undefined) { + hookItem = hookItem.nextHook + callNextHook() + } else { + done(result) + } + } + callNextHook() } function clear() { @@ -137,20 +208,48 @@ RED.hooks = (function() { labelledHooks = {} } + /** + * Check if a hook with the given id exists + * @param {string} hookId The hook identifier, which may include a label (e.g. "viewAddNode.myLabel") + * @returns {boolean} + */ function has(hookId) { - var parts = hookId.split("."); - var id = parts[0], label = parts[1]; + const { label, id } = parseLabelledHook(hookId) if (label) { return !!(labelledHooks[label] && labelledHooks[label][id]) } return !!hooks[id] } - return { - has: has, - clear: clear, - add: add, - remove: remove, - trigger: trigger + function isKnownHook(hookId) { + const { id } = parseLabelledHook(hookId) + return !!VALID_HOOKS[id] } -})(); + + /** + * Split a hook identifier into its id and label components. + * @param {*} hookId A hook identifier, which may include a label (e.g. "viewAddNode.myLabel") + * @returns {{label: string, id: HookId}} + * @private + */ + function parseLabelledHook(hookId) { + if (typeof hookId !== "string") { + return { label: '', id: '' } + } + const parts = hookId.split(".") + const id = parts[0] + const label = parts[1] + return { label, id } + } + + VALID_HOOKS['all'] = true // Special wildcard to allow hooks to indicate they should be triggered for all ids + + return { + has, + clear, + add, + remove, + trigger, + isKnownHook + } +})() diff --git a/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js b/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js index c4931c285..3dba41654 100644 --- a/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js +++ b/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js @@ -392,12 +392,28 @@ RED.debug = (function() { if (o) { stack.push(o); } if (!busy && (stack.length > 0)) { busy = true; - processDebugMessage(stack.shift()); - setTimeout(function() { - busy = false; - handleDebugMessage(); - }, 15); // every 15mS = 66 times a second - if (stack.length > numMessages) { stack = stack.splice(-numMessages); } + const message = stack.shift() + // call any preDebugLog hooks, allowing them to modify the message or block it from being displayed + RED.hooks.trigger('debugPreProcessMessage', { message }).then(result => { + if (result === false) { + return false; // A hook returned false - halt processing of this message + } + return processDebugMessage(message); + }).then(processArtifacts => { + if (processArtifacts === false) { + return false; // A hook returned false - halt processing of this message + } + const { message, element, payload } = processArtifacts || {}; + return RED.hooks.trigger('debugPostProcessMessage', { message, element, payload }); + }).catch(err => { + console.error("Error in debug process message hooks", err); + }).finally(() => { + setTimeout(function() { + busy = false; + handleDebugMessage(); + }, 15); // every 15mS = 66 times a second + if (stack.length > numMessages) { stack = stack.splice(-numMessages); } + }) } } @@ -519,10 +535,13 @@ RED.debug = (function() { sourceId: sourceNode && sourceNode.id, rootPath: path, nodeSelector: config.messageSourceClick, - enablePinning: true + enablePinning: true, + tools: o.tools // permit preDebugLog hooks to add extra tools to the element }); // Do this in a separate step so the element functions aren't stripped debugMessage.appendTo(el); + // add the meta row tools container, even if there are no tools, so that the postProcessDebugMessage hook can add tools + const tools = $('').appendTo(metaRow) // NOTE: relying on function error to have a "type" that all other msgs don't if (o.hasOwnProperty("type") && (o.type === "function")) { var errorLvlType = 'error'; @@ -534,7 +553,6 @@ RED.debug = (function() { msg.addClass('red-ui-debug-msg-level-' + errorLvl); $('function : (' + errorLvlType + ')').appendTo(metaRow); } else { - var tools = $('').appendTo(metaRow); var filterMessage = $('').appendTo(tools); filterMessage.on("click", function(e) { e.preventDefault(); @@ -590,6 +608,14 @@ RED.debug = (function() { if (atBottom) { messageList.scrollTop(sbc.scrollHeight); } + + // return artifacts to permit postProcessDebugMessage hooks to modify the message element, access the + // processed payload or otherwise modify the message after it has been generated. + return { + message: o, // original debug message object, useful for any hook that might have tagged additional info onto it + element: msg, // the top-level element for this debug message + payload // the reconstructed debug message + } } function clearMessageList(clearFilter, filteredOnly) {