FlowFuse / node-red-dashboard

https://dashboard.flowfuse.com
Apache License 2.0
181 stars 45 forks source link

Add API to allow third party ui nodes to pass data between server and client code #1116

Closed colinl closed 2 weeks ago

colinl commented 1 month ago

Description

Please provide methods for third party ui nodes that:

See this forum post for background information.

Have you provided an initial effort estimate for this issue?

I am no FlowFuse team member

omrid01 commented 1 month ago

I believe sending messages from the client to the server node is supported via custom events (just need to fix the reconnection bug #913 )

Let me propose some further requirements to the server node API, to make it complete:

  1. As requested above by @colinl, send a message to a specified client (by _client) or to all clients which include this node
  2. Be able to drop an incoming msg on the server node, before it is broadcasted to the clients (can this be done today by setting the msg to null in onInput?)
  3. Expose a live list of all connected clients (which contain this node), with their respective Ids. I assume such list is maintained in the framework, so just need to expose it to the server node (e.g., like the Datastore API).
  4. Send notifications upon client add/remove. This is available through ui-event/ui-control nodes, but our custom ui nodes need to be self-contained
colinl commented 1 month ago

3. Expose a live list of all connected clients (which contain this node),

Clients which contain the node, or just those showing a page with the node active? Out of interest, what do you want to use that list for?

omrid01 commented 1 month ago

Clients which contain the node, or just those showing a page with the node active?

With the node active.

Out of interest, what do you want to use that list for?

In general, that is good info for the server node to have. Specifically for my ui-tabulator node, the tabulator package is instantiated and processing in the client. When in Shared (as opposed to Multi-user) mode, when I send a msg to the table node, I receive multiple identical replies (one per client), and need to collapse them by msg Id, which is very messy (or not possible in case of events). If I had a client list, I could select any client as "spokesman" and ignore responses/events from others.

joepavitt commented 1 month ago

Allow code in the node's server .js file to send a message (subject to msg._client) to the node's attached clients.

base.emit(event, msg, node) will do the trick. You just need to get reference to the UI Base in your code. If your node bind to a group via a config option, you can do group.getBase(), otherwise a RED.getConfigNode will be required.

It's not a well documented method, but it's used heavily in ui-control if you need examples

Allow code in the node's client implementation to send a message to the server code so that, for example, the state store can be updated without sending a message to attached nodes, and without directly updating the datastore.

As @omrid01 pointed out - the correct approach here is a custom socket handlers

colinl commented 1 month ago

base.emit('my-event:' + node.id, msg, node) in the server, along with this.$socket.on('my-event:' + this.id, (msg) => {...} in the client, works to send a message to the client, thanks.

When using a custom handler as you suggest for sending data back to the server, if in the client I use this.$socket.emit('my-custom-event', this.id, data) and in the server

onSocket: {
  'my-custom-event': function (conn, id, msg) { ...}
}

then again that does work. However, if I have multiple nodes then all server instances receive the message from any node in the clients. Is there a way of including the node id in the event name as is done for sending to the clients? I can't use 'my-custom-event' + msg.id: function (conn, id, msg) { ...} as that is not valid javascript. I see that I can test the passed in id and compare it with node.id if that is the only way.

joepavitt commented 1 month ago

Yeah, two options as you've detailed:

Option 1: handler per node instance

const events = {}
events['my-custom-event' + node.id] = function (conn, id, msg) { ...}
onSocket = events

Option 2: Check in the single handler

onSocket: {
  'my-custom-event': function (conn, id, msg) {
      if (id === node.id) {
         // do stuff
      }
   }
}

Generally SocketIO doesn't like a significant overload of handlers, so I've generally stuck with Option 2

colinl commented 1 month ago

OK. Do I need to disconnect, as is done in the client code, or is that handled automatically in the server?

joepavitt commented 1 month ago

Do I need to disconnect, as is done in the client code, or is that handled automatically in the server?

All handled server-side, automatically. ui-base checks for any open handlers when you delete your last instance of your node and stops the listeners

colinl commented 1 month ago

That all seems to be working, thanks.

bartbutenaers commented 2 weeks ago

Hi @colinl, I think this issue can be closed, since pull request 1123 has been implemented?

colinl commented 2 weeks ago

Yes.