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.
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);