ECMAScript Stage-1 Proposal. J. S. Choi, 2021.
- Specification available
- Polyfill available
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 await–of 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.
(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 iterable to an array, it converts an async 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 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.
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.
In the future, standardizing an async spread operator (like [ 0, await ...v ])
may be useful. This proposal leaves that idea to a separate proposal.
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.
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.
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.
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`,
); |
// 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 ],
);
}); |
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 ],
);
}); |