shelving
Version:
Toolkit for using data in JavaScript.
117 lines (116 loc) • 5.1 kB
JavaScript
import { UnexpectedError } from "../error/UnexpectedError.js";
import { createDeferred, getDelay } from "./async.js";
import { ABORT } from "./constants.js";
/** Turn a `Promise<R>` into an `IteratorAbortResult<R>` */
function _awaitAbortResult(value) {
return value.then(_getAbortResult);
}
/** Turn a result `R` into an `IteratorAbortResult<R>` */
function _getAbortResult(value) {
return { done: ABORT, value };
}
/** Call an iterator's `return()` method (if it exists) with an initial value, and return the `value` it returns. */
async function _iteratorReturn(iterator, initial, caller) {
if (iterator.return) {
const { done, value } = await iterator.return(initial);
if (done)
return value;
throw new UnexpectedError("Iterator return() must return done result", { received: done, iterator, caller });
}
return initial;
}
/**
* Is a value an async iterable object?
* - Any object with a `Symbol.iterator` property is iterable.
* - Note: Array and Map instances etc will return true because they implement `Symbol.iterator`
*/
export function isSequence(value) {
return typeof value === "object" && !!value && Symbol.asyncIterator in value;
}
/** Infinite sequence that yields until a `SIGNAL` is received. */
export async function* repeatUntil(source, ...signals) {
const iterator = source[Symbol.asyncIterator]();
const aborts = signals.map(_awaitAbortResult);
let n;
while (true) {
try {
const next = iterator.next(n);
const { done, value } = await (aborts.length ? Promise.race([next, ...aborts]) : next);
if (done) {
// For aborts, tell the iterator we're no longer using it.
if (done === ABORT)
return _iteratorReturn(iterator, value, repeatUntil);
// Don't need to do this for `done: true` results from the iterator, because we assume the iterator stopped itself.
return value;
}
n = yield value;
}
catch (thrown) {
// If the iterator threw we need to tell the iterator we're no longer using it so it can dispose of its resources.
// Most iterators and generators will already have cleaned up when they threw.
// But the iterator protocol does not _require_ that, so to be safe we explicitly tell the iterator it's no longer being used.
if (iterator.return)
await iterator.return(undefined);
// Rethrow the error.
throw thrown;
}
}
}
/** Infinite sequence that yields every X milliseconds (yields a count of the number of iterations). */
export async function* repeatDelay(ms) {
let count = 1;
while (true) {
await getDelay(ms);
yield count++;
}
}
/** Dispatch items in a sequence to a (possibly async) callback. */
export async function* callSequence(sequence, callback) {
for await (const item of sequence) {
callback(item);
yield item;
}
}
/**
* Iterate over a sequence until the returned `stop()` callback is called.
*
* Regarding errors:
* - Does not stop iterating on errors, simply sends the error to `onError()` and continues to iterate.
* - On the following iterator after throwing an error, a "generator" will return `done: true` (because they regard errors as concluding the iteration).
* - But the iterator protocol does not require this, so this continues to iterate until it's explicitly ended via the returned `stop()` callback.
*
* @return Callback function that can end the sequence run.
*/
export function runSequence(sequence, onNext, onError, onReturn) {
const { promise, resolve } = createDeferred();
void _runSequenceIterator(sequence[Symbol.asyncIterator](), promise, onNext, onError, onReturn);
return (value) => resolve(_getAbortResult(value));
}
async function _runSequenceIterator(iterator, stopped, onNext, onError, onReturn) {
let n;
while (true) {
try {
const { done, value } = await Promise.race([stopped, iterator.next(n)]);
if (done) {
// For manual aborts tell the iterator we're no longer using it.
if (done === ABORT) {
const v = await _iteratorReturn(iterator, value, runSequence);
return onReturn?.(v);
}
// Don't need to do this for `done: true` results from the iterator, because we assume the iterator stopped itself.
return onReturn?.(value);
}
n = onNext?.(value);
}
catch (reason) {
// Just send the reason to the error handler don't stop iteration.
// On the next iteration generators and most iterators that keep internal state will return `done: true` anyway.
onError?.(reason);
}
}
}
/** Merge several sequences (calls the sequences in series). */
export async function* mergeSequences(...sequences) {
for await (const sequence of sequences)
yield* sequence;
}