How to pass async generator through MessageChannel?

84 views Asked by At

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

1

There are 1 answers

0
Eugene On BEST ANSWER

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:

async function* wrapRemoteGenerator(port) {
  try {
    port.postMessage({ name: 'next', arg: void 0 });
    while (true) {
      const res = await new Promise((resolve, reject) => {
        const handlers = { resolve, reject };
        port.onmessage = (evt) => {
          handlers[evt.data.handler](evt.data);
        };
      });
      if (res.done) {
        return res.value;
      }
      const arg = yield res.value;
      port.postMessage({ name: 'next', arg });
    }
  } finally {
    port.close();
  }
}

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 calls next() on our side.