Adaltas

Traverser des arrays en mode asynchronisé dans Node.js avec Each

Les librairies en Node.js permettant de gérer et simplifier les appels asynchrones sont légions. Voici le genre de librairies que chacun écrit pour lui et éventuellement publie. Elles ont pour but de réduire les codes spaghetti constitué d’imbrication de callbacks. Je ne fais pas exception. Après un an et demi d’usage intensif, je pense qu’il est temps de faire honneur à Each, ma propre libraire de gestion de flux asynchrones, communément appelées en anglais “control flow library”.

Toutefois, pour être exact, il ne s’agit pas exactement d’une librairie de gestion de flux asynchrone au sens traditionnel du terme. Each ne fourni pas de mécanisme permettant de chainer et de contrôler les fonctions. Sa conception vient de mon besoin intensif de traverser des arrays et d’effecteur pour chaque élément des appels asynchronisés. Each est en quelques sorte une version asynchrone et boostée de Array.prototype.forEach.

Un exemple simple

Partons de l’hypothèse que nous devons créer 3 répertoires. Cette opération peut tourner en parallèle pour chaque répertoire et peut se décomposer en 3 sous opérations: vérifier si le répertoire existe, créer le répertoire et régler ses permissions.

Voici un exemple de code :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
each([
  '/data/1/my_dir'
  '/data/2/my_dir'
  '/data/3/my_dir'
])
.on 'item', (dir, next) ->
  fs.stat dir, (err, stat) ->
    return next() if stat
    fs.mkdir dir, (err) ->
      next err
.on 'error', (err) ->
  console.error err.message
.on 'end', ->
  console.log 'Success'

A ce stade, constatons que Each emprunte son API des modules Event Emitter et Stream de Node.js.

Pourquoi je n’utilise pas de librairies de gestion de flux asynchrones

Cela peut paraître étrange de commencer ainsi mais puisque je décris Each comme n’étant que partiellement une librairie de gestion de flux asynchrones, je préfère prendre le temps d’expliquer pourquoi Each ne rempli pas entièrement leur cahier des charges et pourquoi je n’utilise aucune autre librairie en complément de Each.

La programmation asynchronisée est merveilleuse mais en Node.js et plus généralement en Javasript, elle conduit à un code peut esthétique lorsque plusieurs callbacks sont imbriqués les uns dans les autres. On appelle plus généralement le résultat un code en spaghetti.

Revenons à notre exémple précédent. Une manière de limiter la profondeur du code est d’isoler le processus de création d’un répertoire en une fonction :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
create = (dir, callback) ->
  fs.stat dir, (err, stat) ->
    return next() if stat
    fs.mkdir dir, (err) ->
      next err
each([
  '/data/1/my_dir'
  '/data/2/my_dir'
  '/data/3/my_dir'
])
.on('item', create)
.on 'error', (err) ->
  console.error err.message
.on 'end', ->
  console.log 'Success'

Toutefois, les librairies de gestion de contrôle des flux asynchrones ne sont pas seulement utiles à réduire la profondeur du code. Elles résolvent des complexes problèmes aussi. Assumons que nous souhaitons créer un fichier, sachant que le répertoire parent peut ne pas exister :

1
2
3
4
5
6
7
8
9
create = (file, content, callback) ->
  dir = path.dirname
  fs.stat dir, (err, stat) ->
    if stat
      fs.writeFile file, content, (err) ->
        callback err
    fs.mkdir dir, (err) ->
      fs.writeFile file, content, (err) ->
        callback err

Ici, le code créant le fichier n’est pas que redondant et horrible, il peut aussi devenir ingérable lorsque celui-ci augmentera en complexité. Après avoir utilisé plusieurs librairies, je suis finalement arrivé à la conclusion que le plus simple et le plus efficace revenait à décomposer le code en plusieurs petites fonctions. Voici comment :

1
2
3
4
5
6
7
8
9
10
11
12
13
create = (file, content, callback) ->
  dir = path.dirname
  checkDir = ->
    fs.stat dir, (err, stat) ->
      unless stat then makeDir() else writeFile()
  makeDir = ->
    fs.mkdir dir, (err) ->
      return callback err if err
      writeFile()]
  writeFile = 
      fs.writeFile file, content, (err) ->
        callback err
  checkDir()

Le résultat est une solution 100% native JavaScript qui est facile à lire et rapide d’exécution. Le code source de mecano est une ressource intéressante qui vous permettra d’approfondir ce modèle.

Et comment j’en suis arrivé à Each

Il reste un usage utile des librairies de gestion de contrôle des flux asynchrones. Elles permettent pour la plupart de traverser des arrays tout en exécutant du code asynchrone. Il n’y pas de solution simple et esthétique permettant de subvenir à ce besoin en JavaScript, du moins dans sa version actuelle. Un tel code peut s’avérer excessivement compliqué lorsqu’il s’agit de couvrir la gestion d’erreur ou encore l’exécution d’un nombre défini de tâches en parallèles.

C’est ainsi que je conçu Each. En ce temps là, j’installais et je gérais un cluster Hadoop. De nombreuses tâches étaient distribuées sur l’ensemble du cluster. Le démarrage de service, le lancement de commandes shell ou le rapatriment de statistiques tournaient (et tournent encore) avec l’aide d’Each.

Au fil du temps, la librairie est devenu ultra flexible et ultra testée. L’API est celle du module Event Emitter, classique d’un module Node.js. Elle emprunte aussi partiellement au module Stream.

Le plus j’utilisais Each et le plus je réalisais que mes problèmes n’était pas autour de l’appel de fonctions asynchronisées. A chaque fois que j’étais tenté d’utiliser un telle librairie, j’étais en fait dans les besoin de traverser des arrays. Il s’agit d’un processus complexe et Each le résout avec élégance. Je vous invite donc à tester Each et à m’aider à en faire une encore meilleur librairie.

A nouveau, je vous rappelle que le code source de mecano](https://github.com/wdavidw/node-mecano/blob/master/src/mecano.coffee) est une excellente source d’inspiration si vous souhaiter consulter une utilisation de Each en cas réel.

Merci pour votre lecture. Vous pouvez consulter site Internet du project et son code source sur GitHub.

Comments