Clean way to wait for first true returned by Promise

后端 未结 6 743
花落未央
花落未央 2021-01-01 09:51

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([Promis         


        
相关标签:
6条回答
  • 2021-01-01 10:21

    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]);

    0 讨论(0)
  • 2021-01-01 10:30

    If you want the cutting edge solution, it sounds like you want Promise.any() which:

    Promise.any() takes an iterable of Promise 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 an AggregateError, a new subclass of Error that groups together individual errors. Essentially, this method is the opposite of Promise.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
    
    0 讨论(0)
  • 2021-01-01 10:33

    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));

    0 讨论(0)
  • 2021-01-01 10:34

    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);
            });   
        });    
    } 
    
    0 讨论(0)
  • 2021-01-01 10:41

    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.

    0 讨论(0)
  • 2021-01-01 10:42

    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);
      
    });

    0 讨论(0)
提交回复
热议问题