web-dev-qa-db-fra.com

NodeJS Expiration d'une promesse en cas d'échec de l'exécution dans le temps

Comment puis-je expirer une promesse après un certain temps? Je sais que Q a une expiration de promesse, mais j'utilise les promesses NodeJS natives et elles n'ont pas de fonction.

Est-ce que j'en manque un ou son emballage est différent?

Sinon, la mise en œuvre ci-dessous est-elle bonne pour ne pas aspirer de mémoire, fonctionne-t-elle comme prévu?

Aussi, puis-je le rendre globalement enveloppé afin de pouvoir l'utiliser pour chaque promesse que je crée, sans avoir à répéter le code setTimeout et clearTimeout?

function run() {
    logger.info('DoNothingController working on process id {0}...'.format(process.pid));

    myPromise(4000)
        .then(function() {
            logger.info('Successful!');
        })
        .catch(function(error) {
            logger.error('Failed! ' + error);
        });
}

function myPromise(ms) {
    return new Promise(function(resolve, reject) {
        var hasValueReturned;
        var promiseTimeout = setTimeout(function() {
            if (!hasValueReturned) {
                reject('Promise timed out after ' + ms + ' ms');
            }
        }, ms);

        // Do something, for example for testing purposes
        setTimeout(function() {
            resolve();
            clearTimeout(promiseTimeout);
        }, ms - 2000);
    });
}

Merci! 

24
AlexD

Les promesses JavaScript natives n'ont pas de mécanisme de délai d'attente.

La question sur votre implémentation conviendrait probablement mieux pour http://codereview.stackexchange.com , mais quelques notes:

  1. Vous ne fournissez aucun moyen de faire quoi que ce soit dans la promesse, et

  2. clearTimeout n'est pas nécessaire dans votre rappel setTimeout, car setTimeout planifie une minuterie ponctuelle.

  3. Puisqu'une promesse ne peut être résolue/rejetée une fois qu'elle a été résolue/rejetée, vous n'avez pas besoin de cette vérification.

Alors peut-être quelque chose dans ce sens:

function myPromise(ms, callback) {
    return new Promise(function(resolve, reject) {
        // Set up the real work
        callback(resolve, reject);

        // Set up the timeout
        setTimeout(function() {
            reject('Promise timed out after ' + ms + ' ms');
        }, ms);
    });
}

Utilisé comme ceci:

myPromise(2000, function(resolve, reject) {
    // Real work is here
});

(Ou vous pouvez vouloir que ce soit un peu plus compliqué, voir la mise à jour sous la ligne ci-dessous.)}

Je serais un peu préoccupé par le fait que la sémantique est légèrement différente (pas de new, alors que vous utilisez new avec le constructeur Promise), vous pouvez donc ajuster cela.

L’autre problème, bien sûr, est que la plupart du temps, vous ne voulez pas construire de nouvelles promesses et ne pouvez donc pas utiliser ce qui précède. La plupart du temps, vous avez déjà une promesse (le résultat d'un précédent appel then, etc.). Mais dans les cas où vous construisez réellement une nouvelle promesse, vous pouvez utiliser quelque chose comme ce qui précède.

Vous pouvez traiter la chose new en sous-classant Promise:

class MyPromise extends Promise {
    constructor(ms, callback) {
        // We need to support being called with no milliseconds
        // value, because the various Promise methods (`then` and
        // such) correctly call the subclass constructor when
        // building the new promises they return.
        // This code to do it is ugly, could use some love, but it
        // gives you the idea.
        let haveTimeout = typeof ms === "number" && typeof callback === "function";
        let init = haveTimeout ? callback : ms;
        super((resolve, reject) => {
            init(resolve, reject);
            if (haveTimeout) {
                setTimeout(() => {
                    reject("Timed out");
                }, ms);
            }
        });
    }
}

Usage:

let p = new MyPromise(300, function(resolve, reject) {
    // ...
});
p.then(result => {
})
.catch(error => {
});

Exemple en direct:

// Uses var instead of let and non-arrow functions to try to be
// compatible with browsers that aren't quite fully ES6 yet, but
// do have promises...
(function() {
    "use strict";
    
    class MyPromise extends Promise {
        constructor(ms, callback) {
            var haveTimeout = typeof ms === "number" && typeof callback === "function";
            var init = haveTimeout ? callback : ms;
            super(function(resolve, reject) {
                init(resolve, reject);
                if (haveTimeout) {
        	        setTimeout(function() {
    	                reject("Timed out");
	                }, ms);
                }
            });
        }
    }
    
    var p = new MyPromise(100, function(resolve, reject) {
        // We never resolve/reject, so we test the timeout
    });
    p.then(function(result) {
    	snippet.log("Resolved: " + result);
    }).catch(function(reject) {
        snippet.log("Rejected: " + reject);
    });
})();
<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="http://tjcrowder.github.io/simple-snippets-console/snippet.js"></script>


Ces deux personnes appellent reject à l'expiration du délai imparti, même si le rappel appelle resolve ou reject en premier. C'est bien, l'état établi d'une promesse ne peut pas être modifié une fois qu'elle est définie et la spécification définit les appels à resolve ou reject sur une promesse déjà réglée en tant que do-nothings qui ne génère pas d'erreur.

Mais si cela vous dérange, vous pouvez envelopper resolve et reject. Voici myPromise fait de cette façon:

function myPromise(ms, callback) {
    return new Promise(function(resolve, reject) {
        // Set up the timeout
        let timer = setTimeout(function() {
            reject('Promise timed out after ' + ms + ' ms');
        }, ms);
        let cancelTimer = _ => {
            if (timer) {
                clearTimeout(timer);
                timer = 0;
            }
        };

        // Set up the real work
        callback(
            value => {
                cancelTimer();
                resolve(value);
            },
            error => {
                cancelTimer();
                reject(error);
            }
        );
    });
}

Vous pouvez utiliser 18 méthodes différentes, mais le concept de base est que resolve et reject nous passons l'exécuteur de la promesse que nous recevons sont des wrappers qui effacent le chronomètre.

Mais, cela crée des fonctions et des appels de fonctions supplémentaires dont vous n’avez pas besoin. La spécification est claire sur ce que font les fonctions de résolution lorsque la promesse est déjà résolue; ils ont quitté assez tôt.

39
T.J. Crowder

Bien qu'il n'y ait peut-être pas de support pour un délai d'attente de promesse, vous pouvez tenir les promesses:

var race = Promise.race([
  new Promise(function(resolve){
    setTimeout(function() { resolve('I did it'); }, 1000);
  }),
  new Promise(function(resolve, reject){
    setTimeout(function() { reject('Timed out'); }, 800);
  })
]);

race.then(function(data){
  console.log(data);
  }).catch(function(e){
  console.log(e);
  });

Un Promise.timeout générique:

Promise.timeout = function(timeout, cb){
  return Promise.race([
  new Promise(cb),
  new Promise(function(resolve, reject){
    setTimeout(function() { reject('Timed out'); }, timeout);
  })
]);
}

Exemple:

    Promise.timeout = function(timeout, cb) {
      return Promise.race([
        new Promise(cb),
        new Promise(function(resolve, reject) {
          setTimeout(function() {
            reject('Timed out');
          }, timeout);
        })
      ]);
    }
    
    function delayedHello(cb){
      setTimeout(function(){
        cb('Hello');
        }, 1000);
      }
    
    Promise.timeout(800, delayedHello).then(function(data){
      console.log(data);
      }).catch(function(e){
      console.log(e);
      }); //delayedHello doesn't make it.

    Promise.timeout(1200, delayedHello).then(function(data){
      console.log(data);
      }).catch(function(e){
      console.log(e);
      }); //delayedHello makes it.

Cela pourrait être un peu coûteux, car vous créez en fait 3 promesses au lieu de 2. Je pense que c'est plus clair de cette façon cependant.

Vous voudrez peut-être configurer une promesse au lieu de la faire construire par la fonction. De cette façon, vous séparez les préoccupations et vous êtes finalement concentré sur la réalisation de votre promesse contre une nouvelle promesse qui rejettera à x miliseconds.

Promise.timeout = function(timeout, promise){
  return Promise.race([
  promise,
  new Promise(function(resolve, reject){
    setTimeout(function() { reject('Timed out'); }, timeout);
  })
]);
}

Comment utiliser:

var p = new Promise(function(resolve, reject){
    setTimeout(function() { resolve('Hello'); }, 1000);
});

Promise.timeout(800, p); //will be rejected, as the promise takes at least 1 sec.
15
MinusFour

C'est une question un peu ancienne, mais je suis tombé sur celle-ci quand je cherchais comment expirer une promesse.
Bien que toutes les réponses soient bonnes, j’ai trouvé l’utilisation de bluebird d’implémentation de Promises comme moyen le plus simple de traiter les délais d'attente :

var Promise = require('bluebird');
var p = new Promise(function(reject, resolve) { /.../ });
p.timeout(3000) //make the promise timeout after 3000 milliseconds
 .then(function(data) { /handle resolved promise/ })
 .catch(Promise.TimeoutError, function(error) { /handle timeout error/ })
 .catch(function(error) { /handle any other non-timeout errors/ });

Comme vous pouvez le constater, cela représente beaucoup moins de travail que les autres solutions proposées. Je pensais le mettre ici pour le rendre plus facile à trouver :) 

Au fait, je ne suis nullement impliqué dans le projet bluebird, je viens de trouver cette solution particulière très soignée.

6
Daniel Gruszczyk

Pour ajouter un délai d'expiration à toute promesse existante, vous pouvez utiliser:

const withTimeout = (millis, promise) => {
    const timeout = new Promise((resolve, reject) =>
        setTimeout(
            () => reject(`Timed out after ${millis} ms.`),
            millis));
    return Promise.race([
        promise,
        timeout
    ]);
};

Puis plus tard:

await withTimeout(5000, doSomethingAsync());
5
Drew Noakes

Bien que les réponses fournies ici soient valables, vous ne devez pas essayer de réinventer la roue, mais utilisez simplement l’un des dizaines de packages disponibles sur NPM pour obtenir une promesse de résolution automatique.

Voici un exemple de NPM :

const { TimeoutResolvePromise, TimeoutRejectPromise } = require('nodejs-promise-timeout');
const TIMEOUT_DELAY = 2000;

// This promise will reject after 2 seconds:
let promise1 = new TimeoutRejectPromise(TIMEOUT_DELAY, (resolve, reject) => {
  // Do something useful here, then call resolve() or reject()
});
0
K48