Here is example code that batches aync ajax calls into blocks of 5 and waits before calling the next set, without blocking the UI thread.

One of the things I needed to do was to update a load of data in may page, but rather than fire off loads of ajax update calls and let jquery/the available browser threads just go at it, I wanted to fire off small blocks of calls to update my data and on success continue to the next block and so on until completion.

I could have just done synchronous calls and waited for each one to complete, slowing down the overall process and blocking the UI thread, well that was out, a set size of 1 that blocks the UI.

So I had to discover how to call sets of async calls and wait for them all to complete and then continue with another block,  you cant just do a for loop, it all has to managed with promises.

The key to the processing loop is recursion and not a for loop, because the processing of the outer loop has to be contained within the promise system and loops well you can’t.  Once I’d got that figured out the rest was a case of utilising the $q.all() and the $q.then() functions to manage single and multiple promises.

forget the structure of the code I need to move the manageasync function out of the Service.

So here is the outline of the algorithm its in angular but could easily be adapted to jQuery with say the $q  library.

'use strict';

//In my original which this is hacked to pieces from I was using ngGrid and Bootstrap
var app = angular.module('MyApp', ['ngGrid', 'ui.bootstrap']);

//You will need $q it does the magic
app.controller('MyController', ['$scope', '$MyDataService', '$q', function ($scope, $MyDataService, $q) {

	$scope.status = "";
    $scope.inProcess = true;
    $scope.needProgressBar = true;

    var myData = {
        items: [],
    };

    $scope.myData = myData;

	$scope.initialise = function initialise() {

        $scope.status = "Fetching Data";

        //I havent documented getMasterData but it fills the Items up with stuff
        var promise = $MyDataService.getMasterData($scope);

        promise.then(
    		function (data) {
    			//put status and or progress indicator completion
    			//set the data
    		    $scope.myData.items = data;
    		},
			function () {
				//set status and or progress indicator on fail
				//if this bit fails well nothing else is gonna work
			}
	    );
    }

	//This is called from an event that starts the managed Async Calls.
    $scope.callAsyncCallManagement = function () {

    	//I made these things up but they are possible things you would do
        $scope.status = "Processing";
        $scope.inProcess = true;
        $scope.needProgressBar = true;

        //This is the Start of it all, we have to make a Call into a Recursive function in order to manage
        //blocks of async calls

        //Remember as all of this stuff is async, this function call will not block, it will run thru
        //and you are in the hands of the promise gods.
        //This ensures that the UI thread isnt blocked
        $scope.recurseCall(0);

    };

    $scope.recurseCall = function (i) {

        if (i < $scope.myData.items.length) {

            item = $scope.myData.items[i];

            //Call into the function that calls other async functions, but 5 at a time
            $MyDataService.manageASetOfAsyncCalls($scope, indexOffset)
			.then(
				function () {
					//Here is where you can calculate progress
				    $scope.progressCurr = Math.floor((i / $scope.myData.items.length) * 100);

				    //OK so the manage function completed all its sub calls sucessfully,
				    //We know that it is doing blocks of 5 so we increment the ordinal pointer by 5
				    //and call into this function again so that we can do the next block

				    $scope.recurseCall(i + 5);
				},
				function () {

				    //At this point something broke, so stop dont process any more sub things
				    //maybe clean up the mess which could be tricky perhaps a status on each item is required
	                $scope.inProcess = false;
		            $scope.needProgressBar = false;
		            $scope.itsBorked = true;

				});
        }
        else {
        	//yay we are all done, remember, its ok to do this here, because
        	//only when we hit the end of the processing will this get called because
        	// of the .then()
            $scope.inProcess = false;
            $scope.needProgressBar = false;
        }
    };

    //run Initialise process at beginning
    $scope.initialise();

}]);

app.service('$MyDataService', function ($q, $http) {

	this.getMasterData = function ($scope) {
		//get some data and return it, you can figure this bit out surely

	}

    this.manageASetOfAsyncCalls = function ($scope, indexOffset) {

        var ajaxCalls = [];
        var deferred = $q.defer();

        for (var j = 0; j < 5; j++) {
            //Handle the fact that the list may not be a multiple of 5 🙂
            if (indexOffset + j  < $scope.myData.items.length)
            {
        		//This Executes a block of 5
            	ajaxCalls.push( this.anAsyncCall($scope, indexOffset + j) );
            }
        }

        //I love this, Wait for all of the async calls to complete before telling the parent caller
        //they are done or borked, if even a single sub call fails the block is marked as failed
        $q.all(ajaxCalls)
        	.then(
        	  function () {
        	      deferred.resolve()
        	  },
	          function () {
	          	//depending on the reason you may or may not want to reject here, it depends on the status
	          	//of each processed item, and what you want to do about it. or check the resulting value of the all call
	          	//which holds the array of promises
	              deferred.reject();
	          }
		);

        return deferred.promise;

    }

    this.anAsyncCall = function ($scope, indexValue) {

        var deferred = $q.defer();

        //Some Soap request (change for rest and use $http if you like but change all the relevant bits)
        var soapEnv = "A Soap Request" + indexValue ;

        $.ajax({
            url: "/sites/AFancyURL/_vti_bin/lists.asmx",
            type: "POST",
            async: true,
            dataType: "xml",
            data: soapEnv,
            contentType: "text/xml; charset="utf-8""
        })
	    	.done(function (xmlResponse) {

	    		//Do some processing here, likly update values on the scope and set it as Dirty

	    		$scope.MessWithMyValues[indexValue] = "true";

	    	    $scope.$apply(deferred.resolve());

	    	})
	    	.fail(function () {
	    		//Perhaps a log of fails rather than setting the main one here, as lots of them could fail and
	    		//a better way is probabyl inspect the returned promises from the $q.all call
	    	    $scope.MessWithMyValues[indexValue].status = "Error failed";
	    	    $scope.$apply(deferred.reject());
	    	});

        return deferred.promise;

    };
});
Advertisements