modern-async
Version:
A modern tooling library for asynchronous operations using async/await, promises and async generators
166 lines (153 loc) • 5.22 kB
JavaScript
import assert from 'nanoassert'
import asyncWrap from './asyncWrap.mjs'
import asyncIterableWrap from './asyncIterableWrap.mjs'
import getQueue from './getQueue.mjs'
import reflectAsyncStatus from './reflectAsyncStatus.mjs'
/**
* @ignore
* @param {*} iterable ignore
* @param {*} iteratee ignore
* @param {*} queueOrConcurrency ignore
* @param {*} ordered ignore
* @returns {*} ignore
*/
async function asyncFindInternal (iterable, iteratee, queueOrConcurrency, ordered) {
assert(typeof iteratee === 'function', 'iteratee must be a function')
iteratee = asyncWrap(iteratee)
const it = asyncIterableWrap(iterable)
const queue = getQueue(queueOrConcurrency)
/**
* @ignore
*/
class CustomCancelledError extends Error {}
let lastIndexFetched = -1
let fetching = false
let hasFetchedValue = false
let fetchedValue = null
let exhausted = false
let shouldStop = false
let lastIndexHandled = -1
const results = []
let waitListIndex = 0
const waitList = new Map()
const addToWaitList = (fct) => {
const identifier = waitListIndex
waitListIndex += 1
const p = (async () => {
return [identifier, await reflectAsyncStatus(fct)]
})()
assert(!waitList.has(identifier), 'waitList already contains identifier')
waitList.set(identifier, p)
}
const raceWaitList = async () => {
assert(waitList.size >= 1, 'Can not race on empty list')
const [identifier] = await Promise.race([...waitList.values()])
const removed = waitList.delete(identifier)
assert(removed, 'waitList does not contain identifier')
}
let scheduledCount = 0
const scheduledList = new Map()
const schedule = (index, value) => {
scheduledCount += 1
const task = {
value,
index,
cancel: null,
state: null
}
scheduledList.set(index, task)
addToWaitList(async () => {
const p = queue.exec(async () => {
if (task.state === 'cancelled') {
throw new CustomCancelledError()
}
assert(task.state === 'scheduled', 'invalid task state')
const removed = scheduledList.delete(index)
assert(removed, 'Couldn\'t find index in scheduledList for removal')
const snapshot = await reflectAsyncStatus(() => iteratee(value, index, iterable))
scheduledCount -= 1
insertInResults(index, value, snapshot)
if (snapshot.status === 'rejected' || (snapshot.status === 'fulfilled' && snapshot.value)) {
shouldStop = true
cancelAllScheduled(ordered ? index : 0)
}
})
assert(task.cancel === null, 'task already has cancel')
task.cancel = () => {
assert(task.state === 'scheduled', 'task should be scheduled')
task.state = 'cancelled'
}
assert(task.state === null, 'task should have no state')
task.state = 'scheduled'
return p
})
}
const cancelAllScheduled = (fromIndex) => {
for (const index of [...scheduledList.keys()].filter((el) => el >= fromIndex)) {
const task = scheduledList.get(index)
assert(task.cancel, 'task does not have cancel')
task.cancel()
const removed = scheduledList.delete(index)
assert(removed, 'Couldn\'t find index in scheduledList for removal')
}
}
const fetch = () => {
fetching = true
addToWaitList(async () => {
const snapshot = await reflectAsyncStatus(() => it.next())
fetching = false
if (snapshot.status === 'fulfilled') {
const { value, done } = snapshot.value
if (!done) {
lastIndexFetched += 1
assert(fetchedValue === null, 'fetchedValue should be null')
fetchedValue = value
assert(!hasFetchedValue, 'hasFetchedValue should be false')
hasFetchedValue = true
} else {
exhausted = true
}
} else {
exhausted = true
lastIndexFetched += 1
const index = lastIndexFetched
insertInResults(index, undefined, snapshot)
cancelAllScheduled(ordered ? index : 0)
}
})
}
const insertInResults = (index, value, snapshot) => {
if (ordered) {
assert(index - lastIndexHandled - 1 >= 0, 'invalid index to insert')
assert(results[index - lastIndexHandled - 1] === undefined, 'already inserted result')
results[index - lastIndexHandled - 1] = { index, value, snapshot }
} else {
results.push({ index, value, snapshot })
}
}
fetch()
while (true) {
await raceWaitList()
while (results.length >= 1 && results[0] !== undefined) {
const result = results.shift()
lastIndexHandled += 1
if (result.snapshot.status === 'rejected') {
throw result.snapshot.reason
} else if (result.snapshot.value) {
return [result.index, result.value]
}
}
if (exhausted && lastIndexFetched === lastIndexHandled) {
return [-1, undefined]
}
if (hasFetchedValue && !shouldStop) {
schedule(lastIndexFetched, fetchedValue)
hasFetchedValue = false
fetchedValue = null
}
if (!fetching && !exhausted && !shouldStop && scheduledCount < queue.concurrency) {
fetch()
}
}
}
export default asyncFindInternal