Améliorer mes compétences GraphQL : souscriptions en temps réel

Pour quelques années maintenant, j’ai tenté d’identifier des cadres, produits et services qui permettent aux technologues de maintenir leur concentration sur l’extension de la valeur de leurs propres intellectuels. Cela demeure un merveilleux voyage pour moi, rempli de opportunités d’apprentissage uniques.

Récemment, l’ingénieur en moi s’est demandé si une situation pouvait exister où je pourrais trouver un avantage secondaire pour un concept existant que j’ai déjà discuté avant. En d’autres termes, pourrais-je identifier un autre avantage avec le même niveau d’impact que la solution parent originale reconnue auparavant ?

Pour cet article, j’ai voulu plonger plus profondément dans GraphQL pour voir ce que je pouvais trouver.

Dans mon article intitulé « When It’s Time to Give REST a Rest« , j’ai discuté de la manière dont GraphQL est préférable à un service RESTful dans certains scénarios réels. Nous avons vu comment construire et déployer une API GraphQL en utilisant Apollo Server.

Dans ce billet de suite, j’ai l’intention d’améliorer mes connaissances sur GraphQL en passant en revue les abonnements pour la récupération de données en temps réel. Nous construirons également un service WebSocket pour consommer les abonnements.

Récap : Cas d’utilisation Customer 360

Mon article précédent se concentrait sur un cas d’utilisation Customer 360, où les clients de mon entreprise fictionnelle maintiennent les collections de données suivantes :

  • Informations sur le client
  • Informations sur l’adresse
  • Modes de contact
  • Attributs de crédit

Un grand avantage de l’utilisation de GraphQL est que une seule requête GraphQL peut récupérer toutes les données nécessaires pour un jeton client (identité unique).

JavaScript

 

type Query {
    addresses: [Address]
    address(customer_token: String): Address
    contacts: [Contact]
    contact(customer_token: String): Contact
    customers: [Customer]
    customer(token: String): Customer
    credits: [Credit]
    credit(customer_token: String): Credit
}

En utilisant une approche RESTful pour récupérer le seul aperçu (360) du client aurait nécessité plusieurs requêtes et réponses pour être jointes. GraphQL nous offre une solution beaucoup plus performante.

Objectifs d’amélioration

Pour améliorer dans n’importe quelle aspecte de la vie, il faut atteindre de nouveaux objectifs. Pour mes propres objectifs ici, cela signifie :

  • Comprendre et implémenter la valeur de la proposition abonnements dans GraphQL
  • Utiliser une implémentation de WebSocket pour consommer un abonnement GraphQL

L’idée d’utiliser des abonnements à la place de requêtes et de mutations dans GraphQL est la méthode préférée lorsque les conditions suivantes sont remplies :

  • Petites modifications incrementales à des objets volumineux
  • Mises à jour en temps réel à bas débit (comme une application de chat)

Cela est important car l’implémentation d’abonnements dans GraphQL n’est pas triviale. Non seulement le serveur sous-jacent nécessitera d’être mis à jour, mais l’application de consommation aussi nécessitera une refonte.

Heureusement, le cas d’utilisation que nous poursuivons avec notre exemple Customer 360 est parfaitement adapté aux abonnements. De plus, nous mettrons en œuvre une approche WebSocket pour exploiter ces abonnements.

Comme avant, je continuerai à utiliser Apollo.

Amélioration avec des abonnements Creds

Premièrement, nous devons installer les bibliothèques nécessaires pour supporter les abonnements avec mon serveur Apollo GraphQL :

Shell

 

npm install ws
npm install graphql-ws @graphql-tools/schema
npm install graphql-subscriptions 

Avec ces éléments installés, j’ai mis l’accent sur la mise à jour du fichier index.ts de mon dépôt original pour étendre la constante typedefs avec les éléments suivants :

JavaScript

 

type Subscription {
    creditUpdated: Credit
}

J’ai également établi une constante pour abriter une nouvelle instance de PubSub et créé un exemple de souscription que nous utiliserons plus tard :

JavaScript

 

const pubsub = new PubSub();

pubsub.publish('CREDIT_BALANCE_UPDATED', {
    creditUpdated: {
    }
});

J’ai nettoyé les résolveurs existants et ajouté un nouveau Subscription pour ce nouvel usage :

JavaScript

 

const resolvers = {
    Query: {
        addresses: () => addresses,
        address: (parent, args) => {
            const customer_token = args.customer_token;
            return addresses.find(address => address.customer_token === customer_token);
        },
        contacts: () => contacts,
        contact: (parent, args) => {
            const customer_token = args.customer_token;
            return contacts.find(contact => contact.customer_token === customer_token);
        },
        customers: () => customers,
        customer: (parent, args) => {
            const token = args.token;
            return customers.find(customer => customer.token === token);
        },
        credits: () => credits,
        credit: (parent, args) => {
            const customer_token = args.customer_token;
            return credits.find(credit => credit.customer_token === customer_token);
        }
    },
    Subscription: {
        creditUpdated: {
            subscribe: () => pubsub.asyncIterator(['CREDIT_BALANCE_UPDATED']),
        }
    }
};

