Clean way to wait for first true returned by Promise
You can implement that combining Promise.race
and Promise.all
:
function firstTrue(promises) {
const newPromises = promises.map(p => new Promise(
(resolve, reject) => p.then(v => v && resolve(true), reject)
));
newPromises.push(Promise.all(promises).then(() => false));
return Promise.race(newPromises);
}
Test for above code:
function firstTrue(promises) {
const newPromises = promises.map(p => new Promise(
(resolve, reject) => p.then(v => v && resolve(true), reject)
));
newPromises.push(Promise.all(promises).then(() => false));
return Promise.race(newPromises);
}
var test = values => firstTrue(
values.map((v) => new Promise((resolve) => {
setTimeout(() => resolve(v), Math.round(Math.random() * 1000));
}))
).then((ret) => console.log(values, ret));
test([true, true, true]);
test([false, false, false]);
test([true, false, false]);
test([false, true, false]);
test([false, false, true]);
If you want the cutting edge solution, it sounds like you want Promise.any()
which:
Promise.any()
takes an iterable ofPromise
objects and, as soon as one of the promises in the iterable fulfils, returns a single promise that resolves with the value from that promise. If no promises in the iterable fulfil (if all of the given promises are rejected), then the returned promise is rejected with anAggregateError
, a new subclass ofError
that groups together individual errors. Essentially, this method is the opposite ofPromise.all()
.
However, that isn't due to be usable until
- Chrome 85
- Firefox 79
- Safari 14
- No IE, Edge, or Opera support
You basically want some()
.
Promise.all()
won't work because it will make you wait for all the Promises to complete.Promise.race()
alone won't work because it only returns the resolution of 1 Promise.
Instead, we need to receive an Array of Promises and keep racing them until one of them returns true
at which point, we should stop. If none of them are true, we still need to stop, but we need to note that none of them were true.
Consider the following example with test harness:
Code
/**
* Promise aware setTimeout based on Angulars method
* @param {Number} delay How long to wait before resolving
* @returns {Promise} A resolved Promise to signal the timeout is complete
*/
function $timeout(delay) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), delay);
});
}
/**
* Return true (early) if any of the provided Promises are true
* @param {Function(arg: *): Boolean} predicate The test the resolved Promise value must pass to be considered true
* @param {Promise[]} arr The Promises to wait on
* @returns {Promise<Boolean>} Whether one of the the provided Promises passed the predicate
*/
async function some(predicate, arr) {
// Don't mutate arguemnts
const arrCopy = arr.slice(0);
// Wait until we run out of Promises
while(arrCopy.length){
// Give all our promises IDs so that we can remove them when they are done
const arrWithIDs = arrCopy.map((p, idx) => p.then(data => ({idx, data})).catch(_err => ({idx, data: false})));
// Wait for one of the Promises to resolve
const soon = await Promise.race(arrWithIDs);
// If it passes the test, we're done
if(predicate(soon.data))return true;
// Otherwise, remove that Promise and race again
arrCopy.splice(soon.idx, 1);
}
// No Promises passed the test
return false;
}
// Test harness
const tests = [
function allTrue(){
console.log(new Date());
return some((v)=>v, [
$timeout(1000).then(() => true),
$timeout(2000).then(() => true),
$timeout(3000).then(() => true)
]).then(d => {
console.log(d);
console.log(new Date());
});
},
function twoSecondsTrue(){
console.log(new Date());
return some((v)=>v, [
$timeout(1000).then(() => false),
$timeout(2000).then(() => true),
$timeout(3000).then(() => true)
]).then(d => {
console.log(d);
console.log(new Date());
});
},
function threeSecondsTrue(){
console.log(new Date());
return some((v)=>v, [
$timeout(1000).then(() => false),
$timeout(2000).then(() => false),
$timeout(3000).then(() => true)
]).then(d => {
console.log(d);
console.log(new Date());
});
},
function allFalse(){
console.log(new Date());
return some((v)=>v, [
$timeout(1000).then(() => false),
$timeout(2000).then(() => false),
$timeout(3000).then(() => false)
]).then(d => {
console.log(d);
console.log(new Date());
});
},
function threeSecondsTrueWithError(){
console.log(new Date());
return some((v)=>v, [
$timeout(1000).then(() => { throw new Error() }),
$timeout(2000).then(() => false),
$timeout(3000).then(() => true)
]).then(d => {
console.log(d);
console.log(new Date());
});
}
]
tests.reduce((acc, curr) => acc.then(()=>curr()), Promise.resolve());
Output
// 1 Second true
2018-07-03T18:41:33.264Z
true
2018-07-03T18:41:34.272Z
// 2 Seconds true
2018-07-03T18:41:34.273Z
true
2018-07-03T18:41:36.274Z
// 3 Seconds true
2018-07-03T18:41:36.274Z
true
2018-07-03T18:41:39.277Z
// 3 Seconds false
2018-07-03T18:41:39.277Z
false
2018-07-03T18:41:42.282Z
// 3 Seconds true with error throwing
2018-07-03T18:41:42.282Z
true
2018-07-03T18:41:45.285Z
You can create a new promise that resolves as soon as any given promise resolves to true
like this:
function promiseRaceTrue(promises) {
return new Promise(function(resolve, reject) {
promises.forEach(promise =>
promise.then(val => val === true && resolve())
// TODO handle resolve with value of "false"?
// TODO handle rejection?
);
// TODO handle all resolved as "false"?
});
}
var a = await promiseRaceTrue([Promise1(), Promise2(), Promise3()]);
It behaves similar to Promise.race
but only resolves if one of the given promises resolves with the value true
instead of resolving as soon as any of the given promises either resolves or rejects.