...

воскресенье, 14 июля 2019 г.

[Перевод] Promise.allSettled

На 71-м митинге Ecma TC39 будет рассматриваться проект и эталонная реализация Promise.allSettled — третьего из четырех основных комбинаторов промисов.

Авторы: Джейсон Вильямс (BBC), Роберт Памли (Bloomberg), Матиас Байненс (Google)
Чемпион: Матиас Байненс (Google)
Этап: 3

Для любителей подкастов, продублировано на YouTube.

В мире промисов существует четыре основных комбинатора:


  • Promise.all (ES2015; замыкается на первом отклоненном промисе)
  • Promise.race (ES2015; замыкается на первом разрешенном промисе)
  • Promise.any (Stage 1; замыкается на первом удовлетворенном промисе)
  • Promise.allSettled (Stage 3 → Stage 4; не замыкается)

Все они широко представлены в обычных пользовательских библиотеках, каждый из них полезен сам по себе и подходит в различных ситуациях.

Основное применение этого комбинатора наступает, когда хочется выполнить действие сразу после завершения множества запросов, вне зависимости, закончились ли они успехом или неудачей. Остальные комбинаторы промисов замыкаются (short-circuit), выбрасывая результаты входящих значений, проигравших в гонке за определённым состоянием системы. Promise.allSettled уникален тем, что всегда ожидает всех, за кого отвечает.

Promise.allSettled возвращает промис, который выполняется с возвращением массива снапшотов состояний промисов, но лишь только после того, как совершенно все исходные промисы разрешены (settled).

Мы говорим, что промис разрешен (settled), если он не подвис в ожидании (pending), т.е. когда он либо удовлетворён, либо отклонён — одно из двух. Чтобы разобраться в терминологии, взгляните на старый документ States and Fates.

А ещё, это имя, allSettled, широко используется в существующих библиотеках, реализующих данную функциональность. Список будет ниже.

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

function reflect(promise) {
  return promise.then(
    (v) => {
      return { status: 'fulfilled', value: v };
    },
    (error) => {
      return { status: 'rejected', reason: error };
    }
  );
}

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.all(promises.map(reflect));
const successfulPromises = results.filter(p => p.status === 'fulfilled');

Предлагаемое API позволяет разработчику обработать эти варианты, без необходимости создавать функцию reflect самостоятельно, или заниматься хранением результатов во временных переменных. Новое API выглядит так:

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);
const successfulPromises = results.filter(p => p.status === 'fulfilled');

Если же нам почему-то нужны отклонённые промисы, то вероятно, нужно собрать причины произошедшего. allSettled позволяет сделать это так же просто.

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];

const results = await Promise.allSettled(promises);
const errors = results
  .filter(p => p.status === 'rejected')
  .map(p => p.reason);

Довольно распространённым является желание знать, что все запросы выполнились, вне зависимости от состояния каждого из них. Это важно, когда хочется в будущем заняться постепенным улучшением. Не всегда нам нужно получить от API ответ.

const urls = [ /* ... */ ];
const requests = urls.map(x => fetch(x)); // Представьте, что-то из этого увенчается успехом, а что-то - нет.

// Вот этот комбинатор остановится на первом же отказе, а ответы потеряются.
try {
  await Promise.all(requests);
  console.log('Все запросы вернулись, можно убрать полоску загрузки.');
} catch {
  console.log('Какой-то из запросов явно отвалился, но другие могут продолжать работать. Ой.');
}

С использованием Promise.allSettled можно написать нечто, что больше соответствует нашим ожиданиям.

// Мы точно знаем, что все запросы к API уже отработали.
Promise.allSettled(requests).finally(() => {
  console.log('Все запросы завершены: успешно или с ошибкой, сейчас всё равно');
  removeLoadingIndicator();
});

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


  • Rust — futures::join;
  • C# — Task.WhenAll. Можно использовать либо try/catch, либо TaskContinuationOptions.OnlyOnFaulted;
  • Python — asyncio.wait с опцией ALL_COMPLETED
  • Java — CompletableFuture.allOf

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий