Gravé dans la pierre

“C’est de la terre que vient notre force. Des montagnes, notre résistance. Nos corps sont forgés à partir de la pierre dans les feux incessants alimentés par notre détermination.”
– Magni Bronzebeard, World of Warcraft

Survivre à la mort

Vous avez vu comment créer un state pour un Elfe en dérivant une classe depuis Elf.Spirit. Cette mécanique reste proche des Goblins, les différences étant principalement le confort d’écriture et d’utilisation des states. Il y a néanmoins une évolution majeure en comparaison des Goblins. Les esprits Spirit sont volatiles car tuer un Elfe puis le re-créer va forcément nous rendre un état initial. En pratique on souhaîte généralement créer des entités qui doivent survivre à la mort, aussi bien pendant le runtime que lors du redémarrage du monde.

Survivre à la mort ?
Hé oui, contrairement aux Goblins, les Elfes sont immortels.

Les archétypes

En une phrase; les states qui dérivent des archétypes sont persistés.

Qu’est-ce que celà implique ?

Quand vous créez un archétype, vous vous assurez de retrouver l’état de votre Elfe après sa mort, et même plus encore. Voyons un exemple où nous allons convertir Galadriel de Spirit à Archetype.

const {Elf} = require('xcraft-core-goblin');
const {id} = require('xcraft-core-goblin/lib/types.js');
const {string} = require('xcraft-core-stones');

class GaladrielShape {
  id = id('galadriel');
  name = string;
  age = number;
}

class GaladrielState extends Elf.Sculpt(GaladrielShape) {}

/* AVANT */
class GaladrielLogic extends Elf.Spirit {
  /* ... */
}

/* APRES */
class GaladrielLogic extends Elf.Archetype {
  static db = 'valinor';
  /* ... */
}

Comme vous pouvez le constater, une ligne a changé et une propriété statique a été ajoutée.

  1. Il faut dériver de la classe Elf.Archetype à la place d’Elf.Spirit.
  2. Il faut spécifier le nom de la base de donnée où l’on souhaite sérialiser cet Elfe.

Avant d’aller plus loin, je suis obligé de vous donner un peu de théorie ennuyeuse. Comme vous le savez, les Elfes s’appuient sur les Goblins qui eux mêmes s’appuient sur le framework Redux. La logique d’un Elfe est donc un ensemble de reducers appelés par Redux et qui doivent être purs. Mais qui dit pur pour les Elfes, dit aussi sérialisable. Ce n’est pas nouveau, car avec les Goblins vous devez également n’utiliser que des types sérialisables avec les reducers. Il y a même des contraintes assez fortes car les types comme Map ou Set ne sont pas tolérés.

Map et Set n’ont pas de représentations particulières sous la forme JSON. Il est nécessaire de travailler avec les types natifs comme object et array.

Quand une action est dispatchée dans un reducer, cette action contient alors uniquement un objet directement représentable en tant que JSON. Ce JSON peut alors être transféré simplement entre les services comme le warehouse. Mais il peut alors aussi très facilement être sérialisé pour une base de données tel que SQLite (par exemple).

Quand un Elfe rencontre Ellen Ripley

Mais c’est quoi ce titre ? Ellen Ripley ? Qu’est-ce qu’elle vient bien faire ici. On saute du coq à l’âne. On était chez Tolkien et d’un coup on arrive sur le Nostromo ?!

Je vois vos yeux. Tout va bien je vais tout vous expliquer. Dans le monde Goblin il existe depuis bien longtemps un middleware pour Redux, qui se nomme Ripley. Ce nom est un hommage à Ellen Ripley du film Alien. C’est surtout un jeu de mot entre replay et ripley. Le fait de rejouer, vient du concept des actions stores.

Des actions stores, mais c’est quoi ça encore ?

Les actions stores sont des bases de données de stockage pour les actions Redux. Ces stockages contiennent les actions dispatchées sur un Goblin (oui, je parle de Goblin car ce n’est pas un nouveau mécanisme) pour autant que le Goblin ait été configuré explicitement pour que certaines actions soient sérialisées dans un stockage Cryo.

Mmmh, actions stores… et c’est quoi ce Cryo ?

Cryo est un module Xcraft (et non Goblin) qui s’occupe d’insérer les actions dans un stockage SQLite. Cryo permet également d’effectuer des requêtes pour faire de la maintenance du stockage, ou simplement pour y extraire des actions.

Le nom de Cryo vient également de l’univers d’Ellen Ripley. Rappelez-vous le début et la fin du film Alien. Ellen sort d’un état de cryogénisation, puis à la fin du film, elle y replonge pour y demeurer aussi longtemps que nécessaire.

Bien, mais rejouer (replay) c’est pour faire quoi ?

L’idée est inspirée (de très loin) de l’event sourcing. En ayant une pile d’actions (au lieu que ce soit des événements) et en les rejouant les un après les autres dans le bon ordre (ce qui revient à les dispatcher avec Redux l’un après l’autre), on est en mesure de recréer un état au complet. Avec un peu d’astuce, on peut modifier certaines actions afin de corriger l’état final (par exemple pour adapter des données à la volée).

Revenons à nos archétypes

