How to implement the "raceToSuccess" helper, given the list of promises?

I am puzzled by something in the ES6 Promise API. I see a clear use case for simultaneously submitting multiple asynchronous jobs and "resolving" on first success. For example, this can lead to a situation where several equivalent servers are available, but some of them may be omitted, while others are heavily loaded and slow, so my goal would be to get an answer from the first in order to succeed, and ignore the rest (yes, I know that this is an unpleasant way for the client to behave from the point of view of the server, but it is great for the end user;)

However, as far as I can see, I have either "everything" or "races" in which you can play. The β€œall” behavior seems to wait until all requests are completed, which means that I have to wait for the slowest, even if the server is already completed (indeed, I may have to wait for a timeout, and this will be a disaster for of this scenario.) The behavior of the "race", however, seems to give me the first to conclude that, if that happens, is also a disaster.

Is there something in the API that allows "raceToSuccess" behavior, or do I need to manually create it. In this regard, how can I create it manually?

As a side note, I found the same puzzle in Java 8 CompletableFuture, which seems to be a closely parallel API. So, am I missing something at the philosophical level?

+17
source share
5 answers

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 actually be successful.

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)
  );
}
+42
source

You can write this quite easily on your own.

function raceToSuccess(promises) {
  return new Promise(
    resolve => 
      promises.forEach(
        promise => 
          promise.then(resolve)
      )
  );
}

This will let go of all the promises, and when either of them succeeds in resolving a new promise with its meaning. Error promises are ignored. Subsequent successful promises do not cause anything because a new promise has already been resolved. Note that the promise received will never allow or will not be rejected if none of the input promises allow.

, , promises :

function raceToSuccess(promises) {
  let numRejected = 0;

  return new Promise(
    (resolve, reject) => 
      promises.forEach(
        promise => 
          promise . 
            then(resolve) .
            catch(
              () => {
                if (++numRejected === promises.length) reject(); 
              }
           )
       )
  );
}

@loganfsmyth; , , . :

function invertPromise(promise) {
  return new Promise(
    (resolve, reject) => 
      promise.then(reject, resolve)
  );
}

function raceToSuccess(promises) {
  return invertPromise(
    Promise.all(
      promises.map(invertPromise)));
}

, promises promises, ( , ), Promise.race:

function pendingPromise()      { return new Promise(() => { }); }
function killRejected(promise) { return promise.catch(pendingPromise); }

function raceToSuccess(promises) {
  return Promise.race(promises.map(killRejected));
}

. , promises . , promises GC'd, .

+6

, Promise.race(), : , :

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

. : , , .

:

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

, , , , , , .

, , , , :

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

( , )

: , @user663031 . .

+3

, ; @loganfsmyth, , , Promise.all():

  • ()
  • 1- ,

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);
});
Hide result
+1

@loganfsmyth , , :

  • promises,
  • , promises (options.timeOutMs),
  • return the first that succeeds.

In the following snippet, you can check it out:

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));
Run codeHide result
0
source

All Articles