2

I have a promise chain problem I have been grappling with. I make a call out to an external api that returns me data that I need to process and ingest into a mongo database. I am using nodejs and mongodb with express. Anyway, the calls to the api are working ok, the problem is that I am making tons of them at once. I want to slow them down, like make all the calls for one set. Wait a minute. Make all the calls for the next set. If this was a known amount of sets I would just promise chain them. I dont know how many sets there are so I am looping through them. I think the closure is the problem but cant work around it. on to the example code!

  function readApi(carFactory){
    var promise = new Promise(function(resolve, reject) {
      // call out to api, get set of car data from factory1

      console.log(carFactory);
      if (true) {
        console.log('resolved');
        resolve("Stuff worked!"+carFactory);
      }
      else {
        reject(Error("It broke"));
      }
    });
    return promise;
  }

  function manager(){

    //singular call
    readApi("this is a singular test").then(returnedThing=>{
      console.log(returnedThing); //Stuff worked! this is a singular test
    });

    let dynamicList = ["carFactory1", "carFactory2","carFactory3","carFactory..N"];
    let readApiAction = [];
    dynamicList.forEach(carIter=>{
      readApiAction.push(readApi(carIter));
    });
    //ok so now I got an array of promise objects.
    //I want to call the first one, wait 1 minute and then call the next one. 

    //originally I was calling promise.all, but there is no way to get at 
    //each promise to pause them out so this code is what I am looking to fix
    let results= Promise.all(readApiAction);
    results.then(data=>{
      data.forEach(resolvedData=>{
        console.log(resolvedData); //Stuff worked carFactory1, etc... 
      });      
    });


    //singular call with timeout, this does not work, each one called at the same time
    let readApiActionTimeouts = [];
    dynamicList.forEach(carIter=>{
      setTimeout(callingTimeout(carIter), 6000);
    });
  }

  //just a function to call the promise in a timeout
  //this fails with this  - TypeError: "callback" argument must be a function
  function callingTimeout(carIter){
    readApi(carIter).then(returnedThing=>{
      console.log("timeout version"+returnedThing);
    });
  }
spartikus
  • 2,852
  • 4
  • 33
  • 38
  • There are dozens of similar type questions on SO about how to handle rate limiting when making lots of API calls to an external server. – jfriend00 Dec 07 '17 at 20:36
  • 2
    Related answers: [Delaying each promise in Promise.all()](https://stackoverflow.com/questions/47383610/nodejs-delay-each-promise-within-promise-all/47394678#47394678), [Controlling how many requests in flight at once](https://stackoverflow.com/questions/47299174/nodejs-async-request-with-a-list-of-url/47299802#47299802), [How to make it so that I can execute say 10 promises at a time in javascript to prevent rate limits on api calls?](https://stackoverflow.com/questions/44666202/how-to-make-it-so-that-i-can-execute-say-10-promises-at-a-time-in-javascript-to/44666278#44666278). – jfriend00 Dec 07 '17 at 20:38
  • More related answers: [Choose proper method for bach processing](https://stackoverflow.com/questions/36730745/choose-proper-async-method-for-batch-processing/36736593#36736593), [Delays between promises in promise chain](https://stackoverflow.com/questions/41079410/delays-between-promises-in-promise-chain/41079572#41079572), [Delay chained promise](https://stackoverflow.com/questions/38734106/delay-chained-promise/38734304#38734304). – jfriend00 Dec 07 '17 at 20:40
  • its not really a limiter question with relation to the api, sorry I must have not been clear. I am trying to make a call to a promise function, then wait, then make another call to that same function, N amount of times. – spartikus Dec 07 '17 at 23:35
  • then you've made it more complicated a question than need be. Just search for "sequencing promises array". There are hundreds of relevant answers. – jfriend00 Dec 07 '17 at 23:50
  • Lots more existing answers for serializing an array of async operations: [How to sequence promises](https://stackoverflow.com/questions/29880715/how-to-synchronize-a-sequence-of-promises/29906506#29906506) or [Async each for promises](https://stackoverflow.com/questions/32028552/es6-promises-something-like-async-each/32028826#32028826) or [Perform a chain of promises in sequence](https://stackoverflow.com/questions/33464813/javascript-perform-a-chain-of-promises-synchronously/33464843#33464843). – jfriend00 Dec 08 '17 at 00:14

3 Answers3

7

A bit on theory. Native Promise.all just groups promises. They're still executed simultaneously (in async way, though, as all JS code, but along each other). This means that it will still congest the API and perform a lot of calls.

Another thing to note is that if you want to delay a promise, you have to delay its return value (e.g. resolve). In order to do so, you may use setTimeout INSIDE new Promise (just look below for more explanation).

Set timeout is asynchronous. It does not work as in other languages (it does not just pause execution). Setting fixed timeout in your code just caused to move ALL the executions by 6s. They still happened in parallel (in different ticks, though, but it's a small difference). Try e.g. generating different timeouts for each one in the loop - you'll notice that they're happening in a different time BUT! This is not a good solution for promisified code!

And now - time for practical answer!

If you use Bluebird, it has a special method to add delay or timeout to each promise. Without it, you would have to write a wrapper around Promise, e.g. resolving it after a particular amount of time and then use it with Promise.all.

First solution (bluebird):

function delayMyPromise(myPromise, myDelay);
  return Promise.delay(myDelay).then(function() {
    return myPromise;
  });
});

and then in your code:

return Promise.all(delayMyPromise(promise1, 1000), delayMyPromise(promise2, 2000)); // Notice different delays, you may generate them programatically

Or even cooler, you can use Promise.map from Bluebird instead of Promise.all that has a special concurrency setting so you may force your promises to be executed in particular sequence, e.g. 2 at a time. This is how I did it on my previous project :)

More here:

Pure native Promise implementation:

function delayMyPromise(myPromise, myDelay) {
  return new Promise(function (resolve, reject) {
    setTimeout(function() {
      return resolve(myPromise);
    }, myDelay);
  });
}

I'd, however, heavily recommend first approach if you don't mind using Bluebird. It's like lodash for Promises and it's really fast :)

