Creating a config node on demand, import broken

Context: I'm trying to fix this regression for the Victron Venus OS, which is caused by a regression in node-red-contrib-victron.

Note that this is not a node-red bug or regression.

The issue is related to creating a config node on demand, when receiving the onadd event: With a pristine install, when importing a flow through the "Import nodes > Paste flow json" feature, the onadd callback never triggers, which is expected. Therefore, however, the import creates nodes that cannot be configured, because the config node they depend upon never got created. (On the server side, the config instantiates a dbus client and sets up caching etc, which many of the victron node-red nodes depend on.)

What I have so far is an ugly solution, where I sleep for a bit, and then trigger the creation of the config node. Having to sleep is necessary, as otherwise RED.nodes.eachConfig() won't include the config node: the state in the browser does not seem to know the config node yet (even once it has been deployed). Sleeping a little solves this, but it is a brittle solution, as the sleep duration may not be enough in some scenarios (slow server side?).

My questions:

  1. In order to avoid the "sleep-for-a-bit" solution, is there an event or other mechanism I can use to ensure I run logic only once all existing (deployed) config nodes are known to RED.nodes.eachConfig()?

  2. Are we doing this wrong (how we create the config node)? I guess we are doing it wrong, the approach suggested in an older discussion seems cleaner. But, could we actually migrate to the cleaner solution in a way that the import works? We would want the import to work for existing flows that do not reference config nodes at all.

Am I missing something? I know that I lack some context related to undeployed / deployed flows, and maybe many other things, which may be relevant here; can anyone point me in the right direction?

P.S. As a new user I am apparently allowed 2 links only. If you allow me to add more links, I'll link more of the code, or do you want me to add relevant code snippets into the topic, rather?

Welcome to the forums @Chris927
Sorry - I Know nothing about victron or associated Nodes, so can only comment on fundamentals.

Does you client nodes (those that depend on the config Nodes) require something to be ready inside the config node?

if so - the way I handle this, is as follows.

Ensure the config Nodes have a way to emit a type of READY event, to any client node listening.
then only have the client node start doing work, after this.

ConfigNode.js

function Init(config) {
    RED.nodes.createNode(this, config);
    const self = this;

    self.clients = {}
    self.regsterClient(id, callback) {
        self.clients[id] = callback
    }

    self.removeClient(id) {
        delete self.clients[id]
    }

    ...

    /* When Smething is ready inside this config Node */
    const clientIds = Object.keys(self.clients);
    clientIds.forEach((c) => {
        self.clients[c]('READY' || <Some Object>)
    })
}

ClientNode.js

function Init(config) {
    RED.nodes.createNode(this, config);
    const self = this;

    self.configNode = RED.nodes.getNode(self.config.configNodeId);

    self.callback = (event) => {
        // Start working, and use what ever was passed here from the config node
    }

    self.configNode.regsterClient(self.id, self.callback)


    ...


    self.on('close', (_, done) => {
        self.configNode.removeClient(self.id)
        done();
    });

}

Apologies if I have not understood the problem fully.

but this method allows the config nodes todo stuff, and at the same have the client nodes, only start using the config (or more, what it has to offer) - once it's ready

If an import has Nodes, that needs a config - usually, the import will also contain the config Node.
the client and config node in the import, will keep their ID's - and the config Node is started first
(I think) - it doesn't t mean the config node is ready to provide service, but my above example, will address that

Thanks for your welcome message, and your swift reply, @marcus-j-davies !

This is definitely helpful, as you explain a possible flow to ensure work in the node continues only once the config node indicates it is ready.

With your approach, how does the config node come to life?

What might be special in our case: We have one global config, and the config is not part of the flow JSON.

Here is what we do: We use a hard-coded ID for the config node, and create the config node on demand (in onadd), basically we do this:

RED.nodes.registerType('my-type', {
  category: 'My Category',
  ...
  onadd: function() {
    let configExists = false;
    RED.nodes.eachConfig(function (n) {
      if (n.id === 'global-config-id') {
        configExists = true;
      }
    });
    if (!configExists) {
      RED.nodes.add({
        id: 'global-config-id',
        type: 'my-config-type',
        ...
      });
      RED.nodes.dirty(true);
    }
  })
});

This means, when importing a flow that needs the global config, in a pristine environment, the config node never gets created, so deploying the flow crashes (as the config is expected to exist).

