Server polling with AngularJS
Asked Answered
W

4

86

I'm trying to learn AngularJS. My first attempt to get new data every second worked:

'use strict';

function dataCtrl($scope, $http, $timeout) {
    $scope.data = [];

    (function tick() {
        $http.get('api/changingData').success(function (data) {
            $scope.data = data;
            $timeout(tick, 1000);
        });
    })();
};

When I simulate a slow server by sleeping the thread for 5 seconds it waits for the response before updating the UI and setting another timeout. The problem is when I rewrote the above to use Angular modules and DI for module creation:

'use strict';

angular.module('datacat', ['dataServices']);

angular.module('dataServices', ['ngResource']).
    factory('Data', function ($resource) {
        return $resource('api/changingData', {}, {
            query: { method: 'GET', params: {}, isArray: true }
        });
    });

function dataCtrl($scope, $timeout, Data) {
    $scope.data = [];

    (function tick() {
        $scope.data = Data.query();
        $timeout(tick, 1000);
    })();
};

This only works if the server response is fast. If there's any delay it spams out 1 request a second without waiting for a response and seems to clear the UI. I think I need to use a callback function. I tried:

var x = Data.get({}, function () { });

but got an error: "Error: destination.push is not a function" This was based on the docs for $resource but I didn't really understand the examples there.

How do I make the second approach work?

Wong answered 2/12, 2012 at 16:7 Comment(0)
C
116

You should be calling the tick function in the callback for query.

function dataCtrl($scope, $timeout, Data) {
    $scope.data = [];

    (function tick() {
        $scope.data = Data.query(function(){
            $timeout(tick, 1000);
        });
    })();
};
Como answered 2/12, 2012 at 17:4 Comment(6)
Excellent, thanks. I didn't know you could put the callback there. That solved the spamming issue. I also moved the data assignment to inside the callback which solved the UI clearing problem.Wong
Assuming the above code is for pageA and controllerA. How do i stop this timer when I navigate to pageB and controllerB ?Dimitris
The process for stopping a $timeout is explained here docs.angularjs.org/api/ng.$timeout. Basically, the $timeout function returns a promise which you need to assign to a variable. Then listen for when that controller gets destroyed: $scope.$on('destroy', fn());. In the callback function call $timeout's cancel method and pass in the promise you saved: $timeout.cancel(timeoutVar); The $interval docs actually have a better example (docs.angularjs.org/api/ng.$interval)Crosslet
@JustinLucas, just in case it should be $scope.$on('$destroy', fn());Pugilist
What is the best way to "stop" this approach once some event occurs on my page. I really need this thread running when the user is in a certain page. Any idea?Bobbyebobbysocks
How to do same in Angular 2?Vivisect
B
33

More recent versions of angular have introduced $interval which works even better than $timeout for server polling.

var refreshData = function() {
    // Assign to scope within callback to avoid data flickering on screen
    Data.query({ someField: $scope.fieldValue }, function(dataElements){
        $scope.data = dataElements;
    });
};

var promise = $interval(refreshData, 1000);

// Cancel interval on page changes
$scope.$on('$destroy', function(){
    if (angular.isDefined(promise)) {
        $interval.cancel(promise);
        promise = undefined;
    }
});
Banker answered 29/1, 2014 at 5:23 Comment(5)
-1, I don't think $interval is suitable, because you can't wait for the server response before sending the next request. This might result in to many requests when the server has a high latency.Notation
@Treur: While that seems to be conventional wisdom these days, I'm not sure I agree. In most cases I'd rather have a more resilient solution. Consider the case where a user goes offline temporarily or the extreme of your case where the server doesn't respond to a single request. The UI will stop updating for users of $timeout since a new timeout will not be set. For users of $interval, the UI will pickup where it left off as soon as connectivity is restored. Obviously picking sane delays is important as well.Banker
I think it is more convenient, but not resilient. (A toilet in my bedroom is also very convenient at night, but eventually it will start smelling bad ;) ) When retrieving actual data using $interval you ignore the servers result. This lacks a method to inform your user, facilitate data integrity or in short: manage your application state in general. However, you could use common $http interceptors for this and cancel the $interval when this happens.Notation
If using $q promises, you can simply use the finally callback to make sure polling continues whether the request fails or not.Undershrub
A better alternative would be to handle not just the success event, but also the error event. This way you can try the request again if it fails. You might even do it at a different interval...Jugum
E
6

