3 ms·
The Phoenix LiveView pattern is one that I thought was the near-ideal interactive page architecture 20 years ago, long before Elixir and even before XHR was sta
by jlokier 6y ago
The Phoenix LiveView pattern is one that I thought was the near-ideal interactive page architecture 20 years ago, long before Elixir and even before XHR was standardised. (We had other methods for AJAX and server-sent events to achieve it in those days.)
It surprises me that there are so few implementations using this pattern today.
The Meteor pattern is another good one for the user, if you like things better optimised for user-visible latency (as user interactions happen at the client first), but it hasn't ended up with much mindshare now. Perhaps because it's more complicated to use, and perhaps because JavaScript isn't ideal for this.
Svelte's precompiled diffs and minimal client-side code is another pattern I like.
For an application I worked on, I developed a combination of those three patterns. It had a nice and very efficient combination of server-side rendering and client-side immediate interaction, without needing custom JavaScript. This made it fast, user-friendly and pleasant to develop in.
Like LiveView, the client would send user events to the server, which would update affected component model state (which could be shared among different clients too). Server would re-render any visual components which used the model data (using automatic dependency tracking from previous renders), and then send an async event to each affected client containing an efficient diff of all rendered changes. If there was much network latency or too much queued, multiple updates would be batched together, merging their diffs efficiently. The result was the update could be worst-case the size of a page replacement for any number of updates, and at best the size of a few DOM edits in place.
But, a little bit like Meteor (but without custom JS), the server could also push "client event-handling hints" in the rendered model to the client. If an event pattern-matched one of these hints, the update could be applied immediately on the client as well as the event sent to the server. The server knew what update would have been applied, and could account for it in the diff. So if the server's new rendering matched the client's immediate rendering, the diff was empty and didn't need to be sent! But if the server got a different result, for example the event caused an operation which failed, the diff caused the client to receive an appropriate correction.
The diffs were calculated efficiently too, not by rendering components and comparing trees, but by precalculating diff trails from state change patterns where those could be recognised. For example when one model value changed, if the diff would always be just a string change in the DOM, it already knew and emitted that diff directly. This meant diff calculation time was as fast as possible in simple cases (event to output in O(1)), and reduced to a reasonable diff time in the most complex cases (such as merged diffs).
Keeping it always perfectly in sync and always efficient, throughout network outages, reordered requests, lost events etc was quite technical, but it wasn't application specific. Only the framework needed to deal with the technicality.
This was lovely, because it always stayed in sync, it was always efficient for any combination of initial loads, reloads, and sync events, both on slow and fast networks, it worked without JavaScript if needed, had client-side immediate updates for simple logic of a general-purpose nature specified by the server-side, could share model state among different clients and pages, and still didn't need any custom JavaScript for the application.
As the server side was written in async coroutines and Perl, it didn't seem worth the effort of packaging and publishing that implementation as Perl's popularity declined (and indeed coroutines seem disfavoured there as well, even though they work brilliantly).
I still think that's a lovely interactive webpage model, possibly the best. Nicer than client-side SPAs and nicer than server-side rendering. It's pleasant to develop for, pleasant as a user as well, very fast in many different scenarios including poor network connectivity, and great for sharing live state that appears in multiple pages or documents.
I'd like to implement that again in a modern language, but I haven't had any compelling reason to work on a webapp lately.
- jononomo 6y agoThis was super-interesting for me to read. I developed in Meteor for about two years. I started out a major fan, but was quite disappointed in the framework by the end. I'm now excited about Elixir/Phoenix/LiveView/BEAM and am about to spend a few months getting myself up to speed with that whole ecosystem.
- dsiegel2275 6y agoSame boat, I got into Meteor for a while years ago (even built a side project that ran in production and made real money for a couple of years). For the last year I have been writing Elixir/Phoenix full-time at work on a greenfield project. We have gone "all in" on LiveView and our using it in a significant portion of our system. I have been very impressed with it so far. When pairing LiveView with Phoenix PubSub, one can achieve Meteor-like 'reactivity' with remarkable ease.
- mst 6y agoThe basic issue was that the coroutine package was segfaulty and maintained by a man so lovely to interact with that the node.js core team eventually invented libuv (to replace his ev library) primarily so they never had to talk to him again. Perl now has http://p3rl.org/Future::AsyncAwait http://p3rl.org/Future::AsyncAwait which is (like any async/await system) a bit more restricted than a full coroutine but works beautifully. If you threw a github repo up somewhere I can think of a few people who might be interested in trying to update it to more recent+reliable perl tech.
- jlokier 6y agoThanks for the suggestions! But I've found Coro highly reliable and effective, including under stressed and complex loads, for about 6 years. Never found it segfaulty. It also performed well, and the API design always impressed me with its cleanliness and good documentation. Also I make extensive use of coroutine-local variables (like `thread_local` in C11) which operate at the same speed as normal `my` lexicals. I.e. as fast as possible. It's just a good way that ordinary non-async modules can be trivially made async-safe, without performance loss, and without becoming tied to async or not-async (they become equally flexible for both uses, which is valuable). (The module to make them run that fast isn't public, but the API it relies on to work is part of Coro. `Future::AsyncAwait` doesn't provide the necessary API at the moment.) > Perl now has http://p3rl.org/Future::AsyncAwait http://p3rl.org/Future::AsyncAwait which is (like any async/await system) a bit more restricted than a full coroutine but works beautifully I'm not convinced. Future::AsyncAwait is a different paradigm, and doesn't do the things I find Coro useful for. Async/await has the "function colour" problem - every module that might logically "block" has to be re-engineered with a new, async version of the same module. Same applies to any function which might call another to any depth in the call graph which then "blocks". People have argued that the "function colour" problem with async/await isn't really a problem. I'd argue that it's fine if you've designed around it from the start and you don't intend to use code in both async and non-async environments. That re-engineering can be done, but it's a massive change to existing modules, and then you end up with something that can only be used in an async/await program. Whereas Coro is non-invasive. I use the same modules in an async server or in a classic process-forking server (without loading Coro and no concerns about async issues), as well as standalone processes (scripts). I choose appropriately depending on the service characteristics (e.g. async is poorly suited to some kinds of loads such as computationally heavy request processing). Also for some things it's good to have the confidence that comes from not loading any fancy stuff, e.g. in security code and standalone scripts. For that dual-use functionality, as far as I can tell with Future::AsyncAwait I'd need to write two separate versions of most things. It turns out a lot of things might call something that, via a deep and opaque call chain, might block somewhere. E.g. any code that loads a config file when it's first run. Anything that uses a template, and calls out to a template compiler. Anything that compiles and loads code lazily. Anything that does something as trivial as stat() on a file. In practice for my web stuff that means nearly everything (as you can imagine from my GP comment). I also make heavy use of JSX-ish templates instead of custom Perl code. It's just cleaner, and they can be targeted to other languages, even compiled to C for speed. Template components end up inheriting the same async properties as the server they are running in. A page may refrence subresources, microservice calls, data fetches or file reads. All fetched or calculated in parallel (calling other services or using multiple cores if necessary), which is good for latency. There's also a software engineering impact. As soon as you get deep into complex logic like filesystems, and especially with memory allocation inside those, which triggers nested filesystem access, it's just too much work to engineer everything in an async state machine way. That's why the Linux kernel was unable to do correct asynchronous I/O through AIO functions for many years, and that's why everything end up choosing threads instead of AIO in userspace too: libuv that you mentioned is a great example! > update it to more recent+reliable perl tech I think "reliable" is misleading. Coro is highly reliable (for me anyway), while Future::AsyncAwait has documented gotchas. For "more recent", the Perl community has picked its path. In my view it's a less useful path, and I'm disappointed Perl core didn't choose to implement a coroutine mechanism. But it didn't. With Coro all the modules just work without anything Coro-specific in them. (Just a few coro-local-data annotations, which are ignored when Coro isn't loaded.) It's neat! As I use the same code in async and non-async contexts, I really don't see me using Future::AsyncAwait and writing two versions of each module. > If you threw a github repo up somewhere I can think of a few people who might be interested I never did get that framework to a point where I'm happy to publish, because it's entangled with commercial and private code. Cleaning up the separation seemed like a goal once, but I think I'd be wasting my time now. It's a great shame when you have a big personal library of Really Useful Modules, but sometimes you just have to start again. The ideas live on!