[Warning: long technical rambling about a specific feature ]
I'm trying to figure out how to support per-browser or per-connection data in FlexDash. Node-RED is not a multi-tenant or really a multi-user system in that everyone operates on a single set of flows. That's OK as-is for simple dashboards that display the state of the world. But it gets a bit awkward when one lets users interact with dashboard elements and use them to issue commands. So I'd like to push the envelope a bit with FlexDash without breaking the fundamental "one set of flows" feature of Node-RED.
(I need this stuff for a project of mine, hence the focus on this now... )
Some use-cases that I have encountered:
- The dashboard shows a table with many rows, there's a row of buttons to filter and show only the "red", "blue", "green", or "yellow" rows. If two people are looking at this table and one of them clicks on a button it's really awkward for the other person.
- The controls for shutters on our windows have two steps: one set of buttons selects the shutter to operate on and then another set of buttons (could also be a slider) selects the position to move the shutter to. This becomes very awkward when two users try to change different shutters at the same time.
- FlexDash supports pop-ups, which can display supplemental information (a pop-up is a grid of widgets), typically in response to a click on some button or other widget. It's very awkward when two users are looking at the same page, one user clicks, and the pop-up shows up for both users.
- Stretching the model a bit, it would be nice if our guests could log into FlexDash and only see/operate a subset of functionality. For example, the set of shutters they could see & operate might be limited to the guest room windows.
The key piece for all this is a way to identify "browsers" and somehow send data only to specific browsers. But this simple statement glosses over a lot of issues. Even the std dashboard is confused, the github repo README states:
Messages coming from the dashboard do have a
msg.socketid
, and updates like change of tab, notifications, and audio alerts will be directed only to that session. Delete themsg.sessionid
to send to all sessions.
I haven't double-checked whether it's a socketid
or a sessionid
, but that's exactly one of the issues! It could also be a userid
! What's the difference?
- socketid would refer to a single browser tab until the user navigates away or reloads, it's basically a websocket connection established when the dashboard is loaded on that tab
- sessionid would refer to all dashboard tabs of a single browser, it's basically an in-memory cookie handed to the browser when it first opens the dashboard
- userid would refer to all dashboard tabs of all browsers of a user, who would have to log in somehow, it's basically an auth cookie handed to a browser when the user logs in
Sad fact is: all three are useful, it depends on the use-case (and there's probably a fourth identifier I forgot). I believe I'm mostly referring to a socket ID in the use-cases I listed above, though. SessionId and UserId are more useful in the context of authorization.
Now what can one do with this? So a message carrying a button press would contain a _flexdash_socket
field that identify the browser tab where they come from and presumably sending messages to a FlexDash widget with such a _flexdash_socket
field would only affect that browser tab.
But FlexDash operates on state not messages! Except that if we're talking about a socket id then the server end doesn't need to keep state 'cause the browser has it and a reload establishes a new socket id which by definition has no state. Hmmm, so as an example, sending a TimePlot data in a message without _flexdash_socket
saves the data as state in the server and sends it to all browsers, while a message with _flexdash_socket
just sends the data to the specific browser tab.
Well, it's not quite so easy. Suppose I have a TimePlot that shows data for today by default, and the dashboard has a row of buttons to select day/yesterday/week/last-week time spans. Easy: a user clicks on 'yesterday' and receives a data message with the appropriate socket id containing the data for the previous day. But then, at the top of the next minute a fresh data value arrives and is broadcast to all, i.e., without _flexdash_socket
, yet it is inappropriate for yesterday's TimePlot.
The simple solution is not to use broadcast in this use-case:
- when the TimePlot widget is loaded it sends a message that triggers the sending of the default 'day' data.
- when the user clicks on 'yesterday' the data is again unicast to the requesting
_flexdash_socket
- when a new data value comes in, the flow needs to decide which, if any, sockets should receive it (in this particular use-case I'm planning to turn it around and have TimePlot request a refresh periodically)
There could be a complicated solution that keeps track of which sockets have received unicast messages and exclude them from broadcasts, but this looks complicated to use.
All this makes me wonder whether one of the standard features of widgets should be a "only allow unicast data/messages" checkbox: if checked any broadcast message sent to the node is rejected as an error (lost the _flexdash_socket
field somewhere). This could surface "programming errors" early.
When it comes to authentication and users, the key element seems to be to associate a browser session with some identifier, typically representing a user. For example, incoming messages could carry a _flexdash_user
field and then nodes could decide what to do with the command and what to send back.
I see several options for populating the _flexdash_user
field:
- an authenticating proxy sitting in front of FlexDash could pass it as a header field
- middleware in front of FlexDash (such as the simple user/pass auth in Node-RED) could tag it onto the request
- a set of nodes could listen for login messages, perform the appropriate lookup (say user/pass in a database), and then inform FlexDash about the desired
_flexdash_socket
->_flexdash_user
mapping.
I'm not ready to tackle that auth stuff, but I see it looming...
Something I'm still struggling with is that all of the above ends up changing the nature of flows. I think of flows in Node-RED as mostly carrying facts about the world. E.g. a message with the temperature in the living room. And those messages are then routed to where they're supposed to go, like a database storage node to save the temperature and a widget node to display it.
When one adds _flexdash_socket
to messages the nature of the flows where those messages circulate changes: the messages no longer carry facts, they carry requests and responses instead. There's nothing fundamentally wrong with that, it just changes the nature of messages and flows.
It does tend to introduce a subtle issue. A lot in Node-RED can be done using a push model: a fact comes in and the message gets propagated forward to where it needs to go. The request/response stuff is fundamentally pull. And that requires storage, which ends up getting more complex quick (where to store, for how long, how to trigger delete, etc).
I'm wondering whether there's a way to structure flows to avoid turning everything into request/response. What I'm imagining is a node that can route messages to the appropriate _flexdash_socket
s. This way the effect of a request would be "bounded" to set the router and then "normal" fact messages can work as usual, except that before getting to widgets a router node would tag the appropriate socket id on. This needs more thought....
I'm sure I've missed stuff, please chime in! (Just remember the fundamental "one set of flows"...)