Angular 2, SSE and changes detection

le 02/11/2016 par Nicolas Carlo
Tags: Software Engineering

In this post, we will make a feedback on a surprising bug we had to solve due to the auto-magical changes detection of Angular 2.

The context

We are currently working on a Dashboard-like application: a screen on which we render different tiles that display various information from an API. Tiles should refresh to keep showing the most up-to-date information. However, there is no need for websockets: Server-Side Events − SSE − make possible for the server to push new information to the client when necessary. But the number of SSE connections is limited for the client. Having a SSE connection for every single tile of the Dashboard is not an option.

Knowing that Angular 2 has adopted RxJS, we decided to go with Observables to propagate events reactively between the composing tiles of the Dashboard.

Le flux de Server-Side Events est converti en Observable dans le Dashboard. Cet Observable est filtré pour que chaque composant récupère les événements qui l’intéresse.Real-time updates with Server-Side Events and Observables

In the Dashboard component, we create a mainStream Observable that emits a new event anytime the server is pushing one. Roughly speaking, we are converting our SSE stream into an events one with RxJS:

getIndicatorsStream(): Observable {
  return Observable.create((observer) => {
    let eventSource = this.sseService
                        .createEventSource('http://localhost:8080/api');
    eventSource.onmessage = (event) => observer.next(JSON.parse(event.data));
    eventSource.onerror = (error) => observer.error(error);
});

In the OctoNumber component − a tile which role is to display a numeric value − we create a new Observable from mainStream by filtering values we are interested in. Then, we just need to give this Observable to the async pipe so Angular 2 subscribe to it and update the displayed value.

Problem is, it doesn't work.

Debugging

It doesn't work, still:

  • If we replace mainStream with Observable.interval( 1000 ) − which increment a counter every second − then it works!
  • If we log values that go through mainStream, we can actually see they are emitted. The thing is, they don't show up.
  • If another tile − let's say OctoQuote − make the view being refreshed, then our tile − OctoNumber − is correctly refreshed with the latest value too.

At this point, debugging is filled with "WTF?!?" as assumptions pop and seem to contradict each others:

  • Does the async pipe even work? It seems so when we give it a basic Observable…
  • Are server events correctly pushed in mainStream? It seems to be so. Furthermore, if we can see logs, that means we are actually doing a subscribe to the Observable with the async pipe, otherwise nothing would happen.
  • Is that an conflict between instances of our components? Well, results are the same even with a single tile…

The enlightenment

Actually, it's an automatic changes detection issue from Angular!

Indeed, if the async pipe in our template is really doing a subscribe to the Observable it gets, it is not directly refreshing the DOM. To do that, Angular has to detect a change. That's why the value gets updated when others tiles are making updates: DOM refresh make the latest value of our tile appear at the same time.

Why is Angular not detecting when we emit a new event in our Observable? Because it is executed in the EventSource: it's the "eventSource.onmessage" callback which does the job here. Angular is not notified about that!

To solve this problem, we need Angular to be notified when callback is executed. This is the role of zone.js from which Angular 2 depends. Therefore, "all we have to do" is to instantiate a NgZone and call "zone.run( callback )" in place of our callback. That way, Angular is notified when change happens.

getIndicatorsStream(): Observable {
  return Observable.create((observer) => {
    let eventSource = this.sseService
                        .createEventSource('http://localhost:8080/api');
    eventSource.onmessage = (event) => {
      this.zone.run(() => observer.next(JSON.parse(event.data)));
    };
    eventSource.onerror = (error) => observer.error(error);
});

And it works!

We can now consume mainStream just like any regular Observable in our components: the async pipe will "understand" there has been some changes when a new event occurs.

Lessons learned

From this point of view, Angular hasn't changed: it is still a black box which is doing a bunch of magic so everything works easily for everyone. But if you have a non-standard use case, you should better know all of the framework concepts so you can cleanly solve your problem… and there are a loooot of concepts.

Also, folks searching for reactive programming might be disappointed. If Angular 2 is using RxJS Observables, it's still passive programming under the hood.

This could be surprising because a truly reactive code wouldn't have cause such a trouble: view rendering would have been a single function, called as a subscribe of the Observable. No concept of zone, out-of-application detection, etc. Just a function… Which always work.

Edit of nov. 3rd, 2016: an issue to support EventSource is currently open on zone.js.

Here are a bunch of resources that have helped us figure it through, if you want to dig further: