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.
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.
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.
It doesn't work, still:
At this point, debugging is filled with "WTF?!?" as assumptions pop and seem to contradict each others:
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.
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: