enque
Version:
Run asynchonous functions in succession while potentially operating on the same data.
233 lines (222 loc) • 8.64 kB
JavaScript
// enQue class
// Author leathan
// License MIT
'use strict'
// **Constructor** Creates a new enQue object with the specified
// functions. `que = new enQue([fn1, fn2, fn3])`
// You can also create an empty object via `new enQue()`
function enQue(init) {
this.que = [];
if(init) this.add(init);
// __returns itself for use in chaining__
return this;
}
// **Compact** Removes undefined and null values.
// This method should never ever be called
// directly, and it will probably become private
// in the future.. but who knows. `que.compact()`
enQue.prototype.compact = function() {
var i = 0;
const res = [];
for (let val of this.que) if(val) res[i++] = val;
this.que = res;
// __returns itself for use in chaining__
return this;
}
// **enQue.fill** Fills an enQue object with `fn`'s `n` times.
// `que.fill((_,n)=>{console.log('works');n()}, 7)`
// running that que will display 'works' 7 times.
enQue.prototype.fill = function(fn, n) {
for(let i = 0; i < n; i++) {
this.que.push(fn);
}
// __returns itself for use in chaining__
return this;
}
// **enQue.add** Adds `fn` to the enQue object.
// `que.add((_,n,__,i)=>{console.log(i)})` or you can
// specify an array of functions `que.add([fn1, fn1])`
enQue.prototype.add = function(fn) {
if(fn.constructor.name === 'Array') {
for(let i = 0, l = fn.length; i < l; i++) {
this.que.push(fn[i]);
}
} else {
this.que.push(fn);
}
// __returns itself for use in chaining__
return this;
}
// **enQue.clear** Clears all functions from the que
// `que.clear()`
enQue.prototype.clear = function() {
this.que = [];
return this;
// __returns itself for use in chaining__
}
// **enQue.remove** Removes an item from the que.
// `que.remove("(_,n,__,i)=>{console.log(i)}")`
// `que.remove(fn1, 2)` removes 2 occurances of fn1
// `que.remove(7)` removes the 8th function (0 indexed)
// to remove an array of function refs and/or strinfified functions.
// `que.remove([fn1, "(_,n,__,i)=>{console.log(i)}", fn2])`
// `que.remove([fn1, fn2, fn3], 2)` will only remove fn1 and fn2.
enQue.prototype.remove = function(item, amount) {
// Here we extract the items type.
let type = item.constructor.name;
if(type === 'Number') {
return this.que.splice(item, 1);
}
else if(type === "Array") {
amount = amount || Infinity;
let removed = 0;
for(let i=0, l=item.length; i<l; i++) {
var check = item[i].constructor.name === 'Function' ? item[i] : item[i].toString();
for(let j=0, l2=this.que.length; j<l2; j++) {
// Make sure we dont remove more than amount!
if(removed === amount) break;
if(this.que[j] === check) {
delete this.que[j];
removed++;
}
}
}
// __returns itself after compacting for use in chaining__
return this.compact();
}
else {
let check = type === 'Function' ? item : item.toString();
amount = amount || Infinity;
let removed = 0;
for(let i=0, l=this.que.length; i<l; i++) {
// Make sure we dont remove more than amount!
if(removed === amount) break;
if(check === check.constructor.name === 'Function' ? this.que[i] : this.que[i].toString()) {
delete this.que[i]
removed++;
}
}
// __returns itself after compacting for use in chaining__
return this.compact();
}
}
// **executeQue** Executes the que, you should not need to call
// this function directly, for example if `data` doesnt exist
// you will not be able to consume/output properly.
// `run` makes sure data exists. On the offchance you need to
// bypass the promise system its avialable `que.executeQue()`
// but remember to pass in `data` and `done` if needed.
enQue.prototype.executeQue = function(data, done) {
// preserve the original callback for potential que rebuilding.
var orig = done;
// `i` is our iterator, `quit` and `inject` are used to check if we need
// to quit early and resolve the data, or inject `Function`.
var i = 0, quit = false, inject = false, injectFn = false;
// The `nest` function allows us to itterate through the que while constantly
// nesting callbacks.
this.nest((done, next) =>
options => {
// Options can be any operation to perform while nesting callbacks.
// Currently options must be a specific `JSON Object`, or a `Number`, if its JSON
// then it needs a `quit` or `inject` property. Otherwise **0** terminates que,
// exposing the data immitiadtly. A negative Number sends the data
// to backwards in the que (to a new que **techincally**), Positive Numbers
// send the data forward, `i` is current callback index being nested.
if(options === 0) return next = orig;
if(options !== data && options === Object(options)) {
if((!options.function && !options.inject) && !options.quit)
throw new Error(`${options} is not supported, valid fomat could be +n, -n, 0, or, {quit:+n}, {inject:+n,function:Function}`)
else if(options.quit) {
quit = options.quit;
}
else if(options.inject) {
inject = options.inject - 1;
injectFn = options.function;
}
}
else if(options !== data && options) {
// creates a temp que to hold our new que.
let tmpQue = [];
// let `j` be the position we are skipping to.
// lets say `next(-3)` was passed, so `options = -3`.
// `i` is the current que spot - 1 that called `next(-3)`.
// lets say `i` was **5**, so we want to go back to **1**.
// so `j = 5 + -3 - (options < 0) = 1`
// so start at position 1, until the end.
var j = i + options - (options < 0);
if(!this.que[j-1]) throw `${options} out of bounds, no que position ${j} exists.`
for(let l = this.que.length; j < l; j++) {
tmpQue.push(this.que[j])
}
// sets the que to the one we just built.
this.que = tmpQue;
// Apends the original data exposure callback.
this.que.push(orig)
this.executeQue(data);
// Makes sure the current execution goes nowhere.
done = ()=>{}
next = ()=>{}
}
// if quit is specified, checks if we need to quit
// and if so sets `next` to resolve `data`, otherwise de-increments
// the checker variable.
if(quit !== false) {
if(quit === 0) { next = orig; quit = false }
else quit--;
}
// if inject is specified, checks if we need to inject
// the `Function` if so waits it injects the function
// **which is NOT part of the que** and hence its execution is
// not synchronized, this will probably be fixed in the future.
// calls `next`, otherwise de-increments checker and calls `next`.
if(inject !== false) {
if(inject === 0) {
next(data, done, i++, orig);
injectFn(data);
injectFn = inject = false;
} else {
inject--;
next(data, done, i++, orig);
}
} else {
// no special object options were specified just proceeds.
next(data, done, i++, orig)
}
}
// this sets our initial accumulator to the function done each successive call
// then creates another function callback nest where the old `done` becomes the callback
// of the new `done` the original `done` passed in here does nothing more then resolve the
// data. Which is passed in immidiatly since right after nesting the entire composition is executed.
, done)(data);
}
// **enQue.run** Creates a promise which resolves when the que ends.
// `que.run(data)` or for ques that dont consume data `que.run()`
// Since a promise is returns you should then call `.then(data=>{})`
// and `.catch(error=>{})`
enQue.prototype.run = function(data) {
// Allow ques that dont need to consume data.
if(!data) data = {};
return new Promise((resolve, reject) => {
try {
this.executeQue(data, () => resolve(data));
} catch(error) {
reject(error);
}
// __returns a promise which can be used for chaining__
})
}
// ***enQue.nest*** This method should never be called directly
// unless you know what you are doing. `que.nest()`
enQue.prototype.nest = function(callback, orig) {
var que = this.que, pos = que.length - 1, nest;
// Set the initial done to resolve(data).
nest = orig;
for (; pos >= 0; pos--) {
// Now our original done is a callback.
// which keeps being nested till pos == 0.
nest = callback(nest, que[pos]);
}
// __returns the fully nested object__
return nest;
};
module.exports = enQue;