UNPKG

node-red-contrib-uibuilder

Version:

Easily create data-driven web UI's for Node-RED. Single- & Multi-page. Multiple UI's. Work with existing web development workflows or mix and match with no-code/low-code features.

301 lines (239 loc) 10.9 kB
/** Takes a msg input and caches it then passes it through. * If it receives a cache-replay control msg, it dumps the cache. * If it receives a cache-empty control msg, it empties the cache. * * Copyright (c) 2022-2024 Julian Knight (Totally Information) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict' /** --- Type Defs --- * @typedef {import('../../typedefs.js').runtimeRED} runtimeRED * @typedef {import('../../typedefs.js').runtimeNodeConfig} runtimeNodeConfig * @typedef {import('../../typedefs.js').runtimeNode} runtimeNode * @typedef {import('../../typedefs.js').senderNode1} senderNode * @typedef {import('../../typedefs.js').cacheNode1} cacheNode * typedef {import('../typedefs.js').myNode} myNode */ //#region ----- Module level variables ---- // /** Main (module) variables - acts as a configuration object * that can easily be passed around. */ const mod = { /** @type {runtimeRED|null} Reference to the master RED instance */ RED: null, /** @type {string} Custom Node Name - has to match with html file and package.json `red` section */ nodeName: 'uib-cache', } //#endregion ----- Module level variables ---- // //#region ----- Module-level support functions ----- // /** Set status msg in Editor * @param {runtimeNode & cacheNode} node Reference to node instance */ function setNodeStatus(node) { let len = 0 if (node.cache) len = Object.keys(node.cache).length node.status({ fill: 'blue', shape: 'dot', text: `${node.cacheKey} entries: ${len}` }) } // ---- end of setStatus ---- // /** Trim all of the cache to the requested number of entries * @param {runtimeNode & cacheNode} node Reference to node instance */ function trimCacheAll(node) { Object.keys(node.cache).forEach( key => { const msgs = node.cache[key] // See if the array is now too long - if so, slice it down to size if ( msgs.length > node.num ) { node.cache[key] = msgs.slice( msgs.length - node.num ) } }) // Save the cache node.setC(node.varName, node.cache, node.storeName) } // ---- end of trimCache ---- // /** Add a new msg to the cache, dropping excessive entries if needed * @param {*} msg The recieved message to add * @param {runtimeNode & cacheNode} node Reference to node instance */ function addToCache(msg, node) { if (mod.RED === null) return if (node.cacheKey === undefined) return // If msg[<cacheKey>] doesn't exist (or is an empty string), do not process if ( !msg[node.cacheKey] ) return // If this is a new property value in the stored variable, create empty array if ( !node.cache[msg[node.cacheKey]] ) node.cache[msg[node.cacheKey]] = [] // HAS to be a CLONE to avoid downstream changes impacting cache const clone = mod.RED.util.cloneMessage(msg) delete clone._msgid // Add a new entry to the array - node.cache[clone[node.cacheKey]].push(clone) // See if the array is now too long - if so, slice it down to size if ( node.cache[clone[node.cacheKey]].length > node.num ) { node.cache[clone[node.cacheKey]] = node.cache[clone[node.cacheKey]].slice( node.cache[clone[node.cacheKey]].length - node.num ) } // Save the cache node.setC(node.varName, node.cache, node.storeName) setNodeStatus(node) } // ---- end of addToCache ---- // /** Clear the cache * @param {runtimeNode & cacheNode} node Reference to node instance */ function clearCache(node) { // Save the cache or initialise it if new node.setC(node.varName, {}, node.storeName) node.cache = {} setNodeStatus(node) } // ---- end of clearCache ---- // /** Send the cache * @param {Function} send Reference to the Node's send function * @param {runtimeNode & cacheNode} node Reference to node instance * @param {object} msg Reference to the input message */ function sendCache(send, node, msg) { const toSend = [] Object.values(node.cache).forEach( cachedMsgs => { // toSend.push(...cachedMsgs) cachedMsgs.forEach( cachedMsg => { if (mod.RED === null) return // Has to be a clone to prevent changes from downstream nodes const clone = mod.RED.util.cloneMessage(cachedMsg) // Add replay indicator if (!clone._uib) clone._uib = {} clone._uib.cache = 'REPLAY' // Add socketId if needed - only for uib control msgs // TODO Add flag override if (msg.uibuilderCtrl && msg._socketId) { clone._socketId = msg._socketId } // send( clone ) toSend.push( clone ) }) }) send([toSend]) } // ---- end of sendCache ---- // // TODO: Adjust processes for all msg caching // TODO: Add clearF(n) to drop the oldest N msgs from cache, clearL(n) to drop the newest N msgs from the cache // TODO: And to the cache control msgs // TODO: Editor option to to ignore replay socket id // TODO: Editor options to clear the cache, clear first n, clear last n entries /** 3) Run whenever a node instance receives a new input msg * NOTE: `this` context is still the parent (nodeInstance). * See https://nodered.org/blog/2019/09/20/node-done * @param {object} msg The msg object received. * @param {Function} send Per msg send function, node-red v1+ * @param {Function} done Per msg finish function, node-red v1+ * @this {runtimeNode & cacheNode} */ function inputMsgHandler(msg, send, done) { // eslint-disable-line no-unused-vars // As a module-level named function, it will inherit `mod` and other module-level variables // If you need it - or just use mod.RED if you prefer: // const RED = mod.RED // Only send if connection is really new (connections=0) - if newcache is selected let sendit = true if ( this.newcache === true && msg.connections && msg.connections > 2 ) sendit = false // Is this a control msg? if ( msg.uibuilderCtrl ) { if ( msg.cacheControl ) { if ( msg.cacheControl === 'REPLAY' && sendit === true ) { // Send the cache sendCache(send, this, msg) } else if ( msg.cacheControl === 'CLEAR' ) { // Clear the cache clearCache(this) } } else if ( msg.uibuilderCtrl === 'client connect' && sendit === true ) { sendCache(send, this, msg) } } else { // Remove ExpressJS msg.res and msg.req because they are recursive objects and cannot be serialised if ( Object.prototype.hasOwnProperty.call(msg, 'req') || Object.prototype.hasOwnProperty.call(msg, 'res') ) { mod.RED.log.info('🌐📘[uib-cache:inputMsgHandler] msg contains Express res/req. These cannot be serialised so removing them.') delete msg.req delete msg.res } // Forward send(msg) // Add to cache addToCache(msg, this) } // We are done // done() } // ----- end of inputMsgHandler ----- // /** 2) This is run when an actual instance of our node is committed to a flow * type {function(this:runtimeNode&senderNode, runtimeNodeConfig & senderNode):void} * @param {runtimeNodeConfig & cacheNode} config The Node-RED node instance config object * @this {runtimeNode & cacheNode} */ function nodeInstance(config) { // As a module-level named function, it will inherit `mod` and other module-level variables // If you need it - which you will here - or just use mod.RED if you prefer: const RED = mod.RED if (RED === null) return // Create the node instance - `this` can only be referenced AFTER here RED.nodes.createNode(this, config) /** Transfer config items from the Editor panel to the runtime */ this.name = config.name this.cacheall = config.cacheall this.cacheKey = config.cacheKey || 'topic' this.newcache = config.newcache ?? true this.num = config.num ?? 1 // zero is unlimited cache this.storeName = config.storeName || 'default' this.storeContext = config.storeContext || 'context' this.varName = config.varName || 'uib_cache' // Show if anything in the cache setNodeStatus(this) // Get ref to this node's context store or the flow/global stores as needed let context = this.context() if ( this.storeContext !== 'context') { context = context[this.storeContext] } this.getC = context.get this.setC = context.set // Get the cache or initialise it if new this.cache = this.getC(this.varName, this.storeName) || {} // Note that the cache is written back in addToCache and clearCache if (this.cacheall === true) { this.cacheKey = undefined } trimCacheAll(this) /** Handle incoming msg's - note that the handler fn inherits `this` */ this.on('input', inputMsgHandler) /** Put things here if you need to do anything when a node instance is removed * Or if Node-RED is shutting down. * Note the use of an arrow function, ensures that the function keeps the * same `this` context and so has access to all of the node instance properties. */ this.on('close', (removed, done) => { done() }) /** Properties of `this` * Methods: updateWires(wires), context(), on(event,callback), emit(event,...args), removeListener(name,listener), removeAllListeners(name), close(removed) * send(msg), receive(msg), log(msg), warn(msg), error(logMessage,msg), debug(msg), trace(msg), metric(eventname, msg, metricValue), status(status) * Other: credentials, id, type, z, wires, x, y * + any props added manually from config, typically at least name and topic */ } //#endregion ----- Module-level support functions ----- // /** 1) Complete module definition for our Node. This is where things actually start. * @param {runtimeRED} RED The Node-RED runtime object */ function UibCache(RED) { // As a module-level named function, it will inherit `mod` and other module-level variables // Save a reference to the RED runtime for convenience mod.RED = RED /** Register a new instance of the specified node type (2) * */ RED.nodes.registerType(mod.nodeName, nodeInstance) } // Export the module definition (1), this is consumed by Node-RED on startup. module.exports = UibCache // EOF