Avançando as Minhas Habilidades em GraphQL: Subscrições em Tempo Real

Há alguns anos, tenho tentado identificar frameworks, produtos e serviços que permitem aos tecnólogos manter seu foco na ampliação do valor da sua propriedade intelectual. Essa continua sendo uma viagem maravilhosa para mim, cheia de oportunidades de aprendizagem únicas.

O engenheiro em mim recentemente questionou se existia alguma situação onde eu poderia encontrar um benefício secundário para um conceito existente que tinha falado antes. Noutras palavras, poderia eu identificar outro benefício com o mesmo nível de impacto que a solução mãe original reconhecida anteriormente?

Para esse artigo, eu quis mergulhar mais fundo em GraphQL para ver o que eu poderia encontrar.

No meu artigo “When It’s Time to Give REST a Rest“, eu falei sobre como existem cenários reais do mundo real em que GraphQL é preferível a um serviço RESTful. Passamos por como construir e implantar um API GraphQL usando o Apollo Server.

Nesta postagem subsequente, eu planejo aprimorar meu conhecimento de GraphQL passando por assinaturas para a recuperação de dados em tempo real. Também vamos construir um serviço WebSocket para consumir as assinaturas.

Resumo: Cenário de Uso do Customer 360

Meu artigo anterior se centrou em um cenário de uso do Customer 360, onde os clientes da minha empresa ficcional mantêm as seguintes coleções de dados:

  • Informações do cliente
  • Informações de endereço
  • Métodos de contato
  • Atributos de crédito

Uma grande vantagem em usar GraphQL é que uma única solicitação GraphQL pode recuperar todos os dados necessários para um token do cliente (identidade única).

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
}

Usando uma abordagem RESTful para recuperar a visão única (360) do cliente teria requerido várias solicitações e respostas para serem juntadas. O GraphQL oferece-nos uma solução que funciona muito melhor.

Meta de Aumento de Nível

Para avançar em qualquer aspecto da vida, é preciso alcançar novos objetivos. Para os meus próprios objetivos aqui, isso significa:

  • Entender e implementar a oferta de valor das assinaturas dentro do GraphQL
  • Usando uma implementação de WebSocket para consumir uma assinatura GraphQL

A ideia de usar assinaturas em vez de consultas e mutações dentro do GraphQL é o método preferido quando as seguintes condições forem atendidas:

  • Pequenas mudanças incrementais em objetos grandes
  • Atualizações em tempo real com baixa latência (como um aplicativo de bate-papo)

Isso é importante, já que a implementação de assinaturas dentro do GraphQL não é trivial. Nem só o servidor subjacente precisará ser atualizado, mas o aplicativo de consumo também exigirá algum redesign.

Fortunatamente, o caso de uso que estamos perseguindo com o exemplo do Customer 360 é um ótimo candidato para assinaturas. Também, vamos implementar uma abordagem WebSocket para aproveitar essas assinaturas.

Como antes, eu continuará usando o Apollo.

Credenciais de Aumento de Nível com Assinaturas

Primeiro, precisamos instalar as bibliotecas necessárias para suportar as subscrições com o meu servidor Apollo GraphQL:

Shell

 

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

Com esses itens instalados, eu fiz foco em atualizar o arquivo index.ts do meu repositório original para estender a constante typedefs com o seguinte:

JavaScript

 

type Subscription {
    creditUpdated: Credit
}

Eu também criei uma constante para alojar uma nova instância de PubSub e criei um exemplo de subscrição que usaremos depois:

JavaScript

 

const pubsub = new PubSub();

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

Eu limpei as resolvers existentes e adicionei um novo Subscription para este novo caso de uso:

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']),
        }
    }
};

Depois, refactorei a configuração do servidor e introduzi o design de subscrição:

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`);
});

Para simular atualizações dirigidas pelo cliente, eu criei o seguinte método para aumentar o saldo de crédito em $50 a cada cinco segundos enquanto o serviço está rodando. Assim que o saldo alcança (ou ultrapassa) o limite de crédito de $10,000, eu redefino o saldo de volta a $2,500, simulando que uma pagamento de saldo foi feito.

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();

O arquivo inteiro index.ts pode ser encontrado aqui.

Deploy para Heroku

Com o serviço pronto, é hora de nós deployar o serviço para que possamos interagir com ele. Já que a Heroku funcionou muito bem a última vez (e é fácil de usar para mim), vamos manter essa abordagem.

Para começar, eu precisava executar os seguintes comandos do CLI 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

O comando também adicionou automaticamente o repositório usado por Heroku como um remoto:

Shell

 

$ git remote
heroku
origin

Como notei em meu artigo anterior, o Apollo Server desabilita o Apollo Explorer em ambientes de produção. Para manter o Apollo Explorer disponível para nossas necessidades, eu precisei definir a variável de ambiente NODE_ENV como desenvolvimento. Eu defino isso com o seguinte comando CLI:

Shell

 

$ heroku config:set NODE_ENV=development

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

Eu estava pronto para deployar meu código para o Heroku:

Shell

 

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

Uma rápida visualização do painel do Heroku mostrou minha instância do Apollo Server rodando sem problemas:

Na seção Settings, eu encontrei a URL do aplicativo Heroku para esta instância de serviço:

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

  • Observe: Este link já não estará em serviço à hora de publicar este artigo.

Para o momento, eu podia acrescentar graphql a esta URL para iniciar o Apollo Server Studio. Isto permitiu que eu visse as subscrições funcionando como esperado:

Note as respostas de subscrição no lado direito da tela.

Avançando com as Habilidades de WebSocket

Nós podemos aproveitar a suporte WebSocket e as capacidades do Heroku para criar uma implementação que consome a subscrição que criamos.

No meu caso, eu criei um arquivo index.js com o seguinte conteúdo. Basicamente, isso criou um cliente WebSocket e também estabeleceu um serviço HTTP de exemplo que eu podia usar para validar que o cliente estava rodando:

JavaScript

 

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

// Criar um servidor HTTP de exemplo para se associar a $PORT do Heroku
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);
}

// Subscrever a subscrição creditUpdated
client.subscribe(
  {
    query,
  },
  {
    next: (data) => handleCreditUpdated(data.data.creditUpdated),
    error: (err) => console.error('Subscription error:', err),
    complete: () => console.log('Subscription complete'),
  }
);

O arquivo completo index.js pode ser encontrado aqui.

Podemos implantar esta simples aplicação Node.js no Heroku, também, certificando-nos de definir a variável de ambiente GRAPHQL_SUBSCRIPTION_HOST para a URL da app Heroku usada anteriormente. 

Criei também o seguinte Procfile para informar ao Heroku como iniciar minha app:

Shell

 

web: node src/index.js

Em seguida, criei uma nova app no 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

Então, defini a variável de ambiente GRAPHQL_SUBSCRIPTION_HOST para apontar para meu servidor GraphQL em execução:

Shell

 

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

Neste ponto, estamos prontos para implantar nosso código no Heroku:

Shell

 

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

Assim que o cliente WebSocket iniciar, podemos ver seu status no Painel do Heroku:

Visualizando os logs no Painel do Heroku para a instância jvc-websocket-example, podemos ver as várias atualizações na propriedade balance do serviço jvc-graphql-server-sub. Em minha demonstração, consegui capturar o caso de uso onde o saldo foi reduzido a zero, simulando que um pagamento foi feito:

No terminal, podemos acessar esses mesmos logs com o comando 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]: }

Não só temos um serviço GraphQL com uma implementação de inscrição em execução, mas agora temos um cliente WebSocket consumindo essas atualizações.

Conclusão

Meus leitores podem se lembrar de minha declaração de missão pessoal, que acho que pode se aplicar a qualquer profissional de TI:

“Focus your time on delivering features/functionality that extends the value of your intellectual property. Leverage frameworks, products, and services for everything else.”

— J. Vester

Neste deep-dive no assunto de subscrições GraphQL, nós consumimos atualizações de um servidor Apollo executando no Heroku, usando outro serviço também executando no Heroku — uma aplicação baseada em Node.js que usa WebSockets. Ao aproveitar subscrições leves, nós evitaram enviar consultas para dados que não mudam, mas simplesmente subscrever para receber atualizações de saldo de crédito conforme elas ocorrem.

Na introdução, eu mencionei procurar um princípio de valor adicional dentro de um tópico que eu já escrevi sobre antes. As subscrições GraphQL são um ótimo exemplo do que eu tinha em mente, pois permite que consumidores recebam atualizações imediatamente, sem precisar fazer consultas contra os dados originais. Isto fará os consumidores dos dados do Customer 360 muito animados, sabendo que eles podem receber atualizações em tempo real conforme acontecem.

Heroku é outro exemplo que continua a adherir à minha declaração de missão, oferecendo uma plataforma que permite que eu prototipeie soluções rapidamente usando uma CLI e comandos Git padrão. Isto não só dá-me uma maneira fácil de mostrar o caso de uso de minhas subscrições, mas também implementar um consumidor usando WebSockets.

Se você estiver interessado no código fonte deste artigo, veja meus repositórios no GitLab:

Sinto-me confiante quando digo que dei um upgrade bem-sucedido nas minhas habilidades em GraphQL com esse esforço. Essa jornada foi nova e desafiante para mim – e também muito divertida!

Planejo mergulhar em autenticação a seguir, o que espero oferecer outra oportunidade para upgrade com GraphQL e o Apollo Server. Fique de olho!

Tenha um dia realmente ótimo!

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