Promesse - est-il possible de forcer l'annulation d'une promesse

91

J'utilise ES6 Promises pour gérer toutes mes récupérations de données réseau et il y a des situations où je dois forcer leur annulation.

Fondamentalement, le scénario est tel que j'ai une recherche de saisie anticipée sur l'interface utilisateur où la demande est déléguée au backend doit effectuer la recherche basée sur l'entrée partielle. Bien que cette requête réseau (n ° 1) puisse prendre un peu de temps, l'utilisateur continue de taper ce qui déclenche éventuellement un autre appel backend (n ° 2)

Ici, le n ° 2 a naturellement la priorité sur le n ° 1, je voudrais donc annuler la demande d'encapsulation Promise n ° 1. J'ai déjà un cache de toutes les promesses dans la couche de données afin que je puisse théoriquement le récupérer lorsque j'essaie de soumettre une promesse pour # 2.

Mais comment annuler la promesse n ° 1 une fois que je la récupère du cache?

Quelqu'un pourrait-il suggérer une approche?

Moonwalker
la source
2
est-ce une option pour utiliser un équivalent d'une fonction anti-rebond pour ne pas se déclencher souvent et devenir des requêtes obsolètes? Disons qu'un délai de 300 ms ferait l'affaire. Par exemple, Lodash a l'une des implémentations - lodash.com/docs#debounce
shershen
C'est à ce moment que des choses comme Bacon et Rx sont utiles.
elclanrs
@shershen oui - nous l'avons mais ce n'est pas tant à propos du problème de l'interface utilisateur ... la requête du serveur peut prendre un peu de temps donc je veux pouvoir annuler les promesses ...
Moonwalker
Essayez les observables de Rxjs
FieryCod

Réponses:

164

Non, nous ne pouvons pas encore faire ça.

Promesses ES6 ne prennent pas en charge l' annulation encore . Il est en route et sa conception est quelque chose sur lequel beaucoup de gens ont travaillé très dur. La sémantique d'annulation du son est difficile à maîtriser et c'est un travail en cours. Il y a des débats intéressants sur le repo «fetch», sur esdiscuss et sur plusieurs autres dépôts sur GH mais je serais juste patient si j'étais vous.

Mais, mais, mais ... l'annulation est vraiment importante!

C'est vrai, la réalité de la question est que l'annulation est vraiment un scénario important dans la programmation côté client. Les cas que vous décrivez comme l'annulation de requêtes Web sont importants et ils sont partout.

Alors ... la langue m'a foutu!

Ouais, désolé pour ça. Les promesses devaient être reçues en premier avant que d'autres choses ne soient spécifiées - elles sont donc entrées sans certaines choses utiles comme .finallyet .cancel- elles sont en route cependant, vers les spécifications via le DOM. L'annulation n'est pas une réflexion après coup, c'est juste une contrainte de temps et une approche plus itérative de la conception d'API.

Alors qu'est-ce que je peux faire?

Vous avez plusieurs alternatives:

  • Utilisez une bibliothèque tierce comme bluebird qui peut se déplacer beaucoup plus rapidement que la spécification et donc avoir une annulation ainsi qu'un tas d'autres goodies - c'est ce que font les grandes entreprises comme WhatsApp.
  • Transmettez un jeton d' annulation .

L'utilisation d'une bibliothèque tierce est assez évidente. En ce qui concerne un jeton, vous pouvez faire en sorte que votre méthode prenne une fonction, puis l'appelle comme telle:

function getWithCancel(url, token) { // the token is for cancellation
   var xhr = new XMLHttpRequest;
   xhr.open("GET", url);
   return new Promise(function(resolve, reject) {
      xhr.onload = function() { resolve(xhr.responseText); });
      token.cancel = function() {  // SPECIFY CANCELLATION
          xhr.abort(); // abort request
          reject(new Error("Cancelled")); // reject the promise
      };
      xhr.onerror = reject;
   });
};

Ce qui vous permettrait de faire:

var token = {};
var promise = getWithCancel("/someUrl", token);

// later we want to abort the promise:
token.cancel();

Votre cas d'utilisation réel - last

Ce n'est pas trop difficile avec l'approche des jetons:

