web-dev-qa-db-fra.com

Résolution synchrone des promesses (bluebird vs jQuery)

J'ai développé une petite bibliothèque pour le service Web Dynamics CRM REST/ODATA (CrmRestKit). La bibliothèque dépend de jQuery et utilise le modèle de promesse, à savoir le modèle semblable à une promesse de jQuery. 

Maintenant, j'aime bien porter cette lib sur bluebird et supprimer la dépendance jQuery. Mais je suis confronté à un problème car bluebird ne prend pas en charge la résolution synchrone des objets de promesse. 

Quelques informations de contexte:  

L'API de CrmRestKit exclut un paramètre facultatif qui définit si l'appel de service Web doit être effectué en mode synchrone ou asynchrone:

CrmRestKit.Create( 'Account', { Name: "foobar" }, false ).then( function ( data ) {
   ....
} );

Lorsque vous passez "true" ou omettez le dernier paramètre, la méthode créera l'enregistrement en synchronisation. mode. 

Il est parfois nécessaire d'effectuer une opération en mode synchronisation. Par exemple, vous pouvez écrire du code JavaScript pour Dynamics CRM impliqué dans l'événement de sauvegarde d'un formulaire. Dans ce gestionnaire d'événements, vous devez effectuer une opération de synchronisation pour la validation ( par exemple, valider l'existence d'un certain nombre d'enregistrements enfants, si le nombre correct d'enregistrements existe, annuler l'opération de sauvegarde et afficher un message d'erreur). 

Mon problème est maintenant le suivant: bluebird ne supporte pas la résolution en mode synchro. Par exemple, lorsque je fais ce qui suit, le gestionnaire "then" est appelé de manière asynchrone:

function print( text ){

    console.log( 'print -> %s', text );

    return text;
}

///
/// 'Promise.cast' cast the given value to a trusted promise. 
///
function getSomeTextSimpleCast( opt_text ){

    var text = opt_text || 'Some fancy text-value';

    return Promise.cast( text );
}

getSomeTextSimpleCast('first').then(print);
print('second');

La sortie est la suivante:

print -> second
print -> first

Je m'attendrais à ce que le "second" apparaisse après le "premier" car la promesse est déjà résolue avec une valeur. Je suppose donc qu'un gestionnaire then-event est immédiatement appelé lorsqu'il est appliqué à un objet promesse déjà résolu .

Quand je ferai la même chose (utiliser une promesse déjà résolue) avec jQuery, j'aurais le résultat attendu:

function jQueryResolved( opt_text ){

    var text = opt_text || 'jQuery-Test Value',
    dfd =  new $.Deferred();

    dfd.resolve(text);

        // return an already resolved promise
    return dfd.promise();
}

jQueryResolved('third').then(print);
print('fourth');

Cela générera la sortie suivante:

print -> third
print -> fourth

Existe-t-il un moyen de faire fonctionner Bluebird de la même manière? 

Mise à jour: Le code fourni était juste pour illustrer le problème. L'idée de la lib est la suivante: quel que soit le mode d'exécution (synchrone, asynchrone), l'appelant traitera toujours d'un objet de promesse. 

En ce qui concerne "... demander à l'utilisateur ... ne semble pas avoir de sens": lorsque vous fournissez deux méthodes "CreateAsync" et "CreateSync", il appartient également à l'utilisateur de décider du mode d'exécution de l'opération. 

Quoi qu'il en soit, avec l'implémentation actuelle, le comportement par défaut (le dernier paramètre est facultatif) est une exécution asynchrone. Ainsi, 99% du code nécessite un objet de promesse, le paramètre facultatif n'est utilisé que dans les cas où 1% nécessite simplement une exécution de synchronisation. De plus, je me suis développé pour lib et j'utilise dans 99,9999% des cas le mode asynchrone, mais j’ai pensé que c’était bien d’avoir la possibilité d’opter pour la synchro road comme bon vous semble. 

