How do you implement a “raceToSuccess” helper, given a list of promises?

后端 未结 7 1786
孤城傲影
孤城傲影 2020-12-02 23:37

I\'m puzzled by something in the ES6 Promise API. I can see a clear use case for submitting multiple async jobs concurrently, and \"resolving\" on the first success. This wo

相关标签:
7条回答
  • 2020-12-02 23:41

    I expanded the @loganfsmyth approach with timeouts and I wrote a small function that:

    • runs all the promises,
    • wait for the promises to succeed for no more than a specified amount of time (options.timeOutMs),
    • return the first that succeed.

    In the following snippet, you can test it:

    const firstThatCompleteSuccessfullyES6 = (options) => {
    
        // return the first promise that resolve
        const oneSuccess = (promises) => Promise.all(promises.map(p => {
                        // If a request fails, count that as a resolution so it will keep
                        // waiting for other possible successes. If a request succeeds,
                        // treat it as a rejection so Promise.all immediately bails out.
                        return p.then(
                            (val) => { return Promise.reject(val); },
                            (err) => { return Promise.resolve(err); }
                        );
                })
                ).then(
                    // If '.all' resolved, we've just got an array of errors.
                    (errors) => { return Promise.reject(errors); },
    
                    // If '.all' rejected, we've got the result we wanted.
                    (val) => { return Promise.resolve(val); }
                );
        
    
        // return the promise or reect it if timeout occur first
        const timeoutPromise = (ms, promise) => new Promise(function(resolve, reject) {
                setTimeout(() => reject(new Error('timeout')), ms);
                promise.then(resolve, reject);
            });
        
    
        if (options.subsystems.length < 1) {
            return Promise.reject('Parameters error, no subSystems specified');
        }
    
        const timedOutSubsystems = options.subsystems.map(function(subsystem){
            return timeoutPromise(options.timeOutMs, subsystem(options));
        });
    
        const startDate = Date.now();
    
        return oneSuccess(
            timedOutSubsystems
        )
        .then((result) => {
            const elapsedTime = Math.abs((startDate - Date.now()) / 1000);
            console.log('firstThatCompleteSuccessfully() done, after s: ' + elapsedTime + ': '+ result);
            return result;
        })
        .catch((error) => {
            const elapsedTime = Math.abs((startDate - Date.now()) / 1000);
            console.error('firstThatCompleteSuccessfully() error/nodata: ' + error);
        });
    
    }
    
    
    
    // example of use with two promises (subsystem1 & subsystem2) that resolves after a fixed amount of time
    
    const subsystem1 = (options) => new Promise(function(resolve, reject) {
            setTimeout(function(){
                console.log('subsystem1 finished');
                resolve('subsystem 1 OK');
            }, 1000);
        });
    
    
    
    const subsystem2 = (options) => new Promise(function(resolve, reject) {
            setTimeout(function(){
                console.log('subsystem2 finished');
                resolve('subsystem 2 OK');
            }, 2000);
        });
    
    
    firstThatCompleteSuccessfullyES6({
        subsystems: [subsystem1, subsystem2],
        timeOutMs: 2000
    })
    .then((result) => console.log("Finished: "+result));

    0 讨论(0)
  • 2020-12-02 23:50

    This is a classic example where inverting your logic makes it much clearer. Your "race" in this case is that you want your rejection behavior to in fact be success behavior.

    function oneSuccess(promises){
      return Promise.all(promises.map(p => {
        // If a request fails, count that as a resolution so it will keep
        // waiting for other possible successes. If a request succeeds,
        // treat it as a rejection so Promise.all immediately bails out.
        return p.then(
          val => Promise.reject(val),
          err => Promise.resolve(err)
        );
      })).then(
        // If '.all' resolved, we've just got an array of errors.
        errors => Promise.reject(errors),
        // If '.all' rejected, we've got the result we wanted.
        val => Promise.resolve(val)
      );
    }
    
    0 讨论(0)
  • 2020-12-02 23:51

    Old topic but here's my entry; it's essentially @loganfsmyth's solution, but with a few more checks to conform to conventions established by Promise.all():

    • Empty array as input returns (synchronously) an already resolved promise
    • Non-promise entries in the array results in the 1st such entry to be used as the resolved value

    Promise.any = a => {
      return !a.length ?
        Promise.resolve() :
        Promise.all(a.map(
          e => (typeof e.then !== 'function') ?
            Promise.reject(e) :
            e.then(
              result => Promise.reject(result),
              failure => Promise.resolve(failure)
            )
        )).then(
          allRejected => Promise.reject(allRejected),
          firstResolved => Promise.resolve(firstResolved)
        );
    };
    
    // Testing...
    
    function delayed(timeout, result, rejected) {
      return new Promise((resolve, reject) => {
        setTimeout(
          () => rejected ? reject(result) : resolve(result),
          timeout);
      });
    }
    
    Promise.any([
      delayed(800, 'a'),
      delayed(500, 'b'),
      delayed(250, 'c', true)
    ]).then(e => {
      console.log('First resolved (expecting b):', e);
    });
    
    Promise.any([
      delayed(800, 'a', true),
      delayed(500, 'b', true),
      delayed(250, 'c', true)
    ]).then(null, e => {
      console.log('All rejected (expecting array of failures):', e);
    });
    
    Promise.any([
      delayed(800, 'a'),
      delayed(500, 'b'),
      delayed(250, 'c', true),
      'd',
      'e'
    ]).then(e => {
      console.log('First non-promise (expecting d):', e);
    });
    
    // Because this is the only case to resolve synchronously,
    // its output should appear before the others
    Promise.any([]).then(e => {
      console.log('Empty input (expecting undefined):', e);
    });

    0 讨论(0)
  • 2020-12-02 23:57

    Is there something in the API that permits a "raceToSuccess" kind of behavior

    Soon, there almost certainly will be. There is a Stage 3 proposal for Promise.any:

    Promise.any() takes an iterable of Promise objects and, as soon as one of the promises in the iterable fulfills, returns a single promise that resolves with the value from that promise.

    So, the following syntax will be valid:

    // assume getApi returns a Promise
    
    const promises = [
      getApi('url1'),
      getApi('url2'),
      getApi('url3'),
      getApi('url4'),
    ];
    Promise.any(promises)
      .then((result) => {
        // result will contain the resolve value of the first Promise to resolve
      })
      .catch((err) => {
        // Every Promise rejected
      });
    

    Promise.any has been implemented in Spidermonkey, and there are some polyfills available.

    0 讨论(0)
  • 2020-12-03 00:00

    I'm using a function based on Promise.race() but with a twist: it ignores rejects, unless all given promises reject:

    // ignores any rejects except if all promises rejects
    Promise.firstResolve = function (promises) {
        return new Promise(function (fulfil, reject) {
            var rejectCount = 0;
            promises.forEach(function (promise) {
                promise.then(fulfil, () => {
                    rejectCount++;
                    if(rejectCount == promises.length) {
                        reject('All promises were rejected');
                    } 
                });
            });
        });
    };
    

    It's based on Rich Harris's Promise polyfill race method. I just made the looping promise reject conditional: it only rejects the main promise, if all given promises failed, otherwise it ignores rejects and resolves the first success.

    Usage:

    // fastest promise to end, but is a reject (gets ignored)
    var promise1 = new Promise((resolve, reject) => {
        setTimeout(() => {
            reject("foo")
        }, 100);
    })
    
    // fastest promise to resolve (wins the race)
    var promise2 = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("bar")
        }, 200);
    })
    
    // Another, slower resolve (gets ignored)
    var promise3 = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("baz")
        }, 300);
    })
    
    Promise.firstResolve([promise1, promise2, promise3])
        .then((res) => {
            console.log(res) // "bar"
        })
        .catch(err => {
            console.log(err) // "All promises were rejected" (if all promises were to fail)
        })
    

    The reason I use this instead of the promise inverting approach, is because in my opinion this is more readable.

    To please the question in the strictest way, below there is a version that resolves the first successful promise but doesn't do anything if all given promises fail:

    // ignores any and all rejects
    Promise.firstResolve = function (promises) {
        return new Promise(function (fulfil) {
            promises.forEach(function (promise) {
                promise.then(fulfil, () => {});
            });
        });
    };
    

    (usage same as above)

    Edit: This is in fact the same as @user663031's suggestion. Which I haven't realized until just now.

    0 讨论(0)
  • 2020-12-03 00:01

    To resolve this problem i used a Promise.rice with a Promise.allSettled.

    The next code wait with the Promise.rice a success value. but if no hay a success result. return a array with all errors.

    const PromiseRiceSuccess = <T = unknown>(promises: Promise<T>[]) => {
      let done: (reason?: T) => void;
      const waitEndAllPromises = new Promise((resolve, reject) => done = reject);
      const waitCatchs = promise => Promise.resolve(promise).catch(() => waitEndAllPromises);
    
      Promise.allSettled(promises).then(r => done(r));
    
      return Promise.race(promises.map(waitCatchs));
    };
    

    Example:

    PromiseRiceSuccess([
      Promise.reject(1),
      new Promise((r) => setTimeout(() => r(2), 4000)),
    ]);
    // 2
    
    PromiseRiceSuccess([
      Promise.reject(1),
      new Promise((resolve, reject) => setTimeout(() => reject(2), 4000)),
    ]);
    // Uncaught (in promise) (2) [{…}, {…}]
    
    0 讨论(0)
提交回复
热议问题