Observer Pattern in Javascript Asynchronous Generators

One of the more recent additions to the Javascript specification is the asynchronous generator protocol. This is an especially useful pattern when you want to consume data off a socket/serial port etc., because it lets you do something like so:

for await (const buffer of readable) {
    await writable.write(buffer);
}

Which is pretty cool, but not a huge improvement on the `pipe` functionality already exposed in Node streams.

Where it really shines is the ability to also yield observations, allowing you to build an observer pattern:

async * download(writable) {
  await this.open();

  try {
    const readable = this.readSectors(...);
    let counter = 0;

    for await (const buffer of readable) {
      const buffer = SECTOR.parse(buffer);
      await writable.write(buffer);

      counter++;
      yield counter;
    }

  } finally {
    await this.close();
  }
}

The primary advantage is the flatness makes our exit handling very obvious. Similarly in readSectors it flattens the entry and exit of the read mode.

Those building React/Redux apps probably want to get those observations into their state. This is relatively easily achieved in redux-saga through the eventChannel API.

function asyncToChannel(generator) {
  return eventChannel(emit => {

    // Set up a promise that iterates the async iterator and emits
    // events on the channel from it.
    (async () => {
      for await (const elem of generator) {
        emit(elem);
      }

      emit(STOP_ITERATION);
    })();

    return () => {
      generator.return();
    };
  });
}

// Saga triggered on DOWNLOAD_BEGIN
function* downloadSaga(action) {
  const writable = ...
  const channel = asyncToChannel(action.data.download(writable));

  // Consume the channel into Redux actions
  while (true) {
    const progress = yield take(channel);

    if (progress === STOP_ITERATION) break;

    yield put(downloadProgress(action.data, progress));
  }

  console.debug("Download complete");
  yield put(downloadComplete(action.data));
}

Tricky to work out, but much more easily read than callback hell.

Author: Danielle

Danielle is an Australian software engineer, computer scientist and feminist. She doesn't really work on GNOME any more (sadly). Opinions and writing are solely her own and so not represent her employer, the GNOME Foundation, or anyone else but herself.