Source: hybrid-array-runner.js

/**
 * Created by HODGESW on 10/18/2016.
 */

let ArrayRunner = require('./array-runner');
let ParallelArrayRunner = require ('parallel-array-runner');
let SerializedArrayRunner = require ('serialized-array-runner');

/**
 * A class for calling a function or method against an array in both a serial and parallel fashion.
 * The passed array is sliced into buckets based on the value of threads.
 * The elements of each buckets are operated on in parallel, while each bucket
 * is operated on serially.
 * @class
 * @extends ArrayRunner
 */
class HybridArrayRunner extends ArrayRunner {

    /**
     * Create the class and optionaly set the behavior.
     * See <a href="#BehaviorsLink">Behaviors</a> for a list of allowed types.
     * @throws will throw an error if an invalid returnBehavior parameter is passed
     * or threads is not a number or < 1
     * @param {string} [returnBehaviorType]  Optional parameter to set the return behavior.
     * The default value is SerializedArrayRunner.LAST_RETURN.
     * @param {number} threads Optional parameter to set the number of parallel threads.
     * Defaults to 5.
     * @constructor
     */
    constructor (returnBehaviorType = HybridArrayRunner.LAST_RETURN, threads = 5) {
        super(returnBehaviorType);

        // Declare the member variables. Some of these are set in the setter
        this._threads = threads;  // Declare the member variable here
        this._serial_runner = new SerializedArrayRunner(returnBehaviorType);
        this._parallel_runner = new ParallelArrayRunner(returnBehaviorType);

        // Call the setters
        this.numberOfThreads = threads;
        this.behaviorType = returnBehaviorType;
    }

    /**
     * The value used to set the behavior to last return. The data from resolved promises for all
     * but the last call will be discarded.
     * @type {string}
     */
    static get LAST_RETURN() {
        return super.LAST_RETURN;
    }

    /**
     * Used to set the behavior to array return. The data from resolved promises for all
     * will be pushed into an array and returned with the resolved promise.
     * @type {string}
     */
    static get ARRAY_RETURN() {
        return super.ARRAY_RETURN;
    }

    /**
     * Used to set the behavior to concatenated array return. The data from resolved promises for is
     * expected to be an array. Each array returned is concatenated and returned with the resolved promise.
     * @type {string}
     */
    static get CONCAT_ARRAY_RETURN() {
        return super.CONCAT_ARRAY_RETURN;
    }

    /**
     * Generates an array of valid return types. Used internally for validation of passed parameters.
     * @returns {Array<string>}
     * @private
     */
    static get VALID_RETURN_BEHAVIOR_TYPES () {
        return super.VALID_RETURN_BEHAVIOR_TYPES;
    }

    /**
     * Getter amd setter for the number of parallel threads to use.
     * Note that the setter will thrown an error if setting the value to
     * not a number or a number < 1
     * @type {number}
     */

    get numberOfThreads () {
        return this._threads;
    }

    //noinspection JSAnnotator
    set numberOfThreads (threads) {
        if ( typeof threads !== 'number' || threads < 1) {
            throw new Error ('Invalid parameter sent for threads');
        }
        this._threads = threads;
    }

    // ------------------------------------------------------------------------------
    /**
     *  <div id="BehaviorsLink"/>
     *  Getter and setter for the runner return behavior.
     *  Note that the setter will throw an error if the string passed is not one
     *  of the types specified by the static methods. The three types allowed are:
     *  <h5>Behaviors:</h5>
     *  <table>
     *  <thead><tr><th>Behavior Type</th><th>Description</th></tr></thead>
     *  <tr><td>HybridArrayRunner.LAST_RETURN</td> <td>All but the last resolved data is discarded.</td></tr>
     *  <tr><td>HybridArrayRunner.ARRAY_RETURN</td> <td>Each resolved data returned is pushed to an array and returned</td></tr>
     *  <tr><td>HybridArrayRunner.CONCAT_ARRAY_RETURN</td> <td>The resolved data is an array it's contents are concatenated to the
     *  final array for each iteration.</td</tr>
     *  </table>
     * @type {string}
     */


    get behaviorType () {
        return super.behaviorType;
    }

    //noinspection JSAnnotator
    set behaviorType (returnType) {
        super.behaviorType = returnType;

        // If the member variable have been defined, set their behavior.
        // This was needed because the base setter is called in the constructor
        this._serial_runner && (this._serial_runner.behaviorType = returnType);
        this._parallel_runner && (this._serial_runner.behaviorType = returnType);

    }

    // ------------------------------------------------------------------------------
    /**
     * Slices the given array into an array of buckets of size <= threads.
     * The contents of each bucket is operated on in parallel.
     * Once all of the elements of a bucket have been called and resolved,
     * the next bucket is operated on likewise, until the array is depleted.
     * Returns a promise when the all iterations are complete.
     * The data resolved depends on the supplied function return data and the configured
     * return behavior
     * @param {Array} arrayToIterate The array who's elements are passed to the function.
     * @param {function} functionToCall The function that gets call. It must return a promise.
     * The first parameter is the current array element.
     * @param {object} scope The scope for the function to be called in. Necessary if the
     * function is class method.
     * @param {...*} args Zero or more arguments to send to the function. Note that
     * the first argument here, is the fourth argument to the called function.
     * @returns {Promise} A resolved promise is returned if all function calls resolve.
     */
    run (arrayToIterate, functionToCall, scope, ...args) {
        return new Promise((resolve, reject) => {
            if (!Array.isArray(arrayToIterate)) {
                return reject(`Expected type array, but passed ${typeof arrayToIterate}`);
            }
            if (typeof functionToCall !== 'function') {
                return reject(`Expected type function, but passed ${typeof functionToCall}`);
            }

            let numberOfElements = arrayToIterate.length;
            let localArray = [];

            // Slice the array, based on the thread count
            for (let i=0; i<numberOfElements; i += this._threads) {
                localArray.push(arrayToIterate.slice(i, i + this._threads));
            }

            // This is a rather complex call. Basically we've sliced the input into an array of arrays.
            // Each array is at max threads size.
            // The outside array is passed to the serial runner who will then iterate the array and pass
            // each inside array in turn to a parallel runner.
            // The parallel runner will apply the given function to each element in the inside array.
            // When the promise for the parallel runner is resolved, then the serial runner starts the process again
            // on the next element of the outside array, util it is depleted.
            // All results bubble back up based on the behavior.

            this._serial_runner.run(localArray, this._parallel_runner.run, this._parallel_runner, functionToCall, scope, ...args)
                .then ( (results) => {
                    if (this.is_last_return) {
                        return resolve(results)
                    }
                    else if (this.is_array_return) {
                        // Here the user is expecting an array but we get back an array of arrays.
                        let resultsArray = [];
                        results.forEach( (elementArray) => {
                            resultsArray = resultsArray.concat(elementArray);
                        });
                        return resolve(resultsArray);
                    }
                    else {
                        return resolve(results);
                    }
                })
                .catch( (err) => {
                    return reject(err);
                });

        });
    }

}

module.exports = HybridArrayRunner;