2

JavaScript provides a mechanism for gather a list of promises:

const tasks = [ fetch('a'), fetch('b'), fetch('c') ];

const results = await Promise.all(tasks); 

However, what if I want to execute all tasks, but with only n executing concurrently?

const results = await MorePromise.allWithConcurrency(2, tasks); // 2 at a time

Does JavaScript provide such functionality? If not, are there any robust libraries that do?

sdgfsdh
  • 24,047
  • 15
  • 89
  • 182
  • _"Does JavaScript provide such functionality?"_ -> No. _"If not, are there any robust libraries that do?"_ -> Not the scope of SO and therefor offtopic – Andreas Feb 28 '18 at 10:23
  • [How to split a long array into smaller arrays, with JavaScript](https://stackoverflow.com/questions/7273668/how-to-split-a-long-array-into-smaller-arrays-with-javascript) + [How to synchronize a sequence of promises?](https://stackoverflow.com/questions/29880715/how-to-synchronize-a-sequence-of-promises) – Andreas Feb 28 '18 at 10:26
  • `fetch('a')` is not an async function, it produces a promise already – Bergi Feb 28 '18 at 10:43
  • https://stackoverflow.com/a/39197252/1048572 – Bergi Feb 28 '18 at 10:44

2 Answers2

1

You can achieve it using a plugin like bluebird which have a function allowing it : example here

const Promise = require('bluebird');
const _ = require('lodash');

let tasks = [...]; // over 10,000 items.

Promise.all(
  _(tasks).map(task => {
    return task.doAsyncThing();
  })
    .value();
);

Promise.map(
  tasks, 
  task => {
    return task.doAsyncThing();
  }, 
  { concurrency: 1000 }
);

You can also create a function yourself that's gonna handle it. Here is one function I've made up myself, ofc it's improvable.

const resultPromises = [];

let nextToCall = 0;

let errorHappened = false;

/**
 * Function that get called when one promise got executed
 */
function callbackOnePromiseGotExecuted({
  error,
  result,
  allExecutedCallback,
  array,
}) {
  resultPromises.push(result);

  // Do nothing if an error got reported
  if (errorHappened) {
    return;
  }

  // Return the error
  if (error) {
    allExecutedCallback(null, resultPromises);

    errorHappened = true;

    return;
  }

  // Check if it was the last promise to execute
  if (resultPromises.length === array.length) {
    allExecutedCallback(null, resultPromises);

    return;
  }

  nextToCall += 1;

  // Stop if we already acalled everything
  if (nextToCall > array.length) return;

  // If it wasn't call a new promise
  array[nextToCall - 1].call()
    .then(ret => callbackOnePromiseGotExecuted({
      error: null,
      result: ret,
      allExecutedCallback,
      array,
    }))
    .catch(e => callbackOnePromiseGotExecuted({
      error: e,
      result: null,
      allExecutedCallback,
      array,
    }));
}

/**
 * Handle the call of multiple promise with concurrency
 */
function promiseWithConcurrencyCallback({
  array,
  concurrencyNumber,
  allExecutedCallback,
}) {
  for (let i = 0; i < concurrencyNumber; ++i) {
    array[nextToCall].call()
      .then(ret => callbackOnePromiseGotExecuted({
        error: null,
        result: ret,
        allExecutedCallback,
        array,
      }))
      .catch(e => callbackOnePromiseGotExecuted({
        error: e,
        result: null,
        allExecutedCallback,
        array,
      }));

    nextToCall += 1;
  }

}

function promiseWithConcurrency(array, concurrencyNumber) {
  return new Promise((resolve, reject) => {
    promiseWithConcurrencyCallback({
      array,
      concurrencyNumber,
      allExecutedCallback: (error, result) => {
        if (error) return reject(error);

        return resolve(result);
      },
    });
  });
}

const array = [
  () => new Promise((resolve) => resolve('01')),
  () => new Promise((resolve) => resolve('02')),
  () => new Promise((resolve) => resolve('03')),
  () => new Promise((resolve) => resolve('04')),
  () => new Promise((resolve) => resolve('05')),
  () => new Promise((resolve) => resolve('06')),
];

promiseWithConcurrency(array, 2)
  .then(rets => console.log('rets', rets))
  .catch(error => console.log('error', error));

EDIT about my comment on @Redu post

var ps      = [new Promise((resolve) => { console.log('A'); setTimeout(() => { resolve(1) }, 5000); }), Promise.resolve(2), Promise.resolve(3), Promise.resolve(4), new Promise((resolve) => { console.log('B'); setTimeout(() => { resolve(5) }, 5000); })],
    groupBy = (n,a) => a.reduce((r,p,i) => !(i%n) ? (r.push([p]),r) : (r[r.length-1].push(p),r),[]);

groupBy(2,ps).map(sps => Promise.all(sps).then(console.log));

As you can see in the snippet you gave I've edited, console.log('A') and console.log('B') are showed right away. It means that the functions are executed at the same time and then resolved two per two. So if the purpose is to limitate the access to a ressource or something, your soluce won't work. They will still all access on the same time.

Orelsanpls
  • 18,380
  • 4
  • 31
  • 54
1

With pure JS ES6 how about..?

var ps      = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3), Promise.resolve(4), Promise.resolve(5)],
    groupBy = (n,a) => a.reduce((r,p,i) => !(i%n) ? (r.push([p]),r) : (r[r.length-1].push(p),r),[]);

