UNPKG

node-red-contrib-msg-router

Version:
472 lines (380 loc) 22.5 kB
module.exports = function(RED) { "use strict"; var WRRPeers = require('weighted-round-robin'); var RandomWeight = require('weighted'); var HashRing = require('hashring'); function MessageRouterNode(config) { RED.nodes.createNode(this,config); this.routerType = config.routerType; this.topicDependent = config.topicDependent; this.counterReset = config.counterReset; this.msgKeyField = config.msgKeyField || 'payload'; this.undefinedHash = config.undefinedHash || false; this.msgOutputField = config.msgOutputField || 'output'; // Config screen doesn't contain a msgOutputField this.delaying = config.delaying; this.msgControl = config.msgControl; this.outputsInfo = config.outputsInfo || []; this.timerIds = []; this.lastUsedOutputIndex = new Map(); this.peers = new WRRPeers(); this.hashRing = new HashRing(); this.activeOutputsIndices = []; this.activeOutputsWeights = []; var node = this; // Test if an object contains the specified property (handles multiple levels like obj.a.b.c). // (See https://www.customd.com/articles/37/checking-javascript-objects-for-existence-of-a-nested-element ) function objectHasProperty(obj, prop) { var parts = prop.split('.'); for (var i = 0, l = parts.length; i < l; i++) { var part = parts[i]; if ((obj !== null) && (typeof(obj) === 'object') && (part in obj)) { obj = obj[part]; } else { return false; } } return true; } function calculateDelays() { var outputInfo = {}; var delaySum = 0; // Calculate the real delays, based on the delay values (of each output) and their relation type. for (var i = 0; i < node.outputsInfo.length; i++) { outputInfo = node.outputsInfo[i]; // Correct the delay by adding the delay sum of the previous 'relevant' output. // Remark: the original delay's can be unknown, wich we will default to 0. outputInfo.correctedDelay = parseInt(outputInfo.delay || 0) + delaySum; switch (node.delaying) { case 'unrelated': // There are no relations between the individual delays, so no adjustements required (delaySum = 0). break; case 'increment_all': // Use all outputs for calculating the delay sum delaySum += parseInt(outputInfo.delay); break; case 'increment_active': // Only use the active outputs for calculating the delay sum if (outputInfo.active) { delaySum += parseInt(outputInfo.delay); } break; } } } // Initialize the Weighted Round Robin pool function calculateWeights() { // Remove all current peers (i.e. remove all peirs where the function returns true) node.peers.remove(function(peer){ return true; }); // Get the weight for every active output (from the config screen), and register the outputs and their weights in the pool. for (var i = 0; i < node.outputsInfo.length; i++) { var weight = parseInt(node.outputsInfo[i].weight); // Ignore the inactive outputs. Remark: you cannot simply register them (in node.peers), since that library sets // the weights to a minimum of 10... if (node.outputsInfo[i].active) { node.peers.add({ server:i, weight:weight }); } } } function calculateHashes() { // Remove all current hashes node.hashRing.reset(); // Remark: the 'hashring' module expects a server hostname, so we will pass it '127.0.0.<output number>' to map to our output numbers. for (var i = 0; i < node.outputsInfo.length; i++) { var weight = parseInt(node.outputsInfo[i].weight); if (node.routerType !== "weightedhashing") { weight = 10; } if (node.outputsInfo[i].active) { node.hashRing.add({['127.0.0.' + i]: weight}); } } } function calculateActiveOutputs() { var outputInfo = null; // Clear the previous values from the arrays node.activeOutputsIndices.length = 0; node.activeOutputsWeights.length = 0; // Create an array of (zero-based) active output indices (i.e. indices of active outputs in node.outputsInfo). for (var i = 0; i < node.outputsInfo.length; i++) { outputInfo = node.outputsInfo[i]; if (outputInfo.active) { node.activeOutputsIndices.push(i); node.activeOutputsWeights.push(parseInt(outputInfo.weight)); } } } calculateDelays(); calculateWeights(); calculateHashes(); calculateActiveOutputs(); this.on("input", function(msg) { var messages = new Array(node.outputsInfo.length); var delayMap = new Map(); var delayMessages = null; var outputInfo = null; var output = 0; var outputIndex = 0; var msgKeyValue; // ------------------------------------------------------------------------------------- // PRE-PROCESSING // ------------------------------------------------------------------------------------- // Caution: the output numbers (specified in the message) will start from 1, not from 0 ! // So not zero-based ... if (msg.hasOwnProperty(node.msgOutputField)) { try { output = RED.util.getMessageProperty(msg, node.msgOutputField); } catch(err) { return node.error("The msg.output field can not be read"); } if (isNaN(output)) { return node.error("The msg.output is not an integer number"); } output = parseInt(output); if (output < 1 || output > node.outputsInfo.length) { return node.error("The msg.output = " + output + " , which should be between 1 and " + node.outputsInfo.length); } // Make sure it is zero based in the remainder of the code output--; // When the outputs can be configured using input messages, check whether the input message contains configuration fields if (node.msgControl) { var controlMessage = false; outputInfo = node.outputsInfo[output]; if (msg.hasOwnProperty('active')) { if (msg.active !== false && msg.active !== true) { return node.error("The msg.active field should contain true or false"); } outputInfo.active = Boolean(msg.active); controlMessage = true; // When outputs are being (de)activated, it could be necessary to recalculate the delays and weights ... calculateDelays(); calculateWeights(); calculateHashes(); calculateActiveOutputs(); } if (msg.hasOwnProperty('weight')) { if (!Number.isInteger(msg.weight)) { return node.error("The msg.weight field should contain a valid integer number"); } outputInfo.weight = parseInt(msg.weight); controlMessage = true; // When weights are changed, it will be necessary to recalculate the weight pool calculateWeights(); } if (msg.hasOwnProperty('delay')) { if (!Number.isInteger(msg.delay)) { return node.error("The msg.delay field should contain a valid integer number"); } outputInfo.delay = parseInt(msg.delay); controlMessage = true; // When delays are changed, it will be necessary to recalculate the real delays per output calculateDelays(); } if (msg.hasOwnProperty('clone')) { if (msg.clone !== false && msg.clone !== true) { return node.error("The msg.clone field should contain true or false"); } outputInfo.clone = Boolean(msg.clone); controlMessage = true; } if (controlMessage) { // When we discovered msg.output together with at least one of the remote control fields (msg.active, msg.weight, msg.clone), // then we are dealing with a control message. Such a message shouldn't be routed to the outputs... return; } } } // Quit when all outputs are inactive, because nothing can be routed anyway if (node.activeOutputsIndices.length === 0) { return; } // Let's start with a null (i.e. no message) for every output messages.fill(null); // ------------------------------------------------------------------------------------- // PROCESSING // ------------------------------------------------------------------------------------- switch (node.routerType) { case "broadcast": // Send the input message to all active output for (var i = 0; i < node.activeOutputsIndices.length; i++) { outputIndex = node.activeOutputsIndices[i]; messages[outputIndex] = msg; } break; case "message": if (!msg.hasOwnProperty(node.msgOutputField)) { return node.error("The input message doesn't have have a msg.output field"); } if (!node.outputsInfo[output].active) { return node.error("The msg.output = " + (output+1) + ", which refers to an inactive output"); } msg.output = output + 1; // Send the message to the specified output messages[output] = msg; break; case "random": // Get a random active output outputIndex = Math.floor(Math.random() * node.activeOutputsIndices.length); var randomOutput = node.activeOutputsIndices[outputIndex]; msg.output = randomOutput + 1; // Send the message to the random determined output, and don't send anything (= null) on the other outputs. messages[randomOutput] = msg; break; case "weightedrandom": // Get a random active output, based on the weights var randomOutput = RandomWeight.select(node.activeOutputsIndices, node.activeOutputsWeights); msg.output = randomOutput + 1; // Send the message to the random determined output, and don't send anything (= null) on the other outputs. messages[randomOutput] = msg; break; case "roundrobin": var topic = node.topicDependent ? msg.topic : "all_topics"; outputIndex = node.lastUsedOutputIndex.get(topic); // When we receive a reset message (and reset messages are allowed), the NEXT message will be send to the first output. // Accomplish this by removing the counter: for the next message a new counter will be created. if (node.counterReset && msg.hasOwnProperty('reset') && msg.reset) { node.lastUsedOutputIndex.delete(topic); return; } if (outputIndex == undefined) { // Start sending from index 1 outputIndex = 0; } else { outputIndex++; // When a message has been send to all outputs yet, start again from output 1 if (outputIndex >= node.activeOutputsIndices.length) { outputIndex = 0; } } var lastUsedOutput = node.activeOutputsIndices[outputIndex]; msg.output = lastUsedOutput + 1; // Send the output message to the next 'active' output number messages[lastUsedOutput] = msg; // Remember the last used index, for the next message node.lastUsedOutputIndex.set(topic, outputIndex); break; case "weightedroundrobin": // When we receive a reset message (and reset messages are allowed), the NEXT message will be send to the first output. if (node.counterReset && msg.hasOwnProperty('reset') && msg.reset) { // We accomplish this by resetting the calculated weights (but not the initial weights that will be remembered) node.peers.reset(); return; } // Get the next active output index from the pool (based on the specified weights). var weightedOutput = node.peers.get().server; msg.output = weightedOutput + 1; // Send the output message only to the weighted output number messages[weightedOutput] = msg; break; // Both types of hashing are implemented with the same coding (so use case fallthrough). // The only difference is the weights that have been applied earlier ... case "hashing": case "weightedhashing": if (!node.msgKeyField) { return node.error("No msg field is specified in config (for hash value)"); } // The input message key field is mandatory except when undefined hashes are allowed if (node.undefinedHash === false && !objectHasProperty(msg, node.msgKeyField)) { return node.error("The input message doesn't have have a msg." + node.msgKeyField + " field"); } try { msgKeyValue = RED.util.getMessageProperty(msg, node.msgKeyField); } catch(err) { node.error("The msg." + node.msgKeyField + " field can not be read"); return; } // Get the output that corresponds to the (hashed) message field value. Remark: since we had to store our // output numbers as server hostname '127.0.0.<output number>', the first slice needs to be removed. var hashedOutput = parseInt(node.hashRing.get(msgKeyValue).slice(8)); msg.output = hashedOutput + 1; // Send the output message only to the hashed output number messages[hashedOutput] = msg; break; } // ------------------------------------------------------------------------------------- // POST-PROCESSING // ------------------------------------------------------------------------------------- for (var i = 0; i < node.outputsInfo.length; i++) { outputInfo = node.outputsInfo[i]; // Clone messages as specified if (outputInfo.clone && messages[i]) { try { messages[i] = RED.util.cloneMessage(messages[i]); } catch(e) { node.warn("The input message cannot be cloned"); // Cloning failed, so don't send a message on this output messages[i] = null; } } // Store all (non-null) messages in the map, GROUPED BY DELAY (also delay 0). // For example: // messages = [null, msg1 (delay X), null, msg2 (delay Y), null, msg3 (delay Y), null] // The resulting delayMap will get following key-value pairs: // X -> [null, msg1 (delay X), null, null, null, null, null] // Y -> [null, null, null, msg2 (delay Y), null, msg3 (delay Y), null] if (messages[i]) { delayMessages = delayMap.get(outputInfo.correctedDelay); if (!delayMessages) { delayMessages = Array(node.outputsInfo.length).fill(null); delayMap.set(outputInfo.correctedDelay, delayMessages); } delayMessages[i] = messages[i]; } } // At this moment the delayMap contains the messages grouped by delay: // X -> [null, msg1 (delay X), null, null, null, null, null] // Y -> [null, null, null, msg2 (delay Y), null, msg3 (delay Y), null] // Only thing we need to do, is to schedule sending those message groups at the corresponding delay delayMap.forEach(function (item, key, mapObj) { if (key === 0) { // For delay 0 milliseconds we can send the messages to the outputs immediately node.send(messages); } else { // When a delay is specified (i.e. non-zero map key), the messages need to be send delayed to the outputs. // Fix: use 'let' instead of 'var' to have a scoped variable! let timerId = setTimeout( function() { node.send(item); var index = node.timerIds.indexOf(timerId); node.timerIds.splice(index, 1); //clearTimeout(timer); // Clear the array item.length = 0; }, key); // Store the timer id, so we can access it afterwards node.timerIds.push(timerId); } }); delayMap.clear(); }); node.on("close", function() { // Stop all the timers that are currently running for(var timerId of node.timerIds) { clearInterval(timerId); } // Clear the array node.timerIds.length = 0; // Remove all peers (i.e. remove all peirs where the function returns true) node.peers.remove(function(peer){ return true; }); }); } RED.nodes.registerType("msg-router",MessageRouterNode); // Make all the static resources from this node public available (i.e. third party JQuery plugin tableHeadFixer.js). // TODO is dit nodig? of gewoon een script file includen op de html RED.httpAdmin.get('/msg_router/js/*', function(req, res){ var options = { root: __dirname /*+ '/static/'*/, dotfiles: 'deny' }; // Send the requested file to the client (in this case it will be tableHeadFixer.js) res.sendFile(req.params[0], options) }); };