Options for simplifying custom node runtime code?

Hi all, moved this from the thread on Noisecraft as promised.

Well that sucks. No matter what I try, I can't get RED.nodes.registerType to accept a callback function in a class. I just get an error: Cannot convert undefined or null to object.

I don't know what you want to do, but have you checked the documentation?

It's appears to be RED.nodes.registerType.

Thanks. Yes, double checked the docs.

I was hoping to be able to create a class that would deal with most of the default boilerplate required by a node's runtime. That would then allow itself to be extended to add any custom processing required by a node. The idea being to make creating at least the runtime code for a new node much simpler to deal with and so make it easier for more people to create useful custom nodes.

But no matter what I do, if the function passed as a 2nd parameter to RED.nodes.registerType is contained in a class, it fails with the error Cannot convert undefined or null to object. I can't find out the details of that error unless I unpack some of Node-RED's core processing which I don't currently have the time to do.

I wish you luck with the endeavour, it's a hard nut to crack making node development simpler.

I think you know my perspective on the matter - use node Red to create node Red nodes! The assumption being that a user knows only node Red and not a third party editor.

When I create a new node, it's four files per node plus four files for the package. A node has a html and js file plus a locale.json and html locale. That's already quite a lot to understand when creating a custom node.

Why don't you share what have and ask others for support?

Yes, good idea. I only had a quick go, not much time today. Trying to sort out a new camera for my wife's birthday. :slight_smile:

I'll try to find some time to put up a new thread to see if anyone can help.

1 Like

Yes, well my initial attempt has ended badly. When you call RED.nodes.registerType, the 2nd argument is a function. I coudn't find a way to have that function in a class instance, each time I get an error.

When I get some time, I'll create a separate thread on the subject.

have you tried bind - pass in the method of a class bound to the object?

No, not yet, trying to work out what this is this at different points of the node-red runtime for a node is hard enough without being even more confused. :confounded:

Clearly, the problem is in this area but I just can't yet see the magic sauce needed to make it work. I will try that though when time permits.

Ultimately though, if the solution is as complex as the initial issue, it won't be worth it.

I've looked at modifying @node-red/registry/lib/loader.js to make changes to the authoring process too, instead looking at automatically creating an HTML file from the editor config object. As unhappy as I am with the current process, I ultimately came to the same conclusion with that approach.

My solution was the development of the nodedev nodes that generate all the files around a node in Node-RED. Each file is a node in a Node-RED flow - again some meta-programming: a flow that generates flow which generates nodes.

What I end up with is a flow which in this case contains all the code for the streaming package. A second flow allows me to publish the package to npmjs and commit to github, all via Node-RED.

Now the advantage is that I don't need to leave Node-RED to extend Node-RED. I don't have worry about remembering all the boilerplate code nor do I have remember the three commands to publish to NPMjs - all that I require is an OTP token.

Disadvantage is that no one understands what I'm doing! But it has sped my development time up enormously and also if I want to update a package, I load the flow from flowhub.org, make my changes, and publish. There is no need to find a directory on my drive where I put the code, I can update my packages from any Node-RED instance and I have a common development environment for all my packages.

Also if a user can use Node-RED, they can then extend Node-RED without using a third-party editor or setup. After all, commonality amongst all Node-RED users is that they can use Node-RED.

A problem that I haven't solved is the resync from github, i.e., if I change the code at github (for example because of a PR) then I need to update my flow - but I can live with that for now.

Just my 2 cents!

Hi all. This thread has strayed very far from talking about Noisecraft and now seems to be on a completely unrelated topic about creating nodes. It's only by chance I saw these updates.

If you want any input from me about core platform issues, please create a new thread rather than let this one go even further off topic.

I had said I would - now I have :slight_smile:

Just to explain. When I looked at how Noisecraft custom nodes were crafted, I noted that they simply extended a core class. This made the code for a node really simple.

It made me think that there might be a nice way to be able to greatly simplify the coding of a node by doing all the standard stuff in a class and defining the specifics for the custom node in a class extension.

I had a quick play with this on Saturday but rapidly hit a brick wall because no matter how I crafted it, when supplying a class method as the 2nd parameter to RED.nodes.registerType, I always got the error: Cannot convert undefined or null to object. That happens whether the whole node definition is in a class or whether the node definition tries to access a class method. It even fails if I used a standard function that referenced a class method.