J’ai ensuite réorganisé la configuration du serveur et引入了订阅设计 :

JavaScript

 

const app = express();
const httpServer = createServer(app);
const wsServer = new WebSocketServer({
    server: httpServer,
    path: '/graphql'
});

const schema = makeExecutableSchema({ typeDefs, resolvers });
const serverCleanup = useServer({ schema }, wsServer);

const server = new ApolloServer({
    schema,
    plugins: [
        ApolloServerPluginDrainHttpServer({ httpServer }),
        {
            async serverWillStart() {
                return {
                    async drainServer() {
                        serverCleanup.dispose();
                    }
                };
            }
        }
    ],
});

await server.start();

app.use('/graphql', cors(), express.json(), expressMiddleware(server, {
    context: async () => ({ pubsub })
}));

const PORT = Number.parseInt(process.env.PORT) || 4000;
httpServer.listen(PORT, () => {
    console.log(`Server is now running on http://localhost:${PORT}/graphql`);
    console.log(`Subscription is now running on ws://localhost:${PORT}/graphql`);
});

Pour simuler les mises à jour commandées par le client, j’ai créé la méthode suivante pour augmenter le solde de crédit de 50 $ tous les cinq secondes pendant que le service est en cours d’exécution. Une fois que le solde atteint (ou dépasse) le plafond de crédit de 10 000 $, je réinitialise le solde à 2 500 $, simulant ainsi un paiement de solde.

JavaScript

 

function incrementCreditBalance() {
    if (credits[0].balance >= credits[0].credit_limit) {
        credits[0].balance = 0.00;
        console.log(`Credit balance reset to ${credits[0].balance}`);

    } else {
        credits[0].balance += 50.00;
        console.log(`Credit balance updated to ${credits[0].balance}`);
    }

    pubsub.publish('CREDIT_BALANCE_UPDATED', { creditUpdated: credits[0] });
    setTimeout(incrementCreditBalance, 5000);
}

incrementCreditBalance();

Le fichier complet index.ts peut être trouvé ici.

Deployer sur Heroku

Le service étant prêt, il est temps de le déployer afin que nous puissions interagir avec lui. Comme Heroku a fonctionné très bien la dernière fois (et qu’il est facile pour moi à utiliser), nous conservons cette approche.

Pour démarrer, j’ai besoin de lancer les commandes suivantes de l’interface en ligne de Heroku :

Shell

 

$ heroku login
$ heroku create jvc-graphql-server-sub

Creating jvc-graphql-server-sub... done
https://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/ | https://git.heroku.com/jvc-graphql-server-sub.git

La commande a également automatiquement ajouté le dépôt utilisé par Heroku en tant que distant :

Shell

 

$ git remote
heroku
origin

Comme je l’ai mentionné dans mon article précédent, Apollo Server désactive Apollo Explorer dans les environnements de production. Afin de pouvoir toujours utiliser Apollo Explorer pour nos besoins, j’ai dû définir l’environnement variable NODE_ENV sur développement. J’ai effectué cette configuration avec la commande CLI suivante :

Shell

 

$ heroku config:set NODE_ENV=development

Setting NODE_ENV and restarting jvc-graphql-server-sub... done, v3
NODE_ENV: development

J’étais prêt à déployer mon code sur Heroku :

Shell

 

$ git commit --allow-empty -m 'Deploy to Heroku'
$ git push heroku

Un aperçu rapide du tableau de bord d’Heroku montra que mon serveur Apollo fonctionnait sans aucun problème :

Dans la section Paramètres, j’ai trouvé l’URL de l’application Heroku pour cette instance de service :

https://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/

  • Notez : Cette lien ne sera plus accessible à la publication de cet article.

Pour le moment, je pouvais ajouter graphql à cette URL pour lancer Apollo Server Studio. Cela m’a permis de voir les souscriptions fonctionnant comme prévu :

Remarquez les réponses de souscription sur le côté droit de l’écran.

Améliorer mes Compétences avec WebSocket

Nous pouvons exploiter la prise en charge de WebSocket et les capacités d’Heroku pour créer une implémentation qui consomme la souscription que nous avons créée.

Dans mon cas, j’ai créé un fichier index.js avec les contenus suivants. En gros, ça créait un client WebSocket et également établit un service HTTP factice que j’ai pu utiliser pour valider que le client était en cours d’execution :

JavaScript

 

import { createClient } from "graphql-ws";
import { WebSocket } from "ws";
import http from "http";

// Créer un serveur HTTP factice pour se lier à Heroku's $PORT
const PORT = process.env.PORT || 3000;
http.createServer((req, res) => res.end('Server is running')).listen(PORT, () => {
  console.log(`HTTP server running on port ${PORT}`);
});

const host_url = process.env.GRAPHQL_SUBSCRIPTION_HOST || 'ws://localhost:4000/graphql';

const client = createClient({
  url: host_url,
  webSocketImpl: WebSocket
});

const query = `subscription {
  creditUpdated {
    token
    customer_token
    credit_limit
    balance
    credit_score
  }
}`;

function handleCreditUpdated(data) {
  console.log('Received credit update:', data);
}

