// operator/sequence.ts
/*
* Copyright (c) 2021-2026 Check Digit, LLC
*
* This code is licensed under the MIT license (see LICENSE.txt for details).
*/
import type { Asyncerator } from '../asyncerator.ts';
import type { Operator } from './index.ts';
/**
* The sequenceFunction will be called repeatedly with an incrementing numerical parameter, returning a Promise
* that resolves with the same type as Input and is inserted into the stream. The sequence operator
* passes through all other values. Because the sequenceFunction returns a Promise, it
* can delay its response (using setTimeout) to emit values on a regular schedule, e.g., once a second.
*
* @param sequenceFunction
*/
export default function (
sequenceFunction: (index: number) => Promise,
): Operator {
return async function* (iterator: Asyncerator) {
const queue: Input[] = [];
let isComplete = false;
let hasThrown = false;
let errorThrown: unknown;
/**
* sequence producer
*/
// eslint-disable-next-line @checkdigit/no-promise-instance-method
(async () => {
// before we do anything, allow the event loop to process.
// if the iterator completes immediately, we do not
// want any sequenceFunction execution.
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
let currentIndex = 0;
// eslint-disable-next-line no-unmodified-loop-condition,@typescript-eslint/no-unnecessary-condition
while (!isComplete && !hasThrown) {
// eslint-disable-next-line no-await-in-loop
queue.push(await sequenceFunction(currentIndex++));
// sequenceFunction may resolve immediately, so we need to allow the event loop to process before repeating
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
})()
// handle sequence errors without blocking the other producer or consumer.
// eslint-disable-next-line unicorn/prefer-await
.catch((error: unknown) => {
hasThrown = true;
errorThrown = error;
});
/**
* pass-through producer
*/
// eslint-disable-next-line @checkdigit/no-promise-instance-method
const passThroughProducer = (async () => {
for await (const item of iterator) {
// if the sequence producer throws an error, exit immediately (effectively cancel this promise)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (hasThrown) {
break;
}
queue.push(item);
}
})()
// the pass-through producer must run concurrently with the consumer.
// eslint-disable-next-line unicorn/prefer-await
.catch((error: unknown) => {
hasThrown = true;
errorThrown = error;
})
// preserve completion notification after the rejection handler settles.
// eslint-disable-next-line unicorn/prefer-await
.finally(() => {
isComplete = true;
});
/**
* queue consumer, runs concurrently with the producers above
*/
// eslint-disable-next-line no-unmodified-loop-condition,@typescript-eslint/no-unnecessary-condition
while (!isComplete && !hasThrown) {
if (queue.length === 0) {
// there's nothing pending yet, so let's allow some IO to occur...
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
// one or more promises may have completed, so yield everything in the queue
// drain into a separate array before yielding so producers can keep adding values.
// eslint-disable-next-line unicorn/no-unnecessary-splice
yield* queue.splice(0);
}
await passThroughProducer;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (hasThrown) {
throw errorThrown;
}
};
}