Inverted Repeaters

Sometimes you want to create async iterators which respond to calls to next as asynchronous events themselves. For instance, you might want create a timer which fires a fixed period of time after next is called, or even throws an error if it is not called within that fixed period of time. You can create these inverted repeaters by taking advantage of the fact that repeaters unwrap and await promises passed to the push function.

const timer = new Repeater(async (push, stop) => {
const timers = [];
let stopped = false;
stop.then(() => (stopped = true));
try {
while (!stopped) {
let resolve;
let reject;
const promise = new Promise((resolve1, reject1) => {
resolve = resolve1;
reject = reject1;
});
const timer = {
resolve,
reject,
timeout: setTimeout(() => {
resolve(Date.now());
timers.unshift();
}, 1000),
};
timers.push(timer);
await push(promise);
}
} finally {
for (const timer of timers) {
timer.reject(new Error("This error is never seen"));
clearTimeout(timer.timeout);
}
}
});

In the example above, we create a promise and retain its resolve and reject functions so that we can settle the promise later. We then call setTimeout and push the promise. Next, we await the push call so that we can create more timeouts as needed by the consumer.

Finally, to cleanup the repeater, we reject any pending promises and call clearTimeout on all outstanding timeouts. Because pushed promises which reject after stop are dropped, the repeater finishes instead of producing new values.

The @repeaterjs/timer package exports the delay and timeout utility functions, which use this inverted repeater pattern described above.