Mais je pense avoir compris l'idée qu'une méthode de synchronisation devrait simplement renvoyer la valeur. Pour la prochaine version (3.0), je vais implémenter "CreateSync" et "CreateAsync". 

Merci pour votre contribution.

Update-2 Mon intention était de garantir un comportement consistend ET d'éviter une erreur de logique. Supposons que vous soyez un consommateur de ma méthode "GetCurrentUserRoles" qui utilise lib. Donc, la méthode retournera toujours une promesse, cela signifie que vous devez utiliser la méthode "then" pour exécuter du code dépendant du résultat. Donc, quand certains écrivent un code comme celui-ci, je conviens que c'est totalement faux:

var currentUserRoels = null;

GetCurrentUserRoles().then(function(roles){

    currentUserRoels = roles;
});

if( currentUserRoels.indexOf('foobar') === -1 ){

    // ...
}

Je conviens que ce code sera rompu lorsque la méthode "GetCurrentUserRoles" passera de sync à async. 

Mais je comprends que ce n’est pas un bon dessin, car le consommateur devrait maintenant traiter avec une méthode asynchrone. 

12
thuld

Version courte: Je comprends pourquoi tu veux faire ça, mais la réponse est non.

Je pense que la question sous-jacente est de savoir si une promesse complétée doit immédiatement être rappelée, si la promesse est déjà complétée. Cela peut être dû à de nombreuses raisons, par exemple une procédure de sauvegarde asynchrone qui ne sauvegarde les données que si des modifications ont été apportées. Il peut être capable de détecter les modifications du côté client de manière synchrone sans avoir à passer par une ressource externe, mais si des modifications étaient détectées, une opération asynchrone serait alors nécessaire.

Dans les autres environnements ayant des appels asynchrones, il semble que le développeur soit responsable de comprendre que son travail peut s'achever immédiatement (par exemple, l'implémentation du modèle async par le framework .NET le permet). Ce n'est pas un problème de conception du framework, c'est la façon dont il est implémenté.

Les développeurs de JavaScript (et de nombreux commentateurs ci-dessus) semblent avoir un point de vue différent sur ce point, insistant sur le fait que si un élément peut être asynchrone, il doit toujours être asynchrone. Que cela soit "correct" ou non est sans importance - selon la spécification que j'ai trouvée à https://promisesaplus.com/ , le point 2.2.4 indique que, fondamentalement, aucun rappel ne peut être appelé tant que vous n'êtes pas sorti de ce que je Je parlerai de "code de script" ou de "code d'utilisateur"; c'est-à-dire que la spécification indique clairement que même si la promesse est remplie, vous ne pouvez pas invoquer le rappel immédiatement. J'ai vérifié quelques autres endroits et ils ne disent rien sur le sujet ou sont d'accord avec la source originale. Je ne sais pas si https://promisesaplus.com/ pourrait être considéré comme une source d’information définitive à cet égard, mais aucune autre source que j’ai vue n’est en désaccord et qui semble être la plus complète.

Cette limitation est quelque peu arbitraire et je préfère franchement la perspective .NET sur celui-ci. Je laisserai aux autres le soin de décider s’ils considèrent comme un «mauvais code» de faire quelque chose qui pourrait être synchrone ou non d’une manière qui semble asynchrone.

Votre vraie question est de savoir si Bluebird peut ou non être configuré pour adopter un comportement autre que JavaScript. En termes de performances, cela peut présenter un avantage mineur. En JavaScript, tout est possible si vous essayez suffisamment, mais à mesure que l’objet Promise devient plus omniprésent sur toutes les plateformes, vous constaterez un changement dans son utilisation en tant que composant natif au lieu d’écriture personnalisée. polyfill ou bibliothèques. Par conséquent, quelle que soit la réponse donnée aujourd'hui, retravailler une promesse dans Bluebird risque de vous poser problème à l'avenir, et votre code ne devrait probablement pas être écrit pour dépendre ou fournir une résolution immédiate d'une promesse.

