Editor Runtime Split

#1

Let's discuss the split of the editor and the runtime!

The WIP spec is in the github wiki: Editor Runtime API

As of late April 2018 there's also a runtime-api branch for the work in the repo.

And here's a quick quote of my "imagining a possible future" post from slack on April 13, 2018:

  • editor which is a front-end-only app, running in a user’s browser
  • an express-based REST API (editor/admin/runtime/whatever) running on a remote cloud server machine that the editor uses to get all the dynamic info it needs and to which the editor saves, deploys, etc.
  • a headless runtime, on a separate cloud server machine, whose job is solely to run flows, (including accepting flow inputs and emitting flow outputs)
  • I imagine the editor having no direct interaction with the runtime. Any such interaction, like showing live flow outputs, would pass through the API, for example editor opening websocket to API, API subscribing to events from runtime and passing received events out the websocket.
  • the API would need storage, and the runtime would need storage, but ideally those could be separate. Any info they need to share, like deploying updates to flows, should use whatever we define as the interface between the API and the runtime.
  • the interface between the API and the runtime should be customizeable — I was thinking that the existing storage API provides a good example — if we primarily define the interface as javascript methods, clearly defining the calls each side needs to make, the info each side needs from the other, then different companies could write custom implementations of such an API to fit everybody’s unique :snowflake: architecture :wink:

And finally, to kick off this thread, a screenshot of an awesome update in slack from @knolleary as of April 24:

29%20PM

Discuss!

2 Likes

#2

Thanks for starting the topic @towynlin. Let me start with a status update on where I've got to with the runtime-api branch.

The work in that branch has been focussed on teasing apart the internals of the node app that is Node-RED to provide as crisps separation as possible between logical components. This is a prelude to those logical components becoming their own node modules.

At a high-level the goal is to have separate editor and runtime node modules (there will likely be other modules split out for reuse but are less relevant to the main split).

The editor module contains the HTML/CSS/JS resources of the editor and an express app that is used to serve them app - implementing the adminAuth security and user management.

The runtime module is the core of node-red responsible for instantiating nodes as per a flow configuration and running them.

Currently there is fair-to-good separation of these two components in the code base, but when you get into the details, they are still too entwined without a complete separation of concerns. This is where the new runtime-api comes in. The runtime-api is exposed by the runtime module. The editor module, when instantiated expects to be given an instance of the runtime-api. This is kinda representing in figure 1 in the following image:

Figure 2 shows how the node-red module will take those two sub-modules and plug one straight into the other. So any request from the editor is passed straight into the runtime via the runtime-api.

Figure 3 shows how the split can be done; the editor module is given a custom implementation of the runtime-api which does whatever it wants - in this instance, some custom IPC mechanism that invokes the runtime-api exposed by the runtime module running elsewhere.

Unlike the existing, undocumented, runtime api, the new runtime-api provides a consistent design to make it easier to create custom implementations of it. In a novel approach, I've been documenting the API as I go (having added jsdoc to the build process) - you can see the full API as it currently stands here. Every function takes a single options object and returns a promise - so every function call can complete asynchronously. The options object has a user property that will identify the user calling the API, plus any function-specific properties.

By providing the user object, a custom implementation can make routing decisions on where to send the request to - as shown in Figure 4 below:

For completeness sake, even for scenarios where there's a single editor and single runtime running in the same node process, the same principle can be used to insert custom logic in the runtime-api path.

As a side note, we cannot lightly change what require('node-red') returns - it may not be documented, but there's too much code out there that depends on it. I'm uneasy about using the 0.x->1.x major version bump as an excuse to break it... although it would be legitimate if we chose to do so. It's a big decision to make.

In its current form, the runtime-api branch implies a set of modules as follows:

  • @node-red/editor - contains the HTML/CSS/JS resources of the editor and an express app that is used to serve them app - implementing the adminAuth security and user management.
  • @node-red/runtime - the core runtime of node-red.
  • @node-red/utils - common util code shared by runtime and editor - logging, i18n
  • @node-red/registry - the runtime component responsible for loading node modules in the runtime

Chris Hiller (@boneskull) has done some great work modelling a monorepo approach. Whilst that has been done independently of the runtime-api branch and the fine details of the modules aren't slightly different, it does a good job of showing the principle of using lerna.js to manage the monorepo.

Current issues

