Cancelar una cadena de promesa ECMAScript 6 de vainilla

110

¿Existe algún método para borrar los .thenmensajes de correo Promiseelectrónico de una instancia de JavaScript ?

Escribí un marco de prueba de JavaScript sobre QUnit . El marco ejecuta pruebas de forma sincrónica ejecutando cada una en un Promise. (Perdón por la longitud de este bloque de código. Lo comenté lo mejor que pude, para que se sienta menos tedioso).

/* Promise extension -- used for easily making an async step with a
       timeout without the Promise knowing anything about the function 
       it's waiting on */
$$.extend(Promise, {
    asyncTimeout: function (timeToLive, errorMessage) {
        var error = new Error(errorMessage || "Operation timed out.");
        var res, // resolve()
            rej, // reject()
            t,   // timeout instance
            rst, // reset timeout function
            p,   // the promise instance
            at;  // the returned asyncTimeout instance

        function createTimeout(reject, tempTtl) {
            return setTimeout(function () {
                // triggers a timeout event on the asyncTimeout object so that,
                // if we want, we can do stuff outside of a .catch() block
                // (may not be needed?)
                $$(at).trigger("timeout");

                reject(error);
            }, tempTtl || timeToLive);
        }

        p = new Promise(function (resolve, reject) {
            if (timeToLive != -1) {
                t = createTimeout(reject);

                // reset function -- allows a one-time timeout different
                //    from the one original specified
                rst = function (tempTtl) {
                    clearTimeout(t);
                    t = createTimeout(reject, tempTtl);
                }
            } else {
                // timeToLive = -1 -- allow this promise to run indefinitely
                // used while debugging
                t = 0;
                rst = function () { return; };
            }

            res = function () {
                clearTimeout(t);
                resolve();
            };

            rej = reject;
        });

        return at = {
            promise: p,
            resolve: res,
            reject: rej,
            reset: rst,
            timeout: t
        };
    }
});

/* framework module members... */

test: function (name, fn, options) {
    var mod = this; // local reference to framework module since promises
                    // run code under the window object

    var defaultOptions = {
        // default max running time is 5 seconds
        timeout: 5000
    }

    options = $$.extend({}, defaultOptions, options);

    // remove timeout when debugging is enabled
    options.timeout = mod.debugging ? -1 : options.timeout;

    // call to QUnit.test()
    test(name, function (assert) {
        // tell QUnit this is an async test so it doesn't run other tests
        // until done() is called
        var done = assert.async();
        return new Promise(function (resolve, reject) {
            console.log("Beginning: " + name);

            var at = Promise.asyncTimeout(options.timeout, "Test timed out.");
            $$(at).one("timeout", function () {
                // assert.fail() is just an extension I made that literally calls
                // assert.ok(false, msg);
                assert.fail("Test timed out");
            });

            // run test function
            var result = fn.call(mod, assert, at.reset);

            // if the test returns a Promise, resolve it before resolving the test promise
            if (result && result.constructor === Promise) {
                // catch unhandled errors thrown by the test so future tests will run
                result.catch(function (error) {
                    var msg = "Unhandled error occurred."
                    if (error) {
                        msg = error.message + "\n" + error.stack;
                    }

                    assert.fail(msg);
                }).then(function () {
                    // resolve the timeout Promise
                    at.resolve();
                    resolve();
                });
            } else {
                // if test does not return a Promise, simply clear the timeout
                // and resolve our test Promise
                at.resolve();
                resolve();
            }
        }).then(function () {
            // tell QUnit that the test is over so that it can clean up and start the next test
            done();
            console.log("Ending: " + name);
        });
    });
}

Si se agota el tiempo de espera de una prueba, mi Promesa de tiempo de espera estará assert.fail()en la prueba para que la prueba se marque como fallida, lo cual está muy bien, pero la prueba continúa ejecutándose porque la Promesa de prueba ( result) todavía está esperando para resolverla.

Necesito una buena forma de cancelar mi prueba. Puedo hacerlo creando un campo en el módulo de marco this.cancelTesto algo, y verificando de vez en cuando (por ejemplo, al comienzo de cada then()iteración) dentro de la prueba si debo cancelar. Sin embargo, idealmente, podría usar $$(at).on("timeout", /* something here */)para borrar los then()s restantes en mi resultvariable, de modo que no se ejecute nada del resto de la prueba.