Scott Rudiger
  • 1,224
  • 12
  • 16
SzybkiSasza
  • 1,591
  • 12
  • 27
  • thanks for this answer, I will give this a go and get back to you – spartikus Dec 08 '17 at 00:12
  • went with the pure promise just so I would not have to add another library, thanks again! – spartikus Dec 08 '17 at 17:10
  • the problem of `setTimeout` approach is if you have several of such delayed promises chained with `Promise.all()` then if 1st one fails – others will still fire after timer runs our, even though they should not, because `all` breaks if one of the promises rejected. Do you know if bluebird solves this problem? – AAverin Sep 13 '18 at 13:53
  • Since Javascript would evaluate eagerly the passed promise I think the parameter to delayMyPromise should be a lambda that returns the promise and then in delayMyPromise call resolve(myPromise()). – madalex Jan 12 '22 at 16:50
1

You could use recursion for something like this.

When you call .forEach, each iteration happens immediately.

In the example below, doSomething is not called until the setTimeout occurs, which means each letter is printed 1 second apart.

let letters = ["a", "b", "c"];

function doSomething(arr) {
  console.log(arr[0]);
  if (arr.length > 1) {
    setTimeout(() => doSomething(arr.slice(1)), 1000);
  }
}

doSomething(letters);

Alternately, for your array of promises:

let promises = [
  Promise.resolve("A"),
  Promise.resolve("B"),
  Promise.resolve("C"),
];

function doSomething(arr) {
  arr[0].then((result) => {
    console.log(result);
    if (arr.length > 1) {
      setTimeout(() => doSomething(arr.slice(1)), 1000);
    }
  })
}

doSomething(promises);
user184994
  • 17,791
  • 1
  • 46
  • 52
1

you get the error : TypeError: "callback" argument must be a function because your callingTimeout returns nothing and setTimeout needs a function as argument, this is how to fixe it :

    let readApiActionTimeouts = [];
    dynamicList.forEach(carIter=>{
          callingTimeout(carIter)
    });

your promise :

function readApi(carFactory){
    var promise = new Promise(function(resolve, reject) {
       //...
       setTimeout(()=>{
          resolve("Stuff worked!"+carFactory);
       }, 6000);
       //...
    });
    return promise;
  }
El houcine bougarfaoui
  • 35,805
  • 9
  • 37
  • 37