Array.fromAsync for JavaScript

ECMAScript Stage-1 Proposal. J. S. Choi, 2021.

Why an Array.fromAsync method

Since its standardization in JavaScript, Array.from has become one of Array’s most frequently used built-in methods. However, no similar functionality exists for async iterators.

Such functionality would be useful for dumping the entirety of an async iterator into a single data structure, especially in unit tests or in command-line interfaces. (Several real-world examples are included in a following section.)

There is an it-all NPM library that performs only this task and which gets about 50,000 weekly downloads daily. This of course does not include any code that uses ad-hoc for awaitof loops with empty arrays:

const arr = [];
for await (const item of asyncItems) {
  arr.push(item);
}

Further demonstrating the demand for such functionality, several Stack Overflow questions have been asked by various developers, asking how to convert async iterators to arrays.

Description

(A formal draft specification is available.)

Similarly to Array.from, Array.fromAsync would be a static method of the Array built-in class, with one required argument and two optional arguments: (items, mapfn, thisArg).

But instead of converting an array-like object or iterable to an array, it converts an async iterable (or array-like object or iterable) to a promise that will resolve to an array.

async function * f () {
  for (let i = 0; i < 4; i++)
    yield i;
}

// Resolves to [0, 1, 2, 3].
await Array.fromAsync(f());

mapfn is an optional function to call on every item value. (Unlike Array.from, mapfn may be an async function. Whenever mapfn returns a promise, that promise will be awaited, and the value it resolves to is what is added to the final returned promise’s array. If mapfn’s promise rejects, then the final returned promise will also reject with that error.)

thisArg is an optional value with which to call mapfn (or undefined by default).

Like for await, when Array.fromAsync receives a sync-iterable object (and that object is not async iterable), then it creates a sync iterator for that object and adds its items to an array. When any yielded item is a promise, then that promise will block the iteration until it resolves to a value (in which case that value is what is added to the array) or until it rejects with an error (in which case the promise returned by Array.fromAsync itself will reject with that error).

Like Array.from, Array.fromAsync also works on non-iterable array-like objects (i.e., objects with a length property and indexed elements). As with sync-iterable objects, any element that is a promise must settle first, and the value to which it resolves (if any) will be what is added to the resulting array.

Also like Array.from, Array.fromAsync is a generic factory method. It does not require that its this value be the Array constructor, and it can be transferred to or inherited by any other constructors that may be called with a single numeric argument.

Other proposals

Object.fromEntriesAsync

In the future, a complementary method could be added to Object.

Type Sync method Async method
Array from fromAsync
Object fromEntries fromEntriesAsync?

It is uncertain whether Object.fromEntriesAsync should be piggybacked onto this proposal or left to a separate proposal.

Async spread operator

In the future, standardizing an async spread operator (like [ 0, await ...v ]) may be useful. This proposal leaves that idea to a separate proposal.

Iterator helpers

The iterator-helpers proposal puts forward, among other methods, a toArray method for async iterators (as well as synchronous iterators). We could consider Array.fromAsync to be redundant with toArray.

However, Array.from already exists, and Array.fromAsync would parallel it. If we had to choose between asyncIterator.toArray and Array.fromAsync, we should prefer Array.fromAsync to asyncIterator.toArray for its parallelism with what already exists.

In addition, the iterator.toArray method already would duplicate Array.from for synchronous iterators. We consider duplication with an Array method as okay anyway. If duplication between syncIterator.toArray and Array.from is already okay, then duplication between asyncIterator.toArray and Array.fromAsync should also be okay.

Records and tuples

The record/tuple proposal puts forward two new data types with APIs that respectively resemble those of Array and Object. The Tuple constructor, too, would probably need an fromAsync method. Whether the Record constructor gets a fromEntriesAsync method depends on whether Object gets fromEntriesAsync.

Set and Map

There is a proposal for Set.from and Map.from methods. If this proposal is accepted before that proposal, then that proposal could also add corresponding fromAsync methods.

Real-world examples

Only minor formatting changes have been made to the status-quo examples.

Status quo With binding
const all = require('it-all');

// Add the default assets to the repo.
const results = await all(
  addAll(
    globSource(initDocsPath, {
      recursive: true,
    }),
    { preload: false },
  ),
);
const dir = results
  .filter(file =>
    file.path === 'init-docs')
  .pop()
print('to get started, enter:\n');
print(
  `\tjsipfs cat` +
  `/ipfs/${dir.cid}/readme\n`,
);

From ipfs-core/src/runtime/init-assets-nodejs.js.

// Add the default assets to the repo.
const results = await Array.fromAsync(
  addAll(
    globSource(initDocsPath, {
      recursive: true,
    }),
    { preload: false },
  ),
);
const dir = results
  .filter(file =>
    file.path === 'init-docs')
  .pop()
print('to get started, enter:\n');
print(
  `\tjsipfs cat` +
  `/ipfs/${dir.cid}/readme\n`,
);
const all = require('it-all');

const results = await all(
  node.contentRouting
    .findProviders('a cid'),
);
expect(results)
  .to.be.an('array')
  .with.lengthOf(1)
  .that.deep.equals([result]);

From js-libp2p/test/content-routing/content-routing.node.js.

const results = await Array.fromAsync(
  node.contentRouting
    .findProviders('a cid'),
);
expect(results)
  .to.be.an('array')
  .with.lengthOf(1)
  .that.deep.equals([result]);
async function toArray(items) {
  const result = [];
  for await (const item of items) {
    result.push(item);
  }
  return result;
}

it('empty-pipeline', async () => {
  const pipeline = new Pipeline();
  const result = await toArray(
    pipeline.execute(
      [ 1, 2, 3, 4, 5 ]));
  assert.deepStrictEqual(
    result,
    [ 1, 2, 3, 4, 5 ],
  );
});

From node-httptransfer/test/generator/pipeline.test.js.

it('empty-pipeline', async () => {
  const pipeline = new Pipeline();
  const result = await Array.fromAsync(
    pipeline.execute(
      [ 1, 2, 3, 4, 5 ]));
  assert.deepStrictEqual(
    result,
    [ 1, 2, 3, 4, 5 ],
  );
});