Q

Q

Transformez votre code Node.js grâce au module de promises Bluebird

Transformez votre code Node.js grâce au module de promises Bluebird

Lorsqu’on parle de promises dans l’écosystème Node.js, on pense immédiatement à la librairie Q. Toutefois, il existe de nombreux modules de promises proposant chacun des choses différentes. En particulier, le module bluebird se démarque grâce à des fonctionnalités tout à fait intéressantes telles que la “promisification”.

Promisification

Les core modules de Node.js fonctionnent à base de callback. Ainsi pour lire un fichier de façon asynchrone, il faut appeler la fonction readFile du module fs et traiter la réponse depuis le callback passé en dernier paramètre de la fonction lors de son appel:

fs.readFile "file.json", (err, val) ->
    if err
        console.error "unable to read file"
        try
            val = JSON.parse(val);
            console.log val.success
        catch e
            console.error "invalid json in file"

Bluebird permet de transformer le code précédent dans le code suivant:

fs.readFileAsync("file.json").then(JSON.parse).then (val) ->
    console.log val.success
.catch SyntaxError, (e) ->
    console.error "invalid json in file"
.catch (e) ->
    console.error "unable to read file"

promisifyAll

Cette transformation est rendue possible grâce à la promisification du module fs, via l’appel de la fonction promisifyAll qui permet de transformer toutes les fonctions exposées en fonctions renvoyant des promises:

fs = require "fs"
Promise.promisifyAll fs

fs.readFileAsync("file.js", "utf8").then(...)

Selon toute vraisemblance, les fonctions du modules sont proxifiées via un wrapping changeant la signature.
On pourra noter que le chaînage de fonctions catch sur la promise permet de différencier le traitement des erreurs en fonction de leur type. Ici, l’erreur de type SyntaxError est traitée différemment des erreurs typées autrement.

promisify

Il est également possible de ne promisifier qu’une seule fonction grâce à la fonction promisify:

redisGet = Promise.promisify(redisClient.get, redisClient)
redisGet('foo').then () ->
    #...

Il y a tout de même un piège puisque la fonction attend 2 paramètres. Le premier étant la référence de la fonction à promisifier, et le second étant l’objet auquel la fonction est rattachée.

nodeify

La fonction nodeify est également très intéressante car elle permet d’enregistrer un callback sur une promise bluebird et d’appeler celui-ci à la résolution de cette dernière:

getDataFor(input, callback) ->
    dataFromDataBase(input).nodeify(callback)

Cette possibilité est particulièrement intéressante, car elle permet de construire des API qui deviennent utilisables aussi bien par du code qui fonctionne à base de callback, qu’avec du code à base de promise.

Ainsi, si le callback est renseigné, il sera appelé. Sinon, il suffira d’exploiter la promise retournée par la fonction pour obtenir et traiter le résultat de l’appel.

Exemple exploitant le mécanisme de promise:

getDataFor("me").then (dataForMe) ->
    console.log dataForMe

Le même exemple exploitant le mécanisme de callback:

getDataFor "me", (err, dataForMe) ->
    if err
        console.error err
    console.log dataForMe

spread

En temps normal, le code suivant donnera en résultat la tableau : [1, 2, 3].

Promise.resolve([1,2,3]).nodeify (err, result) ->
    # err == null
    # result: [1,2,3]

Toutefois, l’option {spread: true} passée à l’appel de la fonction nodeify, permet de dispatcher les valeurs de résultat sur l’ensemble des arguments de la fonction de callback renseignée:

Promise.resolve([1,2,3]).nodeify (err, a, b, c) ->
    # err == null
    # a == 1
    # b == 2
    # c == 3
, {spread: true}

Conclusion

La librairie bluebird est riche en fonctions pour le moins intéressantes, vous pouvez les retrouver sur la page de documentation du projet GitHub:

Lien: https://github.com/petkaantonov/bluebird/blob/master/API.md

Transformez vos callbacks Node.js en promises Q

Transformez vos callbacks Node.js en promises Q

Le callback Hell en JavaScript, on en a tous entendu parler, voir même un peu trop, mais pas sans raison.

De bonnes règles de codage permettent tout de même de gommer en grande partie ce problème, et l’usage des promises est à mettre en tête de liste des bonnes pratiques pour y parvenir.

Nous allons voir dans cet article différentes techniques proposées par le module Q permettant passer d’une écriture de code à base de callback à une écriture à base de promises.

Un exemple de départ

Partons de l’extrait suivant qui permet d’importer des données de conférences:

En JavaScript:

var importConferences = function(conferences, callback) {
  return conferenceImporter.importData(conferences, function(err, result) {
    if (err) {
      return callback(err);
    } else {
      return doSomethingWithResult(result, function(err, otherResult) {
        return callback(err, otherResult);
      });
    }
  });
};

Puis en CoffeeScript:

importConferences = (conferences, callback) ->
    conferenceImporter.importData conferences, (err, result) ->
        if err
            callback(err)
        else
            doSomethingWithResult result, (err, otherResult) ->
                callback(err, otherResult)

L’usage de CoffeeScript permet déjà de gommer une partie l’effet pyramide des appels de fonctions. Néanmoins, ce n’est pas suffisant, et dans le cas où de nombreux appels sont imbriqués, on s’y perd rapidement dans la compréhension du programme.

Q.defer()

Avec l’usage de la fonction defer() de Q, il sera possible de réécrire le même code de la façon suivante:

importConferences = (conferences) ->
    deferred = Q.defer()
    conferenceImporter.importData conferences, (err, result) ->
        doSomethingWithResult result, (err, result) ->
            if err
                deferred.reject(err)
            else
                deferred.resolve(result)
    deferred.promise

C’est un peu plus verbeux, mais la transformation du callback initial en promise permet maintenant d’appeler la fonction importConferences de la façon suivante:

importConferences(conferences)
.then (result) ->
    console.log "Result: #{util.inspect(result)}"
    someOtherPromise()
.then (otherResult)
    console.log "Result: #{util.inspect(otherResult)}"
.fail (err) ->
    console.log "Error - Message: #{err.message}"

En appliquant également la fonction defer() pour réécrire les fonctions importData et doSomethingWithResult, nous obtenons le code suivant:

importConferences = (conferences) ->
    conferenceImporter.importData(conferences)
    .then (result) ->
        doSomethingWithResult(result)

Plus besoin de la fonction defer() puisque nous travaillons avec des fonctions retournant des promises.

La fonction fail() n’est pas utilisée ici, car nous laissons le soin à la fonction appelante d’ajouter la gestion des erreurs à la promise retournée.

deferred.makeNodeResolver()

Dans certains contextes, vous ne pouvez pas vous baser sur une promise retournée par un appel de fonction. C’est par exemple le cas lorsque vous appelez une fonction d’un module qui ne supporte pas les promises.

Pour éviter d’écrire une résolution manuelle de promise basée sur le résultat d’un callback, Q propose la fonction makeNodeResolver. Elle simplifie l’écriture de conversion comme suit:

importConferences = (conferences) ->
    deferred = Q.defer()
    conferenceImporter.importData conferences, (err, result) ->
        doSomethingWithResult result, deferred.makeNodeResolver()
    deferred.promise

Le code n’est finalement pas plus verbeux que l’original, et vous permet de travailler avec des promises plutôt que des callbacks.

Conclusion

Les techniques de conversion visant à remplacer l’usage de callback par des promises sont variées et permettent en général de s’adapter au besoin.

Liens utiles

Pour en savoir plus sur la librairie Q, vous pouvez visiter la page du projet sur GitHub à l’adresse suivante:

https://github.com/kriskowal/q

Mongoose, les promises et Q

Mongoose, les promises et Q

Q est un module de promises pour Node.js qui implémente le standard Promises/A+. Il est devenu peu à peu l’implémentation de référence, et de nombreux tutoriaux en présentent différentes fonctionnalités.

De même, Mongoose est le module de facto à utiliser lorsqu’on intègre la base MongoDB dans un projet Node.js, puisqu’il est supporté officiellement par MongoDB, Inc.

Depuis quelques temps maintenant, le module Mongoose propose le support des promises en plus de son fonctionnement via callback via l’appel de la fonction exec. C’est à dire, qu’il est possible d’écrire:

Conference.findOne({ id: 12 }).exec()
.then (conference) ->
    console.log conference
    next()
, (err) ->
    # handle error here.

Malheureusement, l’implémentation des promises utilisées par Mongoose n’est pas compatible avec l’implémentation Q. Notamment, il n’est pas possible d’utiliser la fonction fail, et d’écrire le code précédent de la façon suivante:

Conference.findOne({ id: 12 }).exec()
.then (conference) ->
    console.log conference
    next()
.fail (err) ->
    # handle error here.

La promise renvoyée par Mongoose n’est donc pas directement intégrable avec les promises Q. On peut donc penser qu’il est compliqué d’intégrer les deux types de promises.

Heureusement Q propose une solution simple pour les intégrer avec la fonction Q() qui permet de wrapper une promise Mongoose dans une promise Q, et donc d’intégrer les promises Mongoose dans des chaînages de promises Q, ou bien d’utiliser tout simplement les syntaxes Q.

Ainsi, vous devrez écrire le code suivant:

Q(Conference.findOne({ id: 12 }).exec())
.then (conference) ->
    console.log conference
    next()
.fail (err) ->
    # handle error here.

De façon plus globale, si vous travaillez avec une autre librairie de promises que Q, vous pouvez wrapper vos promises avec la fonction Q() dans la mesure où votre librairie propose une syntaxe compatible avec les wrappers Q. Dixit la doc de la librairie:

Q can exchange promises with jQuery, Dojo, When.js, WinJS, and more

Pour aller plus loin

La homepage GitHub de la librairie Q et sa documentation:

https://github.com/kriskowal/q