Existe algo como esto?

Actualización rápida

Intenté usar Promise.race([result, at.promise]). No funcionó.

Actualización 2 + confusión

Para desbloquearme, agregué algunas líneas con mod.cancelTest/ polling dentro de la idea de prueba. (También eliminé el activador de eventos).

return new Promise(function (resolve, reject) {
    console.log("Beginning: " + name);

    var at = Promise.asyncTimeout(options.timeout, "Test timed out.");
    at.promise.catch(function () {
        // end the test if it times out
        mod.cancelTest = true;
        assert.fail("Test timed out");
        resolve();
    });

    // ...
    
}).then(function () {
    // tell QUnit that the test is over so that it can clean up and start the next test
    done();
    console.log("Ending: " + name);
});

Establecí un punto de quiebre en la catchdeclaración y está siendo golpeado. Lo que me confunde ahora es que then()no se está llamando a la declaración. Ideas?

Actualización 3

Descubrí la última cosa. fn.call()estaba arrojando un error que no capté, por lo que la promesa de prueba se rechazó antes de que at.promise.catch()pudiera resolverlo.

dx_over_dt
fuente
Es posible hacer la cancelación con las promesas de ES6, pero no es una propiedad de la promesa (más bien, es una propiedad de la función que la devuelve). Puedo hacer un breve ejemplo si está interesado.
Benjamin Gruenbaum
@BenjaminGruenbaum Sé que ha pasado casi un año, pero todavía me interesa si tienes tiempo para escribir un ejemplo. :)
dx_over_dt
1
Ha sido hace un año, pero se ha debatido oficialmente dos días antes de ayer con tokens de cancelación y promesas cancelables que se trasladan a la etapa 1.
Benjamin Gruenbaum
3
La respuesta de ES6 a la cancelación de una promesa es observable. Puede leer más sobre esto aquí: github.com/Reactive-Extensions/RxJS
Frank Goortani
Vinculando mi respuesta sobre el uso de la Prexbiblioteca para la cancelación de promesas.
noseratio

Respuestas:

75

¿Existe algún método para borrar los .thenmensajes de correo electrónico de una instancia de JavaScript Promise?

No. No en ECMAScript 6 al menos. Las promesas (y sus thenmanejadores) no se pueden cancelar de forma predeterminada (desafortunadamente) . Hay un poco de discusión en es-discusion (por ejemplo, aquí ) sobre cómo hacer esto de la manera correcta, pero cualquier enfoque que gane no aterrizará en ES6.

El punto de vista actual es que la subclasificación permitirá crear promesas cancelables utilizando su propia implementación (no estoy seguro de qué tan bien funcionará) .

Hasta que el comité de idiomas haya descubierto la mejor manera (¿ES7 con suerte?) , Aún puede usar las implementaciones de promesas de usuario, muchas de las cuales incluyen la cancelación.

La discusión actual se encuentra en los borradores https://github.com/domenic/cancelable-promise y https://github.com/bergus/promise-cancellation .

Bergi
fuente
2
"Un poco de discusión" - Puedo vincular tal vez a 30 hilos en esdiscuss o GitHub :) (sin mencionar su propia ayuda con la cancelación en bluebird 3.0)
Benjamin Gruenbaum
@BenjaminGruenbaum: ¿Tienes esos enlaces listos para compartir en algún lugar? Durante mucho tiempo he querido resumir opiniones e intentos y publicar una propuesta para esdiscuss, así que estaría feliz si pudiera comprobar que no olvidé nada.
Bergi
Los tengo a mano en el trabajo, así que los tendré en 3-4 días. Puede consultar la especificación de cancelación de promesa en promises-aplus para un buen comienzo.
Benjamin Gruenbaum
1
@ LUH3417: las funciones "normales" son aburridas en ese sentido. Inicia un programa y espera hasta que haya terminado, o killlo ignora e ignora el estado posiblemente extraño en el que los efectos secundarios han dejado su entorno (por lo que normalmente también lo desecha, por ejemplo, cualquier salida a medio terminar). Sin embargo, las funciones no bloqueantes o asíncronas están diseñadas para funcionar en aplicaciones interactivas, en las que desea tener ese tipo de control más preciso sobre la ejecución de operaciones en curso.
Bergi
6
Domenic eliminó la propuesta TC39 ... ... cc @BenjaminGruenbaum
Sergio
50

