seq-prom
Version:
A small library to allow for sequential operations on an array using Promises
207 lines (176 loc) • 5.97 kB
JavaScript
/**
* Created by adrianbrowning
*/
const blankFunction = function () {};
function isTrue(value) {
return (value === true) || (value || '').toLowerCase() === 'true';
}
function typeOf(e) {
return ({}).toString.call(e).match(/\s([a-zA-Z]+)/)[ 1 ].toLowerCase();
}
function trackResponse (response) {
this._responses.push(response);
}
function errorCallBack(self, item) {
return function(reason) {
self._errors.push({
item,
reason
});
self.errorCB(item, reason);
};
}
function buildBatchQueue(self) {
function secondary(item) {
return new Promise(function (resolve, reject) {
return self.cb(item, resolve, reject, self);
})
.then(trackResponse.bind(self))
.catch(errorCallBack(self, item));
}
const createAllPromise = function (items) {
let pList = [];
for (let i = 0; i < items.length; i++) {
pList.push(secondary(items[ i ]));
}
return Promise.all(pList);
};
const nextBatch = function (_start, _size) {
_start = _start || 0;
_size = _size || self.batchSize;
if (self._stopped) return;
return new Promise(res => {
return createAllPromise(self.list.splice(_start, _size)).then(function () {
res();
});
});
};
const loops = Math.ceil(self.list.length / self.poolSize);
for (let i = 0; i < loops; i++) {
self.promise = self.promise.then(nextBatch);
}
self.promise = self.promise
.then(() => self.finalCB(self._errors, self._responses))
.then(() => [ self._errors, self._responses ]);
}
function buildPoolQueue(self) {
function _poolGenerator() {
const item = self.list.shift();
if (!item) return;
return createCBPromise(self, item)
.then(trackResponse.bind(self))
.catch(errorCallBack(self, item))
.then(_poolGenerator);
}
function createCBPromise(self, item) {
return new Promise((resolve, reject) => {
if (self._stopped) return void resolve(null);
return self.cb(item, resolve, reject, self);
});
}
self.promise =
self.promise
.then(function () {
const pool = [];
for (let i = 0; i < self.poolSize; i++) {
pool.push(_poolGenerator());
}
return Promise.all(pool);
})
.then(() => self.finalCB(self._errors, self._responses))
.then(() => [ self._errors, self._responses ]);
}
function createMasterPromise() {
let promise,
resolver = null;
promise = new Promise(resolve => { resolver = resolve;});
return {
resolver,
promise,
};
}
class SeqPromiseClass {
/**
* SeqPromise the actual object is created here, allowing us to 'new' an object without calling 'new'
* @param {Object} options - The options for setting the chain
* @param {number} options.poolSize - The size of "simulated" thread pool
* @param {boolean} options.autoStart - Will start the processing as soon as initialisation is complete
* @param {*[]} options.list - The list of items to iterate over asynchronously
* @param {boolean} options.useBatch - Switches from Stream mode, to batch
* @param {number} options.batchSize - The size of each batch
* @param {Object} options.context - Context to run functions in
* @param {cb} options.cb - A function that returns a promise
* @param {finalCB} options.finalCB - A function that that will be called once all done
* @param {errorCB} options.errorCB - A function that returns a promise
*
* @callback cb
* @param {*} Item - Item from the list
* @param {*} Resolve - Resolve function for successful call
* @param {*} Reject - Reject function for failed call
* @param {*} Self - Item from the list
*
* @callback finalCB
* @param {*[]} Errors - List of failed items
* @param {*[]} Responses - List of completed items
*
* @callback errorCB
* @param {*} Item - Item that failed
* @param {*} Reason - Reason for failure, passed from reject
*/
constructor(options) {
if (typeOf(options.list) !== 'array') {
throw new Error(`Expecting list to be type Array, found type ${typeOf(options.list)}`);
}
if (!(typeOf(options.cb) === 'function' || typeOf(options.cb) === 'asyncfunction')) {
throw new Error(`Expecting cb to be type Function, found type ${typeOf(options.cb)}`);
}
this.list = options.list.slice();
this.cb = options.cb;
this.finalCB = options.finalCB || blankFunction;
this.errorCB = options.errorCB || blankFunction;
this.useBatch = isTrue(options.useBatch);
this.batchSize = options.batchSize || 1;
this.poolSize = options.poolSize || 1;
this.promise = null;
this._globalPromiseResolver = null;
this._stopped = false;
this._errors = [];
this._responses = [];
if (options.context) {
this.cb = options.cb.bind(options.context);
this.finalCB = options.finalCB.bind(options.context);
this.errorCB = options.errorCB.bind(options.context);
}
if (options.poolSize >= options.list.length) {
this.poolSize = options.list.length;
}
let wrappedMasterPromise = createMasterPromise();
this.promise = wrappedMasterPromise.promise;
this._globalPromiseResolver = wrappedMasterPromise.resolver;
//Batch
if (options.useBatch) {
buildBatchQueue(this);
} else {
buildPoolQueue(this);
}
if (isTrue(options.autoStart)) {
this._globalPromiseResolver();
return this;
}
}
start() {
this._globalPromiseResolver();
return this;
}
// noinspection JSUnusedGlobalSymbols
stop() {
this._stopped = !0;
}
}
// 'new' an object
const SeqPromise = function (options) {
return new SeqPromiseClass(options);
};
// trick borrowed from jQuery so we don't have to use the 'new' keyword
SeqPromise.prototype = SeqPromiseClass.prototype;
module.exports = SeqPromise;