Async Generators for User Input
Tags: javascript
Published: 2020-04-16 01:32:43

---
title: Async Generators for User Input
published: true
date: 2020-04-15 00:00:00 UTC
tags: javascript
canonical_url: https://whistlr.info/2020/async-generators-input/
---

# Async Generators for User Input

It's possible to build JS, on the web, with a native-like event loop.This is thanks to async generators and the `await` keyword—are _you_ sick of a twisty maze of `addEventListener`, timeouts and global state?Read on.

## Background

Generators allow us to suspend normal program flow as the interpreter _jumps_ to/from your `yield` statements:

```js
function* generator() {
  yield 1;
  console.info('between');
  yield 2;
}
for (const value of generator()) {
  console.info(value);
}
```

This program will print out "1", "between", and "2".Used at face value, this primitive allows you to create something which vaguely looks like an array.

This isn't all, though: from Chrome 63 [and friends](https://caniuse.com/#feat=mdn-javascript_functions_method_definitions_async_generator_methods), you can perform asynchronous work between each `yield` statement (to be fair, you could already `yield` a `Promise`).The syntax isn't too different, either: just add `async` to your function.

## Event Loop

Most documentation about [JS' event loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop) correctly identifies it as event-driven.This is the normal JS model—if you `addEventListener`, your handler is called, and is expected to complete synchronously.

Instead, let's aim for something more akin to a native event loop, which you could use like:

```js
(async function run()) {
  for await (const event of loop) {
    if (event instanceof MouseEvent) {
      // If the user clicked on something, wait for their result.
      if (event.name === 'click' && event.target === button) {
        await getUserInput('Rate your experience:');
      }
    } else if (event.type === 'keydown') {
      // Submit the form
      if (event.key === 'Enter') {
        // TODO: ...
      }
    }
  }
}());
```

This is basically implementing a state machine—[a DFA](https://en.wikipedia.org/wiki/Deterministic_finite_automaton), where the states are controlled by user input.This is especially userful for complex user interactions—like forms, or games.

<video alt="train game with unique input states" muted autoplay loop width="480" height="480">
    <source src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/train-2020-04-14.mp4" type="video/mp4"></source>
  </video><figcaption>a <a href="https://twitter.com/samthor/status/1249631219563675651">train game</a> with two input states: choosing an edge, and drawing a track</figcaption>

There's a few decisions you might have to make about the input, though.

As you're now consuming time—potentially _asynchronously_ via `await`—to process each event, it's unlikely that your code will be able to handle every event as it arrives.For example, if you're processing `click` events but you do a network round-trip, a user might generate more clicks before the first event is done.This might be intentional, _but you'll need to decide_ what's important to queue up for processing later.

What does the `loop` object look like, then?Well, you can build a generator and a helper to push events into it:

```js
export function buildEventManager() {
  let resolve = () => {};
  const queue = [];

  // (there's no arrow function syntax for this)
  async function* generator() {
    for (;;) {
      if (!queue.length) {
        // there's nothing in the queue, wait until push()
        await new Promise((r) => resolve = r);
      }
      yield queue.shift();
    }
  }

  return {
    push(event) {
      queue.push(event);
      if (queue.length === 1) {
        resolve(); // allow the generator to resume
      }
    },
    loop: generator(),
  };
}
```

This is a bit of code, but it basically just has two parts and a connection between them.First, a `push()` method, which lets you control what events are handled, and pushes them into the queue.Secondly, a generator—which we run, and return as `loop`—which waits for events to appear, and uses `yield` to provide the next available one.

To use it purely to keep a queue of all the pointer events that occur, try this:

```js
const {push, loop} = buildEventManager();

window.addEventListener('pointermove', push);
window.addEventListener('click', push);

(async function run() {
  for await (const event of loop) {
    console.info('mouse now at', event.screenX, event.screenY);
  }
}());
```

This simple example just queues _everything_, rather than trying to e.g., only provide the most recent movement event.

### Not Just User Events

One of the benefits of a generic event loop is that we can process any sort of event we imagine, not just user-generated ones.For example, we could push some custom events and process them in your event loop inline with everything else:

```js
window.setInterval(() => {
  push(new CustomEvent('tick'));
}, 1000);

const ro = new ResizeObserver(() => {
  push(new CustomEvent('resizeElement'));
});
ro.observe(someElement);
```

… of course, you're not just limited to custom events (`push` accepts _any_ object), but this might match the rest of the inputs you're processing.

### State Machines, Oh My

I mentioned that this native-like event loop helps us create state machines.If you just have a single event loop, that's not really true, because you might still have to manage global state yourself.Instead, you can actually use the loop many times.

Unfortunately, using `for await (...)` actually doesn't work here, as you're seemingly not able to use it more than once (I may need to read the ECMAScript spec to find out why).Instead, we can use a generator's [`.next()` method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/next):

```js
(async function run() {
  for (;;) {
    const {value: event} = await loop.next();
    if (event.type !== 'click') {
      continue;
    }
    console.info('start line at', event.screenX, event.screenY);

    for (;;) {
      const {value: innerEvent} = await loop.next();
      if (innerEvent.type === 'click') {
        console.info('draw line to', innerEvent.screenX, innerEvent.screenY);
        break;
      }
    }
  }
}());
```

In this example, we wait for one click, then another.It's a simple example, but shows how you might build up some interesting state—you've started at the default state, then moved to the "waiting for a second point" state, then you're able to draw a line.

## Digression

As a digression, while it's _not an error_ to write something like:

```js
element.addEventListener('click', async (event) => {
  const result = await (fetch('/foo').then((r) => r.json());
  // update the DOM
});
```

… it is basically an antipattern because the `async` keyword masks some possible issues:

1. You could end up handling the event several times in parallel, e.g., submitting a form many times before it's complete
2. Errors aren't caught anywhere (they would appear as an "unhandled promise rejection")
3. Importantly, it _appears_ like the event handler is synchronous and blocking, even though it's not

You can work around the 1<sup>st</sup> issue with something like [cancellable async functions](https://dev.to/chromiumdev/cancellable-async-functions-in-javascript-5gp7).

## Conclusion

What I've covered here is an incredibly basic example of processing events in a queue.You'd want to inspire from these ideas and write something appropriate for your use-case: I know I will for my web-based games.

One key difference from actual native event loops is that on the web, we can't (and probably don't want to) turn off every bit of built-in behavior your browser provides for you.I'm not suggesting that you handle every keystroke in an `<input type="text">`, or overload every click event.But this lets you control perhaps a tricky component which transitions through a bunch of states, especially if it has complex user interactions—keyboard, mouse, and so on.

Finally, there's a [lot of reading out there](https://www.google.com/search?q=async+generators+state+machines) about state machines via async generators—that's great, and I'm glad to see many folks taking advantage of this idea.I've not seen a huge amount of writing about processing user events, though, and that's where I want you to consider the possibilities.