Wednesday, March 13, 2013

Angularjs - $q, $q.all - Using deferred objects to avoid race conditions

Introduction

Since working with Ajax apps and even more so with a combination of Ajax and video players, my team has been plagued with race conditions. If the id of the video to be played is loaded via Ajax and we also need to wait for the player to be ready, how are we supposed to know which one will complete first?
We resorted to flags that only become true once both are complete. But this is an annoying task, very error prone. Deferred/promise objects are the (a) solution to this. Granted, deferred objects are in no way specific to Angular, but their implementation via $q is simple and extremely useful. In this simple app, we combine the loading of a YouTube player and a (fake) Ajax request  to load the actual video id to be played. Code can be found at: git@bitbucket.org:mchauvinc/referredtutorial.git

Note

We are still using an older version of Yeoman, so you will need to adapt the scaffolding to your own version ("yo" instead of "yeoman" in newer versions I think).
Also, we faced some issues with outdated FF Flash plugins, so this is best tested on Chrome.

Scaffolding

Using the extremely convenient Yeoman, let's put things in place. From a console:

yeoman init angular deferred
yeoman init angular:service playerService
yeoman install swfobject

You'll need to add the swfobject script to your index.html.
We'll stick to the "main" view and controller for this simple example. The service will create a swf object and return a promise object. It also defines the onYouTubePlayerReady callback which will be called by the YouTube player, when it is ready. Since the playerService factory is only called once, this should create the function only once (to be tested).

Broadcasting from $rootScope ensures that all children scopes will receive the event. In this case, the main controller's scope will be listening to it.

Service

The service does two things. Create the onYouTubePlayerReady function (once) and return an object that can be used to create (via the "create" function) any number of players which will call the onYouTubePlayerReady function when they're ready. Note that the we are loading a generic player, without any video.

Controller

In the controller, we call the service to create a player. The player variable is initialized without value, and will only be set to the <object> when the player is ready. We also initialize an empty videoId variable which gets set when the data is loaded via the Ajax call. We're faking the setting of the videoId, it does not depend on the actual result of the Ajax call.
Note the $scope.$apply call when resolving the deferred object. This is necessary because the YouTube event is "outside the world of Angular" and so a digest needs to be triggered. Though it still sometimes feels a bit magical, there is a discussion about this on the Angular 1.2 presentation video.
We then combine the two promises httpPromise and ytQ.promise into one using $q.all. It returns a new promise object, which will only be resolved once the two promises are resolved. The results variable in the callback function contains the resolve values of httpPromise and ytQ.promise, in the same order as the promise objects themselves.
By doing so, we ensure that the then callback will only occur after both deferred objects have been resolved, thus solving any form of race condition. By doing so, we make sure that when calling player.loadVideoById(videoId), both player and videoId have been set to their respecting value.
Here's the controller:

View

Using just the main view, we simply show a progress of the app's state using a messages array. Things happen in sequence, as they are expected to.

Conclusion

Deferred objects are not an Angular invention. Actually the $q implementation is clearly based on Kris Kowal's Q implementation. In any case, it allows for a simpler and less error prone solution to race conditions among other cases. $q.all is not limited to 2 promises as seen in this example but can accommodate more promise objects. 

No comments:

Post a Comment