Managing long running operation using promise and $$q

5 באפריל 2015

tags: , ,
no comments

keeping-your-promises

Through my previous post I described the challenges when implementing long running operation. Basically, you need to break your long running operation into small steps where each step is isolated by setTimeout(0) from the next step.

At this post I would like to cover a technique for using promise object to manage the long running operation.

Angular's promises offer the common then/catch functions but also offer an additional function named notify

The notify function can be used to send notifications to the client about the progress of the running operation.

For example, suppose a runTask function is responsible for initiating a long running operation and returns a promise object to represent that operation.

From client perspective, the function can be used as follow

runTask().then(
    function (progress) {
        updateUI("COMPLETED");
    },
    function () {
        updateUI("ERROR");
    },
    function (task) {
        updateUI(task.progress);
 
        task.continue();
    });

The 1st and 2nd functions are the common success/error handlers. The 3rd function is a handler which is invoked by the promise each time the notify function is called.

function runTask() {
    var deferred = $q.defer();
 
    var task = {
        progress: 0,
        continuefunction () {
            step();
        }
    };
 
    function step() {
        sleep(10);
 
        if (++task.progress == 100) {
            deferred.resolve(task);
        }
        else {
            deferred.notify(task);
        }
    }
 
    deferred.promise.then(function noop() { });
    deferred.notify(task);
 
    return deferred.promise;
}

First, we create a deferred object using $q. The deferred object is responsible for managing the state of the long running operation.

When operation completes we invoke the resolve function on the deferred object. During the operation we use the notify function to send notifications to the client. The task parameter being sent allows the client to get some information about the current status and also offers the continue function which signals to the long running operation that it can continue to the next step.

You probably wonder about the following lines

deferred.promise.then(function noop() { });
deferred.notify(task);

The second line is obvious. We notify the client for the first time. The client then calls the continue function which executes the next step … and so on.

But why do we need the second line ?

Strangely (or not, depends who you ask) Angular does nothing when notifying a promise object which has no subscribers. This is surprising since we still have not return the promise object and therefore there will never be any subscriber at that point. To overcome this we register a noop function as a subscriber. Angular sees a "non empty" promise and schedule the notification to be handled at the next tick. You can read more about this 'strange" behavior at Ban Nadel post

Here is the whole sample including controller source code:

angular.module("MyApp", []).controller("HomeCtrl", ["$scope""$$q"function ($scope, $q) {
    $scope.start = function () {
        runTask().then(
            function (progress) {
                updateUI("COMPLETED");
            },
            function () {
                updateUI("ERROR");
            },
            function (task) {
                updateUI(task.progress);
 
                task.continue();
            });
    }
 
    function updateUI(progress) {
        $scope.progress = progress;
        $scope.$apply();
    }
 
    function runTask() {
        var deferred = $q.defer();
 
        var task = {
            progress: 0,
            continuefunction () {
                step();
            }
        };
 
        function step() {
            sleep(10);
 
            if (++task.progress == 100) {
                deferred.resolve(task);
            }
            else {
                deferred.notify(task);
            }
        }
 
        deferred.promise.then(function noop() { });
        deferred.notify(task);
 
        return deferred.promise;
    }
 
    function sleep(timeout) {
        var begin = new Date();
        while (new Date() - begin < timeout) {
        }
    }
}]);

And the HTML is plain simple

<!DOCTYPE html>
 
<html ng-app="MyApp">
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <div ng-controller="HomeCtrl"> 
        <button ng-click="start()">Start</button>
        <div>{{progress}}</div>
    </div>
 
    <script src="~/Scripts/angular.js"></script>
    <script src="~/Scripts/App.js"></script>
</body>
</html>

Surprisingly, running above code (see Plunker) yields a strange behavior. The {{progress}} expression is updated only when the long running operation completes and not during the operation.

Any idea ?

The answer is simple. Although the notify function works in an asynchronous way it does not use any timeouts and therefore the browser has no chance to update the DOM during the operation.

Let's look at Angular implementation for the notify function

notify: function(progress) {
    var callbacks = this.promise.$$state.pending;

    if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) {
        nextTick(function() {
            var callback, result;
            for (var i = 0, ii = callbacks.length; i < ii; i++) {
                result = callbacks[i][0];
                callback = callbacks[i][3];
                try {
                    result.notify(isFunction(callback) ? callback(progress) : progress;
                } catch (e) {
                    exceptionHandler(e);
                }
            }
        });
    }
}

See, it uses the nextTick function to schedule the real notification. nextTick is implemented by the $QProvider as

function $QProvider() {
    this.$get = ['$rootScope''$exceptionHandler'function ($rootScope, $eh) {
        return qFactory(function nextTick(callback) {
            $rootScope.$evalAsync(callback);
        }, $eh);
    }];
}

$evalAsync function is a well documented function which was already explained by many. In case you are not familiar with it, then in general it queues a function to be executed at the next digest iteration before processing any watcher.

In our case, we click the Start button and eventually the notify function is invoked. The button click is executed under $apply phase which means that the $evalAsync queues a request and once the digest phase is entered the request is executed.

Here is a snapshot of how Angular iterates over the async queue (inside $digest function)

while (asyncQueue.length) {
    try {
        asyncTask = asyncQueue.shift();
        asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
    } catch (e) {
        $exceptionHandler(e);
    }
    lastDirtyWatch = null;
}

The nice thing is that $evalAsync pushes new requests to the end of the queue while popping requests from the beginning of the queue (FIFO order). This means that notify function queues a request which is evaluated by above loop without entering a second digest iteration.

In case you lost me, then all I wanted to say is that our runTask function + Angular's notify function are executed inside a loop and therefore the browser has no chance to update the DOM.

The interesting part is that Angular offers an undocumented service named $$q. This service uses setTimeout(0) to schedule notify request. See yourself Angular source code:

function $$QProvider() {
    this.$get = ['$browser''$exceptionHandler'function($browser, $eh) {
        return qFactory(function(callback) {
            $browser.defer(callback);
        }, $eh);
    }];
}

$browser.defer is a small wrapper around setTimeout(0)

We can easily switch from $q to $$q and the progress indication now refreshes as expected.

See Plunker for the fixed code

Enjoy, …

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*