Асинхронный генератор: выдача отклоненного обещания

Я экспериментировал с генераторами async, пытаясь создать генератор "упорядочивания обещаний", который принимает массив обещаний и выдает обещания одно за другим в порядке их разрешения или отклонения. Так что-то вроде:

async function* orderProms(prom_arr) {

    // Make a copy so the splices don't mess it up.
    const proms = [...prom_arr];

    while (proms.length) {
        // Tag each promise with it's index, so that we can remove it for the next loop.
        const {prom, index} = await Promise.race(proms.map((prom, index) => prom.then(
            () => ({prom, index}),
            () => ({prom, index})
        )));

        proms.splice(index, 1);
        yield prom;
    }
}

С идеей потреблять этот генератор так:

const resAfter = (val, delay) => new Promise(res => setTimeout(() => res(val), delay));
const rejAfter = (val, delay) => new Promise((_, rej) => setTimeout(() => rej(val), delay));

const promises = [
    resAfter("Third", 3000),
    resAfter("First", 1000),
    rejAfter("Second", 2000), // NOTE: this one rejects!
];

(async () => {

    let ordered = orderProms(promises);

    let done = false;
    for (let next_promise = ordered.next(); !done; next_promise = ordered.next()) {
        const next = await next_promise
            .catch(err => ({done: false, value: `Caught error: ${err}`}));

        done = next.done;
        if (!done) console.log(next.value);
    }
})()

Однако я заметил, что это дойдет до второго обещания, после чего генератор остановится. Вроде бы из-за отвергнутого "второго" обещания. Вызовyield promв генераторе создаст исключение в генераторе, когдаprom отклонено.

Но это источник моего замешательства. Я не хочу создавать здесь исключение, я просто хочу передать отклоненное обещание в качествеvalueрезультата итератора. Я не хочу, чтобы это разворачивалось. Это почти как с этим обращаются как сyield await prom;, но, как видите, нет await вызов.

Что здесь происходит и как я могу просто передать отклоненное обещание от этого генератора как есть.


Вот приведенный выше код в исполняемом фрагменте:

async function* orderProms(prom_arr) {

    // Make a copy so the splices don't mess it up.
    const proms = [...prom_arr];

    while (proms.length) {
        // Tag each promise with it's index, so that we can remove it for the next loop.
        const {prom, index} = await Promise.race(proms.map((prom, index) => prom.then(
            () => ({prom, index}),
            () => ({prom, index})
        )));

        proms.splice(index, 1);
        yield prom;
    }
}

const resAfter = (val, delay) => new Promise(res => setTimeout(() => res(val), delay));
const rejAfter = (val, delay) => new Promise((_, rej) => setTimeout(() => rej(val), delay));

const promises = [
    resAfter("Third", 3000),
    resAfter("First", 1000),
    rejAfter("Second", 2000), // NOTE: this one rejects!
];

(async () => {

    let ordered = orderProms(promises);

    let done = false;
    for (let next_promise = ordered.next(); !done; next_promise = ordered.next()) {
        const next = await next_promise
            .catch(err => ({done: false, value: `Caught error: ${err}`}));

        done = next.done;
        if (!done) console.log(next.value);
    }
})()

3 ответа

Решение

Это почти как с этим обращаются как с yield await prom. Что здесь происходит?

Именно так ведут себя асинхронные генераторы.

как я могу просто передать отклоненное обещание от этого генератора как есть.

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

try {
    for await (const value of orderProms(promises)) {
        console.log(value);
    }
} catch(err) {
    console.error('Caught error: ', err);
}

В синтаксисе нет упрощения для обработки отдельных ошибок. Когда возникает исключение, цикл останавливается, генератор готов. Точка.

Так что ты можешь сделать? Я вижу три варианта:

  • просто оставьте это как есть и относитесь к раннему отказу как к функции (аналогично Promise.all)
  • обрабатывать ошибки (либо в orderProms или перед передачей в него обещаний) и получить кортежи состояния и значения обещания

    for await (const value of orderProms(promises.map(prom =>
        prom.catch(err => `Caught error: ${err}`)
    ))) {
        console.log(value);
    }
    
  • используйте обычный (неasync) генератор, из которого вы передаете одно обещание за другим вручную, чтобы иметь возможность использовать его так, как вы хотите

Я не могу сказать, что принятый ответ неверен, но и он не очень правильный. Особенно

