Programação assíncrona com Node 7 e async e await é muito

dezembro 7, 2016 4:00 pm Publicado por Deixe um comentário

nodeasyncawaitlove

Resolvemos sortear os ingressos do AgileTrends de Campo Grande que recebemos pelo patrocínio, mas nenhuma ferramenta pra sorteio pra Twitter atendia o que eu queria fazer, que era buscar entre meus seguidores quem tivesse tuitado um texto específico. Resolvi escrever uma ferramenta de linha de comando pra resolver isso – a API do Twitter é bem completa, não deveria ser difícil. Escolhi Node.js, porque, já que que eu estava no mundo JavaScript, por que não ficar nele, certo? Além disso, a infra do NPM pra criar ferramentas é muito legal.

O algoritmo é simples, eu basicamente uso a API de search do Twitter pra buscar tweets com o texto que eu quero (por exemplo: uma hashtag específica), agrego os usuários que fizeram os tweets, escolho de forma aleatória um dos usuários, e verifico se ele me segue – se não seguir, escolho outro usuário.

Primeiro desafio: callbacks

Tudo estaria ótimo, se o JavaScript não exigisse APIs assíncronas. Pior, com callbacks. Mas os callbacks ficaram no passado. Usei o BlueBird e com uma linha de código eu matei o problema:

const Twitter = require('twitter');
const Promise = require("bluebird");
Promise.promisifyAll(Twitter.prototype);

O promisifyAll cria métodos que terminam com “Async” em todos os métodos que encontra, e esses métodos passam a retornar promises. Lindo! RIP callbacks daqui pra frente.

Segundo desafio: laços com promises

Promises são lindas, mas não quando você precisa fazer qualquer laço, como um while ou um for. E eu preciso baixar todos os tweets de acordo com uma busca, e o Twitter só entrega isso de forma paginada, ou seja, preciso chamar a mesma API várias vezes. O outro laço é onde preciso iterar entre os usuários, até achar um que me siga. Se fazer isso com callbacks era quase impossível, com promises já dá pra fazer, mas não fica exatamente o código mais limpo que poderia ficar. Usei recursão pra resolver, o que dá um belo toque, mas não considerei uma solução ótima. Veja a solução adotada para a função getTweets no caso da obtenção paginada dos tweets (simplificada):

const originalOptions = { q: query, count: 100, result_type: 'recent', include_entities: false };
var tweetPage = 0;
function getTweets(statuses, previousPromise) {
    tweetPage++;
    if (!statuses) {
        return getTweets([], client.getAsync('search/tweets', originalOptions));
    }
    return previousPromise.then(tweets => {
        const newStatuses = statuses.concat(tweets.statuses);
        if (!tweets.search_metadata.next_results || tweetPage > (maxNumberOfTwittsSearched / 100))
            return newStatuses;
        const optionsForNextRequest = clone(originalOptions, { max_id: querystring.parse(tweets.search_metadata.next_results).max_id });
        return getTweets(newStatuses, client.getAsync('search/tweets', optionsForNextRequest));
    });
}

Poderia ficar ainda um pouco melhor, mas não tive motivação pra deixar esse código mais bonito, o padrão é um pouco frustrante. Note alguns problemas desse código, motivados pela promise conjugada a um laço:

  • A função getAsync para obter os tweets aparece sendo chamada de forma praticamente duplicada em dois lugares diferentes; a primeira vez ela aparece para iniciar o processo, quando é repassada recursivamente para a função getTweets, e a segunda é para as chamadas seguintes de paginação.
  • Necessidade de uma função aninhada para a promise, o que torna o fluxo de debug descontínuo. Esse problema é um dos mais comuns quando trabalhamos com assincronia e JavaScript, e também acontece com callbacks.
  • As variáveis tweetPage e originalOptions, que deveriam ser de escopo da função, estão com escopo global. Poderiam estar dentro da função desde que fossem passadas como parâmetro, o que deixaria a função ainda mais complexa.
  • Necessidade de propagar o estado como argumento da função. Isso se deve à recursividade que acontece dentro do callback do método then.

Você pode ver o código todo dessa versão no Github. A outra função, de busca dos seguidores, segue padrão semelhante. A chamada que orquestra tudo fica bem melhor que a versão com callback, num padrão conhecido no uso de promises. Veja o resultado:

getTweets()
    .then(tweets => getUsers(tweets))
    .then(userIds => getFollower(userIds))
    .then(userId => getUser(userId))
    .then(user => {
        if (!user) return;
        const url = `https://twitter.com/${user.screen_name}`;
        console.log(`User is: ${user.screen_name}nSee in ${url}`);
        open(url);
    })
    .catch(err => {
        console.log(`Got error: ${err}`);
    });

