web-dev-qa-db-fra.com

Existe-t-il un moyen de court-circuiter 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)

Et si nous voulions interrompre la séquence de l'extérieur, à tout moment?

Par exemple, lorsque fetchMetaData est en cours d'exécution, nous réalisons que le rendu du composant n'est plus nécessaire et nous voulons annuler les opérations restantes (fetchContent et rendu). Est-il possible d'abandonner/d'annuler de l'extérieur par le consommateur?

Nous pourrions vérifier après chaque attente d'une condition, mais cela semble simplement une façon peu élégante de le faire. et il attendra toujours que l'opération en cours se termine.

20
sbr

Je viens de parler de cela - c'est un sujet intéressant, mais malheureusement, vous n'allez pas vraiment aimer les solutions que je vais proposer, car ce sont des solutions de passerelle.

Ce que la spécification fait pour vous

Il est très difficile d'obtenir l'annulation "juste ce qu'il faut". Les gens travaillent là-dessus depuis un moment et il a été décidé de ne pas bloquer les fonctions asynchrones. 

Deux propositions tentent de résoudre ce problème dans le noyau ECMAScript:

  • Jetons d'annulation - qui ajoute des jetons d'annulation ayant pour but de résoudre ce problème.
  • Promesse annulable - qui ajoute la syntaxe catch cancel (e) { et la syntaxe throw.cancel qui visent à résoudre ce problème.

Les deux propositions ont considérablement changé au cours de la dernière semaine donc je ne compterais pas non plus pour arriver dans l'année à venir. Les propositions sont quelque peu complémentaires et ne sont pas contradictoires.

Que pouvez-vous faire pour résoudre ce problème de votre côté?

Les jetons d'annulation sont faciles à mettre en œuvre. Malheureusement, le type d'annulation que vous voulez vraiment voulez (aka " troisième état annulation où l'annulation n'est pas une exception) est impossible avec les fonctions asynchrones pour le moment, car vous ne contrôlez pas la façon dont elles sont ' Vous pouvez faire deux choses:

  • Utilisez plutôt les coroutines - bluebird avec l'annulation du son en utilisant des générateurs et des promesses que vous pouvez utiliser.
  • Implémenter des jetons avec une sémantique abortive - c'est en fait assez facile alors faisons-le ici

AnnulationTokens

Eh bien, un jeton signale l'annulation: 

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); }
}

Cela vous permettrait de faire quelque chose comme:

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

Ce qui est une manière vraiment laide qui fonctionnerait, de manière optimale, vous voudriez que les fonctions asynchrones en soient conscientes, mais elles ne le sont pas ( pour le moment ). 

De manière optimale, toutes vos fonctions intérimaires seraient au courant et throw lors de l'annulation (encore une fois, uniquement parce que nous ne pouvons pas avoir d'état tiers) qui ressemblerait à ceci:

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;
}

Étant donné que chacune de nos fonctions est au courant des annulations, elle peut effectuer une annulation logique réelle: getCdnUrls peut abandonner la demande et lancer, fetchMetaData peut abandonner la demande sous-jacente et lancer, etc.

Voici comment écrire getCdnUrl (notez le singulier) à l'aide de l'API XMLHttpRequest dans les navigateurs:

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;
}

C’est aussi proche que possible des fonctions asynchrones sans coroutines. Ce n'est pas très joli mais c'est certainement utilisable.

Notez que vous souhaitez éviter que les annulations ne soient traitées comme des exceptions. Cela signifie que si vos fonctions throw à l'annulation, vous devez filtrer ces erreurs sur les gestionnaires d'erreur globaux process.on("unhandledRejection", e => ... et autres.

20

Vous pouvez obtenir ce que vous voulez en utilisant TypeScript + Bluebird + cancelable-waiter .

Maintenant que toutes les preuves suggèrent des jetons d'annulation ne pas parvenir à ECMAScript , je pense que la meilleure solution pour les annulations est l'implémentation de bluebird mentionnée par @BenjaminGruenbaum , mais je trouve l'utilisation de co-routines et de générateurs un peu maladroit et mal à l'aise sur les yeux.

Étant donné que j'utilise TypeScript, qui prend désormais en charge la syntaxe async/wait pour les cibles es5 et es3, j'ai créé un module simple qui remplace l'assistant __awaiter par défaut par celui qui prend en charge les annulations de bluebird: https: //www.npmjs. com/package/cancelable-waiter

3
Itay

Malheureusement, non, vous ne pouvez pas contrôler le flux d’exécution du comportement asynchrone/wait par défaut - cela ne signifie pas que le problème lui-même est impossible, cela signifie que vous devez modifier légèrement votre approche.

Tout d’abord, votre proposition de regrouper chaque ligne asynchrone dans un chèque est une solution efficace, et si vous n’avez que quelques emplacements dotés de telles fonctionnalités, il n’y a rien de mal à cela.

Si vous voulez utiliser ce modèle assez souvent, la meilleure solution est probablement pour passer aux générateurs : bien que cela ne soit pas si répandu, ils vous permettent de définir le comportement de chaque étape, et l'ajout de cancel est le plus simple. Les générateurs sont assez puissants , mais, comme je l’ai mentionné plus haut, ils nécessitent une fonction runner et pas aussi simple que async/wait.

Une autre approche consiste à créer modèle de jetons annulable - vous créez un objet, qui remplira une fonction qui veut implémenter cette fonctionnalité:

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

J'ai écrit des articles sur l'annulation et les générateurs:

Pour résumer, vous devez effectuer un travail supplémentaire afin de prendre en charge la canncellation. Si vous souhaitez en faire un citoyen de première classe dans votre application, vous devez utiliser des générateurs.

0
Bloomca