📐 Construire une app en JS
07 Async

Les commandes sont asynchrones

Avez-vous remarqué une évolution importante dans la dernière version de notre test ?

// Avant : le calendrier est vide
const initialCalendar = Calendar.of(accomodation)
expect(initialCalendar.count()).toBe(0)
 
// 2 locataires différents tentent une réservation simultanée
const bookings = await Promise.all(
    book(accomodation, tenant1, stay),
    book(accomodation, tenant2, stay),
)
 
// 1 seule réservation est acceptée.
const alteredCalendar = Calendar.of(accomodation)
expect(alteredCalendar.count()).toBe(1)

La commande book() est désormais asynchrone. Elle ne retourne plus une réservation, mais une promesse de réservation.

Les commandes sont généralement asynchrones. Car une commande modifie l'état du système, et que cette modification s'opère au travers d'une API ou d'une base de données.

Les reducers

On connait une exception notable, fréquente dans le code frontend : les reducers.

Oui, un reducer est une commande, puisqu'elle modifie l'état du système.

Vous pensiez que tout ce qui était écrit jusqu'à présent ne s'appliquait qu'au code backend NodeJS. Pas du tout. C'est justement cela que l'on adore dans JavaScript. La liberté de trancender les frontières du réseau !

L'état du système peut indifféremment être :

  1. une collection d'entités dans une base de données
  2. des données d'un autre système accessibles au travers d'une API
  3. des objets Javascript dans le navigateur web (ex : liste des filtres appliqués à une liste)

Si vous utilisez Redux, vous connaissez cette limitation : un reducer doit être synchrone. Elle est précisément justifée par l'absence de transaction dans Redux. Comme vu dans le chapitre précédent, autoriser des reducers asynchrones pourrait conduire à un état incohérent.

Parfois, on se questionne sur la location d'une commande. Et si je localisais le code de la fonction booking() dans le frontend ?

Rien ne l'empêche après tout. Je monte une instance de BaaS (Backend as a service) et j'ai des APIs pour lire et écrire les données.

Et ça fonctionne. Pendant un moment.

Plus moyen de garantir l'isolation d'une commande de réservation lorsqu'elle s'exécute dans le frontend.

La concurrence d'accès se gère dans le backend. Le frontend se limite aux commandes synchrones. Ce qui ne signifie pas que rien d'est asynchrone dans le backend, nous y reviendront plus tard.

Les événements

Une commande est aussi un événement.

Parce qu'un utilisateur (ou un webhook, un sheduler, ...) déclenche la commande à un moment.

Donc on peut écrire un scénario dans notre test. Un scénario est un enchaînement d'événements, comme dans un film.

// Avant : le calendrier est vide
const initialCalendar = Calendar.of(accomodation)
expect(initialCalendar.count()).toBe(0)
 
// Réservation puis annulation
const bookings = await run(
    Calendar.of(accomodation),
    book(tenant1, stay)
    cancel,
)
 
// Aucune réservation est acceptée.
const alteredCalendar = Calendar.of(accomodation)
expect(alteredCalendar.count()).toBe(0)

Quelle est cette sorcellerie ?

Voici une fonction run() qui enchaîne 3 fonctions, dont 2 commandes book() et cancel().

Observez l'expressivité du code de notre test. Il s'apparente presque à un langage métier (DSL : Doamin Specific Language) qui raconterait :

  • prend le calendier des réservations de l'hébergement
  • ajoute la réservation du locataire tenant pour le séjour stay
  • puis annule la réservation

Un test raconte une histoire.

Une histoire compréhensible par celui qui porte le projet, même s'il ne sait pas coder.

Voilà le genre de test que l'on veut écrire.