I'm currently working on something where I fire out three promises in an array. At the moment it looks something like this
var a = await Promise.all([Promise1(), Promise2(), Promise3()]);
Now all of those promises will either return true or false. But at the moment I'm waiting for all of them to be finished and I could go on as soon as one of them returns true.
I thought of ways to accomplish that but all seem kind of ugly. How would you solve this task?
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]);
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.
Can use Promise.race() and only resolve initial promises when value is true
const doSomething = (bool, val)=>{
return new Promise((resolve, reject)=>{
if (bool){
resolve(val)
}
})
}
const promise1 = doSomething(false,'one')
const promise2 = doSomething(false,'two')
const promise3 = doSomething(true,'three')
const promise4 = doSomething(true,'four')
const promise5 = doSomething(true,'five')
Promise.race([promise1, promise2, promise3, promise4, promise5]).then(value => {
console.log('First true one to resolve is: ', value);
});
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})));
// 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());
});
}
]
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
Advancing off of str's answer I would like to add the ability to provide a callback so this works on more than true/false
Promises.
Adding a callback will allow to test the result of any Promise, and return the promise that successfully matches the callback filter (which should return a true/false
value).
Promise.until = function(callback, ...promises) {
return new Promise(function(resolve, reject) {
promises.forEach(promise =>
promise.then(val => callback(val) === true && resolve(val))
)
})
}
// Create some functions that resolve true/false
function Promise1() {return new Promise(resolve => setTimeout(()=> resolve(false), 1000))}
function Promise2() {return new Promise(resolve => setTimeout(()=> resolve(true), 3000))}
function Promise3() {return new Promise(resolve => setTimeout(()=> resolve(false), 1000))}
// Create som functions that resolve objects
function Promise4() {return new Promise(resolve => setTimeout(()=> resolve({a:1}), 1000))}
function Promise5() {return new Promise(resolve => setTimeout(()=> resolve({a:2}), 3000))}
function Promise6() {return new Promise(resolve => setTimeout(()=> resolve({a:123}), 1000))}
// Create some functions that resolve strings
function Promise7() {return new Promise(resolve => setTimeout(()=> resolve('Brass'), 1000))}
function Promise8() {return new Promise(resolve => setTimeout(()=> resolve('Monkey'), 500))}
function Promise9() {return new Promise(resolve => setTimeout(()=> resolve(['Brass', 'Monkey']), 100))}
// Once one resolves `true` we will catch it
Promise.until(result => result === true, Promise1(), Promise2(), Promise3())
.then(result => console.log(result));
// Once one resolves where `a` equals 123, we will catch it
Promise.until(result => result.a === 123, Promise4(), Promise5(), Promise6())
.then(result => console.log(result));
// Once one resolves where `a` equals 123, we will catch it
Promise.until(result => result === 'Monkey', Promise7(), Promise8(), Promise9())
.then(result => console.log(result));
OK, I will leave the accepted answer as is. But I altered it a little bit for my needs as I thought this one is a little easier to read and understand
const firstTrue = Promises => {
return new Promise((resolve, reject) => {
// map each promise. if one resolves to true resolve the returned promise immidately with true
Promises.map(p => {
p.then(result => {
if(result === true){
resolve(true);
return;
}
});
});
// If all promises are resolved and none of it resolved as true, resolve the returned promise with false
Promise.all(Promises).then(() => {
resolve(Promises.indexOf(true) !== -1);
});
});
}