Comparando com callbacks, muito melhor. Mas ainda poderia melhorar muito. E é ruim depurar.

Deixando tudo mais bonito e organizado: async e await

Foi quando me lembrei que o Node 7 suporta, ainda que de forma “escondida” async e await. Evoluí o projeto nesse commit para fazer uso dele, em seguida refatorei. O resultado ficou infinitamente melhor: mais fácil de depurar e mais limpo e organizado.

Um fato torna o uso de async/await muito fácil de adotar com JavaScript: em vez de criar um novo padrão, ele funciona perfeitamente com promises já existentes. Qualquer método que retorne uma promise pode ser “awaited”. Ou seja, meu código estava com meio caminho andado quando resolvi usar o Bluebird para adotar promises. Eu passei a ter oportunidades interessantes: não precisaria mais de recursividade, e poderia deixar o código mais objetivo. A mesma função getTweets, após atualizada, ficou assim (simplificada):

async function getTweetsAsync(query) {
    var tweetPage = 0;
    var newStatuses = [];
    const originalOptions = { q: query, count: 100, result_type: 'recent', include_entities: false };
    var optionsForNextRequest = originalOptions;
    while (tweetPage < (maxNumberOfTwittsSearched / 100)) {
        tweetPage++;
        const tweets = await client.getAsync('search/tweets', optionsForNextRequest);
        newStatuses = newStatuses.concat(tweets.statuses);
        if (!tweets.search_metadata.next_results) 
            break;
        optionsForNextRequest = clone(originalOptions, { max_id: querystring.parse(tweets.search_metadata.next_results).max_id });
    }
    return newStatuses;
}

Apontando onde evoluiu:

  • Fluxo limpo e fácil de entender, um simples while sem recursão.
  • As condições de saída do laço estão objetivas e fáceis de encontrar.
  • O retorno da função é óbvio.
  • As variáveis que deveriam ser de escopo da função agora estão onde deveriam estar, não são mais globais.
  • Os argumentos de continuidade para recursão foram removidos, já que ela não era mais necessária.
  • Descobri que o principal argumento da função estava faltando, que era o query, vinha de uma variável global. Isso só foi percebido por causa da expressividade extra que a função ganhou.
  • Sem função aninhada, ou seja, o debug fica fácil e contínuo.

A chamada final também evoluiu muito. Veja como ficou:

const query = options[''];
const userName = options[''];
try {
    const tweets = await getTweetsAsync(query);
    const userIds = getUsers(tweets);
    const userId = await getFollowerAsync(userName, userIds);
    const user = await getUserAsync(userId);
    if (!user) process.exit(1);
    const url = `https://twitter.com/${user.screen_name}`;
    console.log(`User is: ${user.screen_name}nSee in ${url}`);
    open(url);
} catch (err) {
    console.log(`Got error: ${err}`);
}

O interessante é que, no processo, notei que algumas funções eram assíncronas e outras não, e coloquei o sufixo “Async” nas que eram de fato assíncronas. Isso não aparecia com as promises, porque o fluxo de “then” esconde isso. Uma das funcionalidades mais interessantes das promises, a capacidade de desenrolar promises e inclusive aceitar valores que não são promises para seguir no fluxo, nesse caso, deixava o código menos explícito.

Não rodei grandes testes de desempenho, mas, olhando por cima, o desempenho parece ter ficado o mesmo.

Rodando na linha de comando

Para rodar, é muito simples. A funcionalidade ainda não está habilitada por padrão no Node.js 7 (nem no último, 7.2.0), mas você consegue habilitar passando a flag “–harmony-async-await”. Então, bastou executar, no powershell:

node --harmony-async-await index.js '#nemchama' giovannibassi

E rodou perfeitamente:

image_1

No bash, seria praticamente o mesmo comando.

Atente para as aspas quando usar uma hashtag no PowerShell no windows, já que a cerquilha “#” é considerada um comentário. Se usar arroba “@” é importante, também é usar aspas.

Para habilitar o log, configure a variável de ambiente “DEBUG” para “twitter”. Isso vem de graça com o uso do pacote debug do NPM, que estou usando.

Debugando com VS Code

Uma das coisas que eu queria verificar é se o debug no VS Code funcionaria. Eu estava duvidando que eu conseguiria fazer funcionar, mas foi bastante simples habilitar. Bastou colocar a flag nos “runtimeArgs” do arquivo launch.json, assim (ocultando partes desnecessárias):

{
    "configurations": [
        {
            //outras opções
            "args": [
                "#nemchama",
                "giovannibassi"
            ],
            "runtimeArgs": [
                "--harmony-async-await"
            ],
            "env": {
                "DEBUG": "twitter"
            }
        }
    ]
}

Note ainda o uso da variável de ambiente configurada como “twitter” para ver os logs e os argumentos da função (a hashtag #nemchama e o meu user do Twitter).

Com isso, já foi possível debugar (clique para ampliar):

nodeasyncawait_thumb

Habilitando como executável no Node.js

Outro ponto importante é que como o Node não roda ainda com async sem a flag mencionada, o executável gerado precisa conter a informação da flag. O NPM observa o arquivo que será executado, ele lê a linha do hashbang para entender como montar o executável. Assim, precisamos informar a flag por lá. Normalmente, basta fazer assim para habilitar um arquivo a rodar como executável:

#!/usr/bin/env node

Mas para rodar com a flag, precisei acrescentá-la no hashbang:

#!/usr/bin/env node --harmony-async-await

Habilitando o ESLint com o VSCode

O VS Code já possui um plugin bem legal para ESLint, e é possível usá-lo com async e await. O plugin vai te sugerir a criação de um arquivo .eslintrc.json, aceite. Algumas alterações interessantes nesse arquivo (mostrando apenas o que é de interesse, ocultando o resto):

{
    "env": {
        "es6": true,
        "node": true
    },
    "parser": "babel-eslint",
    "parserOptions": {
        "ecmaVersion": 7
    },
    "rules": {
        "no-return-await": "error"
    }
}

Note o uso do babel-eslint. Esse pacote deve ser instalado como uma dependência de desenvolvimento do seu projeto. Sem ele, o parser padrão não vai reconhecer as funções com async. A regra no-return-await também é interessante, ela pega um await desnecessário em um método assíncrono.

Veja o VS Code detectando um problema de await em um método sem async:

image_2

Testando a ferramenta

Se você tem o Node 7 instalado na sua máquina, basta instalar com:

npm install –g twitterraffle

E pra rodar, por exemplo:

twitterraffle '#nemchama' giovannibassi

Roadmap do async/await para JavaScript

Não estressei o uso de async/await para garantir sua resiliência em código de produção ainda. A proposta do async e wait já está aceita e vai entrar no EcmaScript 2017 (aka ES7). No entanto, ainda é uma funcionalidade que está em preview no V8 que está rodando com o Node.js, usando a flag que demonstrei acima. Ou seja: não coloque em produção! Mas para pequenas ferramentas com Node.js, como essa, eu diria que o uso já está liberado. Pra colocar em produção, o ideal ainda é usar Babel. Eu imagino que em breve teremos o ES7 funcionando sem necessitar de flags extras, já no Node 7, talvez esse ano ainda, e ela chegará à versão LTS no Node 8 (veja a timeline do Node.js no Github). Depende da agilidade do time do Node.js para integrar as mudanças liberadas no V8 e mais ainda do time do V8 estabilizar a funcionalidade. Segundo aparece na página da funcionalidade, ela já está estável e deve sair no Chrome 55, que tem tido versões beta liberadas com frequência (a última na semana passada). A versão final do Chrome 55 está prevista para 6 de dezembro (semana que vem).

O uso nos navegadores vai demorar mais, até termos uma adoção massiva do Chrome 55 e os outros navegadores implementarem versões estáveis – estimo pelo menos três anos. Veja no Can I Use o suporte para mais detalhes. TL/DR: nenhuma versão estável suporta ainda, mas já dá pra usar em versões de preview no Chrome, Edge, Firefox e Opera, e sem suporte previsto para IE e Safari. Para o Browser, fique com Babel.

Conclusão

É muito interessante ver algo que nasceu no C# ir parar no JavaScript, duas linguagens que eu gosto muito, conversando. O mundo vai ficar muito mais interessante com essa evolução. As possibilidades são gigantescas. O JavaScript está se tornando uma linguagem cada vez mais poderosa.

Você pode ver todo o projeto no GitHub da Lambda3 – o projeto se está em Lambda3/TwitterRaffle.

Dica: experimente também async/await no TypeScript, que, desde a versão 1.7, já funciona com ES6 e, em breve, com o lançamento do 2.1, também vai funcionar com ES5, e até ES3!

Source: IMasters

Categorizados em:

Este artigo foi escrito pormajor

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *