O node.js tornou fáceis os servidores altamente concorrentes ao fazer tudo por meio de I/O não bloqueante, e o custo apareceu na estrutura: cada consulta ao banco de dados ou chamada de API acrescentava um nível de callback, e os próprios projetos de koush, feitos para manter dezenas de milhares de conexões de longa duração, rotineiramente chegavam a sete ou oito níveis de indentação. Uma simples requisição mais consulta já exigia dois callbacks aninhados, o tratamento de erros envolvia cada nível em seu próprio try/catch, e trabalhos serializados, como chamadas paginadas de API, viravam funções de continuação recursivas.

As correções disponíveis em 2011 deslocavam o problema em vez de removê-lo: o operador yield suspendia e retomava iteradores, mas continuava desajeitado, e tradutores de código-fonte para código-fonte, como o TameJS, compilavam código de aparência limpa em JavaScript comum que podia rodar em qualquer lugar, ao preço de depurar o código gerado em vez do código que o desenvolvedor escreveu. A referência de koush era o async/await do C# 5, um recurso explícito da linguagem que permitia que código assíncrono fosse lido de forma síncrona.

Sua iniciativa foi incorporar esse recurso ao runtime: um fork do node.js com corrotinas implementadas no V8, um marcador de função async e um await que encerra a função e a retoma quando o callback é concluído. O parser ainda gerava aproximadamente a mesma AST que a versão com callbacks, de modo que um único try/catch abrangia cada await e um laço while podia serializar requisições paginadas de API, enquanto o ndb e o node-inspector ainda podiam depurar o código original. O preço era uma compilação personalizada do node, que uma equipe que controla seus próprios servidores poderia realisticamente pagar.

Suspender a função no await por meio de corrotinas do V8 para que o código assíncrono seja lido como código síncrono

Callbacks aninhados se multiplicam a cada etapa de I/O e empurram o tratamento de erros para escopos separados; o await mantém um único try/catch em torno de todo um fluxo, de modo que as exceções atravessam os limites de callback como no código síncrono.

Corrotinas reais no runtime, e não tradução de código-fonte para código-fonte, mantêm o código que o desenvolvedor escreve como o código que roda, de modo que o ndb e o node-inspector depuram o original em vez da saída gerada.

Um fluxo de controle de aparência sequencial permite que laços comuns expressem trabalho assíncrono serializado: o exemplo paginado do GDATA substitui uma função recursiva de blocos por um laço while.

Como uma equipe de servidores controla sua implantação, fazer fork do node.js e do V8 era um preço realista, e o design foi entregue como um branch funcional com testes, e não como uma proposta.

Quando as soluções de contorno empurram a complexidade para o código gerado ou para um aninhamento mais profundo, corrija o nível da linguagem: suspenda a função inteira no I/O para que o código legível seja o que roda e o que você depura

O projeto permaneceu um fork, e não um recurso integrado: a wiki, editada pela última vez em abril de 2013 após 25 revisões, apresentava uma compilação personalizada do node como opção de implantação, e seu TODO listava a otimização de branches que continham await. Publicado no Hacker News em 29 de setembro de 2011, recebeu 222 pontos e 101 comentários; os leitores chamaram a abordagem de muito elegante e concordaram que a correção adequada era introduzir corrotinas. Nada no registro mostra o patch entrando no node.js; seu efeito visível foi demonstrar, com código funcional, como o JavaScript poderia obter async/await no nível da linguagem.

CONFIRA AS EVIDÊNCIAS

As fontes

  1. "async" support in node.js (koush/node wiki) GitHub — koush/node wiki · github.com
  2. Fixing the callback spaghetti in node.js | Hacker News Hacker News · news.ycombinator.com ·