17
Joe Friesenhan

Vous pourriez penser que c'est un problème, car il n'y a aucun moyen d'avoir

getSomeText('first').then(print);
print('second');

et getSomeText"first" imprimé avant "second" lorsque la résolution est synchrone. 

Mais je pense que vous avez un problème de logique.

Si votre fonction getSomeText peut être synchrone ou asynchrone, en fonction du contexte, cela ne devrait pas affecter l’ordre d’exécution. Vous utilisez des promesses pour vous assurer que c'est toujours la même chose. Avoir un ordre d'exécution variable deviendrait probablement un bogue dans votre application.

Utilisation

getSomeText('first') // may be synchronous using cast or asynchronous with ajax
.then(print)
.then(function(){ print('second') });

Dans les deux cas (synchrone avec la résolution cast ou asynchrone), vous aurez le bon ordre d'exécution.

Notez que le fait d'avoir une fonction parfois synchrone et parfois non synonyme de synchronisation n'est pas un cas étrange ou improbable (pensez à la gestion du cache ou au pooling). Vous devez juste supposer que c'est asynchrone, et tout ira toujours bien.

Mais demander à l'utilisateur de l'API de préciser avec un argument booléen s'il veut que l'opération soit asynchrone n'a pas de sens si vous ne quittez pas le domaine de JavaScript (c'est-à-dire si vous n'utilisez pas de code natif). ).

8
Denys Séguret

Le but des promesses est de rendre le code asynchrone plus simple, c’est-à-dire plus proche de ce que vous ressentez lorsque vous utilisez synchrone code.

Vous utilisez du code synchrone. Ne compliquez pas les choses.

function print( text ){

    console.log( 'print -> %s', text );

    return text;
}

function getSomeTextSimpleCast( opt_text ){

    var text = opt_text || 'Some fancy text-value';

    return text;
}

print(getSomeTextSimpleCast('first'));
print('second');

Et cela devrait être la fin.


Si vous souhaitez conserver la même interface asynchrone même si votre code est synchrone, vous devez le faire complètement.

getSomeTextSimpleCast('first')
    .then(print)
    .then(function() { print('second'); });

then extrait votre code du flux d'exécution normal, car il est supposé être asynchrone. Bluebird fait le bon chemin là-bas. Une explication simple de ce qu'il fait:

function then(fn) {
    setTimeout(fn, 0);
}

Notez que bluebird ne fait pas vraiment ça, c'est juste pour vous donner un exemple simple.

Essayez le!

then(function() {
    console.log('first');
});
console.log('second');

Cela produira les éléments suivants:

second
first 
7
Florian Margaine

Il y a déjà de bonnes réponses ici, mais pour résumer le noeud du problème de manière très succincte:

Avoir une promesse (ou une autre API asynchrone) qui est parfois asynchrone et parfois synchrone est une mauvaise chose.

Vous pensez peut-être que c'est bien parce que l'appel initial à votre API prend un booléen pour se désactiver entre sync/async. Mais que se passe-t-il si cela est caché dans un code wrapper et que la personne qui utilise que code ne connaisse pas ces manigances? Ils viennent de se retrouver avec un comportement imprévisible sans que ce soit leur faute.

La ligne du bas: N'essayez pas de faire cela. Si vous voulez un comportement synchrone, ne retournez pas une promesse.

Sur ce, je vous laisse avec cette citation de Vous ne savez pas JS :

Un autre problème de confiance est appelé "trop ​​tôt". En termes spécifiques à l'application, cela peut impliquer d'être appelé avant qu'une tâche critique ne soit terminée. Mais plus généralement, le problème est évident dans les utilitaires pouvant appeler le rappel que vous fournissez maintenant (de manière synchrone) ou plus tard (de manière asynchrone).

