We should have a helper like Promise.withResolvers()
to add state from the outside.
A simple implementation would be something like this:
AsyncIterator.withResolvers ??= () => {
let controller;
let closed = false;
const abortController = new AbortController();
const signal = abortController.signal;
const stream = new ReadableStream({
start(controller_) {
controller = controller_;
},
cancel(reason) {
if (!closed) {
controller.error(reason);
closed = true;
}
abortController.abort(reason);
},
});
const [values, complete] = (() => {
const values = stream.values();
const originalReturn = values.return;
// for of .. break support
values.return = function (value) {
if (!closed) {
controller.close();
closed = true;
}
abortController.abort();
return originalReturn.call(this, value);
};
return [values, complete];
function complete(value) {
if (closed) return;
controller.close();
closed = true;
abortController.abort();
return originalReturn.call(values, value);
}
})();
return {
values,
resolve: controller.enqueue.bind(controller),
complete: () => complete(),
reject: controller.error.bind(controller),
signal,
};
}
Explanation of AsyncIterator.withResolvers()
:
This function is a utility to generate an externally controllable asynchronous iterator. It allows you to retrieve values asynchronously using the for await...of
construct.
Features:
- Asynchronous Iterator Generation: Generates an asynchronous iterator that can be used with the
for await...of
syntax. - External Control: Through the
resolve
,complete
, andreject
properties, values can be added, completion can be signaled, and errors can be generated for the iterator. - Early Iterator Termination Support: Ensures the iterator is properly cleaned up when it terminates early, such as by using
break
in afor await...of
loop.
Return Value Properties:
values
: The generated asynchronous iterator. Can be used with thefor await...of
syntax.resolve
: A function to add values to the iterator.complete
: A function to signal the successful completion of the iterator.reject
: A function to generate an error for the iterator.signal
: TheAbortSignal
to monitor for iterator cancellation.
Essentially, AsyncIterator.withResolvers()
allows you to create an AsyncIterator
that responds to external events (like button clicks or intervals) and signals (like abort signals). You can push values into it using resolve()
, signal completion using complete()
, or signal an error using reject()
.
Here's how to use it:
const { values, resolve, complete } = AsyncIterator.withResolvers();
buttonElement.addEventListener('click', resolve, { signal });
signal.addEventListener('abort', complete, {once: true});
for await (const event of values) {
// click event...
}
const { values, resolve, complete } = AsyncIterator.withResolvers();
const clear = setInterval(resolve, 1000);
signal.addEventListener('abort', () => {
clearInterval(clear);
complete();
}, {once:true});
for await (const _ of values) {
// interval ...
}