Websockets
Full of potential, but with inconsistent browser support, websockets often engender intrigue, skepticism and finally despair when surveying the frameworks that attempt to bridge implementation gaps and provide new abstractions. Thanks to Peter Taoussanis, Clojure programmers now have a very comfortable solution available in the Sente library. Sente is mature, well documented, lightweight, works over a secure connection, and falls back to ajax for browsers that don't support websockets.Change
In a connected app we no longer need to remain content with querying the server for new data. The server can push changes to us as they occur, providing immediate updates to the user about their current interest.Clients connect to the server. The server calculates the view of the system state that should be presented to the user. The view is transmitted to the client.
The server calculates new views in response to system state changes, performs a diff against the last view sent, and sends a patch of the change. The client applies the patch to its model, which is rendered reactively with om. As the user moves through the application, app-state such as the current page route is sent to the server which can affect the view the server calculates.
My naming of components favors context over consistency, as I tend to think about the data in either a client or server context.
Server Context:
- view: A limited portion of the whole systems data that is appropriate for a user.
- app-state: Data about what the user is currently doing. A server app-state is a client pose.
- system state: Any data that the server has access to.
- model: Semantically structured data that a client UI needs to convey to a user. A server view is a client model.
- om-state: State that is used in rendering the UI but is of no interest upstream. Lives in the IRenderState owner, manipulated with set-state.
- pose: UI state that is of interest upstream. Equivalent to app-state without the model. A client pose is a server app-state.
- app-state: The model and the pose combine to be the app-state, which is an om root watched for change and causes updates to the browser dom.
On the server views are calculated from system state and app-state. Watches on system state trigger views to be recalculated. Views can depend on poll-only sources such as a database. Poll-only data may be time to live memoized to avoid consuming resources. A timer can also trigger view recalculation.
Everything
Rather than defining events and crafting state transitions, I can focus on the structure, content and context of the data. As for how that data moves, get it right once and it will work for any data.For diff/patching, there are some interesting tradeoffs to be made. Structural diff/patch by updating nested key/values is very convenient. Sequential diff/patch is more general and there is better library support for it, but is not a good fit for maps where the key value sequence ordering is not guaranteed.
I like Clojure's built in diff, but sadly there is no accompanying patch function. Rolling our own is not a problem of course, and boils down to something like this:
(defn patch | |
"Updates a map by removal of keys and addition of values." | |
[m [remove add]] | |
(deep-merge (strip m remove) add)) |
Here's a tiny library I wrote called patchin which does the job for nested maps and sets.
At first I thought this style was sure to break down in the face of true events. Surely we need events? Let's consider a chat program, when someone posts a message, that's got to be an event! Or does it? What if we define our view as being the last 10 messages that were chatted? After all on our chat client we are going to display these lines like that. So far every time I've wanted to create a custom event, I've discovered a stronger representation by making an explicit addition to the model instead.
Another plus is that with the view code living on the server, view tests are much easier to think about and write. For a concrete example, take a look at the Pairwell view tests. I can be confident that so long as the views are constructed correctly, the client model will be correct, and the UI coding is very straightforward rendering.
Conclusion
Connected apps provide an opportunity for simple and immediate state management. Separation of server, client, and UI state helps identify and facilitate the flow of data. Diff/patching streaming is convenient in data oriented languages like Clojure/ClojureScript. Thank you for reading my thoughts on connected apps, I glossed over many technical details to focus on the architectural concepts, so please leave a comment if you are interested in this approach and have questions.