Sergio Xalambrí
January 30, 2019

# Usando Generadores Asíncronos en JavaScript

Tutorial sobre como se pueden usar Generadores Asíncronos de ECMAScript 2018 para realizar colas o paginaciones de un API fácilmente en JavaScript

Async/Await es una de las características de ECMAScript 2017 que más he usado junto a
`Object.entries`. Nos permite escribir de forma más simple código asíncrono, se lee como sincrónico pero se ejecuta asíncrono. Veamos un ejemplo rápido

    async function main() {
      setLoading(true);
      try {
        const response = await fetch("/api/users");
        if (!response.ok) throw new Error("Response not OK");
        return await response.json();
      } catch (error) {
        if (error.message !== "Response not OK") throw error;
        return { error: { message: error.message, code: "not_ok" } };
      } finally {
        setLoading(false);
      }
    }

Esta pequeña función usando promesas se podría escribir de esta forma.

    function main() {
      setLoading(true);
      return fetch("/api/users")
        .then(response => {
          if (!response.ok) throw new Error("Response not OK");
          setLoading(false);
          return response.json();
        })
        .catch(error => {
          setLoading(false);
          if (error.message !== "Response not OK") throw error;
          return { error: { message: error.message, code: "not_ok" } };
        });
    }

Aunque casi tan corta como nuestra función asíncrona es un poco más compleja, por ejemplo necesitamos ejecutar `setLoading(false)` en dos lugares para ocultar un posible spinner.

Resulta que Async/Await está construido sobre dos funcionalidades añadidas en ECMAScript 2015, Promesas y Generadores, ya vimos un ejemplo de Promesas, veamos que son Generadores.

## Generadores

> El objecto Generador es retornado por generator function y conforma tanto un protocolo iterable como un protocolo iterador

Esa es la descripción en español según MDN, lo cual no muy fácil de entender la verdad, vamos a ver un ejemplo, usemos un generadores para calcular los números de la secuencia fibonacci.

    function* fibonacci() {
      let [a, b] = [0, 1];
      while (true) {
        yield a;
        [a, b] = [b, a + b];
      }
    }

    const fib = fibonacci();

    Array.from({ length: 10 }).forEach(() => {
      console.log(fib.next().value);
    });

Como se ve arriba un generador es una función que se define como `function*`, el asterisco la convierte en un generador, dentro de esta función tenemos acceso a la palabra clave `yield` que nos permite devolver un valor (lo que coloquemos a la derecha de `yield`) pero sin terminar la ejecución de nuestro generador, en vez de esto el generador se pausa hasta que ejecutemos el método `next` que nos va a permitir seguir con el código hasta el siguiente `yield`.

Si vemos debajo ejecutamos nuestro generador `fibonacci()` y guardamos el resultado, la constante `fib` es un objeto `Generator` que posee el método `next` con el cual podemos pedirle un valor al generador. Algo importante es que hasta que no ejecutemos este método el generador se mantiene suspendido y no hace absolutamente nada, esto nos permite tener un ciclo infinito dentro del generador sin problemas.

Después vamos a crear un array de 10 elementos y vamos a iterar por este array y hacer un `console.log` del valor que devuelve `fib.next()`, si vemos para acceder al valor usamos la propiedad `value`, esto es porque `next` nos devuelve un objeto con la siguiente sintaxis.

    {
      value: 1,
      done: false
    }

La propiedad `value` como dijimos es el valor devuelto por nuestro generador al hacer `yield` mientras que la propiedad `done` nos indica si el generador ya terminó de ejecutarse, en nuestro caso nunca va a pasar que se termine pues usa un cicle infinito, pero podría pasar que solo se ejecuta una cierta cantidad de `yield` dentro del generador y eventualmente se acabe como una función normal.

¿Por qué es útil? En ECMAScript 2018 se incluyó a JS los Async Generators. Estos nos permiten crear generadores que sean asíncrono, combinando así Async/Await con yield.

## Generadores Asíncronos

Como hicimos antes vamos a ver un ejemplo de uso para entender un poco como funciona.

    const createPromise = () => {
      let resolver;
      let rejecter;
      const promise = new Promise((resolve, reject) => {
        resolver = resolve;
        rejecter = reject;
      });
      return { resolver, promise, rejecter };
    };

    async function* createQueue(callback) {
      while (true) {
        const { resolver, promise } = createPromise();
        const data = yield resolver;
        await Promise.all([callback(data), promise]);
      }
    }

