mirror of https://github.com/node-red/node-red.git
Update FE hooks to be promise chainable
parent
d981771a2d
commit
8ceacdb565
|
|
@ -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<HookId, HookItem|null>} 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<string, Record<HookId, HookItem>>} - 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<hookList.length;i++) {
|
||||
removeHook(hookList[i],labelledHooks[label][hookList[i]])
|
||||
const hookList = Object.keys(labelledHooks[label])
|
||||
for (let i = 0; i < hookList.length; i++) {
|
||||
removeHook(hookList[i], labelledHooks[label][hookList[i]])
|
||||
}
|
||||
delete labelledHooks[label];
|
||||
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];
|
||||
removeHook(id, labelledHooks[label][id])
|
||||
delete labelledHooks[label][id]
|
||||
if (Object.keys(labelledHooks[label]).length === 0) {
|
||||
delete labelledHooks[label]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeHook(id,hookItem) {
|
||||
var previousHook = hookItem.previousHook;
|
||||
var nextHook = hookItem.nextHook;
|
||||
/**
|
||||
* Remove a hook from the linked list of hooks for a given id
|
||||
* @param {HookId} id
|
||||
* @param {HookItem} hookItem
|
||||
* @private
|
||||
*/
|
||||
function removeHook(id, hookItem) {
|
||||
let previousHook = hookItem.previousHook
|
||||
let nextHook = hookItem.nextHook
|
||||
|
||||
if (previousHook) {
|
||||
previousHook.nextHook = nextHook;
|
||||
previousHook.nextHook = nextHook
|
||||
} else {
|
||||
hooks[id] = nextHook;
|
||||
hooks[id] = nextHook
|
||||
}
|
||||
if (nextHook) {
|
||||
nextHook.previousHook = previousHook;
|
||||
nextHook.previousHook = previousHook
|
||||
}
|
||||
hookItem.removed = true;
|
||||
hookItem.removed = true
|
||||
if (!previousHook && !nextHook) {
|
||||
delete hooks[id];
|
||||
delete hooks[id]
|
||||
}
|
||||
}
|
||||
|
||||
function trigger(hookId, payload, done) {
|
||||
var hookItem = hooks[hookId];
|
||||
/**
|
||||
* Trigger a hook, calling all registered callbacks in sequence.
|
||||
* If any callback returns false, the flow is halted and no further hooks are called.
|
||||
* @param {HookId} id The id of the hook to trigger (should not include a label - e.g. "viewAddNode", not "viewAddNode.myLabel")
|
||||
* @param {*} payload The payload to be passed to each hook callback
|
||||
* @param {function(?Error=):void} [done] Optional callback. If not provided, a Promise will be returned.
|
||||
* @return {Promise|undefined} Returns a Promise if the done callback is not provided, otherwise undefined
|
||||
*/
|
||||
function trigger(id, payload, done) {
|
||||
let hookItem = hooks[id]
|
||||
if (!hookItem) {
|
||||
if (done) {
|
||||
done();
|
||||
done()
|
||||
return
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return;
|
||||
}
|
||||
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 = 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
|
||||
}
|
||||
})()
|
||||
|
|
|
|||
Loading…
Reference in New Issue