@virtualstate/union
Version:
Concurrent union
302 lines • 11.5 kB
JavaScript
import { asAsync } from "./async.js";
import { deferred } from "./deferred.js";
import { defaultTask } from "./microtask.js";
import { aggregateError } from "./aggregate-error.js";
import { isReuse } from "./reuse.js";
import { isPromise } from "./is-promise.js";
import { isAsyncIterable } from "./is-async-iterable.js";
const NextMicrotask = Symbol();
function map(iterable, map) {
return [...doMap()];
function* doMap() {
for (const value of iterable) {
yield map(value);
}
}
}
function filter(iterable, filter) {
return [...doFilter()];
function* doFilter() {
for (const value of iterable) {
if (filter(value))
yield value;
}
}
}
export async function* union(source, options = {}) {
const task = options.queueTask || options.queueMicrotask || defaultTask;
const states = new Map();
const inFlight = new Map();
const iterators = new WeakMap();
const knownIterators = [];
let iteratorsDone = false;
let valuesDone = false;
// I wish I could use "const isSourceIterable: source is Iterable<Input<T>> = isIterable(source);"
const isSourceAsyncIterable = isAsyncIterable(source);
const sourceIterator =
// Prefer async iteration, as then it can be chosen as to how the iteration happens
// We are in an async context either way.
// Only if the source is only iterable, we want to use the sync iterator
isAsyncIterable(source) ?
source[Symbol.asyncIterator]() :
source[Symbol.iterator]();
let active = undefined;
let iteratorsPromise = Promise.resolve();
let iteratorAvailable = deferred();
const iteratorsComplete = deferred();
const errorOccurred = deferred();
const errors = [];
let results = [];
let currentTaskPromise;
if (options.eagerInitialTask) {
nextTask();
}
let skipTaskDueToEmptyYield = false;
try {
cycle: do {
const iteration = active = Symbol();
iteratorsPromise = iteratorsPromise.then(() => nextLanes(iteration));
if (!knownIterators.length) {
await Promise.any([
iteratorAvailable.promise,
iteratorsComplete.promise
]);
if (valuesDone) {
// Had no lanes
break;
}
}
const updated = await waitForResult(iteration);
if (errors.length) {
break;
}
for (const result of updated) {
const { iterator, promise } = result;
const currentPromise = inFlight.get(iterator);
if (promise !== currentPromise) {
onError(new Error("Unexpected promise state"));
break cycle;
}
inFlight.set(iterator, undefined);
states.set(iterator, {
...result,
value: result.done ? states.get(iterator)?.value : result.value,
resolvedIteration: iteration
});
}
const finalResults = map(knownIterators, read);
valuesDone = iteratorsDone && finalResults.every(result => result?.done);
const onlyDone = !!updated.every(result => result.done);
// Don't yield only done because the consumer already has received all these values
if (onlyDone) {
continue;
}
if (!valuesDone) {
yield finalResults.map(result => result?.value);
}
} while (!valuesDone);
}
catch (error) {
onError(error);
}
finally {
active = undefined;
await sourceIterator.return?.();
}
if (errors.length) {
throw aggregateError(errors);
}
function read(iterator) {
return states.get(iterator);
}
async function nextLanes(iteration) {
let anyResult = false;
while (active === iteration && !iteratorsDone) {
let result = sourceIterator.next();
if (isPromise(result)) {
result = await result;
}
if (!isIteratorYieldResult(result)) {
iteratorsDone = true;
if (!knownIterators.length) {
valuesDone = true;
}
iteratorsComplete.resolve();
const returned = sourceIterator.return?.();
if (isPromise(returned)) {
await returned;
}
}
else if (result.value) {
const sourceLane = result.value;
const iterator = getIterator(sourceLane);
if (options.reuseInFlight || isReuse(sourceLane)) {
iterators.set(sourceLane, iterator);
}
knownIterators.push(iterator);
/**
* If we have a source iterable, we know we will
* fully resolve our knownIterators within a sync cycle,
* meaning it will be wasteful to create and resolve multiple
* promises. We will resolve at the end now that anyResult is set
*
* If our source is async, we know we will have at least one promise
* resolution in between each available lane. This means we will have
* a break in context between here and the end of this loop. In the
* async case we want to resolve iteratorAvailable ASAP.
*/
anyResult = true;
if (isSourceAsyncIterable) {
iteratorAvailable.resolve();
iteratorAvailable = deferred();
}
}
}
if (!isSourceAsyncIterable && anyResult) {
iteratorAvailable.resolve();
iteratorAvailable = deferred();
}
function getIterator(sourceLane) {
if (options.reuseInFlight || isReuse(sourceLane)) {
const currentIterator = iterators.get(sourceLane);
if (currentIterator) {
const state = read(currentIterator);
if (state?.done !== true) {
// reuse
return currentIterator;
}
}
}
return asAsync(sourceLane)[Symbol.asyncIterator]();
}
function isIteratorYieldResult(result) {
return !result.done;
}
}
async function waitForResult(iteration, emptyDepth = 0) {
if (iteration !== active) {
// Early exit if we actually aren't iterating this any more
// I don't think this can actually trigger, but lets keep it around
return [];
}
if (errors.length) {
// We have a problem, exit
return [];
}
const pendingIterators = filter(knownIterators, iterator => !read(iterator)?.done);
if (iteratorsDone && !pendingIterators.length) {
// No lanes to do anything, exit
return [];
}
// Grab onto this promise early so we don't miss one
const nextIterator = iteratorAvailable.promise;
if (!pendingIterators.length) {
// If our iterators complete while we are waiting for our next iterator,
await Promise.any([
nextIterator,
iteratorsComplete.promise
]);
return waitForResult(iteration);
}
const currentResults = await wait();
if (!currentResults.length) {
if (emptyDepth > 10000) {
throw new Error("Empty depth over 10000");
}
// We have a new lane available, lets loop around and initialise its promise
return waitForResult(iteration, emptyDepth + 1);
}
return currentResults;
async function wait() {
const promises = pendingIterators.map(next);
let anyPromises;
const waitForAtLeast = [
currentTaskPromise,
// Every iterator returned a result before the end of the task
Promise.all(promises),
// Early exit on any errors
errorOccurred.promise,
// We will be able to set up again next loop
iteratorAvailable.promise
];
if (!skipTaskDueToEmptyYield && !currentTaskPromise) {
// We didn't just run a task, and we have no pending next task
nextTask();
}
if (currentTaskPromise) {
// The current task finished and we should yield at least one result
waitForAtLeast.push(currentTaskPromise);
}
else {
// If we are no waiting for a task, we are waiting for at least one result
anyPromises = Promise.any(promises);
waitForAtLeast.push(anyPromises);
}
const reason = await Promise.any(waitForAtLeast);
if (reason === NextMicrotask) {
currentTaskPromise = undefined;
if (!results.length) {
skipTaskDueToEmptyYield = true;
}
}
if (!results.length) {
await Promise.any([
// We must wait for at least one result
anyPromises || Promise.any(promises),
// Or if there is a new iterator available, this iterator could
// potentially produce a result before all others
iteratorAvailable.promise,
errorOccurred.promise
]);
}
if (errors.length) {
return [];
}
if (!results.length) {
return [];
}
// Clone so that it only uses the values we have now
const cloned = [...results];
// Clear to start again
results = [];
return cloned;
}
async function next(iterator) {
const current = inFlight.get(iterator);
if (current)
return current;
const next = iterator.next()
.then((result) => ({
value: result.value,
done: !!result.done,
initialIteration: iteration,
iterator,
promise: next
}))
.catch((localError) => {
onError(localError);
return {
value: undefined,
done: true,
initialIteration: iteration,
iterator,
promise: next
};
})
.then((result) => {
results.push(result);
return result;
});
inFlight.set(iterator, next);
return next;
}
}
function onError(error) {
errors.push(error);
errorOccurred.resolve();
}
function nextTask() {
currentTaskPromise = new Promise(task).then(() => NextMicrotask);
}
}
//# sourceMappingURL=union.js.map