The core problem of web UIs has always been managing state. People like statically-rendered pages because most state gets kept in a) the URL, and b) transient HTML elements, and both are highly visible and easy to deal with in robust ways. When you're forced to have server-side state it's exceptionally painful, but people usually just avoid it altogether which in most cases pushes things towards the virtue of having less state anyway.
For complex UIs with lots of little interactions the above story doesn't really work, so we've invented state management systems for the client. Managing complex state still sucks, but it sucks less with these tools.
Sounds like this approach is just moving that state store back to the server, with a greased pipeline so it isn't so horribly painful to add new pieces of state even though both sides are involved. But you'll still face all the same problems you face with a front-end store: transientness, cache-clearing, even modeling is hard when you're dealing with a big blob of mutable state. And there will be the additional challenge of associating this stuff with a user (which you get for free on the front-end, even with modern state stores).
> Let the browser maintain a DOM tree that templates over a JSON object, then reactive-ly update and it's an amazing experience. You have minimal data transfer, and minimal rendering updates. There is a great deal of power in having your UI be a stateless function over a giant JSON object.
> this is where I have my shitty idea of a programming language for board games
It's funny, I started building a web-based board game to play with family during quarantine, and I landed on this exact approach totally by coincidence :) I'm using polling requests instead of websockets because I'm lazy/it doesn't need to scale, but it's the exact same idea where the server just publishes a giant JSON object of state and the clients are pure functions of it (and send messages to the server to mutate it). I wonder if there's some aspect of board games that pushes one towards that line of thinking.
I think the key thing here is figuring out how the UI can be made mostly declarative and a stateless function of the state. I'm drawing some inspiration from
https://flutter.dev/docs/development/data-and-backend/state-...
and the challenge is the polish to make products great. My strategy is that I'm going to make a UI editor so I can make faster progress on designing the board game UI and then build a minimal set of controls to do data binding. The key is to get into the fight about what is the polish needed, and what reasonable escape hatches are needed.
> I wonder if there's some aspect of board games that pushes one towards that line of thinking.
There is, they are exceptionally complicated!!! SO COMPLICATED! The key is the implicit state machine between all the players, and you just can't quickly model it with traditional tools and databases. All it takes is a single card or game piece (which changes rules) to invalidate EVERYTHING. Games are exceptionally messy!
Thus, my processes clearly has to sustain 100k+ connections. And, at a moment's notice, I should expect the process to go "poof". Maybe someone is going to do a kernel upgrade or someone decided to unplug a machine, I don't know!
Having operated with the above assumptions, I found it hard to be wise to avoid that and then invented a programming language that would operate in a modern cloud environment.
I need better hobbies, or not... who knows.
So, XSLT for JSON basically. I like it.
I like this approach as well, but I do worry that users today expect optimistic updates on the client, and the injection of a X00ms delay would make the app feel sluggish.
Only state shared between multiple clients should go to the server.
In server-rendered app, there's nothing like UI state.
There's a request. This request and the database is enough to build response. There is no UI state. Your app can be (and often is) absolutely stateless and restult depends only on request and a state of the storage.
While when you have SPA, API & server, you lose the application state and it's not possible to store the whole app state in the client for many reasons (security, scalability, synchronisation...). So you have to store subset of it. And you have to build a machinery that will do all this synchronization so your states stay in sync. It's double the effort to get 5% improvement.
Of course there is.
For example, any state related to interactivity like forms, client-side validation, widgets, filters, etc.
Basically, I'm happy with my latency (for board games) which I hope to migrate closer to people as more edge compute offerings emerge. However, I'm wondering what the path is to find some of these ideas in more latency sensitive applications.
The explanation for how the net-code behind Overwatch is a good explanation: https://www.youtube.com/watch?v=W3aieHjyNvw
and I'm just trying to figure out how to build the reactive tree in a way to support it sanely.
1) Nothing bad can happen if we show some optimistic result before server returns, but server side will end up in different result.
2) Something bad can happen. Accordingly, for (1) we can do optimistic update, for (2) can't
Think about the nature of a document store, you put data in and data out. You gain concurrency by using compare and set such that multiple requests can compete, but this is expensive. First, you risk conflicts which require re-execution for the losers. Second, you must download the entire object, make a change, then write the entire object back.
The key to what I am doing is that I'm having the language take a message from a durable queue, integrate that message into the document via some code, then emitting a state change out. The client then subscribes to the document's parts to updates its UI state, and as the document changes, so does the UI.
The UI can be much more complicated while the server can be beautifully simple.
It sounds like the one I was working with ~18 years ago.
Except it used HTTP (long polling or comet) instead of HTTP/3 for the transport. And more partial rendering to DOM-diffs on the server side because it was faster than doing it all on the client, but the client still applied diffs.
The issues with full DOM state synchronisation, handling network errors and recovery, transport of templates and data, minimisation of transport subject to maximising responsiveness, some amount of anticipatory prefetching, were much the same then as now.
A lovely thing about all this stateful streaming cleverness is you end up building a mostly stateless, functional-reactive model on top of it again, that ends up extremely robust.
It's just faster, and it can react immediately to server side data changes as well as client side. And that model can be declarative, it doesn't particularly need to be in JavaScript despite running on the client.
Then other browsers copied it, adding their own quirks. The XHR standard wasn't created until about the same time as IE8 was released. So it's not possible for IE before IE8 to break the XHR standard, as there wasn't an XHR standard before IE8.
Long-polling is still used as a fallback today. The modern alternative is WebSockets, but they do not work reliably in every network environment, either at the client or server side.
Last time I looked, Facebook and Gmail were using long-polling to good effect.
Despite the conceptual ugliness, long-polling when implemented properly works quite well and is extremely reliable. In practice, "quick and dirty" long-polling is not always implemented well, but it's not hard, just takes a little thinking about unreliable networks.
XHR has nothing to do with long-polling particularly, and it doesn't remove the need for long-polling either. Long-polling is usually done with XHR these days, but it can be done easily without XHR, using invisible iframes or dynamic script elements. The latter is called JSONP and you've probably heard of it. SSE can also be used.
All of these things have nothing to do with HTTP/1.1. They work perfectly well over HTTP/1.0, 1.1, HTTP/2 and HTTP/3.
Totally agreed. I even pulled off an Elixir library as a POC to show this concept, here's an example project: https://github.com/surferseo/live_data/tree/master/examples/... (most relevant part of API is here: https://github.com/surferseo/live_data/blob/master/examples/... and here: https://github.com/surferseo/live_data/blob/master/examples/...)
Isn't that Firebase DB?
The key thing that I can do is await on players decision, so I can block the entire process for multiple people to do things.
Interesting that you're exploring this idea in the context of board games. I implemented[0] a similar idea while making a browser-based magic the gathering interface. Basically send commands to update the server state (a big JSON object), and the server does state diffs per client and sends lz compressed JSON patches. Worked quite well, but I haven't played with it for a couple years.
I think a lot of the "best-practices" for SPA is trying to do this. But as streaming the data transfer is not yet common, the data is abstracted as a giant JSON object to be a parameter of the UI as a stateless function (React+Redux/MobX/etc, Vue+VueX). When enforced correctly on the project level, what's complicated _is_ syncing the data to the server. I think you'll like Supabase's subscription approach to reactively bind the JSON for the client.
I think a separation between the info-web (document-content heavy) and the app-web (needing stateful data sync) will be more clearly defined in the next few years. Nobody likes trying to sync data between the client and server over HTTP calls; we just have to wait for streaming to be the norm for the app-web (and native apps, ofcourse).
It is indeed a very powerful and reliable way of ensuring synced state.
The UX back then was horrible, however technology has come a long way since.
It appears to work well. Security was tough work though (at least I thought it was). And scaling up, no matter what tech you have access to, can be costly.
Also, determining how to handle disconnections is quite an interesting UI problem. Auto reconnect might be an option, but then you need to think about a retry policy. And it can be harder than you think... Mobile phones drop connections with surprising frequency, so it's worth thinking about very early on.
The scaling up is a challenge due to the lack of common investments. It can be made absolutely cheap, and it is way cheaper than polling.
People that dismiss websockets have a point that it throws away decades of wisdom, but the winnings and potential are there. The challenge is curating the right ideas and educating everyone... is hard.
I agree with this and it's why I believe that Phoenix LiveView is the best solution to this problem. The programming model and tooling that you get with the BEAM (along with things Phoenix provides like PubSub) are the best there is to manage this complexity.
biff implements this as a framework, backed by the Crux bi-temporal DB. It natively uses EDN rather than JSON, but emitting JSON should be trivial if that's needed.
Something like this: http://jasonette.com
This is the approach we are building in https://braid.org. We are extending HTTP from a state transfer protocol into a state synchronization protocol, so that when you do a GET request, you can also be promised to receive all updated versions of that resource. This extends very nicely with software that binds the resource directly into a DOM element.
I'm making a chat bot thing (https://www.brian.bot/) and have added socket-based web chat... but I don't even know how to phrase the question of "how do I horizontally scale the socket connections".
Webchat <-> Server <-> Slack. Once I have more than one server dyno (heroku), then I can't ensure that the Slack event hits the dyno with the corresponding socket connection.
Thoughts / tips?
Your best bet is to use Amazon and then leverage their ELB offering with sticky routing. HOWEVER, now you have a new problem as capacity dies, gets removed, cycled, added, etc. Sticky routing was designed with the assumption that it was an efficiency play as opposed to a deterministic play.
What you will see happen is that your internal state will be split brain, and people joining the chat will not be globally consistent. Some people will be on host X while some will be on host Y. So, you have two choices. You could use a database to provide the global state, at which point now you have a whole bunch of problems. You can start with polling the database, and the key advantage of the websocket is moving the polling from client to server, so that will be a win for users. However, it will cost you. Second, you could introduce a message broker, but then you have the problem of how to initialize the state. Ultimately, everyone ends up with a hybrid database for storage and message broker for real-time.
In your model, the database becomes like Slack, and your Server becomes a proxy. This could be super neat, but what is the value-add of the Server?
The interesting thing to observe is that the solution is how to turn your front-end app into a database like thing. If the load balancer could route traffic like a database traffic controller thing, then you can put all your state in the front-end and move crazy fast. However, you will move so fast that you will fuck up your state. State is VERY HARD and exceptionally unforgiving to manage yourself.
There is great wisdom in using a database with versioning and all that. It’s a great abstraction having lasted so long. I’m working on the state bits and trying to figure out the people side of discipline with my silly language, and I have some novel contributions to provide over the coming years.
There are some linked pdfs on http://www.adama-lang.org/blog/some-thinky-thoughts-2021 which may provide some more insight.
Short term, I think I can get by with switching from Slack's events API to their real-time widget - aka, the server gets a socket to both the web chat, and the slack.
Long term - probably the Redis pub/sub with the socket-receiving server split out as a microservice (which normally I'd be "eh" about, but this seems perfect).
I currently don't have to worry about multiple people on the webchat (outside of needed features) and even once that's an important thing, I doubt I'll need to worry about exactness. As long as each message get delivered reasonably quickly, and to everyone, should always be good.
hmm, hmm hmm - great stuff, thank you!
When you need to send something through the websocket then publish a message via redis and have handlers in the code that are subscribed to that channel so every server can check if they have the websocket connection and whichever one has it will send the data through
Perhaps you or someone in this topic might be interested in my little project[1] fit for innovative ideas like this. It is not very far from being available for experimental use in place of JSON. A future direction is experimenting with replacing HTML[2].
Have you used HTTP/1.1 comet-stream? I recommend you look at this sites source before you commit further: http://fuse.rupy.se
We are in the dark-ages with respect to streaming sockets, and I am the architect of one of the worlds largest WebSocket and streaming services.
First off, unseating the operational benefits provided by request response is a huge challenge. It's possible, and I've done it. One of these days, my co-authors and I will present an OSDI paper with the broad strokes. It's very easy to list a lot of the challenges with scaling WebSocket, but each of them can be solved.
I believe we will have a nicer world with QUIC --> Http/3 and the up and coming WebTransport discussion. SADLY, that will result in a decade long "caniuse" issue, so we should expect to see WebTransport polyfills to arrive...
Second, HTML over WebSockets is nice, but what is better is reactive data binding over the socket. Let the browser maintain a DOM tree that templates over a JSON object, then reactive-ly update and it's an amazing experience. You have minimal data transfer, and minimal rendering updates. There is a great deal of power in having your UI be a stateless function over a giant JSON object.
Finally, the big challenge is dealing with the server state in a meaningful way. The exceptionally nice property of using request response for everything is the shared-nothing stateless nature of the web server. It's exceptionally forgiving in both operational and developer issues.
It's very easy to fuck up the implicit state tied to the connection, and this requires discipline to get right. It's very easy to ship unreliable software that then has issues based on the connection.
Now, I'm a HUGE believer in that we need to use streams for everything, but it requires a mind shift. The key thing to tackle is dealing with the server side state, and this is where I have my shitty idea of a programming language for board games. In essence, I am building a DIY database such that people connect directly to the database and get that sweet giant JSON object along with a stream of updates.
The DIY part is related to the fact that I invented a programming language to transact on the JSON object such that you get all the benefits of durability with the mental model of working within a single machine.
http://www.adama-lang.org/