Javascript: Entendendo Async Iterators
Tags: javascript, development, iterators, ecmascript
Published: 2019-07-28 16:49:22

---
title: Javascript: Entendendo Async Iterators
tags: ['javascript', 'development', 'iterators', 'ecmascript']
canonical_url: https://medium.com/@khaosdoctor/javascript-entendendo-async-iterators-8322fc7106db
cover_image: https://i.imgur.com/nNzEQds.jpg
published: true
---

Há um tempo atrás, fiz [um post](https://medium.com/trainingcenter/iterators-em-javascript-880adef14495) em meu Medium onde falo tudo sobre o protocolo Iterator e sua interface de uso. Porém, além de APIs como `Promise.finally`, o ECMAScript 2018 trouxe para a gente uma outra forma de tratarmos os nossos iterators. Os *[async iterators](https://github.com/tc39/proposal-async-iteration)*.

## O problema

Vamos nos colocar em uma situação bastante comum. Estamos trabalhando com Node.js e temos que ler um arquivo, linha a linha. O Node possui uma API para este tipo de função chamada `readLine` (veja a documentação completa [aqui](https://nodejs.org/api/readline.html)), esta API é um wrapper para que você possa ler dados de uma stream de entrada linha a linha ao invés de ter que fazer o *parsing* do buffer de entrada e quebrar o texto em pequenas partes.

Ela expõe uma API de eventos, que você pode escutar desta forma:

```js
const fs = require('fs')
const readline = require('readline')
const reader = readline.createInterface({
  input: fs.createReadStream('./arquivo.txt'),
  crlfDelay: Infinity
})

reader.on('line', (line) => console.log(line))
```

Imagine que tenhamos um arquivo simples:

```txt
linha 1
linha 2
linha 3
```

Se rodarmos este código no arquivo que criamos, teremos um output linha a linha no nosso console. Porém, trabalhar com eventos não é uma das melhores formas de fazer código manutenível, pois eventos são completamente assíncronos e eles podem quebrar o fluxo do código, uma vez que eles são disparados fora de ordem e você só consegue atribuir uma ação através de um listener.

## A solução

Além da API de eventos, o `readline` também expõe um `async iterator`. Isso significa que, ao invés de fazermos a leitura da linha através de listeners no evento `line`, vamos fazer a leitura da linha através de uma nova forma de utilização da keyword `for`.

Hoje temos algumas opções de uso para um laço de repetição `for`, a primeira delas é o modelo mais comum, utilizando um contador e uma condição:

```js
for (let x = 0; x < array.length; x++) {
  // Código aqui
}
```

Também podemos utilizar a notação `for … in` para leitura de índices de arrays:

```js
const a = [1,2,3,4,5,6]

for (let index in a) {
  console.log(a[index])
}
```

No caso anterior, vamos ter como saída no `console.log`, os números de 1 a 6, porém se utilizarmos `console.log(index)` vamos logar o índice do array, ou seja, os números de 0 a 5.

Para o próximo caso, podemos usar a notação `for … of` para pegar diretamente as propriedades enumeráveis do array, ou seja, seus valores diretos:

```js
const a = [1,2,3,4,5,6]

for (let item of a) {
  console.log(item)
}
```

Perceba que todas as formas de utilização que descrevi são síncronas, ou seja, como fazemos para que possamos ler uma sequencia de promises em ordem?, imagine que tenhamos uma outra interface que nos retorne sempre uma Promise, que se resolve para a nossa linha do arquivo em questão. Para resolvermos essas promises em ordem, temos de fazer algo assim:

```js
async function readLine (files) {
  for (const file of files) {
    const line = await readFile(file) // Imagine que readFile é o nosso cursor
    console.log(line)
  }
}
```

Porém, graças a magia dos async iterables (como o `readline`) nós podemos fazer o seguinte:

```js
const fs = require('fs')
const readline = require('readline')
const reader = readline.createInterface({
  input: fs.createReadStream('./xpto.txt'),
  crlfDelay: Infinity
})

async function read () {
  for await (const line of reader) {
    console.log(line)
  }
}

read()
```

Perceba que agora estamos usando uma nova definição de `for`, o `for await (const x of y)`.

### For Await e Node.js

A notação `for await` é nativamente suportada no runtime do Node.js da versão 10.x. Se você está usando as versões 8.x ou 9.x então você precisa iniciar o seu arquivo Javascript com a flag `--harmony_async_iteration`. Infelizmente os async iterators não são suportados nas versões 6 ou 7 do Node.js.

## Iterators

Para podermos entender o conceito de async iterators, precisamos dar uma recapitulada sobre o que são iterators em si. O meu artigo anterior é uma fonte maior de informações, mas, em suma, um Iterator é um objeto que expõe uma função `next()` que retorna um outro objeto com a notação `{value: any, done: boolean}` onde `value` é o valor da iteração atual e `done` identifica se há ou não há mais valores na sequencia. Um exemplo simples, é um iterador que passa por todos os itens de um array:

```js
const array = [1,2,3]
let index = 0

const iterator = {
  next: () => {
    if (index >= array.length) return { done: true }
    return {
      value: array[index++],
      done: false
    }
  }
}
```

Sozinho, um iterator não tem nenhuma utilidade prática, para que possamos tirar algum proveito dele, precisamos de um `iterable`. Um `iterable` é um objeto que possui uma chave `Symbol.iterator` que retorna uma função, a qual retorna nosso iterador:

```js
// ... Código do iterador aqui ...

const iterable = {
	[Symbol.iterator]: () => iterator
}
```

Agora podemos utilizar ele normalmente, com `for (const x of iterable)` e teremos todos os valores do `array` sendo iterador um a um.

> Se você quiser saber um pouco mais sobre **Symbols**, dê uma olhada [neste outro artigo que escrevi só sobre o tema](https://medium.com/trainingcenter/javascript-symbols-decifrando-o-mistério-383e359e64e3)

Por baixo dos panos, todos os arrays e objetor possuem um `Symbol.iterator` para que possamos fazer `for (let x of [1,2,3])` e retornar os valores que queremos.

## Async Iterators

Como é de se esperar, um async iterator é exatamente igual a um iterator, exceto que, ao invés de um `Symbol.iterator`, temos um `Symbol.asyncIterator` em nosso iterable e, ao invés de um objeto que retorna `{value, done}` teremos uma Promise que resolve para um objeto com a mesma assinatura.

Vamos transformar nosso iterator acima em um async iterator:

```js
const array = [1,2,3]
let index = 0

const asyncIterator = {
  next: () => {
    if (index >= array.length) return Promise.resolve({done: true})
    return Promise.resolve({value: array[index++], done: false})
  }
}

const asyncIterable = {
  [Symbol.asyncIterator]: () => asyncIterator
}
```

### Iterando assíncronamente

Podemos fazer a iteração de qualquer iterator de forma manual, chamando a função `next()`:

```js
// ... Código do async iterator aqui ...

async function manual () {
	const promise = asyncIterator.next() // Promise
  await p // Object { value: 1, done: false }
  await asyncIterator.next() // Object { value: 2, done: false }
  await asyncIterator.next() // Object { value: 3, done: false }
  await asyncIterator.next() // Object { done: true }
}
```

Para que possamos iterar através do nosso async iterator, temos que utilizar `for await`, porém, lembre-se que a keyword `await` só pode ser usada dentro de uma `async function`, ou seja, temos que ter algo deste tipo:

```js
// ... Código acima omitido ...

async function iterate () {
  for await (const num of asyncIterable) console.log(num) 
}

iterate() // 1, 2, 3
```

Mas, assim como os iteradores assíncronos, não são suportadas no Node 8.x ou 9.x, para podermos utilizar um async iterator nessas versões, nós podemos simplesmente extrair o `next` dos seus objetos e iterar por eles manualmente:

```js
// ... Código do async iterator aqui ...

async function iterate () {
  const {next} = asyncIterable[Symbol.asyncIterator]() // pegamos a função next do iterator
  
  for (let {value, done} = await next(); !done; {value, done} = await next()) {
    console.log(value)
  }
}
```

Perceba que `for await` é muito mais clean e muito mais conciso porque se comporta como um loop comum, mas também, além de ser muito mais simples de entender, checa pelo final do iterador sozinho, através da chave `done`.

#### Tratando erros

O que acontece se nossa promise for rejeitada dentro do nosso iterador? Bem, como qualquer promise rejeitada, podemos pegar seu erro através de um simples `try/catch` (já que estamos usando `await `):

```js
const asyncIterator = { next: () => Promise.reject('Error') }
const asyncIterable = { [Symbol.asyncIterator]: () => asyncIterator }

async function iterate () {
  try {
	  for await (const num of asyncIterable) {}
  } catch (e) {
    console.log(e.message)
  }
}

iterate()
```

## Fallbacks

Algo bastante interessante dos async iterators é que eles possuem um fallback para `Symbol.iterator`, isso significa que você pode utilizar ele também com seus iteradores comuns, por exemplo, um array de promises:

```js
const fetch = require('node-fetch')
const promiseArray = [
  fetch('https://lsantos.dev'),
  fetch('https://lsantos.me')
]

async function iterate () {
  for await (const response of promiseArray) console.log(response.status)
}

iterate() // 200, 200
```

## Async Generators

Em grande parte, iterators e async iterators podem ser criados a partir de [generators](https://medium.com/trainingcenter/javascript-entendendo-generators-408cbce9aee). Generators são funções que permitem que suas execuções sejam pausadas e retomadas, de forma que é possível realizar uma execução e depois buscar um próximo valor através de uma função `next()`.

> Esta é uma descrição muito simplificada de generators, a leitura do artigo que fala somente sobre eles é imprescindível para que você possa entender generators de forma rápida e mais profunda.

Async generators se comportam como um async iterator, porém, você deve implementar o mecanismo de parada manualmente, por exemplo, vamos construir um gerador de mensagens aleatórias para commits do git para deixar seus colegas super felizes com suas contribuições:

```js
const fetch = require('node-fetch')
async function* gitCommitMessageGenerator () {
  const url = 'https://whatthecommit.com/index.txt'

  while (true) {
    const response = await fetch(url)
    yield await response.text() // Retornamos o valor
  }
}
```

Veja que não estamos em nenhum momento retornando um objeto `{value, done}`, então o loop não tem como saber quando a execução terminou. Podemos implementar uma função desta forma:

```js
// Código anterior omitido
async function getCommitMessages (times) {
  let execution = 1
  for await (const message of gitCommitMessageGenerator()) {
    console.log(message)
    if (execution++ >= times) break
  }
}

getCommitMessages(5)
// I'll explain this when I'm sober .. or revert it
// Never before had a small typo like this one caused so much damage.
// For real, this time.
// Too lazy to write descriptive message
// Ugh. Bad rebase.
```

## Caso de uso

Para exemplificar de forma mais interessante, vamos construir um async iterator para um caso de uso real. Atualmente, o driver do Oracle Database para Node.js suporta uma API de `resultSet`, que executa uma query no banco de dados e retorna uma stream de registros que podem ser lidos um a um através do método `getRow()`.

Para criarmos esse `resultSet` precisamos executar uma query no banco, desta forma:

```js
const oracle = require('oracledb')
const options = {
  user: 'usuario',
  password: 'senha',
  connectString: 'string'
}

async function start () {
  const connection = await oracle.getConnection(options)
  const { resultSet } = await connection.execute('query', [], { outFormat: oracle.OBJECT, resultSet: true })
  return resultSet
}

start().then(console.log)
```

Nosso `resultSet` possui um método chamado `getRow()` que nos retorna uma Promise da próxima linha do banco que deve ser trazida, isso é um belo caso de uso para um async iterator não é? Podemos criar um cursor que nos retorna este `resultSet` linha por linha. Vamos deixar um pouco mais complexo através da criação de uma classe `Cursor`:

```js
class Cursor {
  constructor (resultSet) {
    this.resultSet = resultSet
  }

  getIterable () {
    return {
      [Symbol.asyncIterator]: () => this._buildIterator()
    }
  }

  _buildIterator () {
    return {
      next: () => this.resultSet.getRow().then((row) => ({ value: row, done: row === undefined }))
    }
  }
}

module.exports = Cursor
```

Veja que o cursor recebe o `resultSet` que ele deve trabalhar e o armazena em seu estado atual. Então vamos alterar nosso método anterior para que retornemos o cursor ao invés do `resultSet` de uma única vez:

```js
const oracle = require('oracledb')
const options = {
  user: 'usuario',
  password: 'senha',
  connectString: 'string'
}

async function getResultSet () {
  const connection = await oracle.getConnection(options)
  const { resultSet } = await connection.execute('query', [], { outFormat: oracle.OBJECT, resultSet: true })
  return resultSet
}

async function start() {
  const resultSet = await getResultSet()
  const cursor = new Cursor(resultSet)
  
  for await (const row of cursor.getIterable()) {
    console.log(row)
  }
}

start()
```

Desta forma podemos fazer um loop por todas as nossas linhas retornadas sem precisar de uma resolução de Promises individual.

## Conclusão

Async iterators são extremamente poderosos, especialmente em linguagens dinâmicas e assíncronas como o Javascript, com eles você pode transformar uma execução complexa em um código simples, escondendo a maioria da complexidade do usuário.

Não deixe de acompanhar mais do meu conteúdo no [meu blog](https://blog.lsantos.dev/?utm_source=devto&utm_medium=article&utm_campaign=async_iterators) e se [inscreva na newsletter](https://blog.lsantos.dev/signup?utm_source=devto&utm_medium=article&utm_campaign=async_iterators) para receber notícias semanais!