Suppose I have some async iterable objects like this:
const a = { [Symbol.asyncIterator]: async function * () { yield 'a'; await sleep(1000); yield 'b'; await sleep(2000); yield 'c'; }, }; const b = { [Symbol.asyncIterator]: async function * () { await sleep(6000); yield 'i'; yield 'j'; await sleep(2000); yield 'k'; }, }; const c = { [Symbol.asyncIterator]: async function * () { yield 'x'; await sleep(2000); yield 'y'; await sleep(8000); yield 'z'; await sleep(10000); throw new Error('You have gone too far! '); }, };
And for completeness:
// Promisified sleep function const sleep = ms => new Promise((resolve, reject) => { setTimeout(() => resolve(ms), ms); });
Now, suppose I can concat them like this:
const abcs = async function * () { yield * a; yield * b; yield * c; };
The (first 9) items yielded will be:
(async () => { const limit = 9; let i = 0; const xs = []; for await (const x of abcs()) { xs.push(x); i++; if (i === limit) { break; } } console.log(xs); })().catch(error => console.error(error)); // [ 'a', 'b', 'c', 'i', 'j', 'k', 'x', 'y', 'z' ]
But imagine that I do not care about the order, that a
, b
and c
yield at different speeds, and that I want to yield as quickly as possible.
How can I rewrite this loop so that x
s are yielded as soon as possible, ignoring order?
It is also possible that a
, b
or c
are infinite sequences, so the solution must not require all elements to be buffered into an array.
Advertisement
Answer
There is no way to write this with a loop statement. async
/await
code always executes sequentially, to do things concurrently you need to use promise combinators directly. For plain promises, there’s Promise.all
, for async iterators there is nothing (yet) so we need to write it on our own:
async function* combine(iterable) { const asyncIterators = Array.from(iterable, o => o[Symbol.asyncIterator]()); const results = []; let count = asyncIterators.length; const never = new Promise(() => {}); function getNext(asyncIterator, index) { return asyncIterator.next().then(result => ({ index, result, })); } const nextPromises = asyncIterators.map(getNext); try { while (count) { const {index, result} = await Promise.race(nextPromises); if (result.done) { nextPromises[index] = never; results[index] = result.value; count--; } else { nextPromises[index] = getNext(asyncIterators[index], index); yield result.value; } } } finally { for (const [index, iterator] of asyncIterators.entries()) if (nextPromises[index] != never && iterator.return != null) iterator.return(); // no await here - see https://github.com/tc39/proposal-async-iteration/issues/126 } return results; }
Notice that combine
does not support passing values into next
or cancellation through .throw
or .return
.
You can call it like
(async () => { for await (const x of combine([a, b, c])) { console.log(x); } })().catch(console.error);