function last(fn) {
    var lastToken = { cancel: function(){} }; // start with no op
    return function() {
        lastToken.cancel();
        var args = Array.prototype.slice.call(arguments);
        args.push(lastToken);
        return fn.apply(this, args);
    };
}

Ce qui vous permettrait de faire:

var synced = last(getWithCancel);
synced("/url1?q=a"); // this will get canceled 
synced("/url1?q=ab"); // this will get canceled too
synced("/url1?q=abc");  // this will get canceled too
synced("/url1?q=abcd").then(function() {
    // only this will run
});

Et non, les bibliothèques comme Bacon et Rx ne «brillent» pas ici parce que ce sont des bibliothèques observables, elles ont juste le même avantage que les bibliothèques de promesse de niveau utilisateur ont en n'étant pas liées aux spécifications. Je suppose que nous attendrons d'avoir et de voir dans ES2016 quand les observables deviendront natifs. Ils sont cependant très pratiques pour la tête de frappe.

Benjamin Gruenbaum
la source
25
Benjamin, j'ai vraiment apprécié la lecture de votre réponse. Très bien pensé, structuré, articulé et avec de bons exemples pratiques et alternatives. Très utile. Merci.
Moonwalker
Les jetons d'annulation @FranciscoPresencia sont en route en tant que proposition de phase 1.
Benjamin Gruenbaum
Où pouvons-nous lire cette annulation basée sur des jetons? Où est la proposition?
mal le
@harm la proposition est morte à l'étape 1.
Benjamin Gruenbaum
1
J'adore le travail de Ron, mais je pense que nous devrions attendre un peu avant de faire des recommandations pour les bibliothèques que les gens n'utilisent pas encore:] Merci pour le lien mais je vais le vérifier!
Benjamin Gruenbaum
24

Les propositions standard de promesses annulables ont échoué.

Une promesse n'est pas une surface de contrôle pour l'action asynchrone qui l'accomplit; confond le propriétaire avec le consommateur. Au lieu de cela, créez des fonctions asynchrones qui peuvent être annulées via un jeton transmis.

Une autre promesse fait un bon jeton, rendant l'annulation facile à implémenter avec Promise.race:

Exemple: Utilisez Promise.racepour annuler l'effet d'une chaîne précédente:

let cancel = () => {};

input.oninput = function(ev) {
  let term = ev.target.value;
  console.log(`searching for "${term}"`);
  cancel();
  let p = new Promise(resolve => cancel = resolve);
  Promise.race([p, getSearchResults(term)]).then(results => {
    if (results) {
      console.log(`results for "${term}"`,results);
    }
  });
}

function getSearchResults(term) {
  return new Promise(resolve => {
    let timeout = 100 + Math.floor(Math.random() * 1900);
    setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout);
  });
}
Search: <input id="input">

Ici, nous "annulons" les recherches précédentes en injectant un undefinedrésultat et en le testant, mais nous pourrions facilement imaginer rejeter avec à la "CancelledError"place.

Bien sûr, cela n'annule pas réellement la recherche sur le réseau, mais c'est une limitation de fetch. Si fetchvous preniez une promesse d'annulation comme argument, cela pourrait annuler l'activité du réseau.

J'ai proposé ce "modèle d'annulation de promesse" sur es-discuter, exactement pour suggérer que fetchfaire ceci.

foc
la source
@jib pourquoi refuser ma modification? Je viens de le clarifier.
allenyllee
8

J'ai vérifié la référence Mozilla JS et j'ai trouvé ceci:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race

Regardons ça:

var p1 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 500, "one"); 
});
var p2 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 100, "two"); 
});

Promise.race([p1, p2]).then(function(value) {
  console.log(value); // "two"
  // Both resolve, but p2 is faster
});

Nous avons ici p1 et p2 mis en Promise.race(...)arguments, cela crée en fait une nouvelle promesse de résolution, ce dont vous avez besoin.

nikola-miljkovic
la source
NICE - c'est peut-être exactement ce dont j'ai besoin. Je vais essayer.
Moonwalker
Si vous rencontrez des problèmes, vous pouvez coller le code ici pour que je puisse vous aider :)
nikola-miljkovic
5
Essayé. Pas tout à fait là. Cela résout la promesse la plus rapide ... Je dois toujours résoudre la dernière soumission, c'est-à-dire annuler sans condition toutes les promesses plus anciennes ..
Moonwalker
1
De cette façon, toutes les autres promesses ne sont plus traitées, vous ne pouvez pas réellement annuler une promesse.
nikola-miljkovic
Je l'ai essayé, la deuxième promesse (une dans cet ex) ne laisse pas le processus se
terminer
3

