0

I'm building an ionic project where users can play a tour (which the data is from an API)

Every tour has an amount of parts that users can play at a certain point on the map. This app must be able to be a 100% offline app, so when the user enters the code of a tour, the data must be retrieved from the API before the user can proceed (so the app will put all the data of the tour offline). Every part has an image, video, audio which is getting downloaded at start of the app.

The problem is that the function call, who is downloading all the data, is not synchronous. The console.log's are saying that the function already ends before all data is downloaded. Pieces of code below:

     function getAndFillFullTour() {
    vm.showLoader = true;
    // load data
    TourFactory.getFullTour(vm.tourData.number, function(data){
      if(data.state == 'success'){
        vm.tourData = data;
        var test = downloadData(function(){
          // hide loader and continue tour
        });
      } else {
        console.log('error');
      }
    });
  }

This function calls the factory who is getting the full tour including paths of images of each part which is needed to get downloaded on the users device. The downloadData function is the following function:

function downloadData(callback) {
    angular.forEach(vm.tourData.parts, function(value, key){
      var part = value;
      var i = key;

      if(part.image !== "") {
        TourFactory.getPartImage(part, tourId, function(data){
          vm.tourData.parts[i].partImage = data;
          console.log('executed with picture ' + i);
        });
      }

    });

    if(callback)
        callback();
  }

Unfortunately, the forloop itself is performing synchronous but it is not waiting for the factory call to complete. I tried a lot of alternatives with promises, but without luck. Could anyone help? I need to wait for the http call to be finished in order to get a response from the downloadData call.

the getPartImage() is just an example, there are five functions like this each for loop which need to be completed first before I get a response in the downloadData call.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375

2 Answers2

1

Take a look at $q.all or here- it is a promise helper function that can wait for multiple promises to complete. It's result is a promise as well, so you can chain it with other promises.

// Promise function that knows how to download a single part
function downloadPart(myurl) {
  // return http promise
  return $http({
    method: 'GET',
    url: myurl
  });
};

// Aggregat epromise that downloads all parts
function downloadAllParts(parts) {
  var defer = $q.defer(); // Setup return promise
  var partsPromises = []; // Setup array for indivudual part promises
  angular.forEach(parts, function(part) { // Iterate through each part
    // Schedule download of a single
    partsPromises.push(downloadPart(part));
  });
  // Wait for all parts to resolve
  $q.all(partsPromises)
    .then(function(data) {
      // Returned data will be an array of results from each individual http promise
      resData = [];
      angular.forEach(data, function(partData) {
        //handle each return part
        resData.push(partData.data);
      })
      defer.resolve(resData); // Notify client we downloaded all parts
    }, function error(response) { // Handle possible errors
      console.log('Error while downloading parts'
        response);
      defer.reject('Error while downloading parts');
    });
  return defer.promise;
};

Then, in your client you can simply wait for the downloadAllParts to complete:

downloadAllParts(myParts)
 .then(function(data) {
   alert('Success!');
 }, function(error) {
   alert(error);
 })

Since $q.all is a promise as well, you can get rid of defers all together:

// Aggregat epromise that downloads all parts
function downloadAllParts(parts) {
  var partsPromises = []; // Setup array for indivudual part promises
  angular.forEach(parts, function(part) { // Iterate through each part
    // Schedule download of a single
    partsPromises.push(downloadPart(part));
  });
  // Wait for all parts to resolve
  return $q.all(partsPromises)
    .then(function(data) {
      // Returned data will be an array of results from each individual http promise
      var resData = [];
      angular.forEach(data, function(partData) {
        //handle each return part
        resData.push(partData.data);
      })
      return resData;
    });
};

Here is a working jsfiddle: link

