UNPKG

modern-async

Version:

A modern tooling library for asynchronous operations using async/await, promises and async generators

1,351 lines (1,293 loc) 79.7 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var assert = require('nanoassert'); var setImmediate = require('core-js-pure/features/set-immediate.js'); var clearImmediate = require('core-js-pure/features/clear-immediate.js'); var queueMicrotask_ = require('core-js-pure/features/queue-microtask.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var assert__default = /*#__PURE__*/_interopDefaultLegacy(assert); var setImmediate__default = /*#__PURE__*/_interopDefaultLegacy(setImmediate); var clearImmediate__default = /*#__PURE__*/_interopDefaultLegacy(clearImmediate); var queueMicrotask___default = /*#__PURE__*/_interopDefaultLegacy(queueMicrotask_); /** * Wraps an iterable or async iterable into an iterable that is guaranted to be async. * * @param {Iterable | AsyncIterable} iterable An iterable or async iterable object. * @yields {any} The elements returned by the original iterable. * @example * import { asyncIterableWrap } from 'modern-async' * * // example sync generator * function* syncGenerator() { * for (let i = 0; i < 3; i += 1) { * yield i * } * } * * const asyncIterable = asyncIterableWrap(syncGenerator()) * * for await (const el of asyncIterable) { * console.log(el) * } * // will print: * // 0 * // 1 * // 2 */ async function * asyncIterableWrap (iterable) { for await (const el of iterable) { yield el; } } /** * Wraps a function call that may be synchronous in a function that * is guaranted to be async. This is a stricter version of calling a * function and wrapping its result using `Promise.resolve()` as the new function also * handles the case where the original function throws an exception. * * @param {Function} fct The function to wrap. * @returns {Function} The wrapped function. * @example * import { asyncWrap } from 'modern-async' * * const myFunction = () => { * // any kind of function that may or may not return a promise * } * * const asyncFct = asyncWrap(myFunction) * * const promise = asyncFct() * console.log(promise instanceof Promise) // prints true */ function asyncWrap (fct) { assert__default["default"](typeof fct === 'function', 'fct must be a function'); return async function () { return fct(...arguments) } } /** * Immediately calls an asynchronous function and redirects to an error handler if it throws an exception. * The error handler is optional, the default one just outputs the error in the console. * * This function is trivial but useful when you can't use top-level await for compatibility reasons. * * @param {Function} fct An asynchronous function to call. * @param {Function} [errorHandler] A facultative error handler. This function will receive a single argument: * the thrown exception. The default behavior is to output the exception in the console. * @example * import { asyncRoot } from 'modern-async' * * asyncRoot(async () => { * // any code using await * }, (e) => { * console.error("An error occured", e) * process.exit(-1) * }) */ async function asyncRoot (fct, errorHandler = null) { errorHandler = errorHandler || ((e) => { console.error(e); }); const asyncFct = asyncWrap(fct); try { await asyncFct(); } catch (e) { errorHandler(e); } } /** * An error type which is used when a promise is cancelled. */ class CancelledError extends Error { /** * Constructs a new instance. * * @param {string} message The error message */ constructor (message) { super(message); this.name = this.constructor.name; } } /** * A basic class to create a promise with its resolve and reject function in the same object. * * Instances of this class are never returned by any function of this library but it is used * internally and can be useful to code other asynchronous helpers. * * @example * import { Deferred, asyncSleep } from 'modern-async' * * const deferred = new Deferred() * * asyncSleep(10).then(() => { * deferred.resolve('test') * }) * * console.log(await deferred.promise) // will wait 10ms before printing 'test' */ class Deferred { /** * Constructs a deferred object. */ constructor () { this._promise = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); } /** * (Read-only) The promise. * * @member {Promise} * @returns {Promise} ignored */ get promise () { return this._promise } /** * (Read-only) The resolve function. * * @member {Function} * @returns {Function} The resolve function */ get resolve () { return this._resolve } /** * (Read-only) The reject function * * @member {Function} * @returns {Function} The reject function */ get reject () { return this._reject } } /** * A function returning a promise that will be resolved in a later tick of the event loop. * * This function returns both a promise and cancel function in order to cancel the wait time if * necessary. If cancelled, the promise will be rejected with a CancelledError. * * This function uses core-js' shim for `setImmediate()` internally. * * @returns {Array} A tuple of two objects: * * The promise * * The cancel function. It will return a boolean that will be true if the promise was effectively cancelled, * false otherwise. * @example * import { asyncDelayCancellable, CancelledError } from 'modern-async' * * const [promise, cancel] = asyncDelayCancellable() * cancel() * try { * await promise * } catch (e) { * console.log(e instanceof CancelledError) // prints true * } */ function asyncDelayCancellable () { const deferred = new Deferred(); const id = setImmediate__default["default"](deferred.resolve); let terminated = false; return [deferred.promise.finally(() => { terminated = true; }), () => { if (terminated) { return false } else { terminated = true; deferred.reject(new CancelledError()); clearImmediate__default["default"](id); return true } }] } /** * A function returning a promise that will be resolved in a later task of the event loop. * * This function uses core-js' shim for `setImmediate()` internally. * * @returns {Promise<void>} A promise that will be resolved on a later tick of the event loop. * @example * import { asyncDelay } from 'modern-async' * * console.log('this executes in a tick of the event loop') * await asyncDelay() * console.log('this executes in another tick of the event loop') */ async function asyncDelay () { return asyncDelayCancellable()[0] } /** * A class used to spread time or cpu intensive operations on multiple tasks in the event loop in order * to avoid blocking other tasks that may need to be executed. * * It is configured with a trigger time, which represents the maximum amount of time your tasks should * monopolize the event loop. Choosing an appropriate trigger time is both important and hard. If too low * it will impact the performances of your long running algorithm. If too high it will impact the other * tasks that need to run in the event loop. * * When using Delayer your code should contain frequent calls to `await delayer.checkDelay()`, usually * at the end of every loop. `checkDelay()` will check the amount of time that ellasped since the last time * it triggered a new task in the event loop. If the amount of time is below the trigger time it returns * an already resolved promise and the remaining computation will be able to continue processing in a * microtask. If not it will call the `asyncDelay()` function that will retrigger the operation in a later task * of the event loop. * * @example * import { Delayer } from 'modern-async' * * const delayer = new Delayer(10) // a delayer with 10ms trigger time * * // some cpu intensive operation that will run for a long time * for (let i = 0; i < 100000000; i += 1) { * // some code * await delayer.checkDelay() * } */ class Delayer { /** * Constructs a new `Delayer` by specifying its trigger time. * * @param {number} triggerTime The trigger time. */ constructor (triggerTime) { this.triggerTime = triggerTime; this.reset(); } /** * The trigger time of this `Delayer` in milliseconds. The trigger time represent the * maximum amount of time before a call to `checkDelay()` decide to schedule a new task in the event loop. * * @member {number} * @returns {number} ignore */ get triggerTime () { return this._triggerTime } /** * @ignore * @param {number} triggerTime ignore */ set triggerTime (triggerTime) { assert__default["default"](typeof triggerTime === 'number', 'trigger time must be a number'); this._triggerTime = triggerTime; } /** * Resets the internal timer to the current time. */ reset () { this._last = new Date().getTime(); } /** * Checks if a delay must be applied according to the internal timer. If that's the case this method * will call `asyncDelay()` and return `true`. If not it will do nothing and return `false`. * * @returns {boolean} `true` if a new task was scheduled in the event loop, `false` otherwise. */ async checkDelay () { const current = new Date().getTime(); if (current - this._last >= this.triggerTime) { await asyncDelay(); this.reset(); return true } else { return false } } } /** * An alternative to standard `queueMicrotask()` function. * * This is just of mirror of core-js' implementation for compatibility. * * @param {Function} fct The function to call in a microtask. * @example * import { queueMicrotask } from 'modern-async' * * queueMicrotask(() => { * console.log('this resolves in a micro task') * }) */ function queueMicrotask (fct) { queueMicrotask___default["default"](fct); } /** * A class representing a queue. * * Tasks added to the queue are processed in parallel (up to the concurrency limit). * If all slots of the queue are occupied, the task is queued until one becomes available. * When a slot is freed, the pending task with higher priority is executed. If multiple pending tasks have the same * priority the first that was scheduled is executed. * * Once a task is completed, its corresponding promise is terminated accordingly. * * @example * import { Queue, asyncSleep } from 'modern-async' * * const queue = new Queue(3) // create a queue with concurrency 3 * * const array = Array.from(Array(100).keys()) // an array of 100 numbers from 0 to 99 * * const promises = [] * for (const i of array) { * promises.push(queue.exec(async () => { * console.log(`Starting task ${i}`) * await asyncSleep(Math.random() * 10) // waits a random amount of time between 0ms and 10ms * console.log(`Ending task ${i}`) * return i; * })) * } * const results = await Promise.all(promises) * // all the scheduled tasks will perform with a maximum concurrency of 3 and log when they start and stop * * console.log(results) // will display an array with the result of the execution of each separate task */ class Queue { /** * Constructs a queue with the given concurrency * * @param {number} concurrency The concurrency of the queue, must be an integer greater than 0 or * `Number.POSITIVE_INFINITY`. */ constructor (concurrency) { assert__default["default"](Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY, 'concurrency must be an integer or positive infinity'); assert__default["default"](concurrency > 0, 'concurrency must be greater than 0'); this._concurrency = concurrency; this._iqueue = []; this._running = 0; this._checkQueueScheduled = false; } /** * (Read-only) The concurrency of the queue. * * @member {number} * @returns {number} ignore */ get concurrency () { return this._concurrency } /** * (Read-only) The current number of tasks that are processing. * * @member {number} * @returns {number} ignore */ get running () { return this._running } /** * (Read-only) The number of pending tasks. * * @member {number} * @returns {number} ignore */ get pending () { return this._iqueue.length - this.running } /** * Puts a task at the end of the queue. When the task is executed and completes the returned promise will be terminated * accordingly. * * @param {Function} fct An asynchronous functions representing the task. It will be executed when the queue has * available slots and its result will be propagated to the promise returned by exec(). * @param {number} [priority] The priority of the task. The higher the priority is, the sooner the task will be * executed regarding the priority of other pending tasks. Defaults to 0. * @returns {Promise} A promise that will be resolved or rejected once the task has completed. Its state will be the same * than the promise returned by the call to `fct`. */ async exec (fct, priority = 0) { return this.execCancellable(fct, priority)[0] } /** * Puts a task at the end of the queue. When the task is executed and completes the returned promise will be terminated * accordingly. * * This function returns both a promise and a cancel function. The cancel function allows to cancel the pending task, * but only if it wasn't started yet. Calling the cancel function on a task that it already running has no effect. * When a task is cancelled its corresponding promise will be rejected with a `CancelledError`. * * @param {Function} fct An asynchronous functions representing the task. It will be executed when the queue has * available slots and its result will be propagated to the promise returned by exec(). * @param {number} [priority] The priority of the task. The higher the priority is, the sooner the task will be * executed regarding the priority of other pending tasks. Defaults to 0. * @returns {Array} A tuple with two parameters: * * `promise`: A promise that will be resolved or rejected once the task has completed. Its state will be the same * than the promise returned by the call to `fct`. * * `cancel`: A cancel function. When called it will cancel the task if it is still pending. It has no effect is the * task has already started or already terminated. When a task is cancelled its corresponding promise will be * rejected with a `CancelledError`. If will return `true` if the task was effectively pending and was cancelled, * `false` in any other case. */ execCancellable (fct, priority = 0) { assert__default["default"](typeof fct === 'function', 'fct must be a function'); assert__default["default"](typeof priority === 'number', 'priority must be a number'); const deferred = new Deferred(); let i = this._iqueue.length; while (i >= 1) { const t = this._iqueue[i - 1]; if (t.priority >= priority) { break } i -= 1; } const task = { asyncFct: asyncWrap(fct), deferred, priority, state: 'pending' }; this._iqueue.splice(i, 0, task); this._scheduleCheckQueue(); return [deferred.promise, () => { if (task.state !== 'pending') { return false } else { const filtered = this._iqueue.filter((v) => v !== task); assert__default["default"](filtered.length < this._iqueue.length); this._iqueue = filtered; task.state = 'cancelled'; deferred.reject(new CancelledError()); return true } }] } /** * @ignore */ _scheduleCheckQueue () { if (this._checkQueueScheduled) { return } this._checkQueueScheduled = true; queueMicrotask(() => { this._checkQueueScheduled = false; this._checkQueue(); }); } /** * @ignore */ _checkQueue () { while (true) { assert__default["default"](this.running >= 0, 'invalid state'); assert__default["default"](this.running <= this.concurrency, 'invalid state'); if (this.running === this.concurrency) { return } const task = this._iqueue.find((v) => v.state === 'pending'); if (task === undefined) { return } this._running += 1; task.state = 'running'; queueMicrotask(() => { task.asyncFct().finally(() => { this._running -= 1; this._iqueue = this._iqueue.filter((v) => v !== task); }).then(task.deferred.resolve, task.deferred.reject).then(() => { this._scheduleCheckQueue(); }); }); } } /** * Cancels all pending tasks. Their corresponding promises will be rejected with a `CancelledError`. This method will * not alter tasks that are already running. * * @returns {number} The number of pending tasks that were effectively cancelled. */ cancelAllPending () { const toCancel = this._iqueue.filter((task) => task.state === 'pending'); this._iqueue = this._iqueue.filter((task) => task.state !== 'pending'); toCancel.forEach((task) => { task.deferred.reject(new CancelledError()); }); return toCancel.length } } /** * @ignore * @param {*} queueOrConcurrency ignore * @returns {*} ignore */ function getQueue (queueOrConcurrency) { if (typeof queueOrConcurrency === 'number') { return new Queue(queueOrConcurrency) } else { return queueOrConcurrency } } /** * Immediately calls an asynchronous function and wraps its result into a promise that * can only be resolved, not rejected, regardless of the state of the promised returned * by the function. * * The returned promise will contain an object with the following fields: * * * `status`: A string, either "fulfilled" or "rejected", indicating the state of the * original promise. * * `value`: Only present if status is "fulfilled". The value that the promise was * fulfilled with. * * `reason`: Only present if status is "rejected". The reason that the promise was * rejected with. * * This object structure is similar to the one used by the [`Promise.allSettled()` * function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled). * * This function can be useful to make use of other functions in a fault-tolerant way. * * @param {Function} fct An asynchronous function * @returns {Promise<any>} A promise that will always be resolved with an object containing * a snapshot of the original promise state. * @example * import { reflectAsyncStatus, asyncMap, asyncSleep } from 'modern-async' * * const array = [1, 2, 3] * * const result = await asyncMap(array, (v) => reflectAsyncStatus(async () => { * await asyncSleep(10) // waits 10ms * if (v % 2 === 0) { // throws error on some values * throw Error("error") * } * return v * })) * * console.log(result) * // prints: * // [ * // { status: 'fulfilled', value: 1 }, * // { status: 'rejected', reason: Error: error }, * // { status: 'fulfilled', value: 3 } * // ] */ async function reflectAsyncStatus (fct) { try { const res = await fct(); return { status: 'fulfilled', value: res } } catch (e) { return { status: 'rejected', reason: e } } } /** * @ignore * @param {*} iterable ignore * @param {*} iteratee ignore * @param {*} queueOrConcurrency ignore * @param {*} ordered ignore * @returns {*} ignore */ async function asyncFindInternal (iterable, iteratee, queueOrConcurrency, ordered) { assert__default["default"](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__default["default"](!waitList.has(identifier), 'waitList already contains identifier'); waitList.set(identifier, p); }; const raceWaitList = async () => { assert__default["default"](waitList.size >= 1, 'Can not race on empty list'); const [identifier] = await Promise.race([...waitList.values()]); const removed = waitList.delete(identifier); assert__default["default"](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__default["default"](task.state === 'scheduled', 'invalid task state'); const removed = scheduledList.delete(index); assert__default["default"](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__default["default"](task.cancel === null, 'task already has cancel'); task.cancel = () => { assert__default["default"](task.state === 'scheduled', 'task should be scheduled'); task.state = 'cancelled'; }; assert__default["default"](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__default["default"](task.cancel, 'task does not have cancel'); task.cancel(); const removed = scheduledList.delete(index); assert__default["default"](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__default["default"](fetchedValue === null, 'fetchedValue should be null'); fetchedValue = value; assert__default["default"](!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__default["default"](index - lastIndexHandled - 1 >= 0, 'invalid index to insert'); assert__default["default"](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(); } } } /** * Returns the index of the first element of an iterable that passes an asynchronous truth test. * * The calls to `iteratee` will be performed in a queue to limit the concurrency of these calls. * * Whenever a result is found, all the remaining tasks will be cancelled as long * as they didn't started already. In case of exception in one of the iteratee calls the promise * returned by this function will be rejected with the exception and the remaining pending * tasks will also be cancelled. In the very specific case where a result is found and an * already started task throws an exception that exception will be plainly ignored. * * @param {Iterable | AsyncIterable} iterable An iterable or async iterable object. * @param {Function} iteratee A function that will be called with each member of the iterable. It will receive * three arguments: * * `value`: The current value to process * * `index`: The index in the iterable. Will start from 0. * * `iterable`: The iterable on which the operation is being performed. * @param {Queue | number} [queueOrConcurrency] If a queue is specified it will be used to schedule the calls to * `iteratee`. If a number is specified it will be used as the concurrency of a Queue that will be created * implicitly for the same purpose. Defaults to `1`. * @param {boolean} [ordered] If true this function will return on the first element in the iterable * order for which `iteratee` returned true. If false it will be the first in time. * @returns {Promise<number>} A promise that will be resolved with the index of the first found value or rejected if one of the * `iteratee` calls throws an exception before finding a value. If no value is found it will return `-1`. * @example * // example using the default concurrency of 1 * import { asyncFindIndex, asyncSleep } from 'modern-async' * * const array = [1, 2, 3] * const result = await asyncFindIndex(array, async (v) => { * // these calls will be performed sequentially * await asyncSleep(Math.random() * 10) // waits a random amount of time between 0ms and 10ms * return v % 2 === 1 * }) * console.log(result) // prints 0 * @example * // example using a set concurrency * import { asyncFindIndex, asyncSleep } from 'modern-async' * * const array = [1, 2, 3, 4, 5] * const result = await asyncFindIndex(array, async (v) => { * // these calls will be performed in parallel with a maximum of 3 * // concurrent calls * await asyncSleep(Math.random() * 10) // waits a random amount of time between 0ms and 10ms * return v % 2 === 1 * }, 3) * console.log(result) // prints 0 or 2, randomly * @example * // example using infinite concurrency * import { asyncFindIndex, asyncSleep } from 'modern-async' * * const array = [1, 2, 3] * const result = await asyncFindIndex(array, async (v) => { * // these calls will be performed in parallel * await asyncSleep(Math.random() * 10) // waits a random amount of time between 0ms and 10ms * return v % 2 === 1 * }, Number.POSITIVE_INFINITY) * console.log(result) // prints 0 or 2, randomly */ async function asyncFindIndex (iterable, iteratee, queueOrConcurrency = 1, ordered = false) { const result = (await asyncFindInternal(iterable, iteratee, queueOrConcurrency, ordered))[0]; return result } /** * Returns `true` if all elements of an iterable pass a truth test and `false` otherwise. * * The calls to `iteratee` will be performed in a queue to limit the concurrency of these calls. * If any truth test returns `false` the promise is immediately resolved. * * Whenever a test returns `false`, all the remaining tasks will be cancelled as long * as they didn't started already. In case of exception in one of the iteratee calls the promise * returned by this function will be rejected with the exception and the remaining pending * tasks will also be cancelled. In the very specific case where a test returns `false` and an * already started task throws an exception that exception will be plainly ignored. * * @param {Iterable | AsyncIterable} iterable An iterable or async iterable object. * @param {Function} iteratee A function that will be called with each member of the iterable. It will receive * three arguments: * * `value`: The current value to process * * `index`: The index in the iterable. Will start from 0. * * `iterable`: The iterable on which the operation is being performed. * @param {Queue | number} [queueOrConcurrency] If a queue is specified it will be used to schedule the calls to * `iteratee`. If a number is specified it will be used as the concurrency of a Queue that will be created * implicitly for the same purpose. Defaults to `1`. * @returns {Promise<boolean>} A promise that will be resolved to `true` if all values pass the truth test and `false` * if a least one of them doesn't pass it. That promise will be rejected if one of the truth test throws * an exception. * @example * // example using the default concurrency of 1 * import { asyncEvery, asyncSleep } from 'modern-async' * * const array = [1, 2, 3] * * const result = await asyncEvery(array, async (v) => { * // these calls will be performed sequentially * await asyncSleep(10) // waits 10ms * return v > 0 * }) * console.log(result) // prints true * // total processing time should be ~ 30ms * @example * // example using a set concurrency * import { asyncEvery, asyncSleep } from 'modern-async' * * const array = [1, 2, 3] * * const result = await asyncEvery(array, async (v) => { * // these calls will be performed in parallel with a maximum of 2 * // concurrent calls * await asyncSleep(10) // waits 10ms * return v > 0 * }, 2) * console.log(result) // prints true * // total processing time should be ~ 20ms * @example * // example using infinite concurrency * import { asyncEvery, asyncSleep } from 'modern-async' * * const array = [1, 2, 3] * * const result = await asyncEvery(array, async (v) => { * // these calls will be performed in parallel * await asyncSleep(10) // waits 10ms * return v > 0 * }, Number.POSITIVE_INFINITY) * console.log(result) // prints true * // total processing time should be ~ 10ms */ async function asyncEvery (iterable, iteratee, queueOrConcurrency = 1) { assert__default["default"](typeof iteratee === 'function', 'iteratee must be a function'); iteratee = asyncWrap(iteratee); const index = await asyncFindIndex(iterable, async (value, index, iterable) => { return !(await iteratee(value, index, iterable)) }, queueOrConcurrency, false); const result = index === -1; return result } /** * Fully consumes an iteratable or async iterable an returns an array with all the elements it contained. * * @param {Iterable | AsyncIterable} iterable An iterator or async iterator. * @returns {Promise<any[]>} An array. * @example * import { asyncIterableToArray, asyncSleep } from 'modern-async' * * // example async generator * async function* asyncGenerator() { * for (let i = 0; i < 3; i += 1) { * await asyncSleep(10) * yield i * } * } * * console.log(await asyncIterableToArray(asyncGenerator())) * // prints [0, 1, 2] */ async function asyncIterableToArray (iterable) { const it = asyncIterableWrap(iterable); const results = []; for await (const el of it) { results.push(el); } return results } /** * Produces a an async iterator that will return each value or `iterable` after having processed them through * the `iteratee` function. * * The iterator will perform the calls to `iteratee` in a queue to limit the concurrency of * these calls. The iterator will consume values from `iterable` only if slots are available in the * queue. * * If the returned iterator is not fully consumed it will stop consuming new values from `iterable` and scheduling * new calls to `iteratee` in the queue, but already scheduled tasks will still be executed. * * If `iterable` or any of the calls to `iteratee` throws an exception all pending tasks will be cancelled and the * returned async iterator will throw that exception. * * @param {Iterable | AsyncIterable} iterable An iterable or async iterable object. * @param {Function} iteratee A function that will be called with each member of the iterable. It will receive * three arguments: * * `value`: The current value to process * * `index`: The index in the iterable. Will start from 0. * * `iterable`: The iterable on which the operation is being performed. * @param {Queue | number} [queueOrConcurrency] If a queue is specified it will be used to schedule the calls to * `iteratee`. If a number is specified it will be used as the concurrency of a Queue that will be created * implicitly for the same purpose. Defaults to `1`. * @param {boolean} [ordered] If true the results will be yielded in the same order as in the source * iterable, regardless of which calls to iteratee returned first. If false the the results will be yielded as soon * as a call to iteratee returned. Defaults to `true`. * @yields {any} Each element of `iterable` after processing it through `iteratee`. * @example * import {asyncGeneratorMap, asyncSleep} from 'modern-async' * * const iterator = function * () { * for (let i = 0; i < 10000; i += 1) { * yield i * } * } * const mapIterator = asyncGeneratorMap(iterator(), async (v) => { * await asyncSleep(1000) * return v * 2 * }) * for await (const el of mapIterator) { * console.log(el) * } * // Will print "0", "2", "4", etc... Only one number will be printed per second. * // Numbers from `iterator` will be consumed progressively */ async function * asyncGeneratorMap (iterable, iteratee, queueOrConcurrency = 1, ordered = true) { assert__default["default"](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__default["default"](!waitList.has(identifier), 'waitList contains identifier'); waitList.set(identifier, p); }; const raceWaitList = async () => { assert__default["default"](waitList.size >= 1, 'Can not race on empty list'); const [identifier] = await Promise.race([...waitList.values()]); const removed = waitList.delete(identifier); assert__default["default"](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__default["default"](task.state === 'scheduled', 'invalid task state'); const removed = scheduledList.delete(index); assert__default["default"](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') { shouldStop = true; cancelAllScheduled(ordered ? index : 0); } }); assert__default["default"](task.cancel === null, 'task already has cancel'); task.cancel = () => { assert__default["default"](task.state === 'scheduled', 'task should be scheduled'); task.state = 'cancelled'; }; assert__default["default"](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__default["default"](task.cancel, 'task does not have cancel'); task.cancel(); const removed = scheduledList.delete(index); assert__default["default"](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__default["default"](fetchedValue === null, 'fetchedValue should be null'); fetchedValue = value; assert__default["default"](!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__default["default"](index - lastIndexHandled - 1 >= 0, 'invalid index to insert'); assert__default["default"](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 { let yielded = false; try { yield result.snapshot.value; yielded = true; } finally { if (!yielded) { await it.return(); } } } } if (exhausted && lastIndexFetched === lastIndexHandled) { return } if (hasFetchedValue && !shouldStop) { schedule(lastIndexFetched, fetchedValue); hasFetchedValue = false; fetchedValue = null; } if (!fetching && !exhausted && !shouldStop && scheduledCount < queue.concurrency) { fetch(); } } } /** * Produces a an async iterator that will return each value or `iterable` which pass an asynchronous truth test. * * The iterator will perform the calls to `iteratee` in a queue to limit the concurrency of * these calls. The iterator will consume values from `iterable` only if slots are available in the * queue. * * If the returned iterator is not fully consumed it will stop consuming new values from `iterable` and scheduling * new calls to `iteratee` in the queue, but already scheduled tasks will still be executed. * * If `iterable` or any of the calls to `iteratee` throws an exception all pending tasks will be cancelled and the * returned async iterator will throw that exception. * * @param {Iterable | AsyncIterable} iterable An iterable or async iterable object. * @param {Function} iteratee A function that will be called with each member of the iterable. It will receive * three arguments: * * `value`: The current value to process * * `index`: The index in the iterable. Will start from 0. * * `iterable`: The iterable on which the operation is being performed. * @param {Queue | number} [queueOrConcurrency] If a queue is specified it will be used to schedule the calls to * `iteratee`. If a number is specified it will be used as the concurrency of a Queue that will be created * implicitly for the same purpose. Defaults to `1`. * @param {boolean} [ordered] If true the results will be yielded in the same order as in the source * iterable, regardless of which calls to iteratee returned first. If false the the results will be yielded as soon * as a call to iteratee returned. Defaults to `true`. * @yields {any} Each element of `iterable` for which `iteratee` returned `true`. * @example * import {asyncGeneratorFilter, asyncSleep} from 'modern-async' * * const iterator = function * () { * for (let i = 0; i < 10000; i += 1) { * yield i * } * } * const filterIterator = asyncGeneratorFilter(iterator(), async (v) => { * await asyncSleep(1000) * return v % 3 === 0 * }) * for await (const el of filterIterator) { * console.log(el) * } * // will print "0", "3", "6", etc... Only one number will be printed every 3 seconds. */ async function * asyncGeneratorFilter (iterable, iteratee, queueOrConcurrency = 1, ordered = true) { assert__default["default"](typeof iteratee === 'function', 'iteratee must be a function'); iteratee = asyncWrap(iteratee); for await (const [value, pass] of asyncGeneratorMap(iterable, async (v, i, t) => { return [v, await iteratee(v, i, t)] }, queueOrConcurrency, ordered)) { if (pass) { yield value; } } } /** * Returns an array of all the values in `iterable` which pass an asynchronous truth test. * * The calls to `iteratee` will be performed in a queue to limit the concurrency of these calls. * The results will be in the same order than in `iterable`. * * If any of the calls to `iteratee` throws an exception the returned promise will be rejected and the remaining * pending tasks will be cancelled. * * @param {Iterable | AsyncIterable} iterable An iterable or async iterable object. * @param {Function} iteratee A function that will be called with each member of `iterable`. It will receive * three arguments: * * `value`: The current value to process * * `index`: The index in the iterable. Will start from 0. * * `iterable`: The iterable on which the operation is being performed. * @param {Queue | number} [queueOrConcurrency] If a queue is specified it will be used to schedule the calls to * `iteratee`. If a number is specified it will be used as the concurrency of a Queue that will be created * implicitly for the same purpose. Defaults to `1`. * @returns {Promise<any[]>} A promise that will be resolved with an array containing all the values that passed * the truth test. This promise will be rejected if any of the `iteratee` calls throws an exception. * @example * // example using the default concurrency of 1 * import { asyncFilter, asyncSleep } from 'modern-async' * * const array = [1, 2, 3] * const result = await asyncFilter(array, async (v) => { * // these calls will be performed sequentially * await asyncSleep(10) // waits 10ms * return v % 2 === 1 * }) * console.log(result) // prints [1, 3] * // total processing time should be ~ 30ms * @example * // example using a set concurrency * import { asyncFilter, asyncSleep } from 'modern-async' * * const array = [1, 2, 3] * const result = await asyncFilter(array, async (v) => { * // these calls will be performed in parallel with a maximum of 2 * // concurrent calls * await asyncSleep(10) // waits 10ms * return v % 2 === 1 * }, 2) * console.log(result) // prints [1, 3] * // total processing time should be ~ 20ms * @example * // example using infinite concurrency * import { asyncFilter, asyncSleep } from 'modern-async' * * const array = [1, 2, 3] * const result = await asyncFilter(array, async (v) => { * // these calls will be performed in parallel * await asyncSleep(10) // waits 10ms * return v % 2 === 1 * }, Number.POSITIVE_INFINITY) * console.log(result) // prints [1, 3] * // total processing time should be ~ 10ms */ async function asyncFilter (iterable, iteratee, queueOrConcurrency = 1) { return await asyncIterableToArray(asyncGeneratorFilter(iterable, iteratee, queueOrConcurrency)) } /** * Returns the first element of an iterable that passes an asynchronous truth test. * * The calls to `iteratee` will be performed in a queue to limit the concurrency of these calls. * * Whenever a result is found, all the remaining tasks will be cancelled as long * as they didn't started already. In case of exception in one of the `iteratee` calls the promise * returned by this function will be rejected with the exception and the remaining pending * tasks will also be cancelled. In the very specific case where a result is found and an * already started task throws an exception that exception will be plainly ignored. * * @param {Iterable | AsyncIterable} iterable An iterable or async iterable object. * @param {Function} iteratee A function that will be called with each member of the iterable. It will receive * three arguments: * * `value`: The current value to process * * `index`: The index in the iterable. Will start from 0. * * `iterable`: The iterable on which the operation is being performed. * @param {Queue | number} [queueOrConcurrency] If a queue is specified it will be used to schedule the calls to * `iteratee`. If a number is specified it will be used as the concurrency of a Queue that will be created * implicitly for the same purpose. Defaults to `1`. * @param {boolean} [ordered] If true this function will return on the first element in the iterable * order for which `iteratee` returned true. If false it will be the first in time. * @returns {Promise<any | undefined>} A promise that will be resolved with the first found value or rejected if one of the * `iteratee` calls throws an exception before finding a value. If no value is found it will return `undefined`. * @example * // example using the default concurrency of 1 * import { asyncFind, asyncSleep } from 'modern-async' * * const array = [1, 2, 3] * const result = await asyncFind(array, async (v) => { * // these calls will be performed sequentially * await asyncSleep(Math.random() * 10) // waits a random amount of time between 0ms and 10ms * return v % 2 === 1 * }) * console.log(result) // prints 1 * @example * // example using a set concurrency * import { asyncFind, asyncSleep } from 'modern-async' * * const array = [1, 2, 3, 4, 5] * const result = await asyncFind(array, async (v) => { * // these calls will be performed in parallel with a maximum of 3 * // concurrent calls * await asyncSleep(Math.random() * 10) // waits a random amount of time between 0ms and 10ms * return v % 2 === 1 * }, 3) * console.log(result) // prints 1 or 3, randomly * @example * // example using infinite concurrency * import { asyncFind, asyncSleep } from 'modern-async' * * const array = [1, 2, 3] * const result = await asyncFind(array, async (v) => { * // these calls will be performed in parallel * await asyncSleep(Math.random() * 10) // waits a random amount of time between 0ms and 10ms * return v % 2 === 1 * }, Number.POSITIVE_INFINITY) * console.log(result) // prints 1 or 3, randomly */ async function asyncFind (iterable, iteratee, queueOrConcurrency = 1, ordered = false) { const result = (await asyncFindInternal(iterable, iteratee, queueOrConcurrency, ordered))[1]; return result } /** * Calls a function on each element of iterable. * * The calls to `iteratee` will be performed in a queue to limit the concurrency of these calls. * * If any of the calls to iteratee throws an exception the returned promise will be rejected and the remaining * pending tasks will be cancelled. * * @param {Iterable | AsyncIterable} iterable An iterable or async iterable object. * @param {Function} iteratee A function that will be called with each member of the iterable. It will receive * three arguments: * * `value`: The current value to process * * `index`: The index in the iterable. Will start from 0. * * `iterable`: The iterable on which the operation is being performed. * @param {Queue | number} [queueOrConcurrency] If a queue is specified it will be used to schedule the calls to * `iteratee`. If a number is specified it will be used as the concurrency of a Queue that will be created * implicitly for the same purpose. Defaults to `1`. * @returns {Promise} A promise that will be resolved when all the calls to `iteratee` have been done. * This promise will be rejected if any call to `iteratee` throws an exception. * @example * // example using the default concurrency of 1 * import { asyncForEach, asyncSleep } from 'modern-async' * * const array = [1, 2, 3] * await asyncForEach(array, async (v) => { * // these calls will be performed sequentially * await asyncSleep(Math.random() * 10) // waits a random amount of time between 0ms and 10ms * console.log(v) * }) * // prints 1, 2 and 3 in that exact order * @example * /