Si bien no existe una forma estándar de hacer esto en ES6, existe una biblioteca llamada Bluebird para manejar esto.

También hay una forma recomendada que se describe como parte de la documentación de reacción. Es similar a lo que tiene en sus actualizaciones 2 y 3.

const makeCancelable = (promise) => {
  let hasCanceled_ = false;

  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then((val) =>
      hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
    );
    promise.catch((error) =>
      hasCanceled_ ? reject({isCanceled: true}) : reject(error)
    );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled_ = true;
    },
  };
};

const cancelablePromise = makeCancelable(
  new Promise(r => component.setState({...}}))
);

cancelablePromise
  .promise
  .then(() => console.log('resolved'))
  .catch((reason) => console.log('isCanceled', reason.isCanceled));

cancelablePromise.cancel(); // Cancel the promise

Tomado de: https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html

Michael Yagudaev
fuente
1
esta definición de cancelado es simplemente rechazar la promesa. depende de la definición de "cancelado".
Alexander Mills
1
¿Y qué pasa si quieres cancelar una serie de promesas?
Matthieu Brucher
1
El problema con este enfoque es que si tiene una Promesa que nunca se resolverá o rechazará, nunca se cancelará.
DaNeSh
2
Esto es parcialmente correcto, pero si tiene una cadena de promesas larga, este enfoque no funcionaría.
Veikko Karsikko
11

Realmente me sorprende que nadie mencione Promise.racecomo candidato para esto:

const actualPromise = new Promise((resolve, reject) => { setTimeout(resolve, 10000) });
let cancel;
const cancelPromise = new Promise((resolve, reject) => {
    cancel = reject.bind(null, { canceled: true })
})

