Skip to content

Instantly share code, notes, and snippets.

@korzio
Created July 7, 2019 15:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save korzio/682e54b90c33e2eb0dfbcb3eb7ccd19d to your computer and use it in GitHub Desktop.
Save korzio/682e54b90c33e2eb0dfbcb3eb7ccd19d to your computer and use it in GitHub Desktop.

Why would someone use async generators?

It's easy to make promises, is a hard work to keep them, dude

Previous month I've been attending Kyle Simpson workshop "JavaScript: The Recent Parts" in Amsterdam in June, which I enjoyed so much. The material and the show was great, and Kyle is a real master in performing workshops!

One of the most interesting topics I was excited about in the workshop was about async generators just at the end of the event. The audience was intrigued and had some questions regarding different strategies and iterations order that can be achieved with async generators technique. In order to understand it better myself, I've decided to write a short medium post about these patterns. In this post I will explain some concepts related to generators topic and describe these strategies in details.

Iterators

There are plenty of nice articles and tutorials explaining Iterators in details. So, I will do it very briefly for the sake of staying closer to the article's goal.

Iterators are in a nutshell any objects implementing the Iterator protocol. Basically, an Iterator object must contain a next function, returning a value of any type and a Boolean done properties. The next method defines an order on which iteration is happening along with values, produced on every step.

const iterable = {
  [Symbol.iterator]() {
    let step = 0
    let done = false
    let value
    
    return {
      next() {
        step++
            
        switch (step) {
          case 1:
            value = 'hello'
            break
          case 2:
            value = 'world'
            break
          default:
            done = true
        }
        
        return { done, value }
      }
    }
  }
}

To iterate over such an object it's possible to use different techniques:

  • A one is the for-of statement, introduced in ECMAScript 6 update
for (const a of iterable) {
  console.log(a)
}
// -->
// "hello"
// "world"
  • The spread operator helps to convert iterable object into an array.
console.log([...iterable])
// -->
// ["hello", "world"]

// you can try it here - https://jsbin.com/suxikofesa/edit?js,console

Many JavaScript built-in objects, like arrays, strings, maps, and sets implement iterable interface already. Usually, and to be compatible with generators, iterators are declared on a Symbol.iterator property.

const str = 'hi'
const iterator = str[Symbol.iterator]()
iterator.next() // {value: "h", done: false}
iterator.next() // {value: "i", done: false}
iterator.next() // {value: undefined, done: true}
iterator.next() // {value: undefined, done: true}

You might notice, that after reaching the end state, after done appears to be true a next() call to iterator method does return the same result - {value: undefined, done: true}.

Generators

A generator is a function, which simplifies iterators declaration. It can stop it's execution and return multiple values.

function* iterator() {
   yield 1
   yield 2
   yield 3
}

for (const value of iterator()) {
  console.log(value)
}

// -->
// 1
// 2
// 3

A generator starts with function* operator, and it's body is almost like in a normal function, extended with a few features.

  • yield statement should be presented and it stops a function invocation, returning a specified value. The *yield flavour delegates or flats nested generators.

Async Iterators && Generators

Async Iterators allow to delay an iteration process involving asycronous logic. Each result of an async iterator next() method returns a Promise, resolved into an object, containing value and done properties, as usual.

Async functions allow to use await syntax and return Promise. Logically, async generators permit await and yield Promises. 🦄 💬 💬

So, why would someone use async generators?

Good question! Async iterators come with a cool loop construction

for await (const item of asyncIterator) {
  // ...
}

That's why! ;) Meaning, a developer can define any asyncronous iteration order he or she likes to. For instance, iterate over Node read streams.

Don't forget to switch to a newer version of Node:

nvm use 12
Now using node v12.4.0 (npm v6.9.0)
const fs = require('fs')
const readStream = fs.createReadStream('./test.txt', { encoding: 'utf8' })

async function print() { 
  for await (const chunk of readStream) {
    console.log(chunk)
  }
}
  
print()

Please note, that for-await can be used only inside async function. And, implementation-wise, async iterators should be declared with Symbol.asyncIterator properties.

Async Strategies

Imagine, you want to request multiple asyncronous sources:

async function* inOrderRequests(urls) {
  for (const url of urls) {
    const response = await fetch(url)
    const text = await response.text()
    yield text
  }
}

Now, it is possible to iterate urls over in a async function:

async function pageText(urls) {
  for await (const responseText of inOrderRequests(urls)) {
    console.log(responseText) 
  }
}

pageText([location.href])

pageText([location.href]) outputs the initial text of a current browser page.


During the workshop, Kyle identified three different strategies, can be used for requesting asyncronous resources:

  • in order - in order when a request is performed after the other request's response is returned. The pageText above is an example of such a strategy.
  • asap - in order when requests are performed all together, and responses are returned in the initial order. We'll modify a previous example with an asapInOrderRequests function.
async function* asapInOrderRequests(urls) {
  const requests = urls.map((url) => fetch(url))
  
  for await (const response of requests) {
    const text = await response.text()
    yield text
  }
}

async function pageText(urls) {
  for await (const responseText of asapInOrderRequests(urls)) {
    console.log(responseText) 
  }
}

Now, all the requests are performed at once. The function waits for each request to resolve, but overall time spent by requests limited by it's the most delayed Promise.

  • asap - asap strategy is applied when both requests and results are performed and returned as soon as possible.

On the workshop Kyle showed all three possibilities, where asap - asap was the most difficult, as he was improvising. I do find the latter the most interesting too

https://gist.github.com/korzio/52774b2cf2281662ca0e6ef4600bf6bd

In this example the JavaScript closure feature is used to signal of a lately resolved Promise. Whenever it is fulfilled, generator yields the result, so the full race of Promises is happening. It took me some time and attempts to finish the exercise. Hopefully, you find it usefull.

I'm very interested in your feedback. Please let me know what do you think of that piece of code. Whould do you want to use a similar construction in your project? See you in comments :)

Thank you for your time and have a nice coding!

Links

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment