Есть ли способ короткого замыкания async/wait flow?

async function update() {
   var urls = await getCdnUrls();
   var metadata = await fetchMetaData(urls);
   var content = await fetchContent(metadata);
   await render(content);
   return;
}
//All the four functions return a promise. (getCdnUrls, fetchMetaData, fetchContent, render)

Что делать, если мы хотим в любой момент прервать последовательность извне?

Скажем, когда выполняется fetchMetaData, мы понимаем, что компонент больше не нужен для рендеринга, и мы хотим отменить оставшиеся операции (fetchContent и render). Есть ли способ прервать/отменить со стороны потребителя?

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

Ответы

Ответ 1

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

Что делает для вас спецификация

Отмена "правильно" на самом деле очень тяжелая. Люди работали над этим некоторое время, и было решено не блокировать асинхронные функции.

Есть два предложения, которые пытаются решить это в ядре ECMAScript:

  • Аннулирование токенов - это добавляет маркеры отмены, которые направлены на решение этой проблемы.
  • Отменное обещание - добавление catch cancel (e) { синтаксис и синтаксис throw.cancel целью которого является решение этой проблемы.

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

Что вы можете сделать, чтобы решить это с вашей стороны

Аннулирование токенов легко реализовать. К сожалению, такого рода аннулирование, которое вы действительно хотели бы (например, "отмену третьего состояния, когда отмена не является исключением") невозможно с асинхронными функциями в данный момент, так как вы не контролируете, как они работают. Вы можете сделать две вещи:

  • Вместо этого используйте сопрограммы - корабли bluebird со звуковой отменой с использованием генераторов и обещаний, которые вы можете использовать.
  • Реализовать жетоны с абортивной семантикой - это на самом деле довольно легко, поэтому сделаем это здесь

CancellationTokens

Ну, подавление токенов:

class Token {
   constructor(fn) {
      this.isCancellationRequested = false; 
      this.onCancelled = []; // actions to execute when cancelled
      this.onCancelled.push(() => this.isCancellationRequested = true);
      // expose a promise to the outside
      this.promise = new Promise(resolve => this.onCancelled.push(resolve));
      // let the user add handlers
      fn(f => this.onCancelled.push(f));
   }
   cancel() { this.onCancelled.forEach(x => x); }
}

Это позволит вам сделать что-то вроде:

async function update(token) {
   if(token.isCancellationRequested) return;
   var urls = await getCdnUrls();
   if(token.isCancellationRequested) return;
   var metadata = await fetchMetaData(urls);
   if(token.isCancellationRequested) return;
   var content = await fetchContent(metadata);
   if(token.isCancellationRequested) return;
   await render(content);
   return;
}

var token = new Token(); // don't ned any special handling here
update(token);
// ...
if(updateNotNeeded) token.cancel(); // will abort asynchronous actions

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

Оптимально, все ваши временные функции были бы осведомлены и throw бы на отмену (опять же, только потому, что мы не можем иметь третье состояние), которое будет выглядеть так:

async function update(token) {
   var urls = await getCdnUrls(token);
   var metadata = await fetchMetaData(urls, token);
   var content = await fetchContent(metadata, token);
   await render(content, token);
   return;
}

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

Вот как можно было бы написать getCdnUrl (отметить единственное) с помощью API XMLHttpRequest в браузерах:

function getCdnUrl(url, token) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    var p = new Promise((resolve, reject) => {
      xhr.onload = () => resolve(xhr);
      xhr.onerror = e => reject(new Error(e));
      token.promise.then(x => { 
        try { xhr.abort(); } catch(e) {}; // ignore abort errors
        reject(new Error("cancelled"));
      });
   });
   xhr.send();
   return p;
}

Это так близко, что мы можем получить с асинхронными функциями без сопрограмм. Это не очень красиво, но это, безусловно, полезно.