I'm assuming it is something to do with the definition of this but I've not had time to play around further.

Had a day off today and came back to this.

I cannot find a way of creating a node runtime from a class. I'm sure I'm missing something, maybe obvious to others, just not to me.

This is the class module:

class NrNode {
    //#region ---- Class Variables ----
    /** Reference to the master RED instance */
    RED
    /** Custom Node Name - has to match with html file and package.json `red` section */
    nodeName = 'ti-class'
    //#endregion ---- ---- ----

    /** 1) Complete module definition for our Node. This is where things actually start. */
    start(RED) {
        // Save a reference to the RED runtime for convenience
        try {
            this.RED = RED || arguments[0]
        } catch (e) {
            console.error('Could not access `this`. Make sure you bind the start fn to the class instance.')
        }
        try {
            // ! THIS FAILS
            /** Register a new instance of the specified node type (2) */
            this.RED.nodes.registerType(this.nodeName, this.nodeInstance)
        } catch (e) {
            console.trace(`constructor error. ${e.message}`)
        }
    }

    /** 2) This is run when an actual instance of our node is committed to a flow */
    nodeInstance(config) {
        // There is nothing in here because this is never reached, failure is higher up
        try {
            console.log('nodeInstance', this, config)
        } catch (e) {
            console.trace(`nodeInstance - ${e.message}`)
        }
    } // ---- End of nodeInstance ---- //
}

module.exports = NrNode

And this is the node runtime definition:

const NrClass = require('../libs/nr-class')
const nrClass = new NrClass()
// Export the module definition (1), this is consumed by Node-RED on startup.
module.exports = nrClass.start.bind(nrClass)

I've tried lots of different configurations. This is about the simplest variation but they all fail in the same way. I've tried adding different bindings in different places including trying to bind the nodeInstance method.

This is the error I get:

Trace: constructor error. Cannot convert undefined or null to object
    at NrNode.start (D:\src\node-red-testbed\nodes\libs\nr-class.js:43:21)
    at loadNodeSet (D:\src\nr\node_modules\@node-red\registry\lib\loader.js:359:31)
    at D:\src\nr\node_modules\@node-red\registry\lib\loader.js:458:31
    at Array.forEach (<anonymous>)
    at loadNodeSetList (D:\src\nr\node_modules\@node-red\registry\lib\loader.js:453:11)
    at D:\src\nr\node_modules\@node-red\registry\lib\loader.js:145:16

Line 43 is the console.trace line. So clearly something is wrong on the registerType line but I can't find an easy way to find out what.

So really not clear (to me anyway) what is going on and I cannot proceed at this point without some help.

I'm absolutely not sure

The Node definition is missing, you are not using RED.nodes.createNode so the registry tries to modify the constructor prototype with the Node Class.

You can use this kind of Node Class

class AbstractRedNode {
    constructor(config, RED) {
        RED.nodes.createNode(this, config)
        this.RED = RED
    }
}

RED.nodes.createNode is done in the nodeInstance function and we aren't getting that far.

If I amend the class definition to include it, it makes no difference, that part of the code is never reached. It is clear that RED.nodes.registerType does not manage to get to the callback function.

    /** 2) This is run when an actual instance of our node is committed to a flow */
    nodeInstance(config) {
        // There is nothing in here because this is never reached, failure is higher up
        try {
            console.log('nodeInstance', this, config)
            this.RED.nodes.createNode(this, config)
        } catch (e) {
            console.trace(`nodeInstance - ${e.message}`)
        }
    } // ---- End of nodeInstance ---- //

Is probably worth noting that I've previously simplified node runtimes (at least for complex nodes) by refactoring like this:

//#region ----- Module level variables ---- //

// Uncomment this if you want to use the promisified version of evaluateNodeProperty
// const { promisify } = require('util')

/** Main (module) variables - acts as a configuration object
 *  that can easily be passed around.
 */
const mod = {
    /** @type {runtimeRED|undefined} Reference to the master RED instance */
    RED: undefined,
    /** @type {Function|undefined} Reference to a promisified version of RED.util.evaluateNodeProperty*/
    // evaluateNodeProperty: undefined,
    /** @type {string} Custom Node Name - has to match with html file and package.json `red` section */
    nodeName: 'ti-template', // <== CHANGE
}

