An Async Thing

26th Feb 2022

How to work with many async objects and functions

Following the implementation of union I have found working with many async generators as a group has been helpful, but not as close to what I would expect the large majority of JavaScript developers want.

A good amount of async work can be done with individual promises without ever touching generators.

As part of JavaScript, we have Promise.all available.

Using this function we can take a group of promises, and do something once they have all have fulfilled values

Using Promise.allSettled we can also inspect the individual status of each promise, without needing to use catch

Both of these functions return a promise of the final state, in all's case, this promise will be rejected if at least one input promise rejected.

This is helpful where you care about the group of promises as a single piece of information, however if you're looking to know about each status as it happens, you're a bit out of luck with your standard tools

Using previously the mentioned union function, if we were to wrap each of our promises with an async generator we could however capture these statuses as they are available

const promises = [
  Promise.resolve(1),
  Promise.resolve(2),
  new Promise((resolve) => setTimeout(resolve, 10, 3)),
];

for await (const results of union(
  promises.map(async function* (promise) {
    try {
      yield { value: await promise, status: "fulfilled" };
    } catch (reason) {
      yield { reason, status: "rejected" };
    }
  }),
)) {
  console.log({ results });
}

Using this pattern, we can create a generic generator that exposes this functionality for all promises

import { allSettledGenerator } from "@virtualstate/promise/all-settled";

for await (const results of allSettledGenerator(promises)) {
  console.log({ results });
}

For both, we would see

{
  results: [
    { value: 1, status: "fulfilled" },
    { value: 2, status: "fulfilled" },
    undefined
  ]
}
{
  results: [
    { value: 1, status: "fulfilled" },
    { value: 2, status: "fulfilled" },
    { value: 3, status: "fulfilled" }
  ]
}

Now we have a function that we can take our promise, and group together sets of status updates for inspection.

From this we can iteratively update dependent state on the fly as information becomes available while still maintaining connectivity between all the input promises as a whole.

The problem here is you now have to deal with two different functions depending on how you construct these promise groups, or how you want to read them

To solve this, we can expand on JavaScript's native functionality and merge together the two async concepts together

Say we had our generator function, and we first wanted to be able to read it as a promise

import { anAsyncThing } from "@virtualstate/promise/the-thing";
import { allSettledGenerator } from "@virtualstate/promise/all-settled";

const asyncIterable = allSettledGenerator(promises);
const object = anAsyncThing(asyncIterable);

const finalYieldedResult = await object;
console.log({ finalYieldedResult });

For this promise, we would see

{
  finalYieldedResult: [
    { value: 1, status: "fulfilled" },
    { value: 2, status: "fulfilled" },
    { value: 3, status: "fulfilled" }
  ]
}

We are taking a leap here and assuming that each yielded value from an async iterable is its current state, and the final yielded value from it, is representing the "final state" or "returned" value

Next, we would want to be able to from the same object, again use it's original input async iterable

const asyncIterable = allSettledGenerator(promises);
const object = anAsyncThing(asyncIterable);

for await (const results of object) {
  console.log({ results });
}

For this for await, we would see the original

{
  results: [
    { value: 1, status: "fulfilled" },
    { value: 2, status: "fulfilled" },
    undefined
  ]
}
{
  results: [
    { value: 1, status: "fulfilled" },
    { value: 2, status: "fulfilled" },
    { value: 3, status: "fulfilled" }
  ]
}

Now we have this shared object, we can provide this directly as a function that matches both our allSettledGenerator function's signature, but can also match the signature of the built in Promise.allSettle

import { allSettled } from "@virtualstate/promise/all-settled";

const object = allSettled(promises);
console.log({ finalYieldedResult: await object });
for await (const state of object) {
  console.log({ state });
}

Following the implementation of allSettled, we can implement all in the same light

First implementing allGenerator, then a matching all function

import { allGenerator } from "@virtualstate/promise/all";

const asyncIterable = allGenerator(promises);
const object = anAsyncThing(asyncIterable);
import { all } from "@virtualstate/promise/all";

const object = all(promises);
console.log({ finalYieldedResult: await object });
for await (const state of object) {
  console.log({ state });
}

With this, we have a matching signature to Promise.all, and Promise.allSettled, while still being able to inspect individual promise statuses and values as they happen.

Given we have a consistent way to both input and output async iterables and promises, we can open our input up to also include async functions, which return promises, and async generator functions, which return async iterables.

Implementing this involves providing additional mapping for these extra value types, while still providing the default promise resolution.

If we come across an async iterable input type, we again assume that each yielded value is only the objects current state, and that the final fulfilled value is its settled & returned state.

If we come across a function, we assume it is a zero argument function that returns a promise or an async iterable, and then follow the same already defined steps.

Because the all function is implemented using allSettledGenerator as its core source, it required no implementation change for this.

We can see now we can use a mix of inputs:

const asyncValues = [
  Promise.resolve(1),
  async () => 2,
  async function *() {
    yield 3;
  },
  {
    async *[Symbol.asyncIterator]() {
      yield 4;
    }
  }
];
import { allSettled } from "@virtualstate/promise/all-settled";
const object = allSettled(asyncValues);
import { all } from "@virtualstate/promise/all";
const object = all(asyncValues);

The resulting repository & module used in this post can be found at github.com/virtualstate/promise