UNPKG

@virtualstate/union

Version:
302 lines 11.5 kB
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