“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
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.
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.
Elf.Archetype
à la place d’Elf.Spirit
.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
etSet
n’ont pas de représentations particulières sous la forme JSON. Il est nécessaire de travailler avec les types natifs commeobject
etarray
.
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).
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).
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.
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.
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.