Is there a better way how to instantiate global config on demand, in such a way that it also works for importing a flow (through JSON or otherwise)?

On a side note: It may be good for us to migrate to a scenario without a global config (and without hard-coded ID) eventually, similar to this description. This would however not solve our immediate problem, I think.

Not sure this helps your immediate issue but one way to potentially avoid this would be to use a plugin to create the global config. Plugins are executed first and you could create the config node there. Unless the setup time for the global config was really slow, it should be ready before your nodes instantiate.

For my UIBUILDER nodes, I don't use a config node but I do have a number of library modules - these are formed as singleton classes (a class that immediately instantiates itself and does not permit more than a single instance). These can then be required by any node that needs them, each require returns the same class instance.

Thanks, @TotallyInformation, this may help in my case: I didn't find clear documentation on plugins, but I think I found an example on how to initialize a plugin. Is this what you are thinking of?

I will try the plugin route, to instantiate the global config node, and revert.

@TotallyInformation is better experienced to plugins then I, but here is one potential solution

Create a kind of bootstrap plugin, that triggers a routine to check for the config, create it if not found

package.json

"node-red": {
	"nodes": {
		...
	},
	"plugins": {
		"bootstrap": "plugins/Bootstrap.js"
	}
},

Bootstrap.js

module.exports = function (RED) {
   // you dont really have to do anything here
   // I mean - I haven't - but maybe you should?
}

Bootstrap.html

<script type="text/javascript">
  // Instead of looping the config nodes
  let hasClient = (RED.nodes.node('Your-Config-ID') !== undefined)
  if (!hasClient) {
      // Create config
      ...
      RED.notifications.notify('Please Deploy - We have just created a configuration Node')
  }
</script>

I use the bootstrap approach in my own Nodes - to setup side bars etc etc

You can find examples of both an Editor and a Runtime plugin in UIBUILDER. The files are clearly named in the nodes folder. You define them in package.json just like nodes.

The uibuilder editor plugin uses code in a separate file in resources folder to make it easier to develop. So the html file is just 2 lines in my case. One to load the JS, another to load some custom CSS (which you might not need).

If the uibuilder versions are too complex to follow, try the ones in my testbed. I've just updated those to load the editor resources as ES Modules. That gives better isolation of the code.

Thank you! I tried adding a plugin here, unfortunately I am getting an error at runtime:

[node-red/bootstrap-plugin] TypeError: Cannot read properties of undefined (reading 'bootstrap-plugin') (line:8)

This happens, because module.plugins is undefined here. Am I missing some setup, to ensure module.plugins is populated?

This commit moved the instantiation of globalClient to node creation. This means that globalClient will only exist when the editor deploys the config node.

A plugin won't really help because you'll have to deploy to make globalClient exist in any case. Even if the deployment is automated it requires a deployment which is not a good practice.

Checking the existence of the config node in the editor isn't relevant either because the node won't necessarily exist in the runtime.

There's no magic bullet here.

The only possibility I see is to ask the author to initialize globalClient in the module scope AND allow enablingPolling to be changed when editing the config node (which could recreate a client - if the parameter is static).

Hi @GogoVega

Well analyzed! This is one part of the problem, as I understand it, see my previous comment here. I can solve this by storing the global configuration in the file system in addition to the config node.

What I'm trying to understand with this post:

If the config node exists already, RED.nodes.eachConfig() does not include the config node, unless I wait for a bit, as per this commit, and waiting for a bit is obviously not a solid solution. (Without waiting, the config node would be created again, resulting in "duplicate id" errors.)

Is there any mechanism within node-red (plugins or something else) to avoid the arbitrary "wait for a bit"? What gave me hope was the statement by @TotallyInformation that "Plugins are executed first", so I could possibly instantiate the global config once within the plugin, being certain elsewhere (in node implementations) that the config always exists already.

Related question: In general, I feel I don't understand the life cycle / state management of nodes, config nodes, node definitions, deployment, etc. well enough to come up with a solution. Is there any doc or code I can read to understand this better?

I don't recommend creating a config node this way because a user could install the palette without using the nodes immediately. This solution will create the config node every time the editor loads (if the config node doesn't already exist), and the user will have to delete it.

My recommendation would be to keep the onadd event. You can also do this in oneditprepare because if this is called, it means a Victron node exists, but it remains an editor solution; deployment is required to make it exist in the runtime.