Things to consider when adding websocket updates to Ember apps

By Chris Westra on 14 07 2016

It sounds innocuous: "...and if another user edits that thing, we'd like for other users who are looking at the same thing to see that update without having to refresh the page."

Once your app starts responding to events from an external source, you're likely to run into a myriad of issues you would never have to consider previously.
In this post I'm going to share some general gotchas I've encountered along the way in the hope of saving you some time in the future.

The Technical Side

When I say "websocket update" I'm really talking about
"a message/event your application received that results in a state change in the app". In broad strokes it'll boil down to something like:

websocket.on('message', function(data) { doStuff(data); })

The actual implementation of websocket isn't important for the scope of this post. You could use a websocket client like socket.io, you could have a Phoenix Channel, it could be Server Sent Events through via ember-pusher, whatever. I'm talking about what doStuff is doing.

Said stuff will probably end up modifying a model in the store, if you're using Ember Data. Your app might receive a message that says "this model changed, go reload it", or maybe you'll decide to add the new model data directly to the message and then call store.pushPayload() with the updated data. This brings me to my first caveat.

Local State is Tricky

If you have a model in the store that has unsaved changes, you'll need to be aware of what happens when that model receives an update. New data from the server won't overwrite any unsaved attributes (the ones you've defined via DS.attr) on an existing model. Unsaved relationships, however, could act a little funny.

A guided tour

This twiddle will allow you to make some local relationship changes and then try to overwrite them with server-sent updates.
You'll see that dirty attributes will always take precedence over new information from the server.

Unlike attributes, relationship updates from the server will conflict with the local state of your model's relationships. If your app has users modifying relationships and leaving their respective models unsaved for long periods of time you'll need to account for it.

So you push an update into the store, but it doesn't show up. What happened?

Your computed properties might need tweaking

The way that data flows through your app might need to be changed so that your pages will work with arbitrary updates to the store. If you've been manipulating data in places like setupController() you're going to have
to move the logic somewhere that can act when the store's contents change. Ideally most of your
components shouldn't have to care if their data changed from a websocket update or a route transition.

You'll also have to deal with new edge cases. Take, for example, the humble <select>. What happens if the currently selected option is removed by a websocket update? These kinds of edge cases are everywhere and they're specific to each app. If you haven't cared much about Data Down Actions Up you'll be singing its praises soon. It's much easier to reason about how a component's state can change if you only have to consider its attributes in the template.

Local state isn't the only thing that might bite you. You'll also have to be aware of possible timing issues.

Timing is everything

Pushing data into the store is going to introduce you to another class of problems: timing. Even if you're reloading models in response to an update you'll see them. It'll
happen in both production and in testing.

Attempted to handle event 'didCommit' on
<cat-trader@model:cat::ember1083:3c3c7257f2884c37b54c8329f8b4bee8>
while record was in state 'inflight'

I've become very familiar with error messages like that one.

Ember Data's models are state machines that have a finite number of ways to go from one state
to the next. If user A is in the process of saving a model and you receive a websocket update
from user B before the save has completed, Ember will not be happy. In the apps that I've worked on we've implemented some way
of queuing up the incoming update until the record has finished its pending save, either by
polling or subscribing to the record's didUpdate event. If you can get by with simply reloading records via record.reload() rather than pushing new data directly into the store you'll also be able to mitigate some timing issues, but reloading a record could be suboptimal in some other cases.

At least one more timing-related case bears mentioning. Say you have a page for creating widgets.
The page can have 5 widgets, max.
There's an 'Add' button that's disabled if there are 5 widgets on the page; with 4 or less the user is
free to add more. How many ways can the whole thing go wrong? In the case of the widgets, even if you
validate the number just before saving on the client side there's always the possibility of a race
between multiple users. If you've taken certain server-side validations for granted now is the time to add them.

Automated testing is essential

You're going to be incredibly thankful that you have tests for websocket updates. Automated tests,
not "I opened the admin page in two tabs and added a user and it shows up on the other tab."
If you end up missing test cases early on you'll likely end up filling them in later as regressions pop up.

The User Experience

Websocket updates can cause a lot of technical surprises for developers, but without careful consideration
they can bring just as many for the user. "Did that field just change?", "Where did my data go?",
"Why can't I save my changes?". We've found it's incredibly important to consistently notify the user
that live updates are happening, for both their sake and the developers'. It's possible a websocket update will put the user's page into an unrecoverable state. What happens when the widget the user is editing gets deleted by a websocket update? Take time to go over these kinds
of cases and to decide when it's worth it bail - redirect, show a message asking them to reload, etc.
Technically anything is possible, but some solutions are going to be exponentially more time-consuming.

Power for a Price

Some user experiences are made on our ability to update the page's content with them having to refresh. Websocket updates allow us to do really cool stuff, but at the cost of extra complexity that everyone on the team has to contend with. Hopefully I've given you a head start towards making something ambitious!