Pour Node.js et Electron, je recommande vivement d'utiliser les extensions de promesse pour JavaScript (Prex) . Son auteur, Ron Buckton, est l'un des principaux ingénieurs de TypeScript et est également le type derrière la proposition d' annulation ECMAScript actuelle du TC39 . La bibliothèque est bien documentée et il y a des chances que certains de Prex se mettent au standard.

Sur une note personnelle et venant de l'arrière-plan C #, j'aime beaucoup le fait que Prex est modelé sur le framework Cancellation in Managed Threads , c'est-à-dire basé sur l'approche adoptée avec les API CancellationTokenSource/ CancellationToken.NET. D'après mon expérience, ceux-ci ont été très pratiques pour implémenter une logique d'annulation robuste dans les applications gérées.

Je l'ai également vérifié pour fonctionner dans un navigateur en regroupant Prex à l'aide de Browserify .

Voici un exemple de délai avec annulation ( Gist et RunKit , utilisant Prex pour son CancellationTokenet Deferred):

// by @noseratio
// https://gist.github.com/noseratio/141a2df292b108ec4c147db4530379d2
// https://runkit.com/noseratio/cancellablepromise

const prex = require('prex');

/**
 * A cancellable promise.
 * @extends Promise
 */
class CancellablePromise extends Promise {
  static get [Symbol.species]() { 
    // tinyurl.com/promise-constructor
    return Promise; 
  }

  constructor(executor, token) {
    const withCancellation = async () => {
      // create a new linked token source 
      const linkedSource = new prex.CancellationTokenSource(token? [token]: []);
      try {
        const linkedToken = linkedSource.token;
        const deferred = new prex.Deferred();
  
        linkedToken.register(() => deferred.reject(new prex.CancelError()));
  
        executor({ 
          resolve: value => deferred.resolve(value),
          reject: error => deferred.reject(error),
          token: linkedToken
        });

        await deferred.promise;
      } 
      finally {
        // this will also free all linkedToken registrations,
        // so the executor doesn't have to worry about it
        linkedSource.close();
      }
    };

    super((resolve, reject) => withCancellation().then(resolve, reject));
  }
}

/**
 * A cancellable delay.
 * @extends Promise
 */
class Delay extends CancellablePromise {
  static get [Symbol.species]() { return Promise; }

  constructor(delayMs, token) {
    super(r => {
      const id = setTimeout(r.resolve, delayMs);
      r.token.register(() => clearTimeout(id));
    }, token);
  }
}

// main
async function main() {
  const tokenSource = new prex.CancellationTokenSource();
  const token = tokenSource.token;
  setTimeout(() => tokenSource.cancel(), 2000); // cancel after 2000ms

  let delay = 1000;
  console.log(`delaying by ${delay}ms`); 
  await new Delay(delay, token);
  console.log("successfully delayed."); // we should reach here

  delay = 2000;
  console.log(`delaying by ${delay}ms`); 
  await new Delay(delay, token);
  console.log("successfully delayed."); // we should not reach here
}

main().catch(error => console.error(`Error caught, ${error}`));

Notez que l'annulation est une course. Par exemple, une promesse peut avoir été résolue avec succès, mais au moment où vous la respectez (avec awaitou then), l'annulation peut également avoir été déclenchée. C'est à vous de décider comment vous gérez cette course, mais cela ne fait pas de mal d'appeler token.throwIfCancellationRequested()un temps supplémentaire, comme je le fais ci-dessus.

noseratio
la source
1

J'ai rencontré un problème similaire récemment.

J'avais un client basé sur la promesse (pas un client réseau) et je voulais toujours donner les dernières données demandées à l'utilisateur pour que l'interface utilisateur soit fluide.

Après avoir lutté avec l'idée d'annulation, Promise.race(...)et Promise.all(..)j'ai commencé à me souvenir de mon dernier identifiant de demande et lorsque la promesse a été remplie, je ne rendais mes données que lorsqu'elles correspondaient à l'identifiant d'une dernière demande.

J'espère que ça aide quelqu'un.

