Recently, I had a chance to work with websockets in a Single Page Application (SPA) that visualizes incoming real-time data. To build the SPA, I used re-frame, my new favorite SPA client framework in ClojureScript. For websockets, I had several choices but zeroed in on Sente (written in amazingly compact crossover code to make it work both in ClojureScript as well as in Clojure), a thin abstraction layer that takes care of detecting and re-establishing the connection and downgrading to XHR polling if needed. If you don’t know anything about re-frame, I’ll give you the basics - it’s like an event bus or a loop where dispatched UI events cause a computational change in the atomic state of the data (literally, the whole state is kept in one giant hashmap). The UI components then subscribe to react (it’s all React/Reagent based) to specific branches/sections in the state of the data. As the re-frame folks almost euphorically state, it’s quite beautiful really.
Anyhow. What’s even more euphoric, is that after some amount of coding, I realized that the signature of the Sente messages and the re-frame events are almost exactly the same. There’s really no reason for me to have an additional layer to translate a dispatched Sente message from the server before re-dispatching it as a re-frame event. I might just as well dispatch any incoming websocket message as a re-frame event, therefore making an “outer event loop” (see graph below). .
Since the project had a lot of real-time, in-memory data, I figured I can just as well use re-frame on the server as well. So now I have an atomic re-frame state going on both on the client and on the server which immensely pleases my desire for symmetry. Although, I obviously need to be a bit careful of what I stick in the server state because it holds the state of all of the clients at the same time. Not that different from any multiplayer online game though.
SPAs always have some rarely changing data bits that in classic request-response based implementations are typically fetched only once and even if those bits are altered afterwards, the changes are not refectled on the UI after the app is initialized. However, with a two-way communication through the “outer loop” (to the server and back), the cost of propagating the new state to the UI is effectively zero even with any rarely changing data, since the initialization logic is minimal and there’s no reason to aggregate the responses together. As an experiment, log on to your favorite webapp from two different devices, change your name on one and see if the change gets reflected on the other devices.
What I’m advocating is that SPAs should take another look at websockets which I feel is still an underutilized part of the HTML5 spec. Scalability issues with websockets are well understood but you should not see it as binary choice between XMLHttpRequests (XHR) and websockets but the latter as complementary to standard HTTP request/response cycles. If the communication through the “active state” of the webapp was handled through websockets instead of XHR, the programming logic would be vastly simpler. Success/error handlers for most XHR requests are like checked exceptions - you are forced to handle them but often don’t know how to make the error handling actually meaningful - or it may too expensive to handle them locally. Instead, if you establish a communication channel just when you need it, you can tear it down right after and you only need one central location to handle communication channel breakdowns. For the rest of the part-of-the-task messages, you can just optimistically assume they go through. Adding another technology adds complexity to but depending on the application, the gained simplicity in programming logic might well be worth it. Don’t you think?