Here is my version using recursive polling. Which means it'll wait for the server response before initiating the next timeout. Also, when an error occur it'll continue polling but in a more relaxed manor and according to the duration of the error.

Demo is here

Written more about it in here

var app = angular.module('plunker', ['ngAnimate']);

app.controller('MainCtrl', function($scope, $http, $timeout) {

    var loadTime = 1000, //Load the data every second
        errorCount = 0, //Counter for the server errors
        loadPromise; //Pointer to the promise created by the Angular $timout service

    var getData = function() {
        $http.get('http://httpbin.org/delay/1?now=' + Date.now())

        .then(function(res) {
             $scope.data = res.data.args;

              errorCount = 0;
              nextLoad();
        })

        .catch(function(res) {
             $scope.data = 'Server error';
             nextLoad(++errorCount * 2 * loadTime);
        });
    };

     var cancelNextLoad = function() {
         $timeout.cancel(loadPromise);
     };

    var nextLoad = function(mill) {
        mill = mill || loadTime;

        //Always make sure the last timeout is cleared before starting a new one
        cancelNextLoad();
        $timeout(getData, mill);
    };


    //Start polling the data from the server
    getData();


        //Always clear the timeout when the view is destroyed, otherwise it will   keep polling
        $scope.$on('$destroy', function() {
            cancelNextLoad();
        });

        $scope.data = 'Loading...';
   });
Economically answered 10/8, 2016 at 2:2 Comment(0)
B
0

We can do it polling easily using $interval service. here is detail document about $interval
https://docs.angularjs.org/api/ng/service/$interval
Problem using $interval is that if you are doing $http service calling or server interaction and if delayed more than $interval time then before your one request completes, it starts another request.
Solution:
1. Polling should be simple status getting from server like a single bit or lightweight json so should not take longer then your defined interval time. You should also define time of interval appropriately to avoid this issue.
2. Somehow it is still happening due any reason, you should check a global flag that previous request finished or not before sending any other requests. It will miss that time interval but it won't send request prematurely.
Also if you wanted to set threshold value that after some value anyhow polling should be set then you can do it following way.
Here is working example. explained in detail here

angular.module('myApp.view2', ['ngRoute'])
.controller('View2Ctrl', ['$scope', '$timeout', '$interval', '$http', function ($scope, $timeout, $interval, $http) {
    $scope.title = "Test Title";

    $scope.data = [];

    var hasvaluereturnd = true; // Flag to check 
    var thresholdvalue = 20; // interval threshold value

    function poll(interval, callback) {
        return $interval(function () {
            if (hasvaluereturnd) {  //check flag before start new call
                callback(hasvaluereturnd);
            }
            thresholdvalue = thresholdvalue - 1;  //Decrease threshold value 
            if (thresholdvalue == 0) {
                $scope.stopPoll(); // Stop $interval if it reaches to threshold
            }
        }, interval)
    }

    var pollpromise = poll(1000, function () {
        hasvaluereturnd = false;
        //$timeout(function () {  // You can test scenario where server takes more time then interval
        $http.get('http://httpbin.org/get?timeoutKey=timeoutValue').then(
            function (data) {
                hasvaluereturnd = true;  // set Flag to true to start new call
                $scope.data = data;

            },
            function (e) {
                hasvaluereturnd = true; // set Flag to true to start new call
                //You can set false also as per your requirement in case of error
            }
        );
        //}, 2000); 
    });

    // stop interval.
    $scope.stopPoll = function () {
        $interval.cancel(pollpromise);
        thresholdvalue = 0;     //reset all flags. 
        hasvaluereturnd = true;
    }
}]);
Blumenthal answered 2/4, 2017 at 5:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.