На 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
Комментариев нет:
Отправить комментарий