groupBy(2,ps).map(sps => Promise.all(sps).then(console.log));
.as-console-wrapper {
max-height: 100% !important;
}

As per @Grégory NEUT's rightful comment i have two more alternatives just in case where you have an array of data to invoke promise returning functions (i use bring to mimic fetch here)

var bring   = (url,data) => new Promise((v,x) => setTimeout(v, ~~(Math.random()*500), data)); // echoes data in 0~500 ms
    urls    = ["url0", "url1","url2","url3","url4"],
    groupBy = (n,a) => a.reduce((r,u,i) => !(i%n) ? (r.push([u]),r) : (r[r.length-1].push(u),r),[]);

groupBy(2,urls).reduce((p,sus,i) => i ? p.then(r => (console.log(`Here I am doing something with the fetched data from urls ${r}`),
                                                     Promise.all(sus.map(u => bring(u,u.replace(/[^\d]+/,""))))))
                                      : Promise.all(sus.map(u => bring(u,u.replace(/[^\d]+/,"")))), Promise.resolve())
               .then(r => console.log(`Here I am doing something with the fetched data from urls ${r}`), e => console.log(e));

or also a little funky Haskellesque recursive approach by using the spread & rest operators such as;

var bring   = (url,data) => new Promise((v,x) => setTimeout(v, ~~(Math.random()*500), data)); // echoes data in 0~500 ms
    urls    = ["url0", "url1","url2","url3","url4"],
    groupBy = (n,a) => a.reduce((r,u,i) => !(i%n) ? (r.push([u]),r) : (r[r.length-1].push(u),r),[]),
    seqPrms = (sus,...suss) => Promise.all(sus.map(u => bring(u,u.replace(/[^\d]+/,""))))
                                      .then(r => (console.log(`Here I am doing something with the fetched data from urls ${r}`),
                                                  suss.length && seqPrms(...suss)),
                                            e => console.log(e));

seqPrms(...groupBy(2,urls));
Redu
  • 19,106
  • 4
  • 44
  • 59
  • In your solution, all the function are called, and then resolved two by two. But the important thing is they all get executed at the same time. For example, use this : `var ps = [new Promise((resolve) => {console.log('A'); setTimeout(() => resolve(1), 5000);}), Promise.resolve(2), Promise.resolve(3), Promise.resolve(4), Promise.resolve(5)];`. So if the OP want to do things two by two, it won't work. You just resolve two by two. – Orelsanpls Mar 07 '18 at 17:41
  • @Grégory NEUT No. It will perfectly run the promises two by two and you may apply your logic at the `.then(console.log)` part such as `.then(doSomething)` per pair. `doSomething` will be invoked whenever the latest of the pair finishes. – Redu Mar 07 '18 at 21:31
  • @Grégory NEUT I got you. My initial thought were dealing with already at hand promises and processing their `then` stages in groups. You know Fetch API has a lot of chained up `then()` stages. Yet... you are right. I have added two alternative solutions. – Redu Mar 08 '18 at 21:14