// S'abonner à la souscription creditUpdated
client.subscribe(
  {
    query,
  },
  {
    next: (data) => handleCreditUpdated(data.data.creditUpdated),
    error: (err) => console.error('Subscription error:', err),
    complete: () => console.log('Subscription complete'),
  }
);

Le fichier index.js complet peut être trouvé ici.

Nous pouvons également déployer cette application simple en utilisant Node.js sur Heroku, en nous assurant de définir les variable d’environnement GRAPHQL_SUBSCRIPTION_HOST à l’URL de l’application Heroku que nous avons utilisée plus tôt.

J’ai également créé le Procfile suivant pour indiquer à Heroku comment démarrer mon application :

Shell

 

web: node src/index.js

Ensuite, j’ai créé une nouvelle application Heroku :

Shell

 

$ heroku create jvc-websocket-example

Creating jvc-websocket-example... done
https://jvc-websocket-example-62824c0b1df4.herokuapp.com/ | https://git.heroku.com/jvc-websocket-example.git

Puis, j’ai défini la variable d’environnement GRAPHQL_SUBSCRIPTION_HOST pour pointer vers mon serveur GraphQL en cours d’execution :

Shell

 

$ heroku --app jvc-websocket-example \
    config:set \
GRAPHQL_SUBSCRIPTION_HOST=ws://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/graphql

A ce stade, nous sommes prêts à déployer notre code sur Heroku :

Shell

 

$ git commit --allow-empty -m 'Deploy to Heroku'
$ git push heroku

Une fois que le client WebSocket commence, nous pouvons voir son état dans le tableau de bord Heroku :

En consultant les journaux dans le tableau de bord Heroku pour l’instance jvc-websocket-example, nous pouvons voir les mises à jour multiples de la propriété balance du service jvc-graphql-server-sub. Dans mon démonstration, je pouvais même capturer le cas où le solde est ramené à zéro, simulant ainsi qu’une transaction a été effectuée :

Sur le terminal, nous pouvons accéder à ces mêmes journaux en utilisant la commande CLI heroku logs.

Shell

 

2024-08-28T12:14:48.463846+00:00 app[web.1]: Received credit update: {
2024-08-28T12:14:48.463874+00:00 app[web.1]:   token: 'credit-token-1',
2024-08-28T12:14:48.463875+00:00 app[web.1]:   customer_token: 'customer-token-1',
2024-08-28T12:14:48.463875+00:00 app[web.1]:   credit_limit: 10000,
2024-08-28T12:14:48.463875+00:00 app[web.1]:   balance: 9950,
2024-08-28T12:14:48.463876+00:00 app[web.1]:   credit_score: 750
2024-08-28T12:14:48.463876+00:00 app[web.1]: }

Nous avons non seulement un service GraphQL avec une implémentation de souscription en cours d’execution, mais nous avons également un client WebSocket consommant ces mises à jour.

Conclusion

Mon public pourrait se rappeler mon énoncé de mission personnel, qui je pense peut s’appliquer à tout professionnel de l’informatique :

“Concentrez-vous sur la livraison de fonctionnalités qui prolongent la valeur de votre propriété intellectuelle. Utilisez des frameworks, produits et services pour tout le reste.”

— J. Vester

Dans cette immersion profonde dans les abonnements GraphQL, nous avons réussi à consommer les mises à jour d’un serveur Apollo exécutant sur Heroku en utilisant un autre service également exécutant sur Heroku — une application basée sur Node.js utilisant les WebSockets. En utilisant des abonnements légers, nous avons évité l’envoi de requêtes pour des données inchangées, mais nous nous sommes simplement abonnés pour recevoir les mises à jour du solde de crédit au fur et à mesure qu’elles se produisaient.

Dans l’introduction, je mentionnais la recherche d’un autre principe de valeur à l’intérieur d’un sujet que j’ai déjà écrit sur. Les abonnements GraphQL sont un excellent exemple de ce que j’avais en tête parce qu’ils permettent aux consommateurs de recevoir des mises à jour immédiatement, sans avoir besoin de faire des requêtes sur les données sources. Cela rendra les consommateurs des données du Customer 360 très excité, car ils savent qu’ils peuvent recevoir des mises à jour en direct en cours d’execution.

Heroku est un autre exemple qui continue à respecter mon énoncé de mission en offrant une plateforme qui me permet de rapidement prototyper des solutions en utilisant une CLI et des commandes Git standards. Cela ne nous donne qu’une manière aisée de montrer notre cas d’utilisation des abonnements mais également d’implémenter un consommateur utilisant les WebSockets.

Si vous êtes intéressé au code source pour cet article, consultez mes dépôts sur GitLab :

Je suis convaincu que mes compétences en GraphQL ont bien progressé grâce à cet effort. Cette aventure était nouvelle et défi pour moi, mais également très amusante !

J’ai l’intention de plonger dans l’authentification prochainement, ce qui offrirait probablement une autre occasion de progresser en utilisant GraphQL et Apollo Server. Restez à l’affaire !

Je vous souhaite une très belle journée !

Source:
https://dzone.com/articles/leveling-up-my-graphql-skills-real-time-subscriptions