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