So, now we get to the good stuff.... the problems I don't have solutions for yet

  1. This is a big one: any node is free to expose its own HTTP routes on the httpAdmin express app we expose. This is used, for example, by the Serial node so it can retrieve a list of active serial ports when the user is configuring it. If such a node is instantiated on a runtime that is remote to the editor, when an http request comes in to the editor app for such a route we don't know how to handle it. Similarly the HTTP endpoints the Debug and Inject nodes add to handle the presses of their buttons in the editor.
    There are a couple possible solutions:

    a. add to the runtime-api the ability to query what routes a node has added and have a generic route handler in the editor app that is then able to proxy the request. Except... its express.js and some nodes add middleware we don't know about that goes beyond simple http GET/POST - such as serving static content the node provides.

    b. hang all node-specified routes of a well-known path, so the editor app is able to proxy them en-masse.

  2. How to provide the editor as an entirely static set of resources that could be served from S3 or similar.
    The issue here is how to bootstrap the editor so it knows where to send its admin http requests to. The fact the editor's routes for serving static content (default icons, css etc) is muddled up with the admin routes (getting a custom node icon, deploying flows) makes separating the two a bit tricky. This isn't insurmountable - just needs a bit more poking at to find the right model.

Next steps

There are a couple threads of activity. One is to get things moved over to the monorepo structure. This is massively disruptive and will make porting fixes between the 0.18.x stream and the development branch a headache - so we need to pick the right moment to do so.

The other thread is to figure out how to address the node http routes issue.

And finally there is how to make the editor a more standalone set of static resources.

3 Likes

#3

I say Nick, steady on old chap :slight_smile:

Most of this is outside my skill-set by quite a way but I get the gist of it and I don’t envy the tasks ahead of you - I do remember into my dim and distant past trying to refactor complex code and the time and mental energy it took.

All I can offer then is encouragement. This is a great step for Node-RED and encouraging for its future. Looking forward to seeing the results.

1 Like

#4

Thanks for this great writeup.

My unsolicited opinion: If breaking the require('node-red') API is necessary for NR to move forward, it should be done at v1.0.0. If we’re just procrastinating, then let’s not procrastinate.

However, I’m curious about who/what is consuming this API–this sounds like a less severe change than, e.g., breaking a good portion of the contributed Node ecosystem?

0 Likes

#5

I came across this document on experience with lerna that may be useful, and related document on Alle approach. Seems mainly applicable for large mono-repos.

Thought it was interesting how one can avoid the bootstrapping step by putting packages in node_modules instead of packages.

0 Likes

#6

Given our reasonably positive experience with proxying all current Node-RED endpoints en-masse including web sockets, and the simplicity of the approach, I suggest you start with 1.b. for the node http routes issue. Glad to provide code snippets/refs on how we did this although no rocket science there.

1 Like

#7

The Alle approach doesn’t work with all tools; unfortunately, Grunt is one of the tools it doesn’t work with.

Sorry, rather I meant lerna --hoist. Either way, Lerna seems to work well enough for our purposes.

0 Likes

#8

One thing I think needs to be in this API, separate out the logic of building graphs from the UI app. Noflo has the useful element of being able to add by API links between nodes, and create the whole graph from API and this could greatly help node red. This would make life a lot easier for those doing programmatic creation of flows on demand or creating custom UIs.

0 Likes

#9

Interesting idea. I don’t think it’s the right topic to discuss here though. Might be worth opening a ‘feature request’ sub topic around this. The topic here is called Editor Runtime API but I think it might be better called Editor Runtime Split.

Happy to discuss my experience implementing a custom UI to generate flows with the existing API in another thread if you’re interested.

2 Likes

#10

:+1: Agreed — changed the topic name. :bow:

1 Like

#11

Alright @mike, I’ve added a thread here.

0 Likes

#13

I’ve been thinking about this. It would be nice for the run time when split to be able to run individual nodes on the run time. An example use case of this would be to allow for distributed nodes across many clients using MQTT, similar to msgflo.

0 Likes

#14

Yes its a step on the way to this. https://trello.com/c/J7UDbQVP/66-pluggable-message-routing

0 Likes

#15

Yes, I’ve started to jot down some ideas and capture feedback here: https://github.com/node-red/node-red/wiki/Pluggable-Message-Routing. Very much work in progress, just starting to explore where to plug in routing to the current runtime, and support multiple routers (e.g. local, direct network, MQTT/messaging system).

0 Likes