Когда возникает исключение, цикл останавливается, генератор готов. Точка.

часть проблематична.

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

ЧАСТЬ I - Ответ на исходный вопрос

Я не буду вдаваться в подробности, просто отмечу использование в функции генератора orderPromsгде что-то все еще происходит (генератор не работает) после того, как исключение генерируется изнутри. Итак ... один подход мог бы быть похожим;

      async function* orderProms(prom_arr) {

    // Make a copy so the splices don't mess it up.
    var proms = [...prom_arr];

        // Tag each promise with it's index, so that we can remove it for the next loop.
     try {
      while (proms.length) {
            var {prom, index} = await Promise.race(proms.map((prom, index) => prom.then(
                () => ({prom, index}),
                () => ({prom, index})
            )));
            
            proms.splice(index, 1);
            yield prom;
          }
    } finally {
        proms.length && (ordered = orderProms(proms));
      }
}

var resAfter = (val, delay) => new Promise(res => setTimeout(() => res(val), delay)),
    rejAfter = (val, delay) => new Promise((_, rej) => setTimeout(() => rej(val), delay)),
    promises = [ resAfter("Third", 3000)
               , resAfter("First", 1000)
               , rejAfter("Second", 2000) // NOTE: this one rejects!
               ],
    ordered  = orderProms(promises);

async function endPoint() {
    try {
      for await (var value of ordered) {
        console.log(value)
      }
    }
    catch(e){
      console.log(`caught rejection ${e} at endpoint`);
      endPoint();
    }
}

endPoint();

ЧАСТЬ II - Изящное решение проблемы

А теперь представьте ... что, если у нас есть массив, который мы можем заполнить обещаниями точно так же, как мы это делаем с обычным массивом, и обещания в нем автоматически сортируются в соответствии с их разрешением / временем отклонения.

Для начала расширим Arrayвведите и дайте ему особые асинхронные возможности. Следующий код определяет SortedAsyncArrayтипа и просто скелет. Он не прошел тщательную проверку, но его должно быть достаточно, чтобы дать представление. Опять же, обратите внимание на finally часть, поскольку она выполняется только тогда, когда yield зависает из-за исключения или истощения (случай, когда генератор готов).

      class SortedPromisesArray extends Array {
  constructor(...args){
    super(...args);
  }
  async *[Symbol.asyncIterator]() {
    try {
      while(this.length){
        var {v,i} = await Promise.race(this.map((p,i) => p.then(v => ({v,i}))));
        this.splice(i,1);
        yield v;
      }
    } finally {
        this.length && this.splice(i,1);
    };
  };
}

Как же нам тогда потреблять этот асинхронный массив? Я придумал следующий способ.

      var promise  = (val, delay, resolves) => new Promise((v,x) => setTimeout(() => resolves ? v(val) : x(val), delay)),
    promises = [ promise("Third", 3000, true)
               , promise("First", 1000, true)
               , promise("Second", 2000, false) // NOTE: this one rejects!
               ],
    sortedPS = new SortedPromisesArray(...promises);

async function sink() {
  try {
    for await (let value of sortedPS){
      console.log(`Got: ${value}`);
    }
  } catch(err) {
    console.log(`caught at endpoint --> exception ${err}`);
    sink();
  }
}
sink();

Вы можете позволить обещаниям преобразоваться в нечто подобное, как вы получили из Promise.allSettled:

async function* orderProms(prom_arr) {
    // Make a copy so the splices don't mess it up.
    const proms = new Set(prom_arr.map((prom, index) => ({prom, index})));
    while (proms.size) {
        const settled = await Promise.race(Array.from(proms, obj => obj.prom.then(
            value => Object.assign(obj, { value, status: "fulfilled" }),
            error => Object.assign(obj, { error, status: "rejected" }),
        )));
        proms.delete(settled);
        let { prom, ...rest } = settled;
        yield rest;
    }
}

const resAfter = (val, delay) => new Promise(res => setTimeout(() => res(val), delay));
const rejAfter = (val, delay) => new Promise((_, rej) => setTimeout(() => rej(val), delay));

const promises = [
    resAfter("Third", 3000),
    resAfter("First", 1000),
    rejAfter("Second", 2000), // NOTE: this one rejects!
];

(async () => {
    for await (let result of orderProms(promises)) {
        console.log(JSON.stringify(result));
    }
})().catch(err => console.log(err.message));

Другие вопросы по тегам