I have the following code which does what I want to:
function remoteGenerator(port) {
const createPromise = () => {
let handlers;
return {
promise: new Promise(
(resolve, reject) => (handlers = { resolve, reject })
),
get handlers() {
return handlers;
},
};
};
const createIterator = (run) => {
const iterator = {
next: run,
return: (arg) => run(arg, 'return'),
[Symbol.asyncIterator]: () => iterator,
};
return iterator;
};
let done = false;
let { promise, handlers } = createPromise();
const step = createIterator((arg, name = 'next') => {
const original = promise;
if (done) return original;
port.postMessage({ name, arg });
promise = promise.then(() => {
if (done) return original;
const next = createPromise();
handlers = next.handlers;
return next.promise;
});
return original;
});
port.onmessage = (evt) => {
done = evt.data.done;
handlers[evt.data.handler]({ done: evt.data.done, value: evt.data.value });
};
return step;
}
// usage
async function* startCounterAsync(delay = 1000) {
let i = 0;
while (i < 10) {
yield i++;
await new Promise((r) => setTimeout(r, delay));
}
}
const startRemoteGenerator = result => {
const mc = new MessageChannel()
mc.port1.onmessage = async (evt) => {
let nextResult;
try {
nextResult = await result[evt.data.name](evt.data.arg);
mc.port1.postMessage({
name: nextResult.done ? 'return' : 'next',
handler: 'resolve',
value: nextResult.value,
done: nextResult.done,
});
} catch (err) {
mc.port1.postMessage({
name: 'return',
handler: 'reject',
value: err,
done: true,
});
}
nextResult.done && port.close();
};
return remoteGenerator(mc.port2);
}
for await (let value of startRemoteGenerator(startCounterAsync())) {
console.log(value);
}
The function remoteGenerator
receives one of ports from MessageChannel and works with the generator or the async generator on the other end of the message channel to provide an opaque interface to the execution which may happen in different context.
I'm looking on how I could refactor the remoteGenerator
function to be an async generator itself.
So far the main blocker for me is the fact that there is no way to know whether the generator on the other end will return or yield the value.
In order to get the arg to pass to the remote generator, I have to do yield on my end, which in turn should return the yielded value from the other end, and there seems to be no way to cancel or replace ongoing yield with return, so it has { done: true }
set.
The solution I've found so far is wrapping the function into async generator function:
async function* remoteGeneratorWrapper(port) {
const it = remoteGenerator(port);
yield* it;
return (await it.return()).value;
}
But I'd want to simplify the solution, so it doesn't have intermediate async iterator
So, thanks to Bergi for an insight re try..finally, but this doesn't really need it much.
Looks like the following is the replacement for the
remoteGeneratorWrapper
which takes an advantage of using async generators without the need in a custom async iterator:I had an initial struggle with it because forgot that after one calls generator function it suspends immediately and the very first
next(arg)
call's arg can't be received by the generator. Instead the call is starting the generator function until it either yields or returns.By using the fact, we can issue the
next()
call immediately without the need to wait on the actual arg passed. As the generator function suspends on its own after it is called the message will be posted to the remote side only when one callsnext()
on our side.