La función `createPromise` simplemente nos permite de forma fácil crear una promesa y acceder tanto a esta como a su `resolver` y su `rejecter`. Lo importante acá es nuestro generador asíncrono `createQueue`. Este va a recibir al momento de ejecutarse una función que llamamos `callback` y en cada iteración de nuestro ciclo infinito va a crear una promesa y hacer `yield` del resolver de esta, luego vemos que asigna el resultado de `yield` a una constante llamada `data, esto funciona porque si a la función`next`le pasamos un valor este es recibido por un generador (tanto síncrono como asíncrono) como resultado del`yield`, así podemos pasar valores entre el generador y quién usa el generador.

Los siguiente que hacemos una vez tenemos `data` es hacer `await` de ejecutar `callback` pasándole `data` y de la promesa. ¿Cómo sirve esto? Cada vez que le pidamos un valor a nuestra cola este nos va a devolver un `resolver`, nosotros además podemos pasarle información que el generador va a pasar al `callback`, cuando tanto nuestro `callback` complete su ejecución como nosotros ejecutemos el `resolver` recién ahí nuestro generador asíncrono va a ejecutar la siguiente iteración del `while`.

Veamos como se usa en código.

    const sleep = ms => new Promise(r => setTimeout(r, ms));

    const queue = createQueue(async data => {
      await sleep(1000); // hacemos que nuestro callback tarde 1s en terminar de ejecutarse
      console.log(data); // después hacemos log de data
    });

    (await queue.next()).value();

    const { value: resolver1 } = await queue.next("Hello");
    const { value: resolver2 } = await queue.next("World");

    await sleep(500);
    resolver1();
    await sleep(2000);
    resolver2();

Vayamos línea por línea, al principio creamos una pequeña función que recibe un tiempo en mili segundos (`ms`) y devuelve una promesa que se complete solo después de que este tiempo pase.

Luego vamos a crear nuestra cola, el callback va a ser una función asíncrona que cada vez que se ejecute va a dormir por 1 segundo y después va a hacer log de `data`, esto nos sirve en nuestro ejemplo para simular que estamos haciendo lógica.

La siguiente línea es probablemente la más rara, lo que hace es esperar (`await`) a que `queue.next()` devuelva un valor y acceder a este `value` y ejecutarlo (el valor es `resolver`). Esto es necesario porque la primera vez que ejecutamos `next` prendemos nuestro generador y lo ponemos a funcionar, pero simplemente llega hasta el primer `yield` y no hace nada, necesitamos completar una vuelta para que podamos empezar a pasar valores al generador asíncrono usando `next`.

Eso es exactamente lo que hacemos en las siguientes líneas, ejecutamos dos veces seguidas `next` pasando diferentes valores y esperando a que nos responda con un `value` los cuales re nombramos como `resolver1` y `resolver2`. Luego esperamos 500ms y ejecutamos el primer resolver, dos segundos después ejecutamos el segundo resolver.

Si copias y pegas el código de arriba en la consola del navegador se puede observar como aparecen los mensajes Hello y World en diferentes tiempo.

## ¿Para qué más sirve?

Los generadores asíncronos nos pueden servir para muchas cosas, básicamente son la base para implementar Streams, por ejemplo un generador asíncrono podría en Node.js ir leyendo un archivo desde el sistema de archivos e ir pasando partes de información de a poco y solo leer el siguiente cuando manualmente ejecutemos `next`. Otro caso de uso similar a mantener la paginación de un API que en Frontend puede ser un caso interesante.

Vamos a hacer este generador de paginación, para esto vamos a usar un API de pruebas llamada JSONPlacerholder API, más específicamente vamos a traernos el recurso de comentarios usando la URL `https://jsonplaceholder.typicode.com/comments?_page=1` que nos devuelve la página 1 y así podemos pedir las siguiente páginas incrementando dicho número.

Programemos ahora nuestro generador asíncrono.

    async function* fetchPaginated(url, pageQuery, initialPage = 1) {
      let page = initialPage;
      while (true) {
        const response = await fetch(`${url}?${pageQuery}=${page}`);
        if (!response.ok) return { error: await response.text() };
        const data = await response.json();
        if (data.length === 0) return data;
        else yield data;
        page += 1;
      }
    }

    for await (let data of fetchPaginated("https://jsonplaceholder.typicode.com/comments", "_page")) {
      console.log(data);
    }

Si ejecutamos nuestro código en la consola del navegador vamos a ver como de a poco va haciendo log de los comentarios de cada una de las páginas y termina al llegar a la página 50 donde inmediatamente para.

Lo que acabamos de hacer es que al ejecutar `fetchPaginated` le pasamos la URL del recurso a hacer `fetch` y la variable para la página que debemos agregar al query string de nuestra URL, la página inicial la dejamos usar el valor por defecto que es 1. Esto nos devuelve una instancia de nuestro generador que va a en cada iteración hacer `fetch` de la página, si la respuesta es un error va a hacer `return` de un objeto con el mensaje de error, si no va a obtener la información como JSON y se va a fijar si la `data` (un array de comentarios) está vació para hacer `return` o sino hacer `yield` de `data`, por último suma 1 a la página actual.

En un generador `return` funciona al igual que en una función, en el momento en que se ejecuta el generador se termina inmediatamente y ya no sigue procesando valores. Esto nos permite matar el generador cuando hay un error o ya no hay más páginas a las cuales hacerle fetch.

Fuera de nuestro generador hacemos un `for..of` asíncrono, agregando la palabra clave `await`. Esto nos permite iterar sobre un generador asíncrono y guardamos `value` como la variable `data` la cual luego mostramos en consola.

Podríamos entonces usar nuestro nuevo `fetchPaginated` para traerse la primer página de comentarios y que cuando el usuario llegue al final del scroll o haga click en un botón se le pida la siguiente página usando `next` y así hasta terminar.

## Palabras finales

Aunque raros de usar, los generadores y más aún los generadores asíncronos pueden ser muy útiles para ejecutar lógica asíncrona repetitiva de forma más simple.