API Pub/sub
Prérequis
Avant de suivre ce tutoriel vous devez au préalable :
initialiser un jumeau numérique (tutoriel de création d'un jumeau numérique)
Intégrer un réseau GTB au jumeau numérique (tutoriel d'installation du réseau GTB)
installer et déployer le serveur d'API Spinalcore (tutoriel d'installation de serveurs d'api)
Présentation
Le publish/subscribe (pub/sub) est un système de communication asynchrone entre les éditeurs (publisher) et les consommateurs (subscriber). Les éditeurs envoient des événements (création, modification, suppression de données) au service pub/sub sans se préoccuper de comment ni de quand ces événements seront traités. Le pub/sub distribue ensuite les informations aux consommateurs qui se sont abonnés aux événements.
L'api publish/subscribe de SpinalCom est basée sur la version 4.X de la librairie socket.IO, permettant la communication bidirectionnelle et en temps réel, par conséquent cette api peut être consommée que par socket.IO.
Cet article est un tutoriel pas-à-pas expliquant le fonctionnement et la mise en place d'un système client de l'api pub/sub de SpinalCom avec socket.IO.
Tutoriel : Développer un consommateur (subscriber) d'api pub/sub Spinalcom avec nodejs
A - Initialisation du projet
Tout d'abord, créez un dossier nommé (ou un autre nom que vous voulez)
Ouvrez un terminal puis accédez au dossier créé précédemment et initialisez le package.json
> cd spinal-api-pubsub-client /* Deplacement dans le dossier du projet */
> npm init -y /* Initialisation du package.json dans le projet nodejs */
B - Installation Socket.IO
Pour consommer les api publish/subscribe de Spinalcom, il faut dans un premier temps installer la librairie client de socket.IO dans le dossier spinal-api-pubsub-client. Ci-dessous quelques méthodes d'installation. Si vous utilisez un autre gestionnaire de package, reportez-vous à la documentation d'installation de socket.IO
/* Via CDN */
<script src="https://cdn.socket.io/4.4.1/socket.io.min.js" integrity="sha384-fKnu0iswBIqkjxrhQCTZ7qlLHOFEgNkRmK2vaO/LbTZSXdJfAu6ewRBdwHPhBo/H" crossorigin="anonymous"></script>
/* Via npm */
npm install socket.io-client
/* Via yarn*/
yarn add socket.io-client
NB: SpinalCom utilise la version 4 de socket.IO serveur. Veuillez vérifier la compatibilité des versions avant d'installer socket.IO client.
C - Connexion au serveur
Après l'installation de la librairie client, nous pouvons désormais instancier et nous connecter au serveur d'api. Pour cela créez un fichier à la racine du projet nommé index.js avec le contenu ci-dessous en fonction du lien entre votre client et votre serveur.
Si le client et le serveur ont le même nom de domaine
// index.js
const io = require("socket.io-client");
const options = {auth: {token: "XXX"}, transports: ["websocket"]} /* Pour plus de details sur les options, consulter la documentation */
const socket = io(options);
Si le client et le serveur n'ont pas le même nom de domaine
// index.js
const io = require("socket.io-client");
const options = {auth: {token: "XXX"}, transports: ["websocket"]} /* Pour plus de details sur les options, consulter la documentation */
const socket = io("http://www.le_lien_de_mon_serveur.com", options);
Le code ci-dessus permet d'instancier le socket client. Après avoir été initialisé, le client essaie d’établir une connexion websocket avec le serveur. Le serveur s'il reçoit la requête de connexion traite les informations reçues, identifie le client et renvoie au client un événement connect (le client a été identifié et connecté avec success) ou connect_error (la connexion n'a pas pu s'établir).
D - Ecouter les événements à la connexion au serveur
Le Client peut écouter les events (connect et connect_error) envoyés par le serveur pour connaitre la raison de l’échec de connexion, ou pour poursuivre avec d'autres taches. Pour cela nous devons utiliser la méthode "on" de socket. Ci-dessous le code à ajouter au fichier index.js.
// index.js
/* Exécutée une fois que la connexion au serveur est établie */
socket.on("connect", () => { console.log("la connexion au serveur a été établie avec succès") });
/* Exécutée en cas de déconnexion au serveur */
socket.on("disconnect", (reason) => { console.log("Déconnexion du serveur pour la raison :",reason) });
/* Exécutée en cas d'erreur de connexion au serveur */
socket.on("connect_error", (err) => { console.log(err.message) });
E - Souscription aux données (scénario)
1- Comment ça marche ?
L'abonnement aux données se fait en deux (2) étapes. Dans un premier temps le client envoie une requête subscribe au serveur contenant toutes les informations (contexte et nœud) des données auxquelles il veut s'abonner, une option (pour définir s'il faut se souscrire à un sous graph du nœud).
L'api serveur à son tour, répond avec une liste d’événements à écouter par le client et/ou un message erreur pour les données non existantes, ensuite au changement le serveur envoie les données au client.
2 - La requête subscribe
La syntaxe de la requête subscribe se fait comme suit : socket.emit("subscribe", élément1,...,élémentN, options)
paramètres :
subscribe: le nom de l'événement, tout en minuscule
élément1,....,élémentN :
les valeurs auxquelles on veut s'abonner, ils peuvent être regroupé dans une liste ou se suivre successivement en tant que paramètre. les deux (2) formats acceptables sont contextId/nodeId et {contextId: contextId, nodeId: nodeId, option?: option} . contextId et nodeId étant respectivement les ids (dynamic id ou static id) du contexte et du nœud, option n'est pas obligatoire dans l'objet, il permet de définir une option spécifique à l'élément.
options :
les options globales à tous les éléments , ils permettent d'indiquer à quel nœud supplémentaire on veut se souscrire (les enfants du nœud, une arborescence dans le contexte à partir du nœud). options est un objet qui a deux (2) propriétés:
subscribeChildren : (un booléen, par défaut "false") indique s'il faut s'abonner à des noeuds qui ont une relation avec l'élément.
subscribeChildScope: (par défaut "all") permet de faire un filtre sur les nœuds souscrits avec subscribeChildren . Sa valeur peut être:
all : s'abonner à tous les nœuds associés à l'élément
in_context: s'abonner à tous les nœuds associés à l'élément via le contexte
not_in_context: s'abonner à tous les nœuds liés à l'élément et qui ne sont pas dans le contexte
tree_in_context: s'abonner à l'arborescence à partir de l'élément en ne sélectionnant que les nœuds qui sont dans le contexte
NB : le dernier paramètre est automatiquement considéré comme les options globales, si vous ne souhaitez pas passer une option mettez un objet vide à la fin des paramètres.
3 - Réponse à la requête subscribe (subscribed)
Comme indiqué ci-dessus à la requête subscribe, le serveur répond avec un événement subscribed en envoyant un objet ou une liste d'objets contenant la/les réponse(s) pour chaque élément auxquels le client s'est souscrit.
L'objet de réponse contient :
error : le message d'erreur en cas d'erreur, sinon null
eventNames : null en cas d'erreur, sinon une liste de chaines de caractères correspondant aux noms d’événements à écouter
La syntaxe en javascript ci-dessous :
socket.once("subscribed",(result) => {
if(!Array.isArray(result)) result = [result] // convertir le result en tableau s'il ne l'est pas
// Eccouter les événements ICI
})
4- Ecouter les événements
Une fois la réponse reçue, le client à son tour doit parcourir la réponse et écouter aux événements envoyés s'il n'y a pas d'erreur, la syntaxe en Javascript ressemble au code ci-dessous :
if(!Array.isArray(result)) result = [result] // convertir le result en tableau s'il ne l'est pas
// parcourir le resultat
result.map(({ error, eventNames }) => {
if (error) {
console.error(error); // afficher le message d'erreur
return;
}
// ecouter les evenements
eventNames.forEach(eventName => {
socket.on(eventName,(error, dataChanged) => {
// Faire quelque chose avec dataChanged la nouvelle donnée
});
});
});
dataChanged : contiendra les informations du nœud qui a changé, c'est un objet ayant comme propriété :
event : un objet du type {name: updated , nodeId: "id du nœud modifié" }
info : un objet contenant les infos du nœud
element : un objet contenant l'element du nœud
F - Le module subscribe :
Maintenant que nous savons comment marche la souscription, nous allons implémenter le processus d'abonnement en tant que module, pour éviter la répétition à chaque souscription. Créons un fichier nommé subscribe.js à la racine du projet avec le contenu ci-dessous.
// subscribe.js
module.exports = function subscribe(socket, elements,options,onChangeCallback) {
socket.emit("subscribe",...elements,options);
socket.once("subscribed",(result) => {
if(!Array.isArray(result)) result = [result];
result.map(({ error, eventNames }) => {
if (error) {
console.error(error);
return;
}
eventNames.forEach(eventName => {
socket.on(eventName, onChangeCallback)
});
});
})
}
G - Souscription aux données (code)
Réprésentation via le studio du réseau GTB
Réprésentation via l'api REST du réseau GTB
Dans cette section, nous allons utiliser le module créé précédemment pour nous souscrire à un réseau GTB (voir les images ci-dessus) qui représente les disponibilités des places d'un parking. Complétons l'index.js pour nous abonner à toutes les places du parking sous-sol (arborescence du parking sous-sol dans le contexte Reseau GTB.)
/* index.js */
const subscribe = require("./subscribe.js");
const io = require("socket.io-client");
const options = {
auth: { clientId: "XXX", secretId: "XXX" }, transports: ["websocket"]}; /* Pour plus de details sur les options, consulter la documentation */
const socket = io(options);
/* Exécutée une fois que la connexion au serveur est établie */
socket.on("connect", () => {
console.log("la connexion au serveur a été établie avec succès");
});
/* Exécutée en cas de déconnexion au serveur */
socket.on("disconnect", (reason) => {
console.log("Déconnexion du serveur pour la raison", reason);
});
/* Exécutée en cas d'erreur de connexion au serveur */
socket.on("connect_error", (err) => {
console.log(err.message);
});
const elementToSubscribe = "38135984/38147024"; // à remplacer par les bons ids
/*
La variable ci-dessus peut aussi être déclaré comme suit :
const elementToSubscribe = "SpinalContext-29bd465f-6c46-3367-67d0-14c79ffc5c45-17e0b35b32a/38147024";
ou
const elementToSubscribe = "38135984/SpinalNode-68090d6a-bc11-2364-34bb-279855b22d81-17e0b35cf88";
ou
const elementToSubscribe = "SpinalContext-29bd465f-6c46-3367-67d0-14c79ffc5c45-17e0b35b32a/SpinalNode-68090d6a-bc11-2364-34bb-279855b22d81-17e0b35cf88";
ou
const elementToSubscribe = {
contextId: SpinalContext-29bd465f-6c46-3367-67d0-14c79ffc5c45-17e0b35b32a,
nodeId : 38147024
}
*/
const requestOptions = {
subscribeChildren: true,
subscribeChildScope: "tree_in_context"
};
subscribe(socket, elementToSubscribe, requestOptions, (dataChanged) => {
console.log(dataChanged); // exécuter à chaque dans le réseau GTB
});
Maintenant que le code a été intégré, nous allons tester notre projet. Pour cela ouvrez le terminal et tapez la commande node index.js à la racine du projet.