Тайм-аут в async/wait

Я с Node.js и TypeScript, и я использую async/await. Это мой тестовый пример:

async function doSomethingInSeries() {
    const res1 = await callApi();
    const res2 = await persistInDB(res1);
    const res3 = await doHeavyComputation(res1);
    return 'simle';
}

Я хотел бы установить тайм-аут для общей функции. То есть если res1 занимает 2 секунды, res2 занимает 0,5 секунды, res3 занимает 5 секунд. Я хотел бы иметь тайм-аут, который через 3 секунды позволит мне сделать ошибку.

При обычном вызове setTimeout проблема возникает из-за потери области:

async function doSomethingInSeries() {
    const timerId = setTimeout(function() {
        throw new Error('timeout');
    });

    const res1 = await callApi();
    const res2 = await persistInDB(res1);
    const res3 = await doHeavyComputation(res1);

    clearTimeout(timerId);

    return 'simle';
}

И я не могу поймать его с помощью обычного Promise.catch:

doSomethingInSeries().catch(function(err) {
    // errors in res1, res2, res3 will be catched here
    // but the setTimeout thing is not!!
});

Любые идеи о том, как разрешить?

Ответ 1

Вы можете использовать Promise.race, чтобы сделать тайм-аут:

Promise.race([
    doSomethingInSeries(),
    new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 11.5e3))
]).catch(function(err) {
    // errors in res1, res2, res3 and the timeout will be caught here
})

Вы не можете использовать setTimeout, не обернув его обещанием.

Ответ 2

Хорошо, я нашел этот способ:

async function _doSomethingInSeries() {
    const res1 = await callApi();
    const res2 = await persistInDB(res1);
    const res3 = await doHeavyComputation(res1);
    return 'simle';
}

async function doSomethingInSeries(): Promise<any> {
  let timeoutId;

  const delay = new Promise(function(resolve, reject){
    timeoutId = setTimeout(function(){
      reject(new Error('timeout'));
    }, 1000);
  });

  // overall timeout
  return Promise.race([delay, _doSomethingInSeries()])
    .then( (res) => {
      clearTimeout(timeoutId);
      return res;
    });

}

Любые ошибки?

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

Ответ 3

Проблема с @Bergi, ответьте, что doSomethingInSeries продолжает выполняться, даже если вы уже отклонили обещание. Гораздо лучше просто отменить по таймауту.

Вот поддержка отмены:

async function doSomethingInSeries(cancellationToken) {
  cancellationToken.throwIfCancelled();
  const res1 = await callApi();
  cancellationToken.throwIfCancelled();
  const res2 = await persistInDB(res1);
  cancellationToken.throwIfCancelled();
  const res3 = await doHeavyComputation(res1);
  cancellationToken.throwIfCancelled();
  return 'simle';
}

Но еще лучше передать токен отмены каждой асинхронной функции и использовать его там.

Вот реализация отмены:

let cancellationToken = {
  cancelled: false,
  cancel: function() {
    this.cancelled = true;
  },
  throwIfCancelled: function() {
    if (this.cancelled) throw new Error('Cancelled');
  }
}

Вы можете обернуть его как класс, если хотите.

И, наконец, использование:

doSomethingInSeries(cancellationToken);

setTimeout(cancellationToken.cancel, 5000);

Имейте в виду, что задача не отменяется сразу, поэтому продолжение (ожидание, затем или перехват) не вызывается точно через 5 секунд. Чтобы гарантировать, что вы можете совместить это и @Bergi подход.