UNPKG

nonblocking-array

Version:

Provides nonblocking functions of various Array methods. Unlike the standard array methods that execute array iterations in its entirety in one cycle of the event loop and may block the event loop on large arrays, these functions execute each iteration of

182 lines (175 loc) 7.41 kB
function isNodeJs() { return typeof window === "undefined" && typeof process !== "undefined"; } /** * Nonblocking version of Array.prototype.map. Iterates through the array one element at a time, * yielding to the event loop between each iteration using setImmediate (Node.js) or setTimeout (browsers). * Collects the return value of each callback invocation and resolves with the new array. * Supports both sync and async callbacks via Promise.resolve wrapping. * @param {Array} array - The array to iterate over * @param {Function} callback - Called with (currentItem, currentIndex, array) for each element * @param {*} [thisArg] - Value to use as `this` when executing the callback * @returns {Promise<Array>} A new array populated with the results of the callback function */ function map(array, callback, thisArg) { return new Promise((resolve, reject) => { if (!Array.isArray(array)) reject(new TypeError("Invalid argument. Must be an array")); const newArray = []; function help(index) { if (index < array.length) { Promise.resolve(callback.call(thisArg, array[index], index, array)) .then((result) => { newArray.push(result); if (isNodeJs()) setImmediate(help.bind(null, index + 1)); else setTimeout(help.bind(null, index + 1), 0); }) .catch((error) => { reject(error); }); } else { resolve(newArray); } } help(0); }); } /** * Nonblocking version of Array.prototype.filter. Iterates through the array one element at a time, * yielding to the event loop between each iteration. For each element, if the callback returns a * truthy value, the original element is included in the result array. * @param {Array} array - The array to iterate over * @param {Function} callback - Called with (currentItem, currentIndex, array). Return truthy to keep the element * @param {*} [thisArg] - Value to use as `this` when executing the callback * @returns {Promise<Array>} A new array containing only the elements that passed the callback test */ function filter(array, callback, thisArg) { return new Promise((resolve, reject) => { if (!Array.isArray(array)) reject(new TypeError("Invalid argument. Must be an array")); const newArray = []; function help(index) { if (index < array.length) { Promise.resolve(callback.call(thisArg, array[index], index, array)) .then((result) => { if (result) newArray.push(array[index]); if (isNodeJs()) setImmediate(help.bind(null, index + 1)); else setTimeout(help.bind(null, index + 1), 0); }) .catch((error) => { reject(error); }); } else { resolve(newArray); } } help(0); }); } /** * Nonblocking version of Array.prototype.forEach. Iterates through the array one element at a time, * yielding to the event loop between each iteration. Executes the callback for its side effects; * the callback's return value is ignored. Resolves with undefined when all elements have been processed. * @param {Array} array - The array to iterate over * @param {Function} callback - Called with (currentItem, currentIndex, array) for each element * @param {*} [thisArg] - Value to use as `this` when executing the callback * @returns {Promise<undefined>} Resolves with undefined after all elements are processed */ function forEach(array, callback, thisArg) { return new Promise((resolve, reject) => { if (!Array.isArray(array)) reject(new TypeError("Invalid argument. Must be an array")); function help(index) { if (index < array.length) { Promise.resolve(callback.call(thisArg, array[index], index, array)) .then((result) => { if (isNodeJs()) setImmediate(help.bind(null, index + 1)); else setTimeout(help.bind(null, index + 1), 0); }) .catch((error) => { reject(error); }); } else { resolve(undefined); // return; } } help(0); }); } /** * Nonblocking version of Array.prototype.reduce. Executes a reducer callback on each element sequentially, * yielding to the event loop between each iteration. The callback receives the accumulator and current element, * and its return value becomes the new accumulator. If no initialValue is provided, the first element is used * as the initial accumulator and iteration starts from the second element. Rejects with TypeError if called * on an empty array without an initialValue. * @param {Array} array - The array to reduce * @param {Function} callback - Called with (accumulator, currentItem, currentIndex, array) for each element * @param {*} [initialValue] - Initial value for the accumulator. If omitted, array[0] is used * @returns {Promise<*>} Resolves with the final accumulated value */ function reduce(array, callback, initialValue) { return new Promise((resolve, reject) => { if (!Array.isArray(array)) reject(new TypeError("Invalid argument. Must be an array")); if ( (initialValue == undefined || initialValue == null) && array.length === 0 ) reject(new TypeError("Array must not be empty")); let accumulator = initialValue == undefined || initialValue == null ? array[0] : initialValue; function help(index) { if (index < array.length) { Promise.resolve(callback(accumulator, array[index], index, array)) .then((result) => { accumulator = result; if (isNodeJs()) setImmediate(help.bind(null, index + 1)); else setTimeout(help.bind(null, index + 1), 0); }) .catch((error) => { reject(error); return; }); } else { resolve(accumulator); return; } } if (initialValue != undefined || initialValue != null) help(0); else help(1); }); } /** * Nonblocking version of Array.prototype.find. Iterates through the array one element at a time, * yielding to the event loop between each iteration. Returns the first element for which the callback * returns a truthy value, stopping iteration immediately on a match. If no element satisfies the * callback, resolves with undefined. * @param {Array} array - The array to search * @param {Function} callback - Called with (currentItem, currentIndex, array). Return truthy to indicate a match * @param {*} [thisArg] - Value to use as `this` when executing the callback * @returns {Promise<*>} The first matching element, or undefined if no match is found */ function find(array, callback, thisArg) { return new Promise((resolve, reject) => { if (!Array.isArray(array)) reject(new TypeError("Invalid argument. Must be an array")); function help(index) { if (index < array.length) { Promise.resolve(callback.call(thisArg, array[index], index, array)) .then((result) => { if (result) resolve(array[index]); else if (isNodeJs()) setImmediate(help.bind(null, index + 1)); else setTimeout(help.bind(null, index + 1), 0); }) .catch((error) => reject(error)); } else { resolve(undefined); } } help(0); }); } module.exports = { map, forEach, reduce, filter, find };