UNPKG

spex

Version:

Specialized Promise Extensions

171 lines (165 loc) 7.25 kB
'use strict'; /** * @method batch * @summary Resolves a predefined array of $[mixed values]. * @description * Settles (resolves or rejects) every $[mixed value] in the input array, and resolves * with an array of results, if all values have been resolved, or else rejects. * * This method resembles a fusion of `promise.all` + `promise.settle` logic, to resolve with * the same type of result as `promise.all`, while also settling all the promises, similar to * `promise.settle`, adding comprehensive details in case of a reject. * * @param {Array} values * Array of $[mixed values] for asynchronous resolution. * * Passing in anything other than an array will throw `Batch requires an array of values.` * * @param {Function} [cb] * Optional callback to receive the result for each settled value. * * Parameters: * - `index` = index of the value in the source array * - `success` - indicates whether the value was resolved (`true`), or rejected (`false`) * - `data` = resolved data, if `success`=`true`, or else the rejection reason * - `delay` = number of milliseconds since the last call (`undefined` when `index=0`) * * The function is called with the same `this` context as the calling method. * * It can optionally return a promise to indicate that notifications are handled asynchronously. * And if the returned promise resolves, it signals a successful handling, while any resolved * data is ignored. * * If the function returns a rejected promise or throws an error, the entire method rejects, * while the value in the rejected array is reported as object `{success, result, origin}`: * - `success` = `false` * - `result` = the rejection reason or the error thrown by the notification callback * - `origin` = the original data passed into the callback, as object `{success, result}` * * @returns {Promise} * Result for the entire batch, which resolves when every value in the input array has been resolved, * and rejects when: * - one or more values in the array rejected or threw an error as a $[mixed value] * - one or more calls into the notification callback returned a rejected promise or threw an error * * The method resolves with an array of individual resolved results, the same as `promise.all`. * In addition, the array is extended with read-only property `duration` - number of milliseconds * taken to resolve all the data. * * When failed, the method rejects with an array of objects `{success, result, [origin]}`: * - `success` = `true/false`, indicates whether the corresponding value in the input array was resolved. * - `result` = resolved data, if `success=true`, or else the rejection reason. * - `origin` - set only when failed as a result of an unsuccessful call into the notification callback * (see documentation for parameter `cb`) * * In addition, the rejection array is extended with function `getErrors`, which returns the list of just * errors, with support for nested batch results. Calling `getErrors()[0]`, for example, will get the same * result as the rejection reason that `promise.all` would provide. * * In all cases the output array is always the same size as the input one, providing index mapping * between the inputs and the results. */ function batch(values, cb) { if (!Array.isArray(values)) { throw new TypeError("Batch requires an array of values."); } if (!$utils.isNull(cb) && typeof cb !== 'function') { throw new TypeError("Invalid callback function specified."); } if (!values.length) { var empty = []; $utils.extend(empty, 'duration', 0); return $p.resolve(empty); } var self = this, start = Date.now(); return $p(function (resolve, reject) { var cbTime, errors = [], sorted = false, remaining = values.length, result = new Array(remaining); values.forEach(function (item, i) { $utils.resolve.call(self, item, null, function (data) { result[i] = data; step(i, true, data); }, function (reason) { result[i] = {success: false, result: reason}; errors.push(i); step(i, false, reason); }); }); function step(idx, pass, data) { if (cb) { var cbResult, cbNow = Date.now(), cbDelay = idx ? (cbNow - cbTime) : undefined; cbTime = cbNow; try { cbResult = cb.call(self, idx, pass, data, cbDelay); } catch (e) { setError(e); } if ($utils.isPromise(cbResult)) { cbResult .then(function () { check(); }, function (reason) { setError(reason); check(); }); } else { check(); } } else { check(); } function setError(e) { var r = pass ? {success: false} : result[idx]; if (pass) { result[idx] = r; errors.push(idx); } r.result = e; r.origin = {success: pass, result: data} } function check() { if (--remaining) { return; } if (errors.length) { if (errors.length < result.length) { errors.sort(); sorted = true; for (var i = 0, k = 0; i < result.length; i++) { if (i === errors[k]) { k++; } else { result[i] = {success: true, result: result[i]}; } } } $utils.extend(result, 'getErrors', function () { if (!sorted) { errors.sort(); sorted = true; } var err = new Array(errors.length); for (var i = 0; i < errors.length; i++) { err[i] = result[errors[i]].result; if (err[i] instanceof Array && err[i].getErrors instanceof Function) { err[i] = err[i].getErrors(); } } return err; }); reject(result); } else { $utils.extend(result, 'duration', Date.now() - start); resolve(result); } } } }); } var $utils, $p; module.exports = function (config) { $utils = config.utils; $p = config.promise; return batch; };