1) The document discusses updating an Ember application in real-time by connecting to a live events socket to receive a mixed feed of all subscribed events and manually register for chat channels.
2) An initial attempt at going real-time had issues with staying connected to an unpredictable service, updating with messages sent while disconnected, and maintaining state on live chat channels.
3) RxJS was used as a solution but introduced complexity, though it transformed observable sequences into the most recent one and addressed issues like staying connected and updating models.
5. • Everything should be real-time… eventually
• Connect to a live events socket
• Receive a mixed feed of all subscribed events
• Manually register for chat channels
• Forced disconnection every minute
Going Real-time
SERVICE CONSTRAINTS
7. • Staying connected to a unpredictable service
• Updating with messages sent while disconnected
• Maintaining state on live chat channels
• Logic for all event types lived in one service
• Slowly building an app-wide state machine
It Worked! But…
KEY ISSUES
9. RxJSA complicated solution to a complicated problem!
“[…] transforms an observable sequence of
observable sequences into an observable
sequence producing values only from the
most recent observable sequence.”
🤔
Hi everyone, I’m going to give a quick talk today on how Showbie is handling real-time updates in our Ember app
Showbie is an app for teachers in 1:1 classrooms — where every student has a device. We make it easy to share files, add written or audio annotations, grade, and a bunch of other neat stuff.Showbie is a _very old_ Ember app — we started with version 1.6, but our team has worked really hard to keep it up-to-date (mostly…). It’s built exactly as you’d expect, using Ember Data to connect to our service and retrieve data as users navigate through routes.
Recently we added two great new features: group chat, and class discussions! And suddenly, we had a problem… can anyone guess what it is?
It’s that refresh button. Do you know what is _no fun_? Clicking to refresh the feed in a “live” chat. It is a terrible user experience (that we totally launched with)
So clearly, we had the need to go real-time, and that effort was driven by our hard-working and talented, service team! Unfortunately, they had some requirements that made this “not that easy”.
We set out and built a proof-of-concept using Ember Concurrency… but it was quite a bit of work
Ideally, it would be impossible to register for multiple groups…
managing subscriptions to groups would have to be handled manually… tied tightly to entering and exiting groups → difficult to enforce single-group subscription
difficult to extend + add handlers for different events
So we ended up coming to a somewhat surprising solution…
Yeah! We had a hard problem, so we threw an impossibly complicated library at it! Has anyone here ever read up on RxJS? If not, you’re missing out on sweet, helpful stuff like this:
Yeah, I swear that makes sense.
Anyways, Rx has some quasi-impenetrable documentation, but once you start getting a feel for it, you realize that it’s not _too_ different from the concepts we already know from working with Ember.
So Rx has “Observers”, but instead of watching for property changes, they’re a lot more generic. Observers just watch for Events, and emit them in streams. In Rx, _basically everything_ is either a stream of events, or a subscriber to an event stream.
The great thing about Rx Observers, is that the API feel really similar to Array operation. It lets you write code that feels synchronous, even though it really, really isn’t.
So let’s take a quick look at the problems we faced, and how we solved them with Rx
So as we discussed, our service hates us, and likes to do fun things like disconnect us without letting us know every minute.
Let’s take a quick look at how we handled this with Ember… we created a firehose service to connect to the socket and fire off events… and handle all of the reconnection logic dictated by our service team.
And now let’s take a look with Rx.I know this is dense, but it’s not _too_ scary! It’s functionally the same as the previous example, but it’s a lot more concise. Let’s walk through it step by step.
We start by creating an Observable, which I prefer to call a “stream”. This will connect to our web socket, and start receiving service events. Every time our service emits an Event, this Observable will forward it on to all of its subscribers. (We’ll get to subscribers in a little bit)
Now, if an error happens with our websocket connection, `retryWhen` will be called with a single parameter, which is an Observable stream of errors
What `retryWhen` needs back is an Event telling it to try to connect again… so let’s figure out how to get it one!
Here, we do a neat thing. The `zip` method combines streams sequentially.
So we’ll combine our error stream with a numeric range 0–10…
If you take a look at this marble image from rxmarbles.com it helps illustrate what’s happening here… you can see how an event has to fire from each stream before it’s propagated. Also note that “3” and “C” are matched, even though “D” fires before “3”.
So back to the code: the first time an error occurs, it will be matched with the first number from our range (0), the second time an error occurs, it will be combined with the second number from our range (1) and on and on until 10
The last argument is a function. When the last argument to is a function, `zip` will emit a value from the combined streams.
In this case, we’re using it to count how many times an error has occurred trying to connect to the websocket
Now we can chain on the `zip` with a `flatMap`. The `flatMap` lets us transform the values from `zip` back into a stream. In this case, we use that value to create a timer that emits the event, which will kick off `retryWhen`!
This is really the short, short, short version, so sorry if I’m rushing a little… but let’s keep going!
We’ve established what it takes to connect (and stay connected) to the live events service, now let’s move on to actually updating a model
Let’s add a method to our firehose service.Well take the stream we set up earlier, and extending to make a stream just for our group chats…
that multiplex stuff is _super neat_. The first param is set to the service when we connect (registering a group for messages), the second when we disconnect (unregistering a group from messages), and the third is used to filter the incoming events (ensuring only messages for our group are sent). It a single call we manage to knock off a whole bunch of service requirements!
We have a `LiveRoute` mixin, that handles building and disposing our live event stream on `setupController` and `resetController`. It requires us to define a `setupFirehose` method. That’s where we subscribe to the stream. So that method is going to get called with every single event sent by the service.
And what does that data look like?
BOOM! A big, gross pile of JSON
But we only really care about these bits… So `event` tells us what the service wants us to do… That data looks an _awful lot_ like the data that would be returned by a GET request to our endpoint…
So now let’s go back to our route, and actually handle that data! Subscribe needs a method that’s going to handle that data.
So we create a Object we call a reducer, pass it the store, then send it all of our live events.
The reducers job is to take the all of the events, and proxy them to the proper method call.
And the reducer is really simple! It checks for a method that matches the provided event name, and if it exists, passes it the data and the store!
So did you follow all of that?