Thursday 7 November 2019

Async Iterables

I've been playing around lately with Async Iterables (the JavaScript equivalent to C# Async Streams), and I've thought I should share it here for future reference. Same as an object is Iterable if it has a [Symbol.Iterator] method, it is asynchronously iterable if it features a [Symbol.asyncIterator] method. Both method return an object (the iterator) with a next method, that in the first case returns an object {done: bool, value: any} and in the second case a Promise of {done: bool, value: any}. Notice that in both cases the method is called next while in C# we have IEnumerator.MoveNext but IAsyncEnumerator.MoveNextAsync.

I've written some basic code where I define an Iterable class implementing the [Symbol.asyncIterator] method in 3 ways. The first and the second create the iterator "manually" (the only difference is that in one case I use Promise.then and in the other the magic of async-await. In the third case I leverage generators.

function getPost(postId){
    return new Promise((res) => {
        setTimeout(() => {
            if (postId != 4)
                res("post: " + postId + " content");
            else
                res(null);
        }, 500);
    });
}

//in all cases the iterator returned by asyncIterator returns a Promise<{done, value}>
class InfoBox1{
    [Symbol.asyncIterator]() {
        return {
            postId: 0,
            next(){ 
                return getPost(this.postId++)
     .then(post => post === null ? {done: true}: {done: false, value: post});
            }
        };
    }
}

class InfoBox2{
    [Symbol.asyncIterator]() {
        return {
            postId: 0,
            async next(){ 
                let post = await getPost(this.postId++);
                return post === null ? {done: true}
                    : {done: false, value: post};
            }
        };
    }
}


class InfoBox4{
    //"*" cause it contains "yield" statements
    //"async" cause it contains "await" statements
    async *[Symbol.asyncIterator]() {
       let postId = 0;
        while (true){
            let post = await getPost(postId++);
            if (post !== null)
                yield post;
            else 
                return;
        }
    }
}


async function asyncMain(){
    let infoBox = new InfoBox1();
    for await (let post of infoBox){
        console.log(post);
    }
    
    console.log("--------------");
    
    infoBox = new InfoBox2();
    for await (let post of infoBox){
        console.log(post);
    }

    console.log("--------------");

    infoBox = new InfoBox4();
    for await (let post of infoBox){
        console.log(post);
    }
}

asyncMain();

It's important to notice how we can declare async next(), and async *[Symbol.asyncIterator]() but bear in mind that while we can declare an arrow function async, we can not use arrow functions as generators.

It would be pretty convenient to be able to use map, filter... and similar functions with Async Iterables. There are some attempts on this, like wu.js. Well, I've come up with a different approach. We could transform the Async Iterable into an Observable, and then leverage the many rxjs operators. Transforming the Async Iterable into an Observable is as simple as this:

function asyncIterableToObservable(asyncIterable){
    return new Observable(async (observer) => {
        for await (let item of asyncIterable){
            observer.next(item);
        }
        observer.complete();
    })
}

And now you can leverage rxjs operators:

async function* citiesGeneratorFunc() {
    let cities = ["Xixon", "Toulouse", "Lisbon", "Tours"];
    for(let city of cities){
        yield await new Promise(res => {
            setTimeout(()=>res(city.toUpperCase()), 600);
        });
    }
}

let citiesGeneratorObj = citiesGeneratorFunc();
let citiesObservable = asyncIterableToObservable(citiesGeneratorObj);
citiesObservable.pipe(
 filter(it => it.startsWith("T")),
 map(it => "[" + it + "]"))
).subscribe(it => console.log(it));

//[Toulouse]
//[Tours]

No comments:

Post a Comment