Node.js - Funções Generators Assíncronas e Websockets

oieduardorabelo

Eduardo Rabelo

Posted on October 22, 2020

Node.js - Funções Generators Assíncronas e Websockets

As funções generator assíncronas são um novo recurso no ES2018. Node.js adicionou suporte para funções generator assíncrona no Node.js 10. As funções generator assíncrona podem parecer um recurso de nicho bonito, mas apresentam uma ótima oportunidade para estruturar websockets em Node.js. Neste artigo, explicarei como uma estrutura de websocket Node.js pode usar funções generator assíncronas.

Classificando frameworks HTTP

Primeiro, pense em frameworks de servidor HTTP, como Express ou Hapi . Em geral, a maioria das estruturas de servidor HTTP se enquadra em uma das 3 classes:

  1. Resposta explícita - Para enviar uma resposta HTTP no Express, você deve chamar res.end(), res.json() ou alguma outra função no objeto res. Em outras palavras, você precisa chamar explicitamente um método para enviar uma resposta.
  2. Resposta implícita usando return - Por outro lado, o Hapi v17 removeu explicitamente a função reply(). Portanto, Hapi não tem um equivalente a res: para enviar uma resposta, você return um valor de seu manipulador de solicitação. Hapi então converte o valor de return em uma resposta HTTP.
  3. Modifique a resposta no local - Koa usa uma abordagem distinta que é uma mistura das duas anteriores. Em vez de chamar funções res, você modifica um objeto ctx para estruturar sua resposta.

Em outras palavras, alguns frameworks HTTP fazem com que você chame explicitamente uma função para enviar a resposta HTTP, alguns fornecem um objeto de resposta HTTP para modificar e alguns apenas pegam o valor de return de uma função manipuladora de requisição.

A diferença entre websockets e HTTP é que o servidor pode enviar mensagens para o socket sempre que quiser, seja em resposta a uma mensagem ou não. Isso significa que estruturas de websocket de baixo nível como ws se parecem muito com o padrão de "resposta explícita": você precisa chamar explicitamente uma função para enviar uma mensagem.

Mas você poderia fazer algo como resposta implícita com websockets, mantendo o benefício de poder enviar várias mensagens? É aí que entram os geradores assíncronos.

Lendo Pedaços de Informação no Servidor

Suponha que você tenha um cursor Mongoose que lê um monte de documentos, um de cada vez, e deseja enviar cada documento por um websocket assim que o cursor o ler. Isso pode ser útil se você quiser minimizar a quantidade de memória que seu servidor usa a qualquer momento: o cliente obtém todos os dados, mas o servidor nunca precisa manter todos os dados em memória de uma só vez. Por exemplo, veja como você pode ler um cursor usando async/await:

const User = mongoose.model('User', mongoose.Schema({ name: String }));

const cursor = Model.find().cursor();
for await (const doc of cursor) {
  console.log(doc.name); // Imprime os nomes 1 a 1.
}
Enter fullscreen mode Exit fullscreen mode

O que torna os generators tão interessantes é que yield são como um return, exceto que uma função pode fazer yield várias vezes e continuar de onde parou. Portanto, uma função de gerador assíncrono pode fazer várias respostas implícitas.

const User = mongoose.model('User', mongoose.Schema({ name: String }));

async function* streamUsers() {
  const cursor = Model.find().cursor();
  for await (const doc of cursor) {
    // Usando `yield` em cada documento é como usar resposta implícita, caso o
    // framework que você estiver usando suportar essa sintaxe
    yield doc;
  }
}
Enter fullscreen mode Exit fullscreen mode

Veja como você pode construir um servidor websocket com Node.js :

const WebSocket = require('ws');
const server = new WebSocket.Server({
  port: 8080
});

server.on('connection', function(socket) {
  socket.on('message', function(msg) {
    // Trata a mensagem
  });
});
Enter fullscreen mode Exit fullscreen mode

Portanto, agora, o truque é colar o servidor websocket a função streamUsers(). Suponha que cada mensagem recebida seja um JSON válido e tenha as propriedades actione id. Quando action === 'streamUsers', você pode chamar streamUsers() e enviar todos os usuários para o socket conforme eles saem do cursor Mongoose.

const WebSocket = require('ws');
const server = new WebSocket.Server({
  port: 8080
});

server.on('connection', function(socket) {
  socket.on('message', function(msg) {
    msg = JSON.parse(msg);

    if (msg.action === 'streamUsers') {
      void async function() {
        // Envia 1 mensagem por usuário, ao invés de carregar todos os
        // usuários e enviar todos os usuários em 1 mensagem.
        for await (const doc of streamUsers()) {
          socket.send(JSON.stringify({ id: msg.id, doc }));
        }
      }().catch(err => socket.send(JSON.stringify({ id: msg.id, error: err.message })));
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

Veja como você chamaria streamUsers() por meio do cliente websocket:

const client = new WebSocket('ws://localhost:8080');

// Irá imprimir cada usuário, 1 por vez
client.on('message', msg => console.log(msg));

await new Promise(resolve => client.once('open', resolve));

client.send(JSON.stringify({ action: 'streamUsers', id: 1 }));
Enter fullscreen mode Exit fullscreen mode

Finalizando

As funções generators assíncronas fornecem uma oportunidade para criar uma estrutura de websocket de nível superior com base no padrão de resposta implícito que as estruturas HTTP como Hapi e Fastify usam. O principal benefício do padrão de resposta implícito é que sua lógica de negócios não precisa estar ciente se o framework está enviando o resultado via websocket, pesquisa de HTTP ou outra coisa. JavaScript sem framework é mais portátil e fácil de testar.


Créditos

💖 💪 🙅 🚩
oieduardorabelo
Eduardo Rabelo

Posted on October 22, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related