Как реализовать помощник "raceToSuccess", учитывая список обещаний?

Я озадачен чем-то в API ES6 Promise. Я вижу ясный вариант использования для одновременного представления нескольких асинхронных заданий и "разрешения" при первом успехе. Например, это может привести к ситуации, когда доступны несколько эквивалентных серверов, но некоторые из них, возможно, опущены, а другие сильно загружены и замедлены, поэтому моя цель состояла бы в том, чтобы получить ответ от первого, чтобы добиться успеха, и игнорировать остальные ( да, я знаю, что это неприятный способ для клиента вести себя с точки зрения сервера, но он отлично подходит для конечного пользователя;)

Однако, насколько я вижу, у меня есть либо "все", либо "расы", в которых можно играть. "Все" поведение, похоже, ждет, пока все запросы не будут завершены, а это значит, что я должен ждать самого медленного, даже если сервер уже завершен (действительно, мне, возможно, придется ждать тайм-аута, и это будет катастрофа для этого сценария.) Поведение "расы", однако, похоже, дает мне первое, чтобы завершить, что, если это произойдет, также является катастрофой.

Есть ли что-то в API, которое допускает поведение "raceToSuccess", или мне нужно его вручную создавать. В этом отношении, как я могу создать его вручную?

В качестве побочной заметки я нашел ту же самую головоломку в Java 8 CompletableFuture, которая, кажется, является тесно параллельным API. Итак, я что-то упускаю на философском уровне?

Ответ 1

Это классический пример, когда инверсия вашей логики делает ее намного понятнее. Ваша "раса" в этом случае заключается в том, что вы хотите, чтобы ваше поведение отвержения фактически было успешным.

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

Ответ 2

Вы можете написать это довольно легко самостоятельно.

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

Это отпустит все promises, и когда любой из них удастся решить новое обещание с его значением. Ошибка promises игнорируются. Последующий успешный promises не вызывает ничего, потому что новое обещание уже разрешено. Обратите внимание, что полученное обещание никогда не разрешит или не отклонит, если ни один из входных promises не разрешит.

Вот модифицированная версия, которая возвращает отклоненное обещание, если все входные данные 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, или некоторые двигатели могут в конечном итоге жаловаться на них.

Ответ 3

Я использую функцию, основанную на 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 предложение. Что я не понял до сих пор.

Ответ 4

Старая тема, но здесь моя запись; это по существу решение @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);
});

Ответ 5

Я расширил подход @loganfsmyth с таймаутами, и я написал небольшую функцию, которая:

  • запускает все promises,
  • Дождитесь, пока promises преуспеет не более чем на определенное количество времени (options.timeOutMs),
  • верните первый, который преуспеет.

В следующем фрагменте вы можете проверить его:

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