1

I'm writing Javascript in a client-only environment, calling an API of a service that is rate-limited. My environment only lets me load JS libraries by using LazyLoad.js which doesn't work for everything. I have successfully been able to use throttled-queue to rate limit my API requests. But I have not been able to combine using that with leveraging Promise.all to check when a series of API calls have been completed. It works if I leave out the throttling, but not with it.

How can I combine these two? The only approach I've come up with so far, which seems very clunky, is to manually rate limit using setTimeout for each call. Then I found I need to wait the total amount of time (e.g. 200ms for each call) before I check Promises.all, otherwise it resolves too quickly with the first few Promises. Here's what I have:

var deferreds = [];
$.each(evalTempQuestions, function(idx, evalTempQuestion){
  setTimeout(function(){
    deferreds.push(knackAPI('POST', 116, 75, evalTempQuestion.payload));
  }, 200 * idx);
});

setTimeout(function(){
  Promise.all(deferreds).then(function(r) {
    console.log('done');
  }).catch(function(err){
    console.log(err);
  });
}, 200 * evalTempQuestions.length);

How can I do this better?

  • Instead of asynchronously pushing deferred, do immediately push deferreds that you will resolve asynchronously. – Bergi Nov 16 '20 at 22:25
  • You can check out the `rateLimitMap()` function in this answer: [Choose proper async method for batch processing for max requests/sec](https://stackoverflow.com/questions/36730745/choose-proper-async-method-for-batch-processing-for-max-requests-sec/36736593#36736593). It lets you specify how many requests/sec and the max number of in flight requests and then it manages when the requests are sent. – jfriend00 Nov 16 '20 at 22:30
  • @bergi Thanks that sounds like a good idea - can you please explain a little more how I would modify the code to do that? – ericalderman Nov 18 '20 at 16:50

1 Answers1

1

I understand you want to resolve deferreds sequentially. If it is that, you can do it in several ways. One of them is using for and await:

// Helper to await for setTimeout
const delay = async (millis) =>
  new Promise(resolve => setTimeout(resolve, millis));

// Construct `deferreds` using `map` on `evalTempQuestions`
const deferreds = evalTempQuestions
  .map( evalTempQuestion =>
    // Wrap each request into a function, which will be called
    // sequentially and rate-limited

    // If you want to have both the payload and its results
    // available during post-processing, you can do something like:
    // () => {
    //   const {payload} = evalTempQuestion;
    //   return {
    //     payload,
    //     result: knackAPI('POST', 116, 75, payload)
    //   }
    // }
    () => knackAPI('POST', 116, 75, evalTempQuestion.payload) );

const result = [];
for (const next of deferreds) {
  try {
    // Call the wrapper function, which returns `knackAPI` Promise,
    // which probably is the Promise returned by a `fetch`
    const value = await next();
    // If you use the payload+result way, you'll need something like this:
    // const value = await next.result();
    result.push(value);
  } catch(err) {
    console.log(err);
    result.push(null); // <- Depends on what you need to do
  }

  // Rate limit
  await delay(200);
}
console.log('done');

You can see more examples here and here (or many others found in search engines).

emi
  • 2,786
  • 1
  • 16
  • 24
  • Thank you. No, I don't care what order they are resolved in, I only want to know when they are all complete. – ericalderman Nov 16 '20 at 21:30
  • But you say there should be a 200ms between requests... – emi Nov 17 '20 at 11:15
  • Yes it needs to be rate-limited, but it doesn't matter in what order. Does that still mean "sequentially"? – ericalderman Nov 18 '20 at 16:52
  • Yes, it is still sequential. – emi Nov 19 '20 at 07:31
  • @ericalderman Please, if this was helpful, mark the answer as the correct one. – emi Nov 29 '20 at 10:18
  • Thanks. Just to confirm, you mean "deferreds" in your for loop to match my variable, correct? And what you're not showing is pushing the promises to deferreds, which you're saying I can just do in a fast loop without a delay? – ericalderman Dec 02 '20 at 00:41
  • `Thanks. Just to confirm, you mean "deferreds" in your for loop to match my variable, correct`. Yes, sorry. Updating the answer. – emi Dec 02 '20 at 16:58
  • `And what you're not showing is pushing the promises to deferreds, which you're saying I can just do in a fast loop without a delay?` No, sorry, I was wrong: `fetch` starts the download as soon as it is called. You can defer `fetch` call wrapping it into a function. I'm going to update the answer to clarify. – emi Dec 02 '20 at 17:24
  • Also can you please modify the code to reflect a return value from knackAPI? Sorry I didn't specify that in my original question. Thanks, this is a big help! – ericalderman Dec 07 '20 at 16:07
  • I don't know how you will be using this data. I have done one and proposed another in the comments. – emi Dec 08 '20 at 20:09
  • Thanks very much! – ericalderman Dec 08 '20 at 21:43