Write on(target, type). target is anything with addEventListener and removeEventListener: a DOM element, window, or new EventTarget(). on returns an async iterator over the target's events of type type. It's also async iterable (its [Symbol.asyncIterator]() returns itself), so you can write for await (const event of on(button, "click")).
on starts listening as soon as it's called: it calls target.addEventListener(type, listener) once, with a listener function of its own. So events that arrive before the first next() aren't missed.
next() returns a promise of { value: event, done: false }. Events that arrive while no next() is waiting are kept in a buffer, and next() takes the oldest one. When the buffer is empty, next() waits for the next event. Several next() calls can wait at once; they get events in the order they were called.
return() ends the iterator. It drops any buffered events, resolves every waiting next() with { value: undefined, done: true }, and returns a promise of the same. After that, next() resolves with { value: undefined, done: true } straight away. Breaking out of a for await loop calls return().
return() also stops listening: it calls target.removeEventListener(type, listener) once, with the same type and the very same listener function that on passed to addEventListener. removeEventListener only removes a listener when it's given that exact function, so passing a new function, even one with the same code, removes nothing.
const target = new EventTarget();
const pings = on(target, "ping");
const early = new Event("ping");
target.dispatchEvent(early); // nobody is waiting: buffered
await pings.next(); // { value: early, done: false }, at once
const waiting = pings.next(); // the buffer is empty: waits
const later = new Event("ping");
target.dispatchEvent(later);
await waiting; // { value: later, done: false }
await pings.return(); // { value: undefined, done: true }; the listener is removed
await pings.next(); // { value: undefined, done: true }
const target = new EventTarget();
const pings = on(target, "ping");
let result;
pings.next().then((value) => {
result = value;
});
await flush();
expect(result).toBeUndefined();
const event = new Event("ping");
target.dispatchEvent(event);
await flush();
expect(result).toEqual({ value: event, done: false });
const target = new EventTarget();
const pings = on(target, "ping");
const first = new Event("ping");
const second = new Event("ping");
target.dispatchEvent(first);
target.dispatchEvent(new Event("pong"));
target.dispatchEvent(second);
const results = [];
pings.next().then((result) => results.push(result));
pings.next().then((result) => results.push(result));
await flush();
expect(results).toEqual([
{ value: first, done: false },
{ value: second, done: false },
]);
const target = new EventTarget();
const pings = on(target, "ping");
const results = [];
pings.next().then((result) => results.push(["first call", result.value]));
pings.next().then((result) => results.push(["second call", result.value]));
const a = new Event("ping");
const b = new Event("ping");
target.dispatchEvent(a);
target.dispatchEvent(b);
await flush();
expect(results).toEqual([
["first call", a],
["second call", b],
]);
const target = { addEventListener: spy(), removeEventListener: spy() };
on(target, "ping");
expect(target.addEventListener).toHaveBeenCalledTimes(1);
expect(target.addEventListener.calls[0].args[0]).toBe("ping");
expect(target.removeEventListener).not.toHaveBeenCalled();
const target = { addEventListener: spy(), removeEventListener: spy() };
const pings = on(target, "ping");
expect(target.addEventListener).toHaveBeenCalledTimes(1);
const added = target.addEventListener.calls[0].args[1];
let returned;
pings.return().then((result) => {
returned = result;
});
await flush();
expect(returned).toEqual({ value: undefined, done: true });
expect(target.removeEventListener).toHaveBeenCalledTimes(1);
const [type, removed] = target.removeEventListener.calls[0].args;
expect(type).toBe("ping");
// removeEventListener removes nothing unless it gets the very function addEventListener got.
expect({ sameListenerAsAdded: removed === added }).toEqual({ sameListenerAsAdded: true });
const target = new EventTarget();
const pings = on(target, "ping");
const results = [];
pings.next().then((result) => results.push(result));
pings.next().then((result) => results.push(result));
pings.return();
await flush();
expect(results).toEqual([
{ value: undefined, done: true },
{ value: undefined, done: true },
]);
const target = new EventTarget();
const pings = on(target, "ping");
target.dispatchEvent(new Event("ping"));
pings.return();
target.dispatchEvent(new Event("ping"));
const results = [];
pings.next().then((result) => results.push(result));
pings.next().then((result) => results.push(result));
await flush();
expect(results).toEqual([
{ value: undefined, done: true },
{ value: undefined, done: true },
]);
const target = new EventTarget();
const removeEventListener = spy((type, listener) => EventTarget.prototype.removeEventListener.call(target, type, listener));
target.removeEventListener = removeEventListener;
const pings = on(target, "ping");
expect(pings[Symbol.asyncIterator]()).toBe(pings);
const seen = [];
let failure;
(async () => {
for await (const event of pings) {
seen.push(event);
if (seen.length === 2) break;
}
})().catch((error) => {
failure = error;
});
const a = new Event("ping");
const b = new Event("ping");
target.dispatchEvent(a);
target.dispatchEvent(b);
target.dispatchEvent(new Event("ping"));
await flush();
expect(failure).toBeUndefined();
expect(seen).toEqual([a, b]);
expect(removeEventListener).toHaveBeenCalledTimes(1);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Turn events into an async iterator. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.