//#endregion ----- Module level variables ---- //

//#region ----- 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 ModuleDefinition(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

    // Save a ref to a promisified version to simplify async callback handling
    // mod.evaluateNodeProperty = promisify(mod.RED.util.evaluateNodeProperty)

    /** Register a new instance of the specified node type (2) */
    RED.nodes.registerType(mod.nodeName, nodeInstance)
}

/** 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 & tiTemplateNode} config The Node-RED node instance config object
 * @this {runtimeNode & tiTemplateNode}
 */
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

    // @ts-ignore 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.topic = config.topic ?? ''

    /** Handle incoming msg's - note that the handler fn inherits `this` */
    this.on('input', inputMsgHandler)
} // ---- End of nodeInstance ---- //

/** 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 & tiTemplateNode}
 */
async function inputMsgHandler(msg, send, done) { // eslint-disable-line no-unused-vars

    // const RED = mod.RED

    // Pass straight through
    send(msg)

    // We are done - not really needed probably
    done()
} // ----- end of inputMsgHandler ----- //

//#endregion ----- Module-level support functions ----- //

// Export the module definition (1), this is consumed by Node-RED on startup.
module.exports = ModuleDefinition

This example can be found in my node-red-testbed repo.

When registerType is called, this.nodeInstance has no prototype

So the constructor.prototype is undefined and throws the error

if(!(constructor.prototype instanceof Node)) {
  if(Object.getPrototypeOf(constructor.prototype) === Object.prototype) {
    util.inherits(constructor,Node);
  }
}

Oh, well that is a totally obvious way of doing things! Not!! Playing with prototypes is, in my view, the worst thing you can do in JavaScript - I've seen so many really hard to track down bugs created that way.

Nice find.

That also explains a LOT of the weirdness when constructing a node and why, using my other way, nodeInstance does not have a this defined until after you've called createNode.

OK, so any ideas on how I can give it the correct prototype?


For anyone following along. The code @GogoVega referred to is at around line 64 of @node-red\runtime\lib\nodes\index.js.

What I find weird is that if I amend that code to look like this:

    if (type === 'ti-class') console.log('>> registerType:1 >>', constructor.prototype instanceof Node, type)
    if(!(constructor.prototype instanceof Node)) {
        if (type === 'ti-class') console.log('>> registerType:prototype >>', Object.getPrototypeOf(constructor.prototype))
        try {
            if(Object.getPrototypeOf(constructor.prototype) === Object.prototype) {
                util.inherits(constructor,Node);
            } else {
                var proto = constructor.prototype;
                while(Object.getPrototypeOf(proto) !== Object.prototype) {
                    proto = Object.getPrototypeOf(proto);
                }
                //TODO: This is a partial implementation of util.inherits >= node v5.0.0
                //      which should be changed when support for node < v5.0.0 is dropped
                //      see: https://github.com/nodejs/node/pull/3455
                proto.constructor.super_ = Node;
                if(Object.setPrototypeOf) {
                    Object.setPrototypeOf(proto, Node.prototype);
                } else {
                    // hack for node v0.10
                    proto.__proto__ = Node.prototype;
                }
            }
        } catch (e) {
            console.error('boom')
        }
    }
    if (type === 'ti-class') console.log('>> registerType:2 >>', constructor.prototype instanceof Node, type)
    registry.registerType(nodeSet,type,constructor,opts);

Which just adds some console logs (that only log for my test node) and wraps some of the code in an additional try/catch - I get the output >> registerType:1 >> but nothing else. Since constructor.prototype instanceof Node is certainly false as shown by the log, the if statement should be executed but I don't get >> registerType:prototype >> logged as should be expected.

Ah, OK, it is Object.getPrototypeOf(constructor.prototype) that fails which is why I'm not getting the output. And you cannot do a setPrototypeOf either.

Hopefully someone else can work out a way around this but I'm out of ideas again.

Thanks again @GogoVega for getting me further.

Seems work

Add the following before the registerType call

this.nodeInstance.prototype = Object.getPrototypeOf(this.nodeInstance)
2 Likes

Wow! Brilliant - no idea how you worked that out, well beyond my skills. But it seems to work.

I can now move on and see how much further I can get.

3 Likes