diff --git a/dist/index.d.ts b/dist/index.d.ts index 6bc0fe0..d8df79b 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -5,7 +5,6 @@ export { default as findIndex } from "./src/findIndex"; export { default as findLast } from "./src/findLast"; export { default as findLastIndex } from "./src/findLastIndex"; export { default as map } from "./src/map"; -export { default as mapParallel } from "./src/mapParallel"; export { default as reduce } from "./src/reduce"; export { default as reduceRight } from "./src/reduceRight"; export { default as some } from "./src/some"; diff --git a/dist/index.js b/dist/index.js index dabd156..faa2041 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.some = exports.reduceRight = exports.reduce = exports.mapParallel = exports.map = exports.findLastIndex = exports.findLast = exports.findIndex = exports.find = exports.filter = exports.every = void 0; +exports.some = exports.reduceRight = exports.reduce = exports.map = exports.findLastIndex = exports.findLast = exports.findIndex = exports.find = exports.filter = exports.every = void 0; var every_1 = require("./src/every"); Object.defineProperty(exports, "every", { enumerable: true, get: function () { return __importDefault(every_1).default; } }); var filter_1 = require("./src/filter"); @@ -18,8 +18,6 @@ var findLastIndex_1 = require("./src/findLastIndex"); Object.defineProperty(exports, "findLastIndex", { enumerable: true, get: function () { return __importDefault(findLastIndex_1).default; } }); var map_1 = require("./src/map"); Object.defineProperty(exports, "map", { enumerable: true, get: function () { return __importDefault(map_1).default; } }); -var mapParallel_1 = require("./src/mapParallel"); -Object.defineProperty(exports, "mapParallel", { enumerable: true, get: function () { return __importDefault(mapParallel_1).default; } }); var reduce_1 = require("./src/reduce"); Object.defineProperty(exports, "reduce", { enumerable: true, get: function () { return __importDefault(reduce_1).default; } }); var reduceRight_1 = require("./src/reduceRight"); diff --git a/dist/src/map.d.ts b/dist/src/map.d.ts index 48d9290..5f57491 100644 --- a/dist/src/map.d.ts +++ b/dist/src/map.d.ts @@ -1,13 +1,18 @@ /** * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map|MDN Documentation Array.prototype.map} + * Optionally, you can batch your iterations to more efficiently await blocking iteratee function calls using the batchIterations and batchSize attributes on the options parameter. * * @static * @since 1.0.0 * @param {T[]} array * @param {(value: T, index: number) => Promise} iteratee + * @param {{ batchIterations: boolean; batchSize: number }} [options] * @returns {Promise} * @example * const array = [1, 2, 3]; * const doubledArray = await map(array, async (value) => value * 2); */ -export default function map(array: T[], iteratee: (value: T, index: number) => Promise): Promise; +export default function map(array: T[], iteratee: (value: T, index: number) => Promise, options?: { + batchIterations: boolean; + batchSize: number; +}): Promise; diff --git a/dist/src/map.js b/dist/src/map.js index b864608..9b66d78 100644 --- a/dist/src/map.js +++ b/dist/src/map.js @@ -3,19 +3,65 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.default = map; /** * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map|MDN Documentation Array.prototype.map} + * Optionally, you can batch your iterations to more efficiently await blocking iteratee function calls using the batchIterations and batchSize attributes on the options parameter. * * @static * @since 1.0.0 * @param {T[]} array * @param {(value: T, index: number) => Promise} iteratee + * @param {{ batchIterations: boolean; batchSize: number }} [options] * @returns {Promise} * @example * const array = [1, 2, 3]; * const doubledArray = await map(array, async (value) => value * 2); */ -async function map(array, iteratee) { +async function map(array, iteratee, options) { if (!Array.isArray(array) || !array?.length) return []; + if (options?.batchIterations) { + return await mapParallel(array, iteratee, options.batchSize); + } + else { + return await mapSerial(array, iteratee); + } +} +async function mapParallel(array, iteratee, batchSize) { + const arrayWithIndexKeys = array.map((item, i) => { + return { + task: item, + index: i, + }; + }); + const effectiveMaxBatchSize = batchSize > array.length ? array.length : batchSize; + const results = []; + await Promise.all(Array(effectiveMaxBatchSize) + .fill(undefined) + .map(async () => { + const resultsWithIndex = await takeAndCompleteFromQueueUntilDone(arrayWithIndexKeys, iteratee); + resultsWithIndex.forEach((result) => { + results[result.index] = result.result; + }); + })); + return results; +} +/** + * take and complete tasks from a queue until that queue is empty. + * @param {{ task: T; index: number }[]} array + * @param {(value: T, index: number) => Promise} iteratee + */ +async function takeAndCompleteFromQueueUntilDone(array, iteratee) { + const item = array.shift(); + if (!item) { + return []; + } + const completedTask = await iteratee(item.task, item.index); + const followingCompletedTasks = await takeAndCompleteFromQueueUntilDone(array, iteratee); + return followingCompletedTasks.concat({ + result: completedTask, + index: item.index, + }); +} +async function mapSerial(array, iteratee) { const results = []; for (let index = 0; index < array.length; index++) { const element = array[index]; diff --git a/dist/src/mapParallel.d.ts b/dist/src/mapParallel.d.ts deleted file mode 100644 index 0d098e5..0000000 --- a/dist/src/mapParallel.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map|MDN Documentation Array.prototype.map} - * - * @static - * @since 1.0.0 - * @param {T[]} array - * @param {(val: T, index?: number) => Promise} iteratee - * @param {number} [maxParallelBatchSize=10] - * @returns {Promise} - * @example - * const array = [1, 2, 3]; - * const doubledArray = await mapParellel(array, async (value) => value * 2); - */ -export default function mapParallel(array: T[], iteratee: (value: T, index: number) => Promise, maxParallelBatchSize?: number): Promise; diff --git a/dist/src/mapParallel.js b/dist/src/mapParallel.js deleted file mode 100644 index 5beabd8..0000000 --- a/dist/src/mapParallel.js +++ /dev/null @@ -1,54 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.default = mapParallel; -/** - * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map|MDN Documentation Array.prototype.map} - * - * @static - * @since 1.0.0 - * @param {T[]} array - * @param {(val: T, index?: number) => Promise} iteratee - * @param {number} [maxParallelBatchSize=10] - * @returns {Promise} - * @example - * const array = [1, 2, 3]; - * const doubledArray = await mapParellel(array, async (value) => value * 2); - */ -async function mapParallel(array, iteratee, maxParallelBatchSize = 10) { - if (!Array.isArray(array) || !array?.length) - return []; - const arrayWithIndexKeys = array.map((item, i) => { - return { - task: item, - index: i, - }; - }); - const effectiveMaxBatchSize = maxParallelBatchSize > array.length ? array.length : maxParallelBatchSize; - const results = []; - await Promise.all(Array(effectiveMaxBatchSize) - .fill(undefined) - .map(async () => { - const resultsWithIndex = await takeAndCompleteFromQueueUntilDone(arrayWithIndexKeys, iteratee); - resultsWithIndex.forEach((result) => { - results[result.index] = result.result; - }); - })); - return results; -} -/** - * take and complete tasks from a queue until that queue is empty. - * @param {{ task: T; index: number }[]} array - * @param {(value: T, index: number) => Promise} iteratee - */ -async function takeAndCompleteFromQueueUntilDone(array, iteratee) { - const item = array.shift(); - if (!item) { - return []; - } - const completedTask = await iteratee(item.task, item.index); - const followingCompletedTasks = await takeAndCompleteFromQueueUntilDone(array, iteratee); - return followingCompletedTasks.concat({ - result: completedTask, - index: item.index, - }); -} diff --git a/index.ts b/index.ts index 6bc0fe0..d8df79b 100644 --- a/index.ts +++ b/index.ts @@ -5,7 +5,6 @@ export { default as findIndex } from "./src/findIndex"; export { default as findLast } from "./src/findLast"; export { default as findLastIndex } from "./src/findLastIndex"; export { default as map } from "./src/map"; -export { default as mapParallel } from "./src/mapParallel"; export { default as reduce } from "./src/reduce"; export { default as reduceRight } from "./src/reduceRight"; export { default as some } from "./src/some"; diff --git a/src/map.ts b/src/map.ts index 8f8ccff..6864314 100644 --- a/src/map.ts +++ b/src/map.ts @@ -1,10 +1,12 @@ /** * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map|MDN Documentation Array.prototype.map} + * Optionally, you can batch your iterations to more efficiently await blocking iteratee function calls using the batchIterations and batchSize attributes on the options parameter. * * @static * @since 1.0.0 * @param {T[]} array * @param {(value: T, index: number) => Promise} iteratee + * @param {{ batchIterations: boolean; batchSize: number }} [options] * @returns {Promise} * @example * const array = [1, 2, 3]; @@ -12,10 +14,82 @@ */ export default async function map( array: T[], - iteratee: (value: T, index: number) => Promise + iteratee: (value: T, index: number) => Promise, + options?: { batchIterations: boolean; batchSize: number } ): Promise { if (!Array.isArray(array) || !array?.length) return []; + if (options?.batchIterations) { + return await mapParallel(array, iteratee, options.batchSize); + } else { + return await mapSerial(array, iteratee); + } +} + +async function mapParallel( + array: T[], + iteratee: (value: T, index: number) => Promise, + batchSize: number +): Promise { + const arrayWithIndexKeys = array.map((item, i) => { + return { + task: item, + index: i, + }; + }); + + const effectiveMaxBatchSize = + batchSize > array.length ? array.length : batchSize; + + const results: V[] = []; + await Promise.all( + Array(effectiveMaxBatchSize) + .fill(undefined) + .map(async () => { + const resultsWithIndex = await takeAndCompleteFromQueueUntilDone( + arrayWithIndexKeys, + iteratee + ); + + resultsWithIndex.forEach((result) => { + results[result.index] = result.result; + }); + }) + ); + + return results; +} + +/** + * take and complete tasks from a queue until that queue is empty. + * @param {{ task: T; index: number }[]} array + * @param {(value: T, index: number) => Promise} iteratee + */ +async function takeAndCompleteFromQueueUntilDone( + array: { task: T; index: number }[], + iteratee: (value: T, index: number) => Promise +): Promise<{ result: V; index: number }[]> { + const item = array.shift(); + if (!item) { + return []; + } + + const completedTask = await iteratee(item.task, item.index); + const followingCompletedTasks = await takeAndCompleteFromQueueUntilDone( + array, + iteratee + ); + + return followingCompletedTasks.concat({ + result: completedTask, + index: item.index, + }); +} + +async function mapSerial( + array: T[], + iteratee: (value: T, index: number) => Promise +): Promise { const results: V[] = []; for (let index = 0; index < array.length; index++) { const element = array[index]; diff --git a/src/mapParallel.ts b/src/mapParallel.ts deleted file mode 100644 index 5075f05..0000000 --- a/src/mapParallel.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map|MDN Documentation Array.prototype.map} - * - * @static - * @since 1.0.0 - * @param {T[]} array - * @param {(val: T, index?: number) => Promise} iteratee - * @param {number} [maxParallelBatchSize=10] - * @returns {Promise} - * @example - * const array = [1, 2, 3]; - * const doubledArray = await mapParellel(array, async (value) => value * 2); - */ -export default async function mapParallel( - array: T[], - iteratee: (value: T, index: number) => Promise, - maxParallelBatchSize: number = 10 -): Promise { - if (!Array.isArray(array) || !array?.length) return []; - - const arrayWithIndexKeys = array.map((item, i) => { - return { - task: item, - index: i, - }; - }); - - const effectiveMaxBatchSize = - maxParallelBatchSize > array.length ? array.length : maxParallelBatchSize; - - const results: V[] = []; - await Promise.all( - Array(effectiveMaxBatchSize) - .fill(undefined) - .map(async () => { - const resultsWithIndex = await takeAndCompleteFromQueueUntilDone( - arrayWithIndexKeys, - iteratee - ); - - resultsWithIndex.forEach((result) => { - results[result.index] = result.result; - }); - }) - ); - - return results; -} - -/** - * take and complete tasks from a queue until that queue is empty. - * @param {{ task: T; index: number }[]} array - * @param {(value: T, index: number) => Promise} iteratee - */ -async function takeAndCompleteFromQueueUntilDone( - array: { task: T; index: number }[], - iteratee: (value: T, index: number) => Promise -): Promise<{ result: V; index: number }[]> { - const item = array.shift(); - if (!item) { - return []; - } - - const completedTask = await iteratee(item.task, item.index); - const followingCompletedTasks = await takeAndCompleteFromQueueUntilDone( - array, - iteratee - ); - - return followingCompletedTasks.concat({ - result: completedTask, - index: item.index, - }); -} diff --git a/test/map.test.ts b/test/map.test.ts index 512d72e..44e77a8 100644 --- a/test/map.test.ts +++ b/test/map.test.ts @@ -1,16 +1,70 @@ import { map } from "index"; import { describe, it, expect } from "vitest"; +import { sleep } from "./utils"; -describe("map tests", () => { - const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; +describe("mapParallel tests", () => { + const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const expectedDoubledNumbers = numbers.map((number) => number * 2); - it("correctly iterates", async () => { + it("correctly iterates without batching", async () => { const indices: number[] = []; await map(numbers, async (number, index) => { - // await sleep(12 - index); + await sleep(12 - index); indices.push(index); }); expect(indices).to.eql(numbers.map((number) => number - 1)); }); + + it("returns all values when passed a batch options", async () => { + const doubledNumbers = await map( + numbers, + async (number) => { + await sleep(number); + return number * 2; + }, + { batchIterations: true, batchSize: 5 } + ); + expect(doubledNumbers).to.eql(expectedDoubledNumbers); + }); + + it("returns all values when passed a batchSize larger than the length of the array", async () => { + const doubledNumbers = await map( + numbers, + async (number) => { + await sleep(number); + return number * 2; + }, + { batchIterations: true, batchSize: 20 } + ); + expect(doubledNumbers).to.eql(expectedDoubledNumbers); + }); + + it("correctly iterates", async () => { + const indices: number[] = []; + await map( + numbers, + async (number, i) => { + indices.push(i); + }, + { batchIterations: true, batchSize: 5 } + ); + expect(indices).to.eql(numbers.map((number) => number - 1)); + }); + + it("does not wait for all tasks in a batch to finish before starting any new ones", async () => { + const start = Date.now(); + await map( + numbers, + async (number) => { + await sleep(number * 100); // wait `number` deciseconds + return number; + }, + { batchIterations: true, batchSize: 10 } + ); + const end = Date.now(); + // waiting for the first ten to finish and then starting on the 11th would result in a delay of 10 + 11 = 21ds + // starting the 11th after the first resolves would result in a delay of only 1 + 11 = 12ds + expect(end - start).to.be.lessThan(20 * 100); + }); }); diff --git a/test/mapParallel.test.ts b/test/mapParallel.test.ts deleted file mode 100644 index 9931288..0000000 --- a/test/mapParallel.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { mapParallel } from "index"; -import { describe, it, expect } from "vitest"; - -describe("mapParallel tests", () => { - const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - const expectedDoubledNumbers = numbers.map((number) => number * 2); - - it("returns all values when passed a maxParallelBatchSize", async () => { - const doubledNumbers = await mapParallel( - numbers, - async (number) => { - // await sleep(number); - return number * 2; - }, - 5 - ); - expect(doubledNumbers).to.eql(expectedDoubledNumbers); - }); - - it("returns all values when not passed a maxParallelBatchSize", async () => { - const doubledNumbers = await mapParallel(numbers, async (number) => { - // await sleep(number); - return number * 2; - }); - expect(doubledNumbers).to.eql(expectedDoubledNumbers); - }); - - it("returns all values when passed a maxParallelBatchSize larger than the length of the array", async () => { - const doubledNumbers = await mapParallel( - numbers, - async (number) => { - // await sleep(number); - return number * 2; - }, - 20 - ); - expect(doubledNumbers).to.eql(expectedDoubledNumbers); - }); - - it("correctly iterates", async () => { - const indices: number[] = []; - await mapParallel( - numbers, - async (number, i) => { - indices.push(i); - }, - 5 - ); - expect(indices).to.eql(numbers.map((number) => number - 1)); - }); - - it("does not wait for all tasks in a batch to finish before starting any new ones", async () => { - const start = Date.now(); - await mapParallel( - numbers, - async (number) => { - // await sleep(number * 100); // wait `number` deciseconds - return number; - }, - 10 - ); - const end = Date.now(); - // waiting for the first ten to finish and then starting on the 11th would result in a delay of 10 + 11 = 21ds - // starting the 11th after the first resolves would result in a delay of only 1 + 11 = 12ds - expect(end - start).to.be.lessThan(20 * 100); - }); -}); diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000..b92820b --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,4 @@ +// wait for the specified amount of time and then resolve. +export async function sleep(delay = 0): Promise { + return new Promise((resolve) => setTimeout(resolve, delay)); +}