const cancelablePromise = Object.assign(Promise.race([actualPromise, cancelPromise]), { cancel });
Pho3nixHun
fuente
3
No creo que esto funcione. Si cambia la promesa de iniciar sesión, la ejecución cancel()seguirá provocando que se llame al registro. `` `const actualPromise = new Promise ((resolver, rechazar) => {setTimeout (() => {console.log ('actual llamado'); resolve ()}, 10000)}); ``
shmck
2
La pregunta era cómo cancelar una promesa (=> detener thenla ejecución de s encadenados ), no cómo cancelar setTimeout(=> clearTimeout) o código síncrono, donde, a menos que ponga un if después de cada línea ( if (canceled) return), esto no se puede lograr. (No hagas esto)
Pho3nixHun
10
const makeCancelable = promise => {
    let rejectFn;

    const wrappedPromise = new Promise((resolve, reject) => {
        rejectFn = reject;

        Promise.resolve(promise)
            .then(resolve)
            .catch(reject);
    });

    wrappedPromise.cancel = () => {
        rejectFn({ canceled: true });
    };

    return wrappedPromise;
};

Uso:

const cancelablePromise = makeCancelable(myPromise);
// ...
cancelablePromise.cancel();
Slava M
fuente
5

En realidad, es imposible detener la ejecución de la promesa, pero puede secuestrar el rechazo y llamarlo de la promesa misma.

class CancelablePromise {
  constructor(executor) {
    let _reject = null;
    const cancelablePromise = new Promise((resolve, reject) => {
      _reject = reject;
      return executor(resolve, reject);
    });
    cancelablePromise.cancel = _reject;

    return cancelablePromise;
  }
}

Uso:

const p = new CancelablePromise((resolve, reject) => {
  setTimeout(() => {
    console.log('resolved!');
    resolve();
  }, 2000);
})

p.catch(console.log);

setTimeout(() => {
  p.cancel(new Error('Messed up!'));
}, 1000);
nikksan
fuente
1
@dx_over_dt Tu edición sería un gran comentario, pero no una edición. Por favor, deje esas ediciones importantes al ámbito del OP (a menos que la publicación esté marcada como Comunidad Wiki, por supuesto).
TylerH
@TylerH, ¿es el punto de editar para corregir errores tipográficos y cosas por el estilo? ¿O para actualizar la información cuando esté desactualizada? Soy nuevo en la capacidad de editar los privilegios de las publicaciones de otras personas.
dx_over_dt
@dx_over_dt Sí, la edición es para mejorar las publicaciones corrigiendo errores tipográficos, errores gramaticales y agregar resaltado de sintaxis (si alguien solo publica un montón de código pero no lo sangra ni lo etiqueta con `` '', por ejemplo). Agregar contenido sustantivo como explicaciones adicionales o razonamientos / justificaciones de las cosas suele ser competencia de la persona que publicó la respuesta. Puede sugerirlo en los comentarios, y OP será notificado del comentario y luego podrá responderlo, o simplemente pueden incorporar su sugerencia en la publicación ellos mismos.
TylerH
@dx_over_dt Las excepciones son si una publicación está marcada como "Wiki de la comunidad", lo que indica que está destinada a servir como publicación colaborativa (p. ej., Wikipedia), o si hay problemas graves con la publicación, como lenguaje grosero / abusivo, contenido peligroso / dañino ( por ejemplo, sugerencias o códigos que puedan contagiarle un virus o hacer que lo arresten, etc.), o información personal como registros médicos, números de teléfono, tarjetas de crédito, etc .; siéntase libre de eliminarlos usted mismo.
TylerH
Vale la pena señalar que la razón por la que la ejecución no se puede detener dentro de una promesa es que JavaScript es de un solo subproceso. Mientras se ejecuta la función de promesa, no se ejecuta nada más, por lo que no hay nada que desencadene la detención de la ejecución.
dx_over_dt
2

Aquí está nuestra implementación https://github.com/permettez-moi-de-construire/cancellable-promise

Usado como

const {
  cancellablePromise,
  CancelToken,
  CancelError
} = require('@permettezmoideconstruire/cancellable-promise')

const cancelToken = new CancelToken()

const initialPromise = SOMETHING_ASYNC()
const wrappedPromise = cancellablePromise(initialPromise, cancelToken)


// Somewhere, cancel the promise...
cancelToken.cancel()


//Then catch it
wrappedPromise
.then((res) => {
  //Actual, usual fulfill
})
.catch((err) => {
  if(err instanceOf CancelError) {
    //Handle cancel error
  }

  //Handle actual, usual error
})

cual :

  • No toca la API de promesa
  • Hagamos más cancelaciones dentro de la catchllamada
  • Confíe en que la cancelación sea rechazada en lugar de resuelta a diferencia de cualquier otra propuesta o implementación

Se aceptan extracciones y comentarios

Cyril CHAPON
fuente
2

La promesa se puede cancelar con la ayuda de AbortController.

¿Existe un método para borrar entonces? Sí, puede rechazar la promesa con el AbortControllerobjeto y luego promiseomitirá todos los bloques y luego irá directamente al bloque de captura.

Ejemplo:

import "abortcontroller-polyfill";

let controller = new window.AbortController();
let signal = controller.signal;
let elem = document.querySelector("#status")

let example = (signal) => {
    return new Promise((resolve, reject) => {
        let timeout = setTimeout(() => {
            elem.textContent = "Promise resolved";
            resolve("resolved")
        }, 2000);

        signal.addEventListener('abort', () => {
            elem.textContent = "Promise rejected";
            clearInterval(timeout);
            reject("Promise aborted")
        });
    });
}

function cancelPromise() {
    controller.abort()
    console.log(controller);
}

example(signal)
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        console.log("Catch: ", error)
    });

document.getElementById('abort-btn').addEventListener('click', cancelPromise);

HTML


    <button type="button" id="abort-btn" onclick="abort()">Abort</button>
    <div id="status"> </div>

Nota: es necesario agregar polyfill, no es compatible con todos los navegadores.

Ejemplo en vivo

Editar elegant-lake-5jnh3

Sohail
fuente
1

versión simple :

simplemente dé la función de rechazo.

function Sleep(ms,cancel_holder) {

 return new Promise(function(resolve,reject){
  var done=false; 
  var t=setTimeout(function(){if(done)return;done=true;resolve();}, ms);
  cancel_holder.cancel=function(){if(done)return;done=true;if(t)clearTimeout(t);reject();} 
 })
}

una solución de envoltura (fábrica)

la solución que encontré es pasar un objeto cancel_holder. tendrá una función de cancelación. si tiene una función de cancelación, entonces es cancelable.

Esta función de cancelación rechaza la promesa con Error ('cancelado').

Antes de resolver, rechazar u on_cancel, evite que se llame a la función de cancelación sin motivo.

Me ha parecido conveniente pasar la acción de cancelar por inyección

function cancelablePromise(cancel_holder,promise_fn,optional_external_cancel) {
  if(!cancel_holder)cancel_holder={};
  return new Promise( function(resolve,reject) {
    var canceled=false;
    var resolve2=function(){ if(canceled) return; canceled=true; delete cancel_holder.cancel; resolve.apply(this,arguments);}
    var reject2=function(){ if(canceled) return; canceled=true; delete cancel_holder.cancel; reject.apply(this,arguments);}
    var on_cancel={}
    cancel_holder.cancel=function(){
      if(canceled) return; canceled=true;

      delete cancel_holder.cancel;
      cancel_holder.canceled=true;

      if(on_cancel.cancel)on_cancel.cancel();
      if(optional_external_cancel)optional_external_cancel();

      reject(new Error('canceled'));
    };

    return promise_fn.call(this,resolve2,reject2,on_cancel);        
  });
}

function Sleep(ms,cancel_holder) {

 return cancelablePromise(cancel_holder,function(resolve,reject,oncacnel){

  var t=setTimeout(resolve, ms);
  oncacnel.cancel=function(){if(t)clearTimeout(t);}     

 })
}


let cancel_holder={};

// meanwhile in another place it can be canceled
setTimeout(function(){  if(cancel_holder.cancel)cancel_holder.cancel(); },500) 

Sleep(1000,cancel_holder).then(function() {
 console.log('sleept well');
}, function(e) {
 if(e.message!=='canceled') throw e;
 console.log('sleep interrupted')
})
Shimon Doodkin
fuente
1

Pruebe la promesa abortable : https://www.npmjs.com/package/promise-abortable

$ npm install promise-abortable
import AbortablePromise from "promise-abortable";

const timeout = new AbortablePromise((resolve, reject, signal) => {
  setTimeout(reject, timeToLive, error);
  signal.onabort = resolve;
});

Promise.resolve(fn()).then(() => {
  timeout.abort();
});
Devi
fuente
1

Si su código se coloca en una clase, podría usar un decorador para eso. Tienes dicho decorador en utils-decorators ( npm install --save utils-decorators). Cancelará la invocación anterior del método decorado si antes de la resolución de la llamada anterior se realizó otra llamada para ese método específico.

import {cancelPrevious} from 'utils-decorators';

class SomeService {

   @cancelPrevious()
   doSomeAsync(): Promise<any> {
    ....
   }
}

https://github.com/vlio20/utils-decorators#cancelprevious-method

vlio20
fuente
0

Si desea evitar que se ejecuten todos los thens / catchs, puede hacerlo inyectando una promesa que nunca se resolverá. Probablemente tenga reproches de pérdida de memoria, pero solucionará el problema y no debería causar demasiada memoria desperdiciada en la mayoría de las aplicaciones.

new Promise((resolve, reject) => {
    console.log('first chain link executed')
    resolve('daniel');
}).then(name => {
    console.log('second chain link executed')
    if (name === 'daniel') {
        // I don't want to continue the chain, return a new promise
        // that never calls its resolve function
        return new Promise((resolve, reject) => {
            console.log('unresolved promise executed')
        });
    }
}).then(() => console.log('last chain link executed'))

// VM492:2 first chain link executed
// VM492:5 second chain link executed
// VM492:8 unresolved promise executed
DanLatimer
fuente
0

Establezca una propiedad "cancelada" en el Promise para señalar then()y catch()salir temprano. Es muy eficaz, especialmente en los Web Workers que tienen microtareas existentes en cola en Promesas de los onmessageadministradores.

// Queue task to resolve Promise after the end of this script
const promise = new Promise(resolve => setTimeout(resolve))

promise.then(_ => {
  if (promise.canceled) {
    log('Promise cancelled.  Exiting early...');
    return;
  }

  log('No cancelation signaled.  Continue...');
})

promise.canceled = true;

function log(msg) {
  document.body.innerHTML = msg;
}

AnthumChris
fuente
0

La respuesta de @Michael Yagudaev funciona para mí.

Pero la respuesta original no encadenó la promesa envuelta con .catch () para manejar el manejo de rechazos, aquí está mi mejora además de la respuesta de @Michael Yagudaev:

const makeCancelablePromise = promise => {
  let hasCanceled = false;
  const wrappedPromise = new Promise((resolve, reject) => {
    promise
      .then(val => (hasCanceled ? reject({ isCanceled: true }) : resolve(val)))
      .catch(
        error => (hasCanceled ? reject({ isCanceled: true }) : reject(error))
      );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled = true;
    }
  };
};

// Example Usage:
const cancelablePromise = makeCancelable(
  new Promise((rs, rj) => {
    /*do something*/
  })
);
cancelablePromise.promise.then(() => console.log('resolved')).catch(err => {
  if (err.isCanceled) {
    console.log('Wrapped promise canceled');
    return;
  }
  console.log('Promise was not canceled but rejected due to errors: ', err);
});
cancelablePromise.cancel();

fuente
0

Si p es una variable que contiene una Promesa, entonces p.then(empty);debería descartar la promesa cuando finalmente se complete o si ya está completa (sí, sé que esta no es la pregunta original, pero es mi pregunta). "vacío" es function empty() {}. Soy solo un principiante y probablemente estoy equivocado, pero estas otras respuestas parecen demasiado complicadas. Se supone que las promesas son simples.

David Spector
fuente
0

Todavía estoy trabajando en esta idea, pero así es como he implementado una Promesa cancelable usando setTimeoutcomo ejemplo.

La idea es que una promesa se resuelva o rechace siempre que tú lo hayas decidido, por lo que debería ser cuestión de decidir cuándo quieres cancelar, cumplir el criterio y luego llamar reject()tú mismo a la función.

  • Primero, creo que hay dos razones para terminar una promesa antes de tiempo: terminarla (que he llamado resolver ) y cancelar (que he llamado rechazar ). Por supuesto, ese es solo mi sentimiento. Por supuesto que hay un Promise.resolve()método, pero está en el propio constructor y devuelve una promesa resuelta ficticia. Este resolve()método de instancia realmente resuelve un objeto de promesa instanciado.

  • En segundo lugar, se puede añadir nada felizmente te gusta a un objeto de la promesa de nueva creación antes de devolverlo, y por lo que acaba de agregar resolve()y reject()métodos para que sea autónomo.

  • En tercer lugar, el truco es poder acceder al ejecutor resolvey las rejectfunciones más tarde, así que simplemente las he almacenado en un objeto simple desde dentro del cierre.

Creo que la solución es simple y no veo ningún problema importante en ella.

function wait(delay) {
  var promise;
  var timeOut;
  var executor={};
  promise=new Promise(function(resolve,reject) {
    console.log(`Started`);
    executor={resolve,reject};  //  Store the resolve and reject methods
    timeOut=setTimeout(function(){
      console.log(`Timed Out`);
      resolve();
    },delay);
  });
  //  Implement your own resolve methods,
  //  then access the stored methods
      promise.reject=function() {
        console.log(`Cancelled`);
        clearTimeout(timeOut);
        executor.reject();
      };
      promise.resolve=function() {
        console.log(`Finished`);
        clearTimeout(timeOut);
        executor.resolve();
      };
  return promise;
}

var promise;
document.querySelector('button#start').onclick=()=>{
  promise=wait(5000);
  promise
  .then(()=>console.log('I have finished'))
  .catch(()=>console.log('or not'));
};
document.querySelector('button#cancel').onclick=()=>{ promise.reject(); }
document.querySelector('button#finish').onclick=()=>{ promise.resolve(); }
<button id="start">Start</button>
<button id="cancel">Cancel</button>
<button id="finish">Finish</button>

Manngo
fuente