Ce non-déterminisme autour du comportement sync ou async va presque toujours rendre très difficile la recherche de bogues. Dans certains milieux, le monstre fictif provoquant la folie nommé Zalgo est utilisé pour décrire les cauchemars sync/async. "Ne libère pas Zalgo!" C’est un cri commun qui donne lieu à de très judicieux conseils: invoquez toujours des rappels de manière asynchrone, même si c’est «tout de suite» au prochain tour de la boucle d’événements, afin que tous les rappels soient asynchrones de manière prévisible.

Remarque: pour plus d'informations sur Zalgo, voir «Ne libérez pas Zalgo!» D'Oren Golan. ( https://github.com/oren/oren.github.io/blob/master/posts/zalgo.md ) et "Conception d'API pour l'asynchronie" de Isaac Z. Schlueter ( http: // blog .izs.me/post/59142742143/drawing-apis-for-asynchrony ).

Considérer:

function result(data) {
    console.log( a );
}

var a = 0;

ajax( "..pre-cached-url..", result );
a++;`

Ce code imprimera-t-il 0 (invocation de rappel sync) ou 1 (invocation de rappel async)? Cela dépend ... des conditions.

Vous pouvez voir à quelle vitesse l'imprévisibilité de Zalgo peut menacer n'importe quel programme JS. Donc, le ridicule "ne relâche jamais Zalgo" est en fait un conseil incroyablement commun et solide. Toujours être async.

2
JLRishe

Qu'en est-il de ce cas, également lié CrmFetchKit qui, dans la dernière version utilise Bluebird. J'ai mis à niveau à partir de la version 1.9 qui était basée sur jQuery. L'ancien code d'application qui utilise CrmFetchKit contient toujours des méthodes dont je ne peux ni ne veux changer les prototypes.

Code d'application existant

CrmFetchKit.FetchWithPaginationSortingFiltering(query.join('')).then(
    function (results, totalRecordCount) {
        queryResult = results;

        opportunities.TotalRecords = totalRecordCount;

        done();
    },
    function err(e) {
        done.fail(e);
    }
);

Ancienne implémentation de CrmFetchKit (une version personnalisée de fetch ())

function fetchWithPaginationSortingFiltering(fetchxml) {

    var performanceIndicator_StartTime = new Date();

    var dfd = $.Deferred();

    fetchMore(fetchxml, true)
        .then(function (result) {
            LogTimeIfNeeded(performanceIndicator_StartTime, fetchxml);
            dfd.resolve(result.entities, result.totalRecordCount);
        })
        .fail(dfd.reject);

    return dfd.promise();
}

Nouvelle implémentation de CrmFetchKit

function fetch(fetchxml) {
    return fetchMore(fetchxml).then(function (result) {
        return result.entities;
    });
}

Mon problème est que l'ancienne version avait le dfd.resolve (...) où je pouvais passer n'importe quel nombre de paramètres dont j'avais besoin.

La nouvelle implémentation revient, le parent semble appeler le rappel, je ne peux pas l'appeler directement.

Je suis allé faire une version personnalisée de la fetch () dans la nouvelle implémentation

function fetchWithPaginationSortingFiltering(fetchxml) {
    var thePromise = fetchMore(fetchxml).then(function (result) {
        thePromise._fulfillmentHandler0(result.entities, result.totalRecordCount);
        return thePromise.cancel();
        //thePromise.throw();
    });

    return thePromise;
}

Mais le problème est que le rappel est appelé deux fois, une fois lorsque je le fais explicitement et deuxièmement par le cadre, mais il ne lui transmet qu'un paramètre. Pour tromper et "dire" de ne rien appeler car je le fais explicitement, j'essaie d'appeler .cancel () mais c'est ignoré. J'ai compris pourquoi mais quand même comment faites-vous le "dfd.resolve (result.entities, result.totalRecordCount);" dans la nouvelle version sans avoir à modifier les prototypes de l'application qui utilise cette bibliothèque?

0
Nicolas