Обратите внимание, что вы хотите избежать отмены, рассматриваемого как исключения. Это означает, что если ваши функции throw вам необходимо отфильтровать эти ошибки для глобальных обработчиков ошибок process.on("unhandledRejection", e =>... и т.д.

Ответ 2

Вы можете получить то, что хотите, с помощью машинописного текста + Bluebird + cancelable-awaiter.

Теперь, когда все доказательства указывают на то, что маркеры отмены не попадают в ECMAScript, я считаю, что лучшим решением для отмены является реализация bluebird, упомянутая @BenjaminGruenbaum, однако я нахожу, что использование ко-подпрограмм и генераторов немного неуклюжие и непростые на глазах,

Поскольку я использую TypScript, который теперь поддерживает синтаксис async/await для целей es5 и es3, я создал простой модуль, который заменяет помощника __awaiter по умолчанию на тот, который поддерживает отмену __awaiter: https://www.npmjs.com/пакет/отменяемый-awaiter

Ответ 3

К сожалению, нет, вы не можете контролировать поток выполнения поведения по умолчанию async/await - это не значит, что сама проблема невозможна, это означает, что вам нужно немного изменить свой подход.

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

Если вы хотите использовать этот шаблон довольно часто, лучшим решением, вероятно, является переход на генераторы: в то время как они не так широко распространены, они позволяют вам определять поведение каждого шага, и добавление отмены является самым простым. Генераторы довольно мощные, но, как я уже упоминал, они требуют функции runner и не так просто, как async/await.

Другим подходом является создание шаблона отмененных токенов - вы создаете объект, который будет заполнен функцией, которая хочет реализовать эту функциональность:

async function updateUser(token) {
  let cancelled = false;

  // we don't reject, since we don't have access to
  // the returned promise
  // so we just don't call other functions, and reject
  // in the end
  token.cancel = () => {
    cancelled = true;
  };

  const data = await wrapWithCancel(fetchData)();
  const userData = await wrapWithCancel(updateUserData)(data);
  const userAddress = await wrapWithCancel(updateUserAddress)(userData);
  const marketingData = await wrapWithCancel(updateMarketingData)(userAddress);

  // because we've wrapped all functions, in case of cancellations
  // we'll just fall through to this point, without calling any of
  // actual functions. We also can't reject by ourselves, since
  // we don't have control over returned promise
  if (cancelled) {
    throw { reason: 'cancelled' };
  }

  return marketingData;

  function wrapWithCancel(fn) {
    return data => {
      if (!cancelled) {
        return fn(data);
      }
    }
  }
}

const token = {};
const promise = updateUser(token);
// wait some time...
token.cancel(); // user will be updated any way

Я написал статьи, как об отмене, так и генераторах:

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

Ответ 4

Вот простой пример, который требует от вас создания нового обещания:

let resp = await new Promise(function(resolve, reject) {
    timeout(5000).then(() => resolve('Promise RESOLVED !'), reject);
    $('#btn').click(() => resolve('Promise CANCELED !'));
});

Пожалуйста, посмотрите этот кодовый блок для демонстрации

Ответ 5

Как и в обычном коде, вы должны выдать исключение из первой функции (или каждой из следующих функций) и попробовать блок вокруг всего набора вызовов. Нет необходимости иметь дополнительные if-elses. Это один из приятных битов об async/await, который вы получите, чтобы обрабатывать ошибки, как мы привыкли из обычного кода.

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

Ответ 6

Пример написан на узле с Typescript вызова, который можно прервать извне:

function cancelable(asyncFunc: Promise<void>): [Promise<void>, () => boolean] {
  class CancelEmitter extends EventEmitter { }

  const cancelEmitter = new CancelEmitter();
  const promise = new Promise<void>(async (resolve, reject) => {

    cancelEmitter.on('cancel', () => {
      resolve();
    });

    try {
      await asyncFunc;
      resolve();
    } catch (err) {
      reject(err);
    }

  });

  return [promise, () => cancelEmitter.emit('cancel')];
}

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

const asyncFunction = async () => {
  // doSomething
}

const [promise, cancel] = cancelable(asyncFunction());

setTimeout(() => {
  cancel();
}, 2000);

(async () => await promise)();