Get which promise completed in Promise.race
Here's a minimalistic implementation that returns the promise that wins the Promise.race
. It uses JavaScript iterators, so it doesn't create new arrays/maps:
/**
* When any promise is resolved or rejected,
* returns that promise as the result.
* @param {Iterable.<Promise>} iterablePromises An iterable of promises.
* @return {{winner: Promise}} The winner promise.
*/
async function whenAny(iterablePromises) {
let winner;
await Promise.race(function* getRacers() {
for (const p of iterablePromises) {
if (!p?.then) throw new TypeError();
const settle = () => winner = winner ?? p;
yield p.then(settle, settle);
}
}());
// return the winner promise as an object property,
// to prevent automatic promise "unwrapping"
return { winner };
}
// test it
function createTimeout(ms) {
return new Promise(resolve =>
setTimeout(() => resolve(ms), ms));
}
async function main() {
const p = createTimeout(500);
const result = await whenAny([
createTimeout(1000),
createTimeout(1500),
p
]);
console.assert(result.winner === p);
console.log(await result.winner);
}
main().catch(e => console.warn(`caught on main: ${e.message}`));
The "remove from queue" step should happen by the completed promise itself (using then
) instead of relying on the returned promise from Promise.race
. It seems this is the only way around it.
async function asyncLoop(asyncFns, concurrent = 5) {
// queue up simultaneous calls
let queue = [];
let ret = [];
for (let fn of asyncFns) {
// fire the async function, add its promise to the queue, and remove
// it from queue when complete
const p = fn().then(res => {
queue.splice(queue.indexOf(p), 1);
return res;
});
queue.push(p);
ret.push(p);
// if max concurrent, wait for one to finish
if (queue.length >= concurrent) {
await Promise.race(queue);
}
}
// wait for the rest of the calls to finish
await Promise.all(queue);
};
Npm module: https://github.com/rxaviers/async-pool
Credits to @Dan D. who deleted their answer shortly after posting:
let [completed] = await Promise.race(queue.map(p => p.then(res => [p])));
This creates a promise for each of the elements in the queue that when the promise completes returns the promise. Then by racing those you get the promise that first completed.
Originally there was not brackets around completed
or p
. Since p
is a promise and has a then
method, the promise was chained again, returning the promise's resolved value rather than the promise (thus it didn't work). I assume that's why the answer was deleted. By wrapping the promise in an array, then using an Array Destructuring assignment, you can prevent it from chaining again, getting the promise.
Rather than a single queue, why not have 5 "serial" queues
async function asyncLoop(asyncFns, concurrent = 5) {
const queues = new Array(concurrent).fill(0).map(() => Promise.resolve());
let index = 0;
const add = cb => {
index = (index + 1) % concurrent;
return queues[index] = queues[index].then(() => cb());
};
let results = [];
for (let fn of asyncFns) {
results.push(add(fn));
}
await Promise.all(results);
};
OK ... firstly, it's not pretty, but it seems to work - however, this assumes asyncFns
is an Array - probably simple to "fix" for an Object using Object.values
const asyncLoop = (asyncFns, concurrent = 5) => {
let inFlight = 0;
let pending = [];
const end = result => {
inFlight--;
var job = pending.shift();
job && job();
return result;
};
const begin = (fn) => {
if (inFlight < concurrent) {
inFlight++;
return fn();
}
let resolver;
const promise = new Promise(resolve => {
resolver = () => {
inFlight ++;
resolve(fn());
}
});
pending.push(resolver);
return promise;
}
return Promise.all(asyncFns.map(fn => begin(fn).then(end)));
};
const fns = new Array(25).fill(0).map((v, index) => () => new Promise(resolve => {
let timeout = 1000;
if (index == 6 || index == 11) {
timeout = 2000;
}
setTimeout(resolve, timeout, index);
}));
console.time('timeToComplete');
asyncLoop(fns, 5).then(result => {
console.timeEnd('timeToComplete');
console.log(JSON.stringify(result));
});