# Usando Generadores Asíncronos 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

```js
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.

```js
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](https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Objetos_globales/Generador),
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.

```js
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.

```js
{
  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.

```js
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.

```js
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](https://jsonplaceholder.typicode.com/),
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.

```js
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.