Igor Słomski
la source
Slomski la question n'est pas de savoir quoi montrer sur l'interface utilisateur. Il s'agit d'annuler la promesse
CyberAbhay
0

Vous pouvez faire rejeter la promesse avant de terminer:

// Our function to cancel promises receives a promise and return the same one and a cancel function
const cancellablePromise = (promiseToCancel) => {
  let cancel
  const promise = new Promise((resolve, reject) => {
    cancel = reject
    promiseToCancel
      .then(resolve)
      .catch(reject)
  })
  return {promise, cancel}
}

// A simple promise to exeute a function with a delay
const waitAndExecute = (time, functionToExecute) => new Promise((resolve, reject) => {
  timeInMs = time * 1000
  setTimeout(()=>{
    console.log(`Waited ${time} secs`)
    resolve(functionToExecute())
  }, timeInMs)
})

// The promise that we will cancel
const fetchURL = () => fetch('https://pokeapi.co/api/v2/pokemon/ditto/')

// Create a function that resolve in 1 seconds. (We will cancel it in 0.5 secs)
const {promise, cancel} = cancellablePromise(waitAndExecute(1, fetchURL))

promise
  .then((res) => {
    console.log('then', res) // This will executed in 1 second
  })
  .catch(() => {
    console.log('catch') // We will force the promise reject in 0.5 seconds
  })

waitAndExecute(0.5, cancel) // Cancel previous promise in 0.5 seconds, so it will be rejected before finishing. Commenting this line will make the promise resolve

Malheureusement, l'appel de récupération a déjà été effectué, vous verrez donc la résolution de l'appel dans l'onglet Réseau. Votre code l'ignorera simplement.

Rashomon
la source
0

En utilisant la sous-classe Promise fournie par le package externe, cela peut être fait comme suit: Démo en direct

import CPromise from "c-promise2";

function fetchWithTimeout(url, {timeout, ...fetchOptions}= {}) {
    return new CPromise((resolve, reject, {signal}) => {
        fetch(url, {...fetchOptions, signal}).then(resolve, reject)
    }, timeout)
}

const chain= fetchWithTimeout('http://localhost/')
    .then(response => response.json())
    .then(console.log, console.warn);

//chain.cancel(); call this to abort the promise and releated request
Dmitriy Mozgovoy
la source
-1

Parce que @jib rejette ma modification, je publie donc ma réponse ici. C'est juste la modification de la réponse de @ jib avec quelques commentaires et en utilisant des noms de variables plus compréhensibles.

Ci-dessous, je montre juste des exemples de deux méthodes différentes: l'une est résoudre () l'autre est rejeter ()

let cancelCallback = () => {};

input.oninput = function(ev) {
  let term = ev.target.value;
  console.log(`searching for "${term}"`);
  cancelCallback(); //cancel previous promise by calling cancelCallback()

  let setCancelCallbackPromise = () => {
    return new Promise((resolve, reject) => {
      // set cancelCallback when running this promise
      cancelCallback = () => {
        // pass cancel messages by resolve()
        return resolve('Canceled');
      };
    })
  }

  Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => {
    // check if the calling of resolve() is from cancelCallback() or getSearchResults()
    if (results == 'Canceled') {
      console.log("error(by resolve): ", results);
    } else {
      console.log(`results for "${term}"`, results);
    }
  });
}


input2.oninput = function(ev) {
  let term = ev.target.value;
  console.log(`searching for "${term}"`);
  cancelCallback(); //cancel previous promise by calling cancelCallback()

  let setCancelCallbackPromise = () => {
    return new Promise((resolve, reject) => {
      // set cancelCallback when running this promise
      cancelCallback = () => {
        // pass cancel messages by reject()
        return reject('Canceled');
      };
    })
  }

  Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => {
    // check if the calling of resolve() is from cancelCallback() or getSearchResults()
    if (results !== 'Canceled') {
      console.log(`results for "${term}"`, results);
    }
  }).catch(error => {
    console.log("error(by reject): ", error);
  })
}

function getSearchResults(term) {
  return new Promise(resolve => {
    let timeout = 100 + Math.floor(Math.random() * 1900);
    setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout);
  });
}
Search(use resolve): <input id="input">
<br> Search2(use reject and catch error): <input id="input2">

allenyllee
la source