diff --git a/modern-async.d.ts b/modern-async.d.ts index db7e14e..ad74911 100644 --- a/modern-async.d.ts +++ b/modern-async.d.ts @@ -168,6 +168,29 @@ declare module "reflectAsyncStatus" { export default reflectAsyncStatus; function reflectAsyncStatus(fct: () => Promise | T): Promise>; } +declare module "generatorEntries" { + export default generatorEntries; + function generatorEntries(obj: object): Iterable<[any, any]>; +} +declare module "asyncFromEntries" { + export default asyncFromEntries; + function asyncFromEntries(iterable: Iterable | [K, V]> | AsyncIterable<[K, V]>): Promise; +} +declare module "asyncMapEntries" { + export default asyncMapEntries; + function asyncMapEntries(obj: object, iteratee: (value: V, key: K, obj: object) => Promise<[any, any]> | [any, any], queueOrConcurrency?: Queue | number): Promise; + import Queue from "Queue"; +} +declare module "asyncMapKeys" { + export default asyncMapKeys; + function asyncMapKeys(obj: object, iteratee: (value: V, key: K, obj: object) => Promise | any, queueOrConcurrency?: Queue | number): Promise; + import Queue from "Queue"; +} +declare module "asyncMapValues" { + export default asyncMapValues; + function asyncMapValues(obj: object, iteratee: (value: V, key: K, obj: object) => Promise | any, queueOrConcurrency?: Queue | number): Promise; + import Queue from "Queue"; +} declare module "modern-async" { export { default as asyncIterableWrap } from "asyncIterableWrap"; export { default as asyncRoot } from "asyncRoot"; @@ -200,4 +223,9 @@ declare module "modern-async" { export { default as asyncTimeoutPrecise } from "asyncTimeoutPrecise"; export { default as asyncIterableToArray } from "asyncIterableToArray"; export { default as reflectAsyncStatus } from "reflectAsyncStatus"; + export { default as generatorEntries } from "generatorEntries"; + export { default as asyncFromEntries } from "asyncFromEntries"; + export { default as asyncMapEntries } from "asyncMapEntries"; + export { default as asyncMapKeys } from "asyncMapKeys"; + export { default as asyncMapValues } from "asyncMapValues"; } diff --git a/src/Queue.mjs b/src/Queue.mjs index c23cd76..146d441 100644 --- a/src/Queue.mjs +++ b/src/Queue.mjs @@ -118,7 +118,6 @@ class Queue { * `false` in any other case. */ execCancellable (fct, priority = 0) { - assert(typeof fct === 'function', 'fct must be a function') assert(typeof priority === 'number', 'priority must be a number') const deferred = new Deferred() let i = this._iqueue.length diff --git a/src/asyncEvery.mjs b/src/asyncEvery.mjs index 867d3a0..ac4d405 100644 --- a/src/asyncEvery.mjs +++ b/src/asyncEvery.mjs @@ -1,13 +1,12 @@ import Queue from './Queue.mjs' import asyncWrap from './asyncWrap.mjs' -import assert from 'nanoassert' import asyncFindIndex from './asyncFindIndex.mjs' /** * 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. + * The calls to `iteratee` will be performed asynchronously in a {@link Queue}, allowing control over the concurrency of those 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 @@ -22,8 +21,8 @@ import asyncFindIndex from './asyncFindIndex.mjs' * * `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 + * @param {Queue | number} [queueOrConcurrency] If a {@link 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 {@link Queue} that will be created * implicitly for the same purpose. Defaults to `1`. * @returns {Promise} 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 @@ -70,7 +69,6 @@ import asyncFindIndex from './asyncFindIndex.mjs' * // total processing time should be ~ 10ms */ async function asyncEvery (iterable, iteratee, queueOrConcurrency = 1) { - assert(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)) diff --git a/src/asyncFilter.mjs b/src/asyncFilter.mjs index 33df40a..5a63036 100644 --- a/src/asyncFilter.mjs +++ b/src/asyncFilter.mjs @@ -6,7 +6,7 @@ import asyncGeneratorFilter from './asyncGeneratorFilter.mjs' /** * 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 calls to `iteratee` will be performed asynchronously in a {@link Queue}, allowing control over the concurrency of those 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 @@ -18,8 +18,8 @@ import asyncGeneratorFilter from './asyncGeneratorFilter.mjs' * * `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 + * @param {Queue | number} [queueOrConcurrency] If a {@link 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 {@link Queue} that will be created * implicitly for the same purpose. Defaults to `1`. * @returns {Promise} 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. diff --git a/src/asyncFilterObject.mjs b/src/asyncFilterObject.mjs new file mode 100644 index 0000000..4a6bb8a --- /dev/null +++ b/src/asyncFilterObject.mjs @@ -0,0 +1,20 @@ + +import Queue from './Queue.mjs' +import asyncFromEntries from './asyncFromEntries.mjs' +import asyncGeneratorFilter from './asyncGeneratorFilter.mjs' +import generatorEntries from './generatorEntries.mjs' +import asyncWrap from './asyncWrap.mjs' + +/** + * @param obj + * @param iteratee + * @param queueOrConcurrency + */ +async function asyncFilterObject (obj, iteratee, queueOrConcurrency = 1) { + iteratee = asyncWrap(iteratee) + return await asyncFromEntries(asyncGeneratorFilter(generatorEntries(obj), async ([k, v]) => { + return await iteratee(v, k, obj) + }, queueOrConcurrency)) +} + +export default asyncFilterObject diff --git a/src/asyncFind.mjs b/src/asyncFind.mjs index 478325f..3112591 100644 --- a/src/asyncFind.mjs +++ b/src/asyncFind.mjs @@ -5,7 +5,7 @@ import Queue from './Queue.mjs' /** * 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. + * The calls to `iteratee` will be performed asynchronously in a {@link Queue}, allowing control over the concurrency of those 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 @@ -19,8 +19,8 @@ import Queue from './Queue.mjs' * * `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 + * @param {Queue | number} [queueOrConcurrency] If a {@link 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 {@link 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. diff --git a/src/asyncFindIndex.mjs b/src/asyncFindIndex.mjs index 80da901..ded020c 100644 --- a/src/asyncFindIndex.mjs +++ b/src/asyncFindIndex.mjs @@ -5,7 +5,7 @@ import Queue from './Queue.mjs' /** * 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. + * The calls to `iteratee` will be performed asynchronously in a {@link Queue}, allowing control over the concurrency of those 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 @@ -19,8 +19,8 @@ import Queue from './Queue.mjs' * * `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 + * @param {Queue | number} [queueOrConcurrency] If a {@link 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 {@link 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. diff --git a/src/asyncFindInternal.mjs b/src/asyncFindInternal.mjs index 387b45d..3dece65 100644 --- a/src/asyncFindInternal.mjs +++ b/src/asyncFindInternal.mjs @@ -14,7 +14,6 @@ import reflectAsyncStatus from './reflectAsyncStatus.mjs' * @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) diff --git a/src/asyncFindKey.mjs b/src/asyncFindKey.mjs new file mode 100644 index 0000000..f7bb453 --- /dev/null +++ b/src/asyncFindKey.mjs @@ -0,0 +1,22 @@ + +import asyncFindInternal from './asyncFindInternal.mjs' +import generatorEntries from './generatorEntries.mjs' +import asyncWrap from './asyncWrap.mjs' +import Queue from './Queue.mjs' + +/** + * @param obj + * @param iteratee + * @param queueOrConcurrency + * @param ordered + */ +async function asyncFindKey (obj, iteratee, queueOrConcurrency = 1, ordered = false) { + iteratee = asyncWrap(iteratee) + // eslint-disable-next-line no-unused-vars + const [k, _] = (await asyncFindInternal(generatorEntries(obj), async ([k, v]) => { + return await iteratee(v, k, obj) + }, queueOrConcurrency, ordered))[1] + return k +} + +export default asyncFindKey diff --git a/src/asyncForEach.mjs b/src/asyncForEach.mjs index 67f9c07..ca99da0 100644 --- a/src/asyncForEach.mjs +++ b/src/asyncForEach.mjs @@ -5,7 +5,7 @@ import Queue from './Queue.mjs' /** * 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. + * The calls to `iteratee` will be performed asynchronously in a {@link Queue}, allowing control over the concurrency of those 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. @@ -16,8 +16,8 @@ import Queue from './Queue.mjs' * * `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 + * @param {Queue | number} [queueOrConcurrency] If a {@link 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 {@link 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. @@ -59,7 +59,7 @@ import Queue from './Queue.mjs' */ async function asyncForEach (iterable, iteratee, queueOrConcurrency = 1) { // eslint-disable-next-line no-unused-vars - for await (const _el of asyncGeneratorMap(iterable, iteratee, queueOrConcurrency)) { + for await (const _el of asyncGeneratorMap(iterable, iteratee, queueOrConcurrency, false)) { // do nothing } } diff --git a/src/asyncForEachObject.mjs b/src/asyncForEachObject.mjs new file mode 100644 index 0000000..0046e50 --- /dev/null +++ b/src/asyncForEachObject.mjs @@ -0,0 +1,22 @@ + +import asyncGeneratorMap from './asyncGeneratorMap.mjs' +import generatorEntries from './generatorEntries.mjs' +import Queue from './Queue.mjs' +import asyncWrap from './asyncWrap.mjs' + +/** + * @param obj + * @param iteratee + * @param queueOrConcurrency + */ +async function asyncForEachObject (obj, iteratee, queueOrConcurrency = 1) { + iteratee = asyncWrap(iteratee) + // eslint-disable-next-line no-unused-vars + for await (const _el of asyncGeneratorMap(generatorEntries(obj), async ([k, v]) => { + await iteratee(v, k, obj) + }, queueOrConcurrency, false)) { + // do nothing + } +} + +export default asyncForEachObject diff --git a/src/asyncFromEntries.mjs b/src/asyncFromEntries.mjs new file mode 100644 index 0000000..1a59ce2 --- /dev/null +++ b/src/asyncFromEntries.mjs @@ -0,0 +1,51 @@ + +import asyncIterableWrap from './asyncIterableWrap.mjs' + +/** + * Fully consumes an iterable or async iterable containing key-value pairs an returns + * a new object built with those key-value pairs. + * + * This function is an alternative to standard `Object.fromPairs` but accepting an async + * iterable. + * + * @param {Iterable | AsyncIterable} iterable An iterable or async iterable yielding key-value pairs. + * Key value-pairs must be tuples containing two objects: + * * The key + * * The value + * @returns {Promise} A promise that will be resolved with a new object built with the + * key-value pairs of the iterable. + * @see {@link generatorEntries} to convert an object to an iterable of key-value pairs + * @example + * // Example using a synchronous iterable + * import { asyncFromEntries } from 'modern-async' + * + * const entries = [["a", 1], ["b", 2], ["c", 3]] + * + * const obj = await asyncFromEntries(entries) + * console.log(obj) // prints Object { a: 1, b: 2, c: 3 } + * @example + * // Example using an asynchronous iterable + * import { asyncFromEntries, asyncSleep } from 'modern-async' + * + * async function * asyncEntryGenerator() { + * await asyncSleep(10) // waits 10ms + * yield ["a", 1] + * await asyncSleep(10) // waits 10ms + * yield ["b", 2] + * await asyncSleep(10) // waits 10ms + * yield ["c", 3] + * } + * + * const obj = await asyncFromEntries(asyncEntryGenerator()) + * console.log(obj) // prints Object { a: 1, b: 2, c: 3 } + */ +async function asyncFromEntries (iterable) { + const it = asyncIterableWrap(iterable) + const obj = {} + for await (const [key, value] of it) { + obj[key] = value + } + return obj +} + +export default asyncFromEntries diff --git a/src/asyncFromEntries.test.mjs b/src/asyncFromEntries.test.mjs new file mode 100644 index 0000000..fd6ec85 --- /dev/null +++ b/src/asyncFromEntries.test.mjs @@ -0,0 +1,31 @@ + +import { expect, test } from '@jest/globals' +import asyncFromEntries from './asyncFromEntries.mjs' + +test('asyncFromEntries base sync', async () => { + const entries = [['a', 1], ['b', 2], ['c', 3]] + + const obj = await asyncFromEntries(entries) + + expect(obj).toEqual({ + a: 1, + b: 2, + c: 3 + }) +}) + +test('asyncFromEntries base async', async () => { + const asyncEntryGenerator = async function * () { + yield ['a', 1] + yield ['b', 2] + yield ['c', 3] + } + + const obj = await asyncFromEntries(asyncEntryGenerator()) + + expect(obj).toEqual({ + a: 1, + b: 2, + c: 3 + }) +}) diff --git a/src/asyncGeneratorFilter.mjs b/src/asyncGeneratorFilter.mjs index ce9ee4f..a9fffad 100644 --- a/src/asyncGeneratorFilter.mjs +++ b/src/asyncGeneratorFilter.mjs @@ -1,21 +1,20 @@ import asyncGeneratorMap from './asyncGeneratorMap.mjs' -import assert from 'nanoassert' import Queue from './Queue.mjs' import asyncWrap from './asyncWrap.mjs' /** - * Produces a an async iterator that will return each value or `iterable` which pass an asynchronous truth test. + * Produces a an async iterable 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 + * The iterable will perform the calls to `iteratee` asynchronously in a {@link Queue} to limit the concurrency of + * these calls. The iterable 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 + * If the returned iterable 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. + * returned async iterable 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 @@ -23,8 +22,8 @@ import asyncWrap from './asyncWrap.mjs' * * `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 + * @param {Queue | number} [queueOrConcurrency] If a {@link 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 {@link 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 @@ -33,22 +32,21 @@ import asyncWrap from './asyncWrap.mjs' * @example * import {asyncGeneratorFilter, asyncSleep} from 'modern-async' * - * const iterator = function * () { + * const generator = function * () { * for (let i = 0; i < 10000; i += 1) { * yield i * } * } - * const filterIterator = asyncGeneratorFilter(iterator(), async (v) => { + * const filterGenerator = asyncGeneratorFilter(generator(), async (v) => { * await asyncSleep(1000) * return v % 3 === 0 * }) - * for await (const el of filterIterator) { + * for await (const el of filterGenerator) { * 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(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)] diff --git a/src/asyncGeneratorMap.mjs b/src/asyncGeneratorMap.mjs index c799444..9e4856e 100644 --- a/src/asyncGeneratorMap.mjs +++ b/src/asyncGeneratorMap.mjs @@ -7,18 +7,18 @@ import Queue from './Queue.mjs' import reflectAsyncStatus from './reflectAsyncStatus.mjs' /** - * Produces a an async iterator that will return each value or `iterable` after having processed them through + * Produces a an async iterable 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 + * The iterable will perform the calls to `iteratee` asynchronously in a {@link Queue} to limit the concurrency of + * these calls. The iterable 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 + * If the returned iterable 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. + * returned async iterable 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 @@ -26,8 +26,8 @@ import reflectAsyncStatus from './reflectAsyncStatus.mjs' * * `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 + * @param {Queue | number} [queueOrConcurrency] If a {@link 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 {@link 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 @@ -36,23 +36,22 @@ import reflectAsyncStatus from './reflectAsyncStatus.mjs' * @example * import {asyncGeneratorMap, asyncSleep} from 'modern-async' * - * const iterator = function * () { + * const generator = function * () { * for (let i = 0; i < 10000; i += 1) { * yield i * } * } - * const mapIterator = asyncGeneratorMap(iterator(), async (v) => { + * const mapGenerator = asyncGeneratorMap(generator(), async (v) => { * await asyncSleep(1000) * return v * 2 * }) - * for await (const el of mapIterator) { + * for await (const el of mapGenerator) { * console.log(el) * } * // Will print "0", "2", "4", etc... Only one number will be printed per second. - * // Numbers from `iterator` will be consumed progressively + * // Numbers from `generator` will be consumed progressively */ async function * asyncGeneratorMap (iterable, iteratee, queueOrConcurrency = 1, ordered = true) { - assert(typeof iteratee === 'function', 'iteratee must be a function') iteratee = asyncWrap(iteratee) const it = asyncIterableWrap(iterable) const queue = getQueue(queueOrConcurrency) diff --git a/src/asyncGroupBy.mjs b/src/asyncGroupBy.mjs new file mode 100644 index 0000000..a374308 --- /dev/null +++ b/src/asyncGroupBy.mjs @@ -0,0 +1,26 @@ + +import asyncGeneratorMap from './asyncGeneratorMap.mjs' +import asyncWrap from './asyncWrap.mjs' +import Queue from './Queue.mjs' + +/** + * @param iterable + * @param iteratee + * @param queueOrConcurrency + */ +async function asyncGroupBy (iterable, iteratee, queueOrConcurrency = 1) { + iteratee = asyncWrap(iteratee) + const groups = {} + for await (const [group, value] of asyncGeneratorMap(iterable, async (v, i, it) => { + const group = await iteratee(v, i, it) + return [group, v] + }, queueOrConcurrency)) { + if (!(group in groups)) { + groups[group] = [] + } + groups[group].push(value) + } + return groups +} + +export default asyncGroupBy diff --git a/src/asyncIterableToArray.mjs b/src/asyncIterableToArray.mjs index 88fb5f7..74e3242 100644 --- a/src/asyncIterableToArray.mjs +++ b/src/asyncIterableToArray.mjs @@ -2,9 +2,9 @@ import asyncIterableWrap from './asyncIterableWrap.mjs' /** - * Fully consumes an iteratable or async iterable an returns an array with all the elements it contained. + * Fully consumes an iterable or async iterable an returns an array with all the elements it contained. * - * @param {Iterable | AsyncIterable} iterable An iterator or async iterator. + * @param {Iterable | AsyncIterable} iterable An iterable or async iterable. * @returns {Promise} An array. * @example * import { asyncIterableToArray, asyncSleep } from 'modern-async' diff --git a/src/asyncMap.mjs b/src/asyncMap.mjs index 57e79ef..d67f4e0 100644 --- a/src/asyncMap.mjs +++ b/src/asyncMap.mjs @@ -6,7 +6,7 @@ import asyncIterableToArray from './asyncIterableToArray.mjs' /** * Produces a new collection of values by mapping each value in `iterable` through the `iteratee` function. * - * The calls to `iteratee` will be performed in a queue to limit the concurrency of these calls. + * The calls to `iteratee` will be performed asynchronously in a {@link Queue}, allowing control over the concurrency of those 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. @@ -17,8 +17,8 @@ import asyncIterableToArray from './asyncIterableToArray.mjs' * * `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 + * @param {Queue | number} [queueOrConcurrency] If a {@link 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 {@link Queue} that will be created * implicitly for the same purpose. Defaults to `1`. * @returns {Promise} A promise that will be resolved with an array containing all the mapped value, * or will be rejected if any of the calls to `iteratee` throws an exception. @@ -31,7 +31,7 @@ import asyncIterableToArray from './asyncIterableToArray.mjs' * // these calls will be performed sequentially * await asyncSleep(10) // waits 10ms * return v * 2 - * }, 2) + * }) * console.log(result) // prints [2, 4, 6] * // total processing time should be ~ 30ms * @example diff --git a/src/asyncMapEntries.mjs b/src/asyncMapEntries.mjs new file mode 100644 index 0000000..0b0574f --- /dev/null +++ b/src/asyncMapEntries.mjs @@ -0,0 +1,82 @@ + +import Queue from './Queue.mjs' +import generatorEntries from './generatorEntries.mjs' +import asyncFromEntries from './asyncFromEntries.mjs' +import asyncGeneratorMap from './asyncGeneratorMap.mjs' +import asyncWrap from './asyncWrap.mjs' + +/** + * Maps the key-value pairs found in an object by calling the `iteratee` + * function, returning a new key-value pair, and re-construct a new object with the new + * key-value pairs. + * + * The calls to `iteratee` will be performed asynchronously in a {@link Queue}, allowing control over the concurrency of those 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 {object} obj The object to iterate over values. + * @param {Function} iteratee A function that will be called with each key-value pair of the object. It will receive + * three arguments: + * * `value`: The current value to process + * * `key`: The current key to process + * * `obj`: The object on which the operation is being performed. + * + * That function must return a tuple containing two objects: + * * The mapped key. + * * The mapped value. + * @param {Queue | number} [queueOrConcurrency] If a {@link 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 {@link Queue} that will be created + * implicitly for the same purpose. Defaults to `1`. + * @returns {Promise} A promise that will be resolved with a new object built with the + * key-value pairs returned by `iteratee`. + * @see {@link asyncMapValues} to map values of an object + * @see {@link asyncMapKeys} to map keys of an object + * @example + * // example using the default concurrency of 1 + * import { asyncMapEntries, asyncSleep } from 'modern-async' + * + * const obj = {a: 1, b: 2, c: 3} + * const result = await asyncMapEntries(obj, async (v, k) => { + * // these calls will be performed sequentially + * await asyncSleep(10) // waits 10ms + * return [k + 'x', v * 2] + * }) + * console.log(result) // prints Object { ax: 2, bx: 4, cx: 6 } + * // total processing time should be ~ 30ms + * @example + * // example using a set concurrency + * import { asyncMapEntries, asyncSleep } from 'modern-async' + * + * const obj = {a: 1, b: 2, c: 3} + * const result = await asyncMapEntries(obj, async (v, k) => { + * // these calls will be performed in parallel with a maximum of 2 + * // concurrent calls + * await asyncSleep(10) // waits 10ms + * return [k + 'x', v * 2] + * }, 2) + * console.log(result) // prints Object { ax: 2, bx: 4, cx: 6 } + * // total processing time should be ~ 20ms + * @example + * // example using infinite concurrency + * import { asyncMap, asyncSleep } from 'modern-async' + * + * const obj = {a: 1, b: 2, c: 3} + * const result = await asyncMapEntries(obj, async (v, k) => { + * // these calls will be performed in parallel with a maximum of 2 + * // concurrent calls + * await asyncSleep(10) // waits 10ms + * return [k + 'x', v * 2] + * }, Number.POSITIVE_INFINITY) + * console.log(result) // prints Object { ax: 2, bx: 4, cx: 6 } + * // total processing time should be ~ 10ms + */ +async function asyncMapEntries (obj, iteratee, queueOrConcurrency = 1) { + iteratee = asyncWrap(iteratee) + return await asyncFromEntries(asyncGeneratorMap(generatorEntries(obj), async ([k, v]) => { + const [nk, nv] = await iteratee(v, k, obj) + return [nk, nv] + }, queueOrConcurrency)) +} + +export default asyncMapEntries diff --git a/src/asyncMapEntries.test.mjs b/src/asyncMapEntries.test.mjs new file mode 100644 index 0000000..e3bd9c5 --- /dev/null +++ b/src/asyncMapEntries.test.mjs @@ -0,0 +1,22 @@ + +import { expect, test } from '@jest/globals' +import asyncMapEntries from './asyncMapEntries.mjs' + +test('asyncMapEntries base', async () => { + const obj = { + a: 1, + b: 2, + c: 3 + } + + const nobj = await asyncMapEntries(obj, async (v, k, o) => { + expect(o).toBe(obj) + return [k + 'x', v * 2] + }) + + expect(nobj).toEqual({ + ax: 2, + bx: 4, + cx: 6 + }) +}) diff --git a/src/asyncMapKeys.mjs b/src/asyncMapKeys.mjs new file mode 100644 index 0000000..5b36fe4 --- /dev/null +++ b/src/asyncMapKeys.mjs @@ -0,0 +1,75 @@ + +import Queue from './Queue.mjs' +import asyncMapEntries from './asyncMapEntries.mjs' +import asyncWrap from './asyncWrap.mjs' + +/** + * Maps the keys found in an object by calling the `iteratee` + * function, returning a new key, and re-construct a new object with the new keys. + * + * The calls to `iteratee` will be performed asynchronously in a {@link Queue}, allowing control over the concurrency of those 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 {object} obj The object to iterate over keys. + * @param {Function} iteratee A function that will be called with each key-value pair of the object. It will receive + * three arguments: + * * `value`: The current value to process + * * `key`: The current key to process + * * `obj`: The object on which the operation is being performed. + * @param {Queue | number} [queueOrConcurrency] If a {@link 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 {@link Queue} that will be created + * implicitly for the same purpose. Defaults to `1`. + * @returns {Promise} A promise that will be resolved with a new object built with the + * new keys returned by `iteratee`. + * @see {@link asyncMapValues} to map values of an object + * @see {@link asyncMapEntries} to map both keys and values of an object simultaneously + * @example + * // example using the default concurrency of 1 + * import { asyncMapKeys, asyncSleep } from 'modern-async' + * + * const obj = {a: 1, b: 2, c: 3} + * const result = await asyncMapKeys(obj, async (v, k) => { + * // these calls will be performed sequentially + * await asyncSleep(10) // waits 10ms + * return k + 'x' + * }) + * console.log(result) // prints Object { ax: 1, bx: 2, cx: 3 } + * // total processing time should be ~ 30ms + * @example + * // example using a set concurrency + * import { asyncMapKeys, asyncSleep } from 'modern-async' + * + * const obj = {a: 1, b: 2, c: 3} + * const result = await asyncMapKeys(obj, async (v, k) => { + * // these calls will be performed in parallel with a maximum of 2 + * // concurrent calls + * await asyncSleep(10) // waits 10ms + * return k + 'x' + * }, 2) + * console.log(result) // prints Object { ax: 1, bx: 2, cx: 3 } + * // total processing time should be ~ 20ms + * @example + * // example using infinite concurrency + * import { asyncMapKeys, asyncSleep } from 'modern-async' + * + * const obj = {a: 1, b: 2, c: 3} + * const result = await asyncMapKeys(obj, async (v, k) => { + * // these calls will be performed in parallel with a maximum of 2 + * // concurrent calls + * await asyncSleep(10) // waits 10ms + * return k + 'x' + * }, Number.POSITIVE_INFINITY) + * console.log(result) // prints Object { ax: 1, bx: 2, cx: 3 } + * // total processing time should be ~ 10ms + */ +async function asyncMapKeys (obj, iteratee, queueOrConcurrency = 1) { + iteratee = asyncWrap(iteratee) + return await asyncMapEntries(obj, async (v, k, o) => { + const nk = await iteratee(v, k, o) + return [nk, v] + }, queueOrConcurrency) +} + +export default asyncMapKeys diff --git a/src/asyncMapKeys.test.mjs b/src/asyncMapKeys.test.mjs new file mode 100644 index 0000000..4716791 --- /dev/null +++ b/src/asyncMapKeys.test.mjs @@ -0,0 +1,22 @@ + +import { expect, test } from '@jest/globals' +import asyncMapKeys from './asyncMapKeys.mjs' + +test('asyncMapKeys base', async () => { + const obj = { + a: 1, + b: 2, + c: 3 + } + + const nobj = await asyncMapKeys(obj, async (v, k, o) => { + expect(o).toBe(obj) + return k + 'x' + }) + + expect(nobj).toEqual({ + ax: 1, + bx: 2, + cx: 3 + }) +}) diff --git a/src/asyncMapValues.mjs b/src/asyncMapValues.mjs new file mode 100644 index 0000000..79e2f9e --- /dev/null +++ b/src/asyncMapValues.mjs @@ -0,0 +1,75 @@ + +import Queue from './Queue.mjs' +import asyncMapEntries from './asyncMapEntries.mjs' +import asyncWrap from './asyncWrap.mjs' + +/** + * Maps the values found in an object by calling the `iteratee` + * function, returning a new value, and re-construct a new object with the new values. + * + * The calls to `iteratee` will be performed asynchronously in a {@link Queue}, allowing control over the concurrency of those 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 {object} obj The object to iterate over key-value pairs. + * @param {Function} iteratee A function that will be called with each key-value pair of the object. It will receive + * three arguments: + * * `value`: The current value to process + * * `key`: The current key to process + * * `obj`: The object on which the operation is being performed. + * @param {Queue | number} [queueOrConcurrency] If a {@link 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 {@link Queue} that will be created + * implicitly for the same purpose. Defaults to `1`. + * @returns {Promise} A promise that will be resolved with a new object built with the + * new values returned by `iteratee`. + * @see {@link asyncMapKeys} to map keys of an object + * @see {@link asyncMapEntries} to map both keys and values of an object simultaneously + * @example + * // example using the default concurrency of 1 + * import { asyncMapValues, asyncSleep } from 'modern-async' + * + * const obj = {a: 1, b: 2, c: 3} + * const result = await asyncMapValues(obj, async (v) => { + * // these calls will be performed sequentially + * await asyncSleep(10) // waits 10ms + * return v * 2 + * }) + * console.log(result) // prints Object { a: 2, b: 4, c: 6 } + * // total processing time should be ~ 30ms + * @example + * // example using a set concurrency + * import { asyncMapValues, asyncSleep } from 'modern-async' + * + * const obj = {a: 1, b: 2, c: 3} + * const result = await asyncMapValues(obj, async (v) => { + * // these calls will be performed in parallel with a maximum of 2 + * // concurrent calls + * await asyncSleep(10) // waits 10ms + * return v * 2 + * }, 2) + * console.log(result) // prints Object { a: 2, b: 4, c: 6 } + * // total processing time should be ~ 20ms + * @example + * // example using infinite concurrency + * import { asyncMapValues, asyncSleep } from 'modern-async' + * + * const obj = {a: 1, b: 2, c: 3} + * const result = await asyncMapValues(obj, async (v) => { + * // these calls will be performed in parallel with a maximum of 2 + * // concurrent calls + * await asyncSleep(10) // waits 10ms + * return v * 2 + * }, Number.POSITIVE_INFINITY) + * console.log(result) // prints Object { a: 2, b: 4, c: 6 } + * // total processing time should be ~ 10ms + */ +async function asyncMapValues (obj, iteratee, queueOrConcurrency = 1) { + iteratee = asyncWrap(iteratee) + return await asyncMapEntries(obj, async (v, k, o) => { + const nv = await iteratee(v, k, o) + return [k, nv] + }, queueOrConcurrency) +} + +export default asyncMapValues diff --git a/src/asyncMapValues.test.mjs b/src/asyncMapValues.test.mjs new file mode 100644 index 0000000..1fb0ca9 --- /dev/null +++ b/src/asyncMapValues.test.mjs @@ -0,0 +1,22 @@ + +import { expect, test } from '@jest/globals' +import asyncMapValues from './asyncMapValues.mjs' + +test('asyncMapValues base', async () => { + const obj = { + a: 1, + b: 2, + c: 3 + } + + const nobj = await asyncMapValues(obj, async (v, k, o) => { + expect(o).toBe(obj) + return v * 2 + }) + + expect(nobj).toEqual({ + a: 2, + b: 4, + c: 6 + }) +}) diff --git a/src/asyncReduce.mjs b/src/asyncReduce.mjs index dac1bed..0f4ef48 100644 --- a/src/asyncReduce.mjs +++ b/src/asyncReduce.mjs @@ -1,5 +1,4 @@ -import assert from 'nanoassert' import asyncWrap from './asyncWrap.mjs' /** @@ -31,7 +30,6 @@ import asyncWrap from './asyncWrap.mjs' * // total processing time should be ~ 20ms */ async function asyncReduce (iterable, reducer, initial = undefined) { - assert(typeof reducer === 'function', 'iteratee must be a function') reducer = asyncWrap(reducer) if (initial !== undefined) { let current = initial diff --git a/src/asyncReduceRight.mjs b/src/asyncReduceRight.mjs index b5ec563..3fd516b 100644 --- a/src/asyncReduceRight.mjs +++ b/src/asyncReduceRight.mjs @@ -1,12 +1,17 @@ import asyncReduce from './asyncReduce.mjs' -import assert from 'nanoassert' import asyncWrap from './asyncWrap.mjs' +import asyncIterableToArray from './asyncIterableToArray.mjs' /** * Performs a reduce operation as defined in the `Array.reduceRight()` method but using an asynchronous * function as reducer. The reducer will be called sequentially. * + * Please note that this function exists only to provide compatibility with standard `Array.reduceRight()`. + * Internally it only reads all values from the given iterator, store them all in an array, revert that array + * and performs a common {@link asyncReduce} call. (Functions iterating on arrays from right to left do not + * apply properly to the iterable concept and should be avoided when trying to use that concept.) + * * @param {Iterable | AsyncIterable} iterable An iterable object. * @param {Function} reducer The reducer function. It will be called with four arguments: * * `accumulator`: The last calculated value (or the first value of the iterable if no initial value is provided) @@ -33,12 +38,8 @@ import asyncWrap from './asyncWrap.mjs' * // total processing time should be ~ 20ms */ async function asyncReduceRight (iterable, reducer, initial = undefined) { - assert(typeof reducer === 'function', 'iteratee must be a function') reducer = asyncWrap(reducer) - const arr = [] - for await (const el of iterable) { - arr.push(el) - } + const arr = await asyncIterableToArray(iterable) arr.reverse() return asyncReduce(arr, async (accumulator, value, index, iterable) => { return reducer(accumulator, value, arr.length - 1 - index, iterable) diff --git a/src/asyncSleep.mjs b/src/asyncSleep.mjs index 2a79af1..51562fd 100644 --- a/src/asyncSleep.mjs +++ b/src/asyncSleep.mjs @@ -9,6 +9,9 @@ import asyncSleepCancellable from './asyncSleepCancellable.mjs' * * @param {number} amount An amount of time in milliseconds * @returns {Promise} A promise that will be resolved after the given amount of time has passed. + * @see {@link asyncSleepCancellable} for a cancellable sleep implementation + * @see {@link asyncSleepPrecise} for a sleep implementation that can't trigger before the asked delay + * @see {@link asyncSleepPreciseCancellable} for a cancellable sleep implementation that can't trigger before the asked delay * @example * import { asyncSleep } from 'modern-async' * diff --git a/src/asyncSleepCancellable.mjs b/src/asyncSleepCancellable.mjs index c139e86..758c5f9 100644 --- a/src/asyncSleepCancellable.mjs +++ b/src/asyncSleepCancellable.mjs @@ -16,6 +16,9 @@ import CancelledError from './CancelledError.mjs' * * `promise`: The promise * * `cancel`: The cancel function. It will return a boolean that will be `true` if the promise was effectively cancelled, * `false` otherwise. + * @see {@link asyncSleep} for a base sleep implementation + * @see {@link asyncSleepPrecise} for a sleep implementation that can't trigger before the asked delay + * @see {@link asyncSleepPreciseCancellable} for a cancellable sleep implementation that can't trigger before the asked delay * @example * import { asyncSleepCancellable } from 'modern-async' * diff --git a/src/asyncSleepPrecise.mjs b/src/asyncSleepPrecise.mjs index 8b4c566..6834343 100644 --- a/src/asyncSleepPrecise.mjs +++ b/src/asyncSleepPrecise.mjs @@ -4,7 +4,7 @@ import asyncSleepPreciseCancellable from './asyncSleepPreciseCancellable.mjs' /** * Waits a given amount of time. * - * This function is similar to `asyncSleep()` except it ensures that the amount of time measured + * This function is similar to {@link asyncSleep} except it ensures that the amount of time measured * using the `Date` object is always greater than or equal the asked amount of time. * * This function can imply additional delay that can be bad for performances. As such it is @@ -14,6 +14,9 @@ import asyncSleepPreciseCancellable from './asyncSleepPreciseCancellable.mjs' * * @param {number} amount An amount of time in milliseconds * @returns {Promise} A promise that will be resolved after the given amount of time has passed. + * @see {@link asyncSleep} for a base sleep implementation + * @see {@link asyncSleepCancellable} for a cancellable sleep implementation + * @see {@link asyncSleepPreciseCancellable} for a cancellable sleep implementation that can't trigger before the asked delay * @example * import { asyncSleepPrecise } from 'modern-async' * diff --git a/src/asyncSleepPreciseCancellable.mjs b/src/asyncSleepPreciseCancellable.mjs index d73be03..059b7e0 100644 --- a/src/asyncSleepPreciseCancellable.mjs +++ b/src/asyncSleepPreciseCancellable.mjs @@ -9,7 +9,7 @@ import Deferred from './Deferred.mjs' * 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 is similar to `asyncSleep()` except it ensures that the amount of time measured + * This function is similar to {@link asyncSleep} except it ensures that the amount of time measured * using the `Date` object is always greater than or equal the asked amount of time. * * This function can imply additional delay that can be bad for performances. As such it is @@ -22,6 +22,9 @@ import Deferred from './Deferred.mjs' * * `promise`: The promise * * `cancel`: The cancel function. It will return a boolean that will be `true` if the promise was effectively cancelled, * `false` otherwise. + * @see {@link asyncSleep} for a base sleep implementation + * @see {@link asyncSleepCancellable} for a cancellable sleep implementation + * @see {@link asyncSleepPrecise} for a sleep implementation that can't trigger before the asked delay * @example * import { asyncSleepPreciseCancellable } from 'modern-async' * diff --git a/src/asyncSome.mjs b/src/asyncSome.mjs index a6475cf..4ab47f4 100644 --- a/src/asyncSome.mjs +++ b/src/asyncSome.mjs @@ -5,7 +5,7 @@ import Queue from './Queue.mjs' /** * Returns `true` if at least one element 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 + * The calls to `iteratee` will be performed asynchronously in a {@link Queue}, allowing control over the concurrency of those calls. If any * truth test returns `true` the promise is immediately resolved. * * Whenever a test returns `true`, all the remaining tasks will be cancelled as long @@ -20,8 +20,8 @@ import Queue from './Queue.mjs' * * `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 + * @param {Queue | number} [queueOrConcurrency] If a {@link 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 {@link Queue} that will be created * implicitly for the same purpose. Defaults to `1`. * @returns {Promise} A promise that will be resolved to `true` if at least one value pass the truth test and `false` * if none of them do. That promise will be rejected if one of the truth test throws an exception. diff --git a/src/generatorEntries.mjs b/src/generatorEntries.mjs new file mode 100644 index 0000000..1fde1ea --- /dev/null +++ b/src/generatorEntries.mjs @@ -0,0 +1,45 @@ + +import assert from 'nanoassert' + +/** + * An alternative to standard `Object.entries()` function, but returning an iterable + * returning each key-value pair in the object. + * + * Using this function is more memory efficient than using `Object.entries()` when iterating + * over objects key-value pairs in the case of big objects. + * + * @param {object} obj The object to iterate over key-value pairs. + * @yields {Array} Each key-value pair in the object as a tuple of two objects: + * * The key + * * The value + * @see {@link asyncFromEntries} to convert a sync or async iterable of key-value pairs to an object + * @example + * import { generatorEntries } from 'modern-async' + * + * const obj = { + * a: 1, + * b: 2, + * c: 3 + * } + * + * const it = generatorEntries(obj) + * + * for (const [key, value] of it) + * { + * console.log(key, value) + * } + * // prints: + * // "a" 1 + * // "b" 2 + * // "c" 3 + */ +function * generatorEntries (obj) { + assert(obj instanceof Object, 'obj must be an object') + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + yield [key, obj[key]] + } + } +} + +export default generatorEntries diff --git a/src/generatorEntries.test.mjs b/src/generatorEntries.test.mjs new file mode 100644 index 0000000..c89beff --- /dev/null +++ b/src/generatorEntries.test.mjs @@ -0,0 +1,37 @@ + +import { expect, test } from '@jest/globals' +import generatorEntries from './generatorEntries.mjs' + +test('generatorEntries base', async () => { + const obj = { + a: 1, + b: 2, + c: 3 + } + + expect(Object.entries(obj)).toStrictEqual([['a', 1], ['b', 2], ['c', 3]]) + + const arr = Array.from(generatorEntries(obj)) + + expect(arr).toEqual([['a', 1], ['b', 2], ['c', 3]]) +}) + +test('generatorEntries do not iterate over non owned values', async () => { + const proto = { + a: 1 + } + + const obj = { + b: 2, + c: 3 + } + + Object.setPrototypeOf(obj, proto) + + expect(obj.a).toStrictEqual(1) + expect(Object.entries(obj)).toStrictEqual([['b', 2], ['c', 3]]) + + const arr = Array.from(generatorEntries(obj)) + + expect(arr).toEqual([['b', 2], ['c', 3]]) +}) diff --git a/src/modern-async.mjs b/src/modern-async.mjs index 85e355e..edb20d1 100644 --- a/src/modern-async.mjs +++ b/src/modern-async.mjs @@ -30,3 +30,5 @@ export { default as TimeoutError } from './TimeoutError.mjs' export { default as asyncTimeoutPrecise } from './asyncTimeoutPrecise.mjs' export { default as asyncIterableToArray } from './asyncIterableToArray.mjs' export { default as reflectAsyncStatus } from './reflectAsyncStatus.mjs' +export { default as generatorEntries } from './generatorEntries.mjs' +export { default as asyncFromEntries } from './asyncFromEntries.mjs'