spex
Version:
Specialized Promise Extensions
189 lines (176 loc) • 7.76 kB
JavaScript
/**
* @method sequence
* @summary Resolves a dynamic sequence of $[mixed values].
* @description
* Acquires $[mixed values] from the source function, one at a time, and resolves them,
* till either no more values left in the sequence or an error/reject occurs.
* It supports both [linked and detached sequencing](../concept/sequencing.md).
* @param {function} source
* Expected to return the next $[mixed value] to be resolved. If the value resolves
* into `undefined`, it signals the end of the sequence, and the method resolves.
*
* Parameters:
* - `index` = current request index in the sequence
* - `data` = resolved data from the previous call (`undefined` when `index=0`)
* - `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.
*
* If the function throws an error or returns a rejected promise, the sequence terminates,
* and the method rejects with object `{index, error, source}`:
* - `index` = index of the request that failed
* - `error` = the error thrown or the rejection reason
* - `source` = resolved `data` that was passed into the function
*
* Passing in anything other than a function will throw `Invalid sequence source.`
*
* @param {function} [dest]
* Optional destination function (notification callback), to receive resolved data for each index,
* process it and respond as required.
*
* Parameters:
* - `index` = index of the resolved data in the sequence
* - `data` = the data resolved
* - `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 object, if data processing is done asynchronously.
* If a promise is returned, the method will not request the next value from the `source` function,
* until the promise has been resolved.
*
* If the function throws an error or returns a promise that rejects, the sequence terminates,
* and the method rejects with object `{index, error, dest}`:
* - `index` = index of the data that was processed
* - `error` = the error thrown or the rejection reason
* - `dest` = resolved data that was passed into the function
*
* Passing in a non-empty value other than a function will throw `Invalid sequence destination.`
*
* @param {Number} [limit=0]
* Limits the maximum size of the sequence. If the value is an integer greater than 0,
* the method will successfully resolve once the specified limit has been reached.
* By default, the sequence is unlimited, and will continue till either `source` function
* returns `undefined` or an error/reject occurs.
*
* @param {Boolean} [track=false]
* This parameter changes the type of data to be resolved by this method.
* By default, it is `false` (see the return result).
* When set to be `true`, it instructs the method to track/collect all resolved data into
* an array internally, so it can be resolved with once the method has finished successfully.
*
* It must be used with caution, as to the size of the sequence, because accumulating data for
* a very large sequence can result in consuming too much memory.
*
* @returns {Promise}
* When successful, the resolved data depends on parameter `track`. When `track` is `false`
* (default), the method resolves with object `{total, duration}`:
* - `total` = number of values resolved by the sequence
* - `duration` = number of milliseconds consumed by the method
*
* When `track` is `true`, the method resolves with an array of all the data that has been resolved,
* the same way that the standard `promise.all` resolves. In addition, the array comes extended with
* read-only property `duration` - number of milliseconds consumed by the method.
*
* When the method fails, the reject result depends on which function caused the failure - `source`
* or `dest`. See the two parameters for the rejection details.
*/
function sequence(source, dest, limit, track) {
if (typeof source !== 'function') {
throw new TypeError("Invalid sequence source.");
}
if (!$utils.isNull(dest) && typeof dest !== 'function') {
throw new TypeError("Invalid sequence destination.");
}
limit = (parseInt(limit) === limit && limit > 0) ? limit : 0;
var self = this, data, srcTime, destTime, result = [], start = Date.now();
return $p(function (resolve, reject) {
function loop(idx) {
var srcNow = Date.now(),
srcDelay = idx ? (srcNow - srcTime) : undefined;
srcTime = srcNow;
$utils.resolve.call(self, source, [idx, data, srcDelay], function (value, delayed) {
data = value;
if (data === undefined) {
finish();
} else {
if (track) {
result.push(data);
}
if (dest) {
var destResult, destNow = Date.now(),
destDelay = idx ? (destNow - destTime) : undefined;
destTime = destNow;
try {
destResult = dest.call(self, idx, data, destDelay);
} catch (e) {
reject({
index: idx,
error: e,
dest: data
});
return;
}
if ($utils.isPromise(destResult)) {
destResult
.then(function () {
next(true);
}, function (reason) {
reject({
index: idx,
error: reason,
dest: data
});
});
} else {
next(delayed);
}
} else {
next(delayed);
}
}
}, function (reason) {
reject({
index: idx,
error: reason,
source: data
});
});
function next(delayed) {
idx++;
if (limit === idx) {
finish();
} else {
if (delayed) {
loop(idx);
} else {
$p.resolve()
.then(function () {
loop(idx);
});
}
}
}
function finish() {
var length = Date.now() - start;
if (track) {
$utils.extend(result, 'duration', length);
} else {
result = {
total: idx,
duration: length
}
}
resolve(result);
}
}
loop(0);
});
}
var $utils, $p;
module.exports = function (config) {
$utils = config.utils;
$p = config.promise;
return sequence;
};
;