Promesa: ¿es posible forzar la cancelación de una promesa?

Resuelto Moonwalker asked hace 9 años • 13 respuestas

Utilizo ES6 Promises para administrar toda la recuperación de datos de mi red y hay algunas situaciones en las que necesito forzar su cancelación.

Básicamente, el escenario es tal que tengo una búsqueda de escritura anticipada en la interfaz de usuario donde la solicitud se delega al backend y tiene que realizar la búsqueda en función de la entrada parcial. Si bien esta solicitud de red (n.° 1) puede demorar un poco, el usuario continúa escribiendo, lo que eventualmente desencadena otra llamada de backend (n.° 2).

Aquí el n.° 2, naturalmente, tiene prioridad sobre el n.° 1, por lo que me gustaría cancelar la solicitud de envoltorio de promesa n.° 1. Ya tengo un caché de todas las Promesas en la capa de datos, por lo que, en teoría, puedo recuperarlas mientras intento enviar una Promesa para el n.° 2.

Pero, ¿cómo cancelo la Promesa n.° 1 una vez que la recupero del caché?

¿Alguien podría sugerir un enfoque?

Moonwalker avatar May 14 '15 16:05 Moonwalker
Aceptado

En JavaScript moderno, no

Las promesas se han cumplido (ja) y parece que nunca será posible cancelar una promesa (pendiente).

En su lugar, existe una primitiva de cancelación multiplataforma (nodo, navegadores, etc.) como parte de WHATWG (un organismo de estándares que también crea HTML) llamada AbortController. Puedes usarlo para cancelar funciones que devuelven promesas en lugar de promesas mismas:

// Take a signal parameter in the function that needs cancellation
async function somethingIWantToCancel({ signal } = {}) {
  // either pass it directly to APIs that support it
  // (fetch and most Node APIs do)
  const response = await fetch('.../', { signal });
  // return response.json;

  // or if the API does not already support it -
  // manually adapt your code to support signals:
  const onAbort = (e) => {
    // run any code relating to aborting here
  };
  signal.addEventListener('abort', onAbort, { once: true });
  // and be sure to clean it up when the action you are performing
  // is finished to avoid a leak
  // ... sometime later ...
  signal.removeEventListener('abort', onAbort);
}

// Usage
const ac = new AbortController();
setTimeout(() => ac.abort(), 1000); // give it a 1s timeout
try {
  await somethingIWantToCancel({ signal: ac.signal });
} catch (e) {
  if (e.name === 'AbortError') {
    // deal with cancellation in caller, or ignore
  } else {
    throw e; // don't swallow errors :)
  }
}

No. No podemos hacer eso todavía.

Las promesas de ES6 aún no admiten la cancelación . Está en camino y su diseño es algo en lo que mucha gente trabajó muy duro. La semántica de cancelación de sonido es difícil de lograr y esto es un trabajo en progreso. Hay debates interesantes sobre el repositorio "fetch", sobre esdiscuss y sobre varios otros repositorios de GH, pero tendría paciencia si fuera usted.

Pero, pero, pero... ¡la cancelación es realmente importante!

Lo es, la realidad del asunto es que la cancelación es realmente un escenario importante en la programación del lado del cliente. Los casos que usted describe, como la cancelación de solicitudes web, son importantes y están en todas partes.

Entonces... ¡el idioma me jodió!

Sí, lo siento por eso. Las promesas tenían que llegar primero antes de que se especificaran más cosas, por lo que entraron sin algunas cosas útiles como .finallyy .cancel, aunque está en camino hacia la especificación a través del DOM. La cancelación no es una ocurrencia tardía, es solo una limitación de tiempo y un enfoque más iterativo para el diseño de API.

¿Entonces Que puedo hacer?

Tienes varias alternativas:

  • Utilice una biblioteca de terceros como bluebird , que puede moverse mucho más rápido que la especificación y, por lo tanto, tener cancelación y muchas otras ventajas; esto es lo que hacen las grandes empresas como WhatsApp.
  • Pasar un token de cancelación .

Usar una biblioteca de terceros es bastante obvio. En cuanto a un token, puedes hacer que tu método tome una función y luego la llame, como tal:

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

Lo que te permitiría hacer:

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

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

Su caso de uso real:last

Esto no es demasiado difícil con el enfoque simbólico:

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

Lo que te permitiría hacer:

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

Y no, las bibliotecas como Bacon y Rx no "brillan" aquí porque son bibliotecas observables, simplemente tienen la misma ventaja que tienen las bibliotecas de promesas a nivel de usuario al no estar sujetas a especificaciones. Supongo que esperaremos para ver en ES2016 cuándo los observables se vuelven nativos. Sin embargo , son ingeniosos para escribir con anticipación.

Benjamin Gruenbaum avatar May 14 '2015 10:05 Benjamin Gruenbaum

Con AbortController

Es posible utilizar el controlador de aborto para rechazar la promesa o resolver según su demanda:

let controller = new AbortController();

let task = new Promise((resolve, reject) => {
  const abortListener = ({target}) => {
    controller.signal.removeEventListener('abort', abortListener);
    reject(target.reason);
  }
  controller.signal.addEventListener('abort', abortListener);

  // some logic ...
});

controller.abort('cancelled reason'); // task is now in rejected state

También es mejor eliminar el detector de eventos al cancelar para evitar pérdidas de memoria.

Y luego puede verificar si el error se produjo al cancelar verificando la controller.signal.abortedpropiedad booleana como:

const res = task.catch((err) => (
  controller.signal.aborted 
    ? { value: err } 
    : null
));

Abortar el controlador no cancelará la ejecución de la lógica interna en Promise . Pero no invocar resolveor rejectdará como resultado el pendingestado de Promesa para siempre, en ese caso tampoco será .catchdespedido con ningún error si esa es su intención (pero esto conducirá a una pérdida de memoria), por ejemplo, la recuperación cancelada activa el rechazo.

xhr.onload = () => {
  if(controller.signal.aborted) reject(controller.signal.reason);
  resolve(xhr.responseText);
}

Lo mismo funciona para cancelar la recuperación:

let controller = new AbortController();
fetch(url, {
  signal: controller.signal
});

o simplemente pasar el controlador:

let controller = new AbortController();
fetch(url, controller);

Y llame al método de aborto para cancelar una o una cantidad infinita de recuperaciones donde pasó este controlador controller.abort();

Tomas avatar Jan 20 '2021 08:01 Tomas

Las propuestas estándar de promesas cancelables han fracasado.

Una promesa no es una superficie de control para la acción asíncrona que la cumple; Confunde propietario con consumidor. En su lugar, cree funciones asincrónicas que puedan cancelarse mediante algún token pasado.

Otra promesa es una buena señal, lo que hace que cancelar sea fácil de implementar con Promise.race:

Ejemplo: Úselo Promise.racepara cancelar el efecto de una cadena anterior:

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">
Expandir fragmento

Aquí estamos "cancelando" búsquedas anteriores inyectando un undefinedresultado y probándolo, pero podríamos imaginar fácilmente rechazarlo "CancelledError"en su lugar.

Por supuesto, esto en realidad no cancela la búsqueda en la red, pero es una limitación de fetch. Si fetchtomáramos una promesa de cancelación como argumento, entonces podría cancelar la actividad de la red.

He propuesto este "patrón de promesa de cancelación" en es-discuss, exactamente para sugerir que fetchse haga esto.

jib avatar Jan 01 '2017 17:01 jib