JavaScript's async function and topological ordering
Tags: webdev, javascript, beginners, programming
Published: 2023-08-26 15:24:53

Today we all use web browsers.
And this is like saying that we all use JavaScript.
There are many reasons behind this,
but one of the main reasons may be that JavaScript supports powerful asynchronous functions.
However, crafting intricate asynchronous logic tends to be notably more challenging than devising procedural logic.
How can write asynchronous logic more easily?

## JavaScript is a single-threaded language.
This is an important issue.
A single thread means that everything is done synchronously.
Let's look at the example below.
```js
const { log } = console
async function a() {
    log(2)
    b()
    log(5)
}
async function b() {
    log(3)
    log(4)
}
log(1)
a()
log(6)
```
What would be the output result of the above code?
It's `1, 2, 3, 4, 5, 6`.
By default, the `async` keyword does not affect the execution order.
Now let's look at the second example.
```js
const { log } = console
async function a() {
    log(2)
    await b()
    log(5)
}
async function b() {
    Promise.resolve()
        .then(() => log(4))
        .then(() => log(6))
}
log(1)
a()
log(3)
```
Similarly, the code above also prints `1, 2, 3, 4, 5, 6`.
To understand this, you need to know about the [Microtask queue](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide).
`log(4)` is enqueued in the queue as soon as `Promise.resolve()` is completed inside the `function b`.
After that, `function b` exits, and due to the `await`, `log(5)` is enqueued in the queue.
Finally, when `function a` finishes and `log(3)` is executed, after the initially queued `log(4)` is executed, `log(6)` is enqueued in the queue.
What happens if you add `await` before `Promise.resolve()` here?
```js
const { log } = console
async function a() {
    log(2)
    await b()
    log(6)
}
async function b() {
    await Promise.resolve()
        .then(() => log(4))
        .then(() => log(5))
}
log(1)
a()
log(3)
```
The example above is mostly similar to the second example, but `log(6)` is enqueued in the queue after `log(5)` is completed. As a result, the code above also prints `1, 2, 3, 4, 5, 6`.
The examples above are all logic executed within JavaScript, and therefore, all operations are carried out synchronously.

## External APIs for JavaScript
Then, what are the cases in which operations are performed asynchronously in JavaScript?
In today's JavaScript runtime environments, such as web browsers and Node.js, several external APIs enable asynchronous operations using multithreading. Some of these include familiar features like `setTimeout`, `fetch`, and `HTMLElement.addEventListener`.
The following example uses `setTimeout`.
```js
const { log } = console
async function a() {
    log(2)
    await b()
    queueMicrotask(() => log(4))
}
async function b() {
    new Promise(
        resolve => setTimeout(() => resolve(log(5)))
    ).then(() => log(6))
}
log(1)
a()
log(3)
```
Indeed, it's a code that prints `1, 2, 3, 4, 5, 6`.
Just to clarify, `queueMicrotask` is a function that directly schedules a callback in the microtask queue.
Then why is `log(5)` executed later than `log(4)`?
That's because JavaScript's external APIs rely on a feature called the [Event loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop).
The event loop operates as a separate queue from the microtask queue, and when all microtask is complete, the next event callback is executed.
Due to this structure, even though `log(5)` is enqueued in the event callback queue before `queueMicrotask` or `log(4)`, it is executed after `log(4)` in the microtask queue has been executed.

## Promise Chaining and DAG
Sure, now let's go back to the original question.
How can write asynchronous logic more easily?
The answer is to construct complex asynchronous logic into DAGs.
"DAG" stands for directed acyclic graph.
Let's consider a scenario where we use a user ID to retrieve user information and a list of posts.
Subsequently, for each post, we fetch the corresponding comments.
Once all these tasks are completed, we need to display the gathered information on the view.
Representing this procedure as a graph would look like the following
![Dag Example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z6e1du4at77o93d3qx17.png)
And this could be implemented as follows
```js
async function get_user_info(user_id) {}

async function get_user_posts(user_id) {}

async function get_post_comments(post_id) {}

function render(contents) {}

async function get_contents(user_id) {
    const user_info_promise = get_user_info(user_id)
    const user_posts_promise = get_user_posts(user_id)
    const posts_comments_promise = user_posts_promise
        .then(
            posts => Promise.all(
                posts.map(
                    post => get_post_comments(post.post_id)
                )
            )
        )
    const user_info = await user_info_promise
    const user_posts = await user_posts_promise
    const posts_comments = await posts_comments_promise
    return user_posts.map((post, i) => {
        post.user_name = user_info.user_name
        post.comments = posts_comments[i]
        return post
    })
}

const contents = await get_contents(123)
render(contents)
```
By appropriately utilizing `Promise.all` and lazy `await`, scenarios like this DAG can be easily resolved in a topological order.
However, if the composition of the DAG becomes more complex, solving the problem using this approach alone can become cumbersome.

## Async Lube: Simplify Asynchronous Operations in JavaScript
[Async Lube](https://www.npmjs.com/package/async-lube) is a simple library that makes it easy to create complex asynchronous operations.
Using `async-lube`, the example above could be written as follows
```js
import { dag } from "async-lube"

/* omit */

async function get_contents(user_id) {
    const get_posts_comments = posts => Promise.all(
        posts.map(
            post => get_post_comments(post.post_id)
        )
    )
    const merge_contents = dag()
        .add(get_user_info, user_id)
        .add(get_user_posts, user_id)
        .add(get_posts_comments, get_user_posts)
        .add(
            (user_info, user_posts, posts_comments) =>
                user_posts.map((post, i) => {
                    post.user_name = user_info.user_name
                    post.comments = posts_comments[i]
                    return post
                }),
            get_user_info,
            get_user_posts,
            get_posts_comments
        )
    return merge_contents()
}

const contents = await get_contents(123)
render(contents)
```
The `dag().add` function takes a `callback` and `...dependencies` as arguments, and `dag()()` executes the callbacks in the order of topological sorting.
By using this approach, the complexity of even intricate asynchronous logic can be constrained to increase linearly with the amount of code.
If you'd like to see code that uses the functioning `async-lube`, an example is shown below.

{% embed https://stackblitz.com/edit/rxjs-1bzymx?file=index.ts %}