JavaScript async generators
Coding
Say you want to consume data from a JSON REST API. As soon as there is more than a couple of objects to retrieve, the API will probably have some pagination mechanism. The thing is you want to abstract this at the fetcher level, processing objects one by one.
So, let's start with the processing loop. Basically, we wish to write it like this:
But request()
is actually async
, that's where
the for await … of magic happen:
Here is a working example using GitHub's API. It is a bit more complex
because the search()
function has a while
looping through the result pages.
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | /* * A simple NodeJS example using async generators. */ 'use strict'; const debug = require('debug')('search'); const parse = require('parse-link-header'); const request = require('request-promise-native'); // GitHub code search result generator. async function *search(q) { const get = request.defaults({ method: 'GET', json: true, // parse the result as JSON resolveWithFullResponse: true, headers: { // see https://developer.github.com/v3/#user-agent-required 'User-Agent': 'js async generator demo', }, }); // setup the first URL to be requested. The followings requests will use // the rel="next" URL found in the response Link HTTP header, see // https://developer.github.com/v3/guides/traversing-with-pagination/ let url = new URL('https://api.github.com/search/code'); url.searchParams.set('q', q); // Main loop going through the paginated results. while (url) { debug(`requesting ${url}`); const rsp = await get({ url }); if (rsp.statusCode !== 200 /* OK */) { throw new Error(`expected 200 OK but got ${rsp.statusCode}`); } debug(`yielding ${rsp.body.items.length} items`); yield* rsp.body.items; const link = parse(rsp.headers.link); if (link && link.next) { // Setup the next page URL for the next loop iteration. url = new URL(link.next.url); } else { // We've reached the last page, exit the loop. url = null; } } } // await may only be used from inside an async function. async function main() { // You may change the search query, but beware of GitHub search rate limit // https://developer.github.com/v3/search/#rate-limit for await (const { score, name, repository } of search('OpenBSD+user:kAworu')) { console.log(`→ ${score.toFixed(2)}: ${name} (in ${repository.full_name})`); } } main(); |
Running
I made a simple project to run the code above:
Now let's run the script with DEBUG=* node index.js
so that
we can see when search()
actually perform the network
requests:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | search requesting https://api.github.com/search/code?q=OpenBSD%2Buser%3AkAworu +0ms search yielding 30 items +749ms → 15.32: openbsd-daily-on-linux.erb (in kAworu/kaworu.ch) → 13.97: daily (in kAworu/kaworu.ch) → 13.42: weekly (in kAworu/kaworu.ch) → 13.00: programming.erb (in kAworu/kaworu.ch) → 12.22: explicit_bzero.c (in kAworu/cryptopals) → 12.22: freezero.c (in kAworu/cryptopals) → 12.22: freezero.h (in kAworu/cryptopals) → 12.22: reallocarray.h (in kAworu/cryptopals) → 12.15: explicit_bzero.h (in kAworu/cryptopals) → 12.15: strlcat.h (in kAworu/cryptopals) → 12.15: strlcpy.h (in kAworu/cryptopals) → 12.15: timingsafe_bcmp.h (in kAworu/cryptopals) → 12.15: recallocarray.h (in kAworu/cryptopals) → 12.08: strlcat.h (in kAworu/tagutil) → 12.08: strlcpy.h (in kAworu/tagutil) → 11.48: monthly (in kAworu/kaworu.ch) → 11.43: timingsafe_bcmp.c (in kAworu/cryptopals) → 10.94: strlcat.c (in kAworu/cryptopals) → 10.94: strlcpy.c (in kAworu/cryptopals) → 10.94: reallocarray.c (in kAworu/cryptopals) → 10.83: strlcat.c (in kAworu/tagutil) → 10.83: strlcpy.c (in kAworu/tagutil) → 10.64: index.js (in kAworu/js-async-generator) → 10.60: t_toolkit.c (in kAworu/tagutil) → 10.56: recallocarray.c (in kAworu/cryptopals) → 10.44: strdup.c (in kAworu/tagutil) → 8.99: .zshrc (in kAworu/dotfiles) → 7.97: vimrc (in kAworu/dotfiles) → 7.93: perl.h (in kAworu/perl1) → 7.19: kaworu (in kAworu/dotfiles) search requesting https://api.github.com/search/code?q=OpenBSD%2Buser%3AkAworu&page=2 +4ms search yielding 2 items +427ms → 6.35: break_mac.c (in kAworu/cryptopals) → 5.23: srp.c (in kAworu/cryptopals) |
As we can see, we awaited on two network requests: the first one got 30
entries and the second one got 2. The nice thing is that our processing
code (i.e. the main()
function) is completely unaware of
this implementation detail.
That's it! I'll be happy to hear from you if this trick is useful, or more for await … of examples. Cheers.