Node object and closure

I'm trying to refactor my custom node so that some complex domain logic can stay in its own routines.
However, I found that although the node object can be passed around, some of it's methods don't work as expected.

module.exports = function(RED) {
    function RunNode_MyTask(config) {
        RED.nodes.createNode(this, config);
        var node = this;
        node.on('input', function(input, send, done) {
            warn_inside(node);
            warn_outside(node);
            warn_closure_outside();
            warn_outside_outside(node);

            function warn_inside(node) {
                node.warn('hello inside!');
            }
        }

        node.on('close', (removed, done) => {
            if (done) { done();}
        });

        function warn_outside(node) {
           node.warn('hello outside!');
        }

        function warn_closure_outside() {
            node.warn('hello closure outside!');
        }
    }
    // <REGISTER_ACTION
    RED.nodes.registerType('My_Node', RunNode_MyTask);
 };

function warn_outside_outside(node) {
    node.warn('hello outside outside!');
}

I was expecting warn_inside(), warn_outside(), warn_closure_outside(), and warn_outside_outside() to all print to the debug console in Node-Red, but in reality, only warn_inside() works as expected.

I'm not totally sure how "this" and closure work, but from VS Code debugger I can see that the node prototype symbol is viewable every step the way, and .warn() is clearly defined along the call stack.
But I just can't see the log in the debug console.

Am I mistaken about the object model so that I'm not supposed to use node objects outside their own event handlers?

BTW, I can't "use strict'" inside node functions. Am I not supposed to? I was trying to let the interpreter help me catch problems.

When you say they don't work as expected, what do you mean? Can you be more specific?

Looking at the code, I don't see any reason for them not to work. node is properly in scope - and where you pass it in as an argument, its definitely in scope.

Not sure about your code but did you see my post on restructuring custom nodes a while back?

I now use a form that, to me at least, makes a lot more sense and enables the logic to be followed more easily while creating a structure that is a lot more modular.

You generally use that at the module level which forces the whole module to be strict.

@knolleary

I did indicate my "expectation in my post"

I was expecting warn_inside() , warn_outside() , warn_closure_outside() , and warn_outside_outside() to all print to the debug console in Node-Red,

I got an empty debug console after executing those functions except warn_inside().

@TotallyInformation Thanks, I did read your code carefully. That was part of my motivation to restructure mine. But along the course, I just don't see why my closures could not "let the node reference pass in fully", neither could my functions use the node input argument to produce the debug print when it comes to node.warn() and node.error(). I must admit that I'm not yet familiar enough with JS. I'm still reading getify's YDKJY.

But since I don't get any runtime woes about node.warn() or node.error() inside my closures, I suppose those functions did run.

But it's just that the Node-RED debug console shows nothing, but my warn_inside() function shows debug print normally.

Try changing them to console.log & check the console.

@Steve-Mcl
That's what I do right now, but can someone explain to me why this is not working the way I expect?
I'm really curious. If other side-effects like this exist, I'm really scared to go on refactoring!

Right now we need node.warn()/node.error() to give the end-user useful feedback in the browser, to save the trouble of switching between the Node-RED editor and Terminal/Console/. The target users are not tech-savvy.

I think that if you changed the functions to arrow functions, you should be able to use this rather than node. I can't see why node doesn't work though. You could try moving the function definitions above the node.on however since though JS is generally forgiving about such things, it is possible that it wants them to be defined first.

A would also recommend using VSCode with ESLINT set up. It helps find issues like this.

But, is console.log() sending the values to console? I wanted to prove that the code lines are being being executed & if they are, then to figure out why node.warn() isnt appearing in the browser.

You clearly are not using an editor with linting. As soon as I pasted your code from above into my editor it told me that you had a missing ) at the end of the on input.

After I fixed that, it told me that I was using the functions before they were defined. However, I ignored that and everything worked.

So I would say that your problem lies somewhere else, maybe your HTML file isn't well formed?

Here is an example using my own restructuring that also works - it has the name jktest3 so you should be able to rename everything to suite:

js file:

/** A simple template for defining custom nodes
 * Destructured to make for easier and more consistent logic.
 */
'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').myNode} myNode
 */

//#region ----- Module level variables ---- //
 
/** Main (module) variables - acts as a configuration object
 *  that can easily be passed around.
 */
const mod = {
    /** @type {runtimeRED} Reference to the master RED instance */
    RED: undefined,
    /** @type {string} Custom Node Name - has to match with html file and package.json `red` section */
    nodeName: 'jktest3',

    // Add anything else here that you may wish
    // to access from any function.
    // Having one object also makes it much easier
    // to pass this to external modules as needed.
}

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

//#region ----- Module-level support functions ----- //

/** 1a) Runs once when Node-RED (re)starts or when the node package is first added */
function moduleSetup() {
    // As a module-level named function, it will inherit `mod` and other module-level variables
    //const RED = mod.RED

    console.log('>>>=[1a]=>>> [moduleSetup] Startng')

    // Do stuff here that only needs doing once
    // Don't forget to push anything that might be needed by other functions and modules
    // into the `mod` variable so that it is easily accessible and can be passed on.
}

/** 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+
 */
function inputMsgHandler(msg, send, done) {
    // 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

    console.log('>>>=[3]=>>> [inputMsgHandler] Startng', msg) //, this)
  
    send(msg)
    done()

}

/** 2) This is run when an actual instance of our node is committed to a flow
 * @param {runtimeNodeConfig} config The Node-RED node instance config object
 */
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

    console.log('>>>=[2]=>>> [nodeInstance] Startng')
    //console.log('>>>=[2a]=>>>', config)

    // 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`
     *  The inputMsgHandler function is executed every time this instance
     *  of the node receives a msg in a flow.
     */
    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) => { 
        console.log('>>>=[4]=>>> [nodeInstance:close] Closing. Removed?: ', removed)
       

        // Give Node-RED a clue when you have finished (more important if your shutdown
        // process includes an async task, make sure done() is executed when the async
        // task completes, not when this function ends).
        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
     */
    //console.log('>>>=[2b]=>>>', this)
}

//#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 Test3(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
    // This allows you to access it from any other function
    // defined above.
    mod.RED = RED

    // Add function calls here for setup that only needs doing
    // once. Node-RED loads this once no matter how many instances
    // of you node you add to flows.
    moduleSetup() // (1a)

    // 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 = Test3

html file:


<script type="text/javascript">
    (function () { // eslint-disable-line strict
        'use strict' // eslint-disable-line strict
    
        RED.nodes.registerType('jktest3', { // eslint-disable-line no-undef
            category: 'Testing',
            color: '#E6E0F8',
            defaults: {
                name: { value: '' },
                topic: { value: '' },
            },
            inputs: 1,
            inputLabels: 'Msg to send to front-end',
            outputs: 1,
            outputLabels: ['Data from front-end'],
            icon: 'ui_template.png',
            paletteLabel: 'jktest3',
            label: function () { return this.name || 'jktest3' },
            
        }) // ---- End of registerType() ---- //
    
    }())
    </script>
    
    <script type="text/html" data-template-name="jktest3">
        <div class="form-row">
            <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
            <input type="text" id="node-input-name">
        </div>
        <div class="form-row">
            <label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
            <input type="text" id="node-input-topic">
        </div>
    </script>

I would strongly suggest sticking to the standard format we use and understand why that is not working in your case before introducing lots of other changes that Julian suggests.

I don't know if anyone here as actually tested the code you've shared to see if this is reproducible - I'll have a proper look when I'm back at my computer later today.

Yes, that's what I just tested. It worked as soon as I fixed the syntax error.

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.