billstclair/elm-websocket-client

Allow Self Managed Loading of PortFunnel Modules

Closed this issue · 12 comments

Currently, PortFunnel.js loads registered modules like WebSocket.js itself. This process has two disadvantages: first, it requires the JavaScript files to be available as separate files to load over HTTP and second, it dynamically inserts JavaScript code into the site by using document.head.AppendChild(script).

The first disadvantage is an performance issue, because the browser has to load a separate artifact from the server. Today the common practice is to bundle all required JavaScript files together into one bundle file which is also minified.

The second disadvantage is a security issue. Dynamic inserts of JavaScript code are often used for XSS attacks. Disabling those completely will greatly help to prevent XSS attacks. One way to do this is to use the CSP header. By using dynamic insert yourself, you can't disable them.

I would recommend to separate the module loading from the usage. One could simply change the API of PortFunnel from:

PortFunnel.subscribe
     (app, {portnames: ['cmdPort', 'subPort'],
            modules: ['Module1', ...],
            moduleDirectory: 'js/PortFunnel'
           });

to:

PortFunnel.subscribe
     (app, {portnames: ['cmdPort', 'subPort'],
            modules: [module1, ...]
           });

With that, module1 has to be load either by requiring it in a Webpack bundle or by loading it in another way and assign it to the module1 var.

Thanks. This will possibly motivate me to fix this. But my focus right now is on my new application, so it probably won't happen right away.

If you do it, in a way that will work for ALL the port funnel modules, and submit it as a pull request, here, and/or for https://github.com/billstclair/elm-port-funnel, I'm more likely to get to it quickly. As long as it still works for a stand-alone system that loads the JS files separately.

I don't think there are a lot of people using my PortFunnel packages, so the impact of this change will likely be less than the impact on you of my change to billstclair/elm-websocket-client that made it work in the PortFunnel environment.

Thanks for your work and fast responses. I also work on an application. WebSockets are essential to it, but I also have to balance my time working on PortFunnels with my time on the application. If I have a good idea, I'll definitely open a PR.

Did anyone think much more about this? This is not my expertise, but @alexanderkiel's suggestion doesn't look trivial. It requires importing the WebSocket module before calling PortFunnel.subscribe, but WebSocket.js is trying to immediately access PortFunnel keys that haven't been setup yet.

I'm thinking of breaking PortFunnel.subscribe into two parts. The part that only requires the Elm app and the part that also requires the port funnel modules.

Or maybe each module should have an init() to be called during subscribe?

This wasn't hard to fix. js/PortFunnels.js is no longer aware of which modules are loaded. It just dispatches for whatever moduleName the Elm code sends it.

Each module is now responsible for waiting for PortFunnels.js to startup before installing itself and notifying the JavaScript code that it's ready From js/PortFunnel/WebSocket.js:

(function(scope) {
  var moduleName = 'WebSocket';

  var sub;

  function init() {
    var PortFunnel = scope.PortFunnel;
    if (!PortFunnel || !PortFunnel.sub || !PortFunnel.modules) {
      // Loop until PortFunnel.js has initialized itself.
      setTimeout(init, 10);
      return;
    }

    sub = PortFunnel.sub;
    PortFunnel.modules[moduleName] = { cmd: dispatcher }

    // Let the Elm code know we've started
    sub.send({ module: moduleName,
               tag: "startup",
               args : null
             });
  }
  init();

Live at https://billstclair.github.io/elm-websocket-client

Cool. I'll check this out shortly.

EDIT: That allows me to build and I see a call to cmdPort.subscribe, so next step is to write some tests and wire it up. Thanks!

The entry point to the modules is a function that takes a scope argument. It looks like this needs to be global so that both PortFunnel.js and WebSocket.js see the same scope.PortFunnel.

What is scope supposed to be and how is it expected to be passed correctly to PortFunnel and WebSocket?

EDIT: Maybe it's a function of the bundler, but I've been using parceljs and I need to use this which is Window instead of scope. So in PortFunnel.js's I say this.PortFunnel = PortFunnel and in WebSocket.js's init() I say var PortFunnel = this.PortFunnel. I pretty sure this is all some JS module / bundling knowledge deficiency for me.

You almost certainly want to use the example's example/src/PortFunnels.elm file, as boilerplate, unless you want to mix in another PortFunnel-wired module.

Ask if you need me to elucidate, but before doing that work through https://github.com/billstclair/elm-websocket-client/blob/master/example/src/PortFunnels.elm, until you figure out how the plumbing there works, or learn how Main.elm uses it well enough that you can just trust the plumbing.

The example is live at https://billstclair.github.io/elm-websocket-client

But maybe you only need an answer to your direct question.

Yes, scope is window in web browsers, global scope. PortFunnel.js creates it, as an object, and WebSocket.js waits for it to exist, before registering itself there.

https://github.com/billstclair/elm-websocket-client/blob/master/example/site/js/PortFunnel.js is basically:

(function(scope) {

PortFunnel = {};
scope.PortFunnel = PortFunnel;

...

}(this))

I am using an unmodified version of example/src/PortFunnels.elm. That last edit switching from scope to this is the last step I needed to get it working, at least at first blush. Maybe that's unnecessarily putting the PortFunnel into too broad a scope, but scope in WebSocket.js' init() was always an empty object, never containing scope.PortFunnel leaving init() continually in a setTimeout loop.

There are myriad communication mechanisms written on top of JavaScript. I'm using a single global variable, PortFunnel for communication between all PortFunnel-aware modules.

OK. I'm guessing my bundler or something else local doesn't pass Window in as scope. It's an easy tweak for me to get it going.

Thanks so much for the package and your attention. If you'd like me to investigate, just say the word.