Maksym
  • 1,430
  • 1
  • 11
  • 13
  • no need for `$q.defer()` ... just return `$q.all()` but make sure to return data inside `then()` – charlietfl Sep 16 '16 at 19:33
  • also need to get the data out of each `downloadPart()` response object – charlietfl Sep 16 '16 at 19:39
  • downloadPart returns a promise that returns the data, so 'data' in $q.all promise should be an array of results from all $http calls. – Maksym Sep 16 '16 at 19:42
  • right but will be array of the response objects and the data is a property of those... `$http.get(url).then(function(response){ var dataFromServer = response.data; alert(response.status)})` – charlietfl Sep 16 '16 at 19:43
  • yeah... that streamlined it! – charlietfl Sep 16 '16 at 19:59
  • keep in mind that $q.all will exit after first reject, if you want to be sure that all promises ended (resolved or rejected), you may use angular promise extras module (https://github.com/ohjames/angular-promise-extras) whith its $q.allSettled method, so return $q.all(partsPromises) may be replaced with return $q.allSettled(partsPromises) --- http://stackoverflow.com/questions/39386811/how-to-execute-only-after-all-ajaxcalls-are-made/39387466#39387466 – Andriy Sep 16 '16 at 20:39
  • Good to know, thanks! I guess another alternative is to wrap $http promise with another defered promise that resolves with an empty result.data on failure. – Maksym Sep 16 '16 at 21:10
  • just a quick suggestion. You could make it even tighter with: var partsPromises = parts.map(downloadPart); That will kick off the download for every url, return the promise, and drop it into the promises array for you. – Gopherkhan Sep 16 '16 at 22:44
  • Thanks! I'll try the solutions in a couple of hours and let you guys know which solution worked for me! – Luuk de Bruin Sep 18 '16 at 07:28
0

Thanks all! The following code worked for me. I merged the solutions from the comments with some own stuff, and this solution made it to work for me.

// Aggregat epromise that downloads all parts
  function downloadAllParts(parts) {
    vm.showLoader = true;
    var defer = $q.defer(); // Setup return promise
    var partsPromises = []; // Setup array for indivudual part promises
    angular.forEach(parts, function(part, key) { // Iterate through each part
      // Schedule download of a single
      if(typeof part.image !== 'undefined' && part.image !== "") {
        partsPromises.push(downloadPartImage(part));
      }

      if(typeof part.audio !== 'undefined' && part.audio !== "") {
        partsPromises.push(downloadPartAudio(part));
      }

      if(typeof part.video !== 'undefined' && part.video !== "") {
        partsPromises.push(downloadPartVideo(part));
      }

      if(key > 0){
        vm.tourData.parts[key].available = false;
      } else {
        vm.tourData.parts[key].available = true;
      }
    });
    // Wait for all parts to resolve
    $q.all(partsPromises)
      .then(function(data) {
        // Returned data will be an array of results from each individual http promise
        resData = [];
        angular.forEach(data, function(partData) {
          //handle each return part
          resData.push(partData);
        })
        defer.resolve(resData); // Notify client we downloaded all parts
      }, function error(response) { // Handle possible errors
        console.log('Error while downloading parts' + response);
        defer.reject('Error while downloading parts');
      });
    return defer.promise;
  }

  function downloadPartImage(part) {
    var data = {
      oid: tourId,
      plid: part.image,
      func: 'image'
    };

    return TourFactory.getSynchronousPartImage(part, tourId).then(function(data){
      part.partImage = data.data;
      return data;
    });
  };

  function downloadPartAudio(part) {
    var targetPath = cordova.file.externalDataDirectory + tourId + '/audio/' + part._id.$id + '.mp3';
    var url = "https://www.tourtodo.com/gameinfo/" + part.audio;
    var trustHosts = true;
    var options = {};

    return $cordovaFileTransfer.download(url, targetPath, {}, true).then(function (result) {
      console.log('Save file on '+targetPath+' success!');
      part.audioSrc = targetPath;
      return result;
    }, function (error) {
      console.log('Error Download file');
      console.log(JSON.stringify(error));
      return error;
    }, function (progress) {
      console.log((progress.loaded / progress.total) * 100);
    });
  }

  function downloadPartVideo(part) {
    var targetPath = cordova.file.externalDataDirectory + tourId + '/video/' + part._id.$id + '.mp4';
    var url = "https://www.tourtodo.com/gameinfo/" + part.video;
    var trustHosts = true;
    var options = {};

    return $cordovaFileTransfer.download(url, targetPath, {}, true).then(function (result) {
      console.log('Save file on '+targetPath+' success!');
      part.videoSrc = targetPath;
      return result;
    }, function (error) {
      console.log('Error Download file');
      console.log(JSON.stringify(error));
      return error;
    }, function (progress) {
      console.log((progress.loaded / progress.total) * 100);
    });
  }

  function getAndFillFullTour() {
    vm.showLoader = true;
    // load data
    TourFactory.getFullTour(vm.tourData.number, function(data){
      if(data.state == 'success'){
        vm.tourData = data;

        downloadAllParts(vm.tourData.parts)
         .then(function(data) {
            vm.showLoader = false;
            vm.showStartButton = true;
            alertPopup = $ionicPopup.alert({
              title: 'Gelukt!',
              template: 'De tour is opgehaald. Druk op start om de tour te starten.'
            });
            localStorage.setItem('tourdata', JSON.stringify(vm.tourData));
            console.log(JSON.parse(localStorage.getItem('tourdata')));
         }, function(error) {
           console.log('error');
           console.log(error);
         })
      } else {
        console.log('error');
      }
    });
  }
  • Luuk, you can accept your own solution if that's what worked for you. Otherwise the question will linger in the 'Opened questions' queue. Cheers! – Maksym Sep 18 '16 at 15:49