Maintenant que vous avez compris de quoi on parle, il est temps de revenir aux archétypes pour terminer la conversion de l’esprit Galadriel. Pour se faire, je vais uniquement reprendre le code concerné et y ajouter la classe Elf correspondante. Je ne revient pas sur les définitions du state et du shape. Pour celà, voir la section Son état.

class GaladrielLogic extends Elf.Archetype {
  static db = 'valinor';
  state = new ElrondState();

  create(id, name, age) {
    const {state} = this;
    state.id = id;
    state.name = name;
    state.age = age;
  }
}

class Galadriel extends Elf {
  logic = Elf.getLogic(GaladrielLogic);
  state = new GaladrielState();

  async create(id, desktopId, name, age) {
    this.logic.create(id, name, age);
    return this;
  }
}

Cette implémentation va réveiller Ellen Ripley chaque fois qu’une quête create est appelée sur une Galadriel qui n’existe pas encore dans le système. L’appel sur le reducer this.logic.create() va, à travers le middleware Ripley pour Redux, émettre un ordre de freeze au module Cryo. L’action create avec son payload va alors être sérialisé et persisté dans la base de donnée valinor.

Ici c’est très bien, mais ce n’est pas encore suffisant car en recréant la même Galadriel (après un redémarrage du serveur, par exemple) vous ne retrouverez pas son dernier état.

Les transactions

Pas de bras, pas d’chocolat disait l’autre. Alors non, ici je ne vais pas vous parler de transactions SQL. On pourrait le penser au premier abord car je vous ai dis plus haut que Cryo exploite une base de donnée SQLite. Ici il est question des transactions métiers. Persister des actions dans un stockage c’est effectivement la première chose à faire. Il faut par contre se poser la question de comment ça marche pour qu’un état puisse être remonté dans un Elfe.

Voici comment on clos une transaction :

class Galadriel extends Elf {
  /* ... */

  async create(id, desktopId, name, age) {
    this.logic.create(id, name, age);
    await this.persist(); /* <- */
    return this;
  }
}

J’ai simplement ajouté l’appel await this.persist(). Cette méthode réservée de l’Elfe permet de générer un instantané de son état. En effet, chaque action sur un reducer va générer une entrée dans Cryo. Mais une action ne contient pas forcément tout l’état de l’Elfe. C’est là qu’intervient la méthode persist() et qui dans les fait va engendrer une action de type persist avec l’entier du contenu du state de l’Elfe.

Voici un autre exemple afin de mieux comprendre :

class Galadriel extends Elf {
  /* ... */

  async addPower(power) {
    this.logic.addPower(power);
  }

  async setCapability(capability) {
    this.logic.setCapability(capability);
  }

  async levelUp() {
    this.logic.levelUp();
    await this.persist();
  }
}

/* Quelque part, mais ailleurs */
await galadriel.addPower(1500);
await galadriel.addCapability('longbow');
await galadriel.addCapability('sword');
await galadriel.levelUp();

Dans cet exemple, on informe Galadriel qu’elle a gagné 1500 de puissance, qu’elle a de nouvelles compétences comme l’usage des arcs et des épées. Puis on indique qu’elle monte de niveau. Dans ce scénario, on ne souhaite pas qu’un “reader” sur la base de donnée valinor puisse lire une Galadriel qui a (par exemple) la compétence longbow sans avoir la compétence sword et bien entendu, sans avoir augmenté de niveau. Ce qui veut dire que la lecture va se baser uniquement sur les actions de type persist. Ces actions contiennent alors l’état complet de l’Elfe (instantané).

En terminant la quête levelUp, on provoque alors le persist qui (en quelque sorte) ferme la transaction.

En pratique vous constaterez que la plupart du temps, chaque appel de quête se termine par un persist. En effet, il est d’usage de faire en sorte d’avoir un état lisible depuis l’extérieur après chaque quête. Mais gardez à l’esprit que ce système de transaction permet de gérer (par exemple) un formulaire avec un bouton de validation. Il faut savoir qu’il existe un mécanisme de synchronisation (pour avoir des Elfes distribués) qui s’appuie complètement sur ce mécanisme de transaction.

La synchronisation (en quelques mots)

Maintenant que vous savez comment persister l’état de votre Elfe, il m’est nécessaire de vous parler brièvement de la synchronisation. Je ne vais pas rentrer dans les détails car ici il est question des Elfes et pas de l’infrastructure complète d’un projet basé sur Xcraft.

Quand un Elfe doit être synchronisé entre un serveur et des clients, les bases de données Cryo rentrent en jeu. Les actions entre deux persist sont envoyées au serveur, qui va alors les rejouer (merci Ellen Ripley). Une fois la pile d’actions rejouée (il n’y a pas d’actions persist dans celle-ci), un nouvel état est généré (un persist) qui est alors une fusion de l’état d’origine sur le serveur, modifié par la pile d’actions reçues. C’est un peu comme appliquer des différences très granulaire sur un état qui ne correspond pas forcément à celui du client. Ce nouveau persist est ainsi envoyé au client, il deviendra la nouvelle vérité pour l’Elfe.

Je vais m’arrêter ici à propos de la synchronisation. C’est un mécanisme assez complexe qui mérite une section à part entière.