What are repeaters?
Note: These docs assume some familiarity with recent javascript features, specifically promises, async/await and iterators/generators.
The Repeater
object represents an asynchronous sequence of values. These values can be read using the methods found on the async iterator interface. Repeater.prototype.next
returns a promise which resolves to the next iteration result, and Repeater.prototype.return
prematurely ends iteration. Repeaters are most useful when consumed via for await…of
loops, which call and await the repeater’s next
and return
methods automatically.
Repeaters are designed with the explicit goal of behaving exactly like async generator objects and contain no methods or properties not found on async iterator interface. If you discover a discrepancy between repeaters and async generators, please open an issue.
Creating repeaters
Inspired by the Promise
constructor, the Repeater
constructor takes an executor, a function which is passed the arguments push
and stop
. These arguments are analogous to the resolve
and reject
functions passed to the promise executor: push
can be called with a value so that next
resolves to that value, and stop
can be called with an error so that next
rejects with that error.
const repeater = new Repeater((push, stop) => {push(1);stop(new Error("My error"));});(async () => {console.log(await repeater.next()); // { value: 1, done: false }try {console.log(await repeater.next()); // This line throws an error.} catch (err) {console.log(err); // Error: My error}})();
However, unlike resolve
, push
can be called more than once to enqueue multiple values, and unlike reject
, stop
can be called with no arguments to close the repeater without error.
const repeater = new Repeater((push, stop) => {push(1);push(2);push(3);push(4);stop();});(async () => {console.log(await repeater.next()); // { value: 1, done: false }console.log(await repeater.next()); // { value: 2, done: false }console.log(await repeater.next()); // { value: 3, done: false }console.log(await repeater.next()); // { value: 4, done: false }console.log(await repeater.next()); // { done: true }})();
In addition, the executor API exposes promises which resolve according to the state of the repeater. The push
function returns a promise which resolves when next
is called, and the stop
function doubles as a promise which resolves when the repeater is stopped. As a promise, stop
can be awaited to defer event listener cleanup.
const repeater = new Repeater(async (push, stop) => {console.log("repeater started!");await push(1);console.log("pushed 1");await push(2);console.log("pushed 2");await stop;console.log("done");});(async () => {console.log(await repeater.next());// repeater started!// { value: 1, done: false }console.log(await repeater.next());// "pushed 1"// { value: 2, done: false }console.log(await repeater.return());// "pushed 2"// "done"// { done: true }})();
These two arguments make it easy to setup and teardown callbacks within the executor, and they can be exposed to parent closures to model architectural patterns like generic pubsub classes and semaphores.
Acknowledgments
Thanks to Clojure’s core.async
for inspiring the basic data structure and algorithm for pushing and pulling values. The implementation of repeaters is more or less based on this presentation explaining core.async
internals.
Thanks to this StackOverflow answer for providing a helpful overview of the different types of async APIs available in javascript.