node.js facilitó los servidores altamente concurrentes al hacerlo todo mediante E/S no bloqueante, y el costo se manifestó en la estructura: cada consulta a base de datos o llamada a API añadía un nivel de callback, y los propios proyectos de koush, construidos para sostener decenas de miles de conexiones de larga duración, solían llegar a siete u ocho niveles de indentación. Una simple petición más consulta ya necesitaba dos callbacks anidados, el manejo de errores envolvía cada nivel en su propio try/catch, y el trabajo serializado, como las llamadas paginadas a API, se convertía en funciones de continuación recursivas.

Las soluciones disponibles en 2011 trasladaban el problema en lugar de eliminarlo: el operador yield suspendía y reanudaba iteradores pero seguía siendo torpe, y los traductores de fuente a fuente como TameJS compilaban código de aspecto limpio a JavaScript ordinario que podía ejecutarse en cualquier parte, a costa de depurar el código generado en lugar del código que el desarrollador escribió. El punto de referencia de Koush era async/await de C# 5, una característica explícita del lenguaje que permitía que el código asíncrono se leyera de forma síncrona.

Su movimiento fue incorporar esa característica al runtime: un fork de node.js con corrutinas implementadas en V8, un marcador de función async y un await que termina la función y la reanuda cuando el callback se completa. El parser seguía generando aproximadamente el mismo AST que la versión con callbacks, de modo que un solo try/catch abarcaba cada await y un bucle while podía serializar peticiones paginadas a API, mientras que ndb y node-inspector todavía podían depurar el código original. El precio era una compilación personalizada de node, que un equipo que controla sus propios servidores podía pagar de forma realista.

Suspender la función en await mediante corrutinas de V8 para que el código asíncrono se lea como código síncrono

Los callbacks anidados se multiplican con cada paso de E/S y empujan el manejo de errores a ámbitos separados; await mantiene un solo try/catch alrededor de todo un flujo, de modo que las excepciones cruzan los límites de callback como en el código síncrono.

Las corrutinas reales en el runtime, no la traducción de fuente a fuente, mantienen el código que el desarrollador escribe como el código que se ejecuta, de modo que ndb y node-inspector depuran el original en lugar de la salida generada.

El flujo de control de aspecto secuencial permite que los bucles ordinarios expresen trabajo asíncrono serializado: el ejemplo paginado de GDATA reemplaza una función recursiva por fragmentos con un bucle while.

Dado que un equipo de servidores controla su despliegue, bifurcar node.js y V8 era un precio realista, y el diseño se publicó como una rama funcional con pruebas en lugar de una propuesta.

Cuando los rodeos empujan la complejidad al código generado o a un anidamiento más profundo, corrige el nivel del lenguaje: suspende la función entera en la E/S para que el código legible sea lo que se ejecuta y lo que se depura

El proyecto siguió siendo un fork en lugar de una característica fusionada: la wiki, editada por última vez en abril de 2013 tras 25 revisiones, planteaba una compilación personalizada de node como opción de despliegue, y su TODO enumeraba la optimización de ramas que contenían await. Publicado en Hacker News el 29 de septiembre de 2011, atrajo 222 puntos y 101 comentarios; los lectores calificaron el enfoque de muy elegante y coincidieron en que la solución adecuada era introducir corrutinas. Nada en el registro muestra que el parche entrara en node.js; su efecto visible fue demostrar, con código funcional, cómo JavaScript podía obtener async/await a nivel del lenguaje.

CONSULTA LAS PRUEBAS

Las fuentes

  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 ·