0

I have an AngularJS controller that is making asynchronous calls to a service that returns promises.

$myService.getData(ctrl.state)
.then(function(data) {
    updateInterface(data);
});

When the promises return, I am updating data in the interface. However, if I make two calls, and the first call returns after the second call, then the interface will update with the incorrect data, based on stale state.

I have thought of a few ways of dealing with this problem:

  • Keep track of some sort of identifier of the promise, and when a promise returns, only handle it if it matches the latest.
  • Keep track of a promise until it returns. If another call is made, cancel the promise. The canceling could be handled by the service.

The getData method makes an ajax call ($http.get), so I could could cancel that by calling resolve() on the _httpTimeout object. However this seems very specific to the logic within the promise.

Are there best practices for handling this async issue? JsFiddle is here.

var testDiv = document.getElementById('test');

function loadData(query, pause) {
   var data = query;

   return new Promise((resolve, reject) => {
    setTimeout(function() {
        resolve(data); 
    }, pause);
  });
}

var test = function(query, pause) {
  loadData(query, pause)
  .then(function(data) {
      console.log("got back test data", data);
      testDiv.innerHTML = data;
  });
};

test("first", 1000);
test("second", 0);
<div id="test">
</div>
Roshana Pitigala
  • 8,437
  • 8
  • 49
  • 80
roob
  • 2,419
  • 3
  • 29
  • 45
  • "then the interface will update with the incorrect data" I don't know angular, but are you sure about this? Seems silly, since the callback is directly chained to the request. That's what promises are for, no? – Kevin Jun 26 '18 at 19:19
  • @Kevin I have added a jsfiddle in vanilla javascript that illustrates the issue. – roob Jun 26 '18 at 20:13
  • `then` also returns a promise. You can assign this to a local variable and cancel it if a second call takes place if the executing call has not yet completed.. – Igor Jun 26 '18 at 20:15
  • @georgeawg No it is not async. Please see my jsfiddle I added to the question for a stripped down version of what I am doing. In my angular app the `updateInterface` call just changes the value of a $scope variable that is used by the directive template. – roob Jun 26 '18 at 20:15
  • 1
    On how to cancel a promise see: https://stackoverflow.com/a/37492399/1260204 or https://stackoverflow.com/a/30235261/1260204 – Igor Jun 26 '18 at 20:22
  • 1
    @Igor thanks. I had heard of bluebird but did not want to use an external library. That article also includes a vanilla js solution which is what I will use. – roob Jun 26 '18 at 20:26
  • 1
    resolving _httpTimeout seems to be fine in this flow. – skyboyer Jun 26 '18 at 20:26

2 Answers2

2

One solution is to chain the second call from the first:

function test(query, pause) {
  return loadData(query, pause)
  .then(function(data) {
      console.log("got back test data", data);
      testDiv.innerHTML = data;
      return data;
  });
}

test("first", 1000)
  .then(function(data) {
    return test("second", 0);
});

The .then method returns a new promise which is resolved or rejected via the return value of the successCallback, errorCallback (unless that value is a promise, in which case it is resolved with the value which is resolved in that promise using promise chaining.

georgeawg
  • 48,608
  • 13
  • 72
  • 95
  • 1
    This is an interesting idea. In some cases this would be useful, but in this specific case, I would rather cancel the first promise instead of blocking on it. – roob Jun 26 '18 at 20:21
0

This solution is to avoid the long promise chain which may create problem if the chain becomes long like 15 promises in the chain.

Please check the solution with synchronization queue. With syncArray, the synchronization queue is implemented. If any asynchronous request is pending then it just put the request in the queue and process based on first come first serve.

The following line push the item in the queue if it's not process from queue request:

if(!isProcessFromQueue) {
    syncArray.push({query:query, pause:pause});
}

If one request is already in progress, then it sleeps for 1 second and call for processing from queue again.

if(isRunning) {
    setTimeout(function() {
    test(null, null, true);   
    }, 1000);
    return;
  }

Then, if the queue is empty, it returns:

if(syncArray.length < 1) {
    return;
}

Finally, on asynchronous request complete if the queue is not empty, then request to process from queue again and set the isRunning to false to inform that previous request is completed:

  isRunning = false;
  if(syncArray.length > 0) {
   test(null, null, true);
  }

The complete code snippet is given below:

var testDiv = document.getElementById('test');

function loadData(query, pause) {
   var data = query;

   return new Promise((resolve, reject) => {
    setTimeout(function() {
        resolve(data); 
    }, pause);
  });
}
var syncArray = [];
var concatData = "";
var isRunning = false;
var test = function(query, pause, isProcessFromQueue) {
  
  if(!isProcessFromQueue) {
    syncArray.push({query:query, pause:pause});
  }
  
  if(isRunning) {
    setTimeout(function() {
    test(null, null, true);   
    }, 1000);
    return;
  }
  if(syncArray.length < 1) {
    return;
  }
  var popedItem = syncArray.shift();
  isRunning = true;
  loadData(popedItem.query, popedItem.pause)
  .then(function(data) {
      console.log("got back test data", data);
      concatData = concatData + "<br>" + data;
      testDiv.innerHTML = concatData;
      isRunning = false;
      if(syncArray.length > 0) {
       test(null, null, true);
      }
      
  });
};

test("first", 1000, false);
test("second", 0, false);
test("third", 500, false);
test("forth", 0, false);
<div id="test">
</div>

Finally, the promise can not be cancelled with current Promise implementation. The cancel option will be added in later release. To achieve it, you can use third party library bluebird. The Promise.race can be used for some cases.

The Promise.race implementation is given below:

var p1 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 500, 'one'); 
});
var p2 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 100, 'two'); 
});

Promise.race([p1, p2]).then(function(value) {
  console.log(value); // "two"
  // Both resolve, but p2 is faster
});
I. Ahmed
  • 2,438
  • 1
  • 12
  • 29