forejs
Version:
A lightweight module which provides powerful functionality to organize asynchronous JavaScript code.
1,226 lines (1,077 loc) • 36.6 kB
JavaScript
var symbolsSupported = typeof Symbol === "function" && typeof Symbol.iterator === "symbol";
var arrayValuesSupported = typeof Array.prototype.values === "function";
/**
* Optimized Array#forEach
* @template T
* @param {T[]|Arguments} array
* @param {function(T, number)} f
*/
function each(array, f) {
var i = -1, length = array.length;
while (++i < length) {
f(array[i], i);
}
}
/**
* Optimized Array#map
* @template T
* @template S
* @param {T[]|Arguments} array
* @param {function(T, number): S} f
* @return S[]
*/
function map(array, f) {
var i = -1, length = array.length, result = new Array(length);
while (++i < length) {
result[i] = f(array[i], i);
}
return result;
}
/**
* Optimized in-place Array#map
* @template T
* @template S
* @param {T[]|Arguments} array
* @param {function(T, number): S} f
* @return S[]
*/
function replace(array, f) {
var i = -1, length = array.length;
while (++i < length) {
array[i] = f(array[i], i);
}
return array;
}
/**
* Optimized Array#every
* @template T
* @param {T[]|Arguments} array
* @param {function(T, number): boolean} predicate
* @return boolean
*/
function every(array, predicate) {
var i = -1, length = array.length;
while (++i < length) {
if (!predicate(array[i], i))
return false;
}
return true;
}
/**
* Id function
* @template T
* @param {T} arg
* @return {T}
*/
function id(arg) {
return arg;
}
/**
* @callback Callback
* @param {*} err
* @param {*=} res
*/
/**
* The main entry point. Supports two modes:
* <ul>
* <li>chain: In chain mode, <code>fore</code> accepts a list of functions that are executed sequentially.</li>
* <li>auto: In auto mode, <code>fore</code> accepts an object with identifiers as keys and functions as values. The
* identifiers can be referenced by other functions to retrieve its "return value" (see {@link inject} and
* {@link fore.ref}). ForeJs now figures out the perfect execution order and runs as much code in parallel as
* possible.</li>
* </ul>
*
* The functions passed may have one of the following forms:
* <ul>
* <li>Node-style asynchronous function: Accepts a number of arbitrary arguments followed by an error-first callback
* function. The function must call this callback with either a non-null first argument to signal an error or
* with null as first argument followed by any number of "return values".
* In chain mode those "return values" are directly passed on to the next function.
* In auto mode those values are passed to all dependent functions. If more than one value is passed to the
* callback, those values are passed as array to the dependents. Additionally all arguments but the last
* (callback) must be eliminated by injections ({@link inject}).</li>
* <li>Synchronous function: Sometimes you want to mix synchronous functions into asynchronous code. This is perfectly
* ok: Simply put a plain <code>return</code> statement and ignore the callback. If you want to return
* <code>undefined</code> on purpose you will need to invoke the callback, however.</li>
* <li>Promise: Promises are supported as well. Plain promises must be in a root position.</li>
* <li>Promise returning function: A function may as well return a promise instead of invoking the callback.
* Unfortunately, the function will still get passed a callback as last argument. If your function cannot cope
* with the extra argument, simply wrap it with another function.</li>
* <li>Array: As shown in {@link Injector.prototype.args} injections can be syntactically sugared. If you are looking
* for a way to iterate over arrays see {@link fore.each}.</li>
* <li>Instances of {@link Injector}: An Injector is simply the wrapping type of injected functions.</li>
* </ul>
*
* @example
* // chain mode:
* fore(
* f1,
* f2,
* ...
* );
* // auto mode:
* fore({
* a: fa,
* b: fb,
* c: ["a", "b", fc] // the results of fa and fb are injected into fc once they are ready
* });
* @param {(Object.<String, function|Array|Injector|Promise>)} functions For auto mode: an object hash with ids as keys and
* functions as values.
* @param {...function|Array|Injector|Promise} arguments For chain mode: a list of functions.
*/
function fore(functions) {
if (typeof functions === "object" && Object.getPrototypeOf(functions) === Object.prototype) {
dependentExecution(functions);
} else {
simpleChain(arguments);
}
}
/**
* @param {Object.<String, function>} functions
*/
function dependentExecution(functions) {
var ids = Object.getOwnPropertyNames(functions);
var valuePipes = {};
each(ids, function (id) {
valuePipes[id] = new ValuePipe();
});
var rootInjectors = [];
each(ids, function (id) {
var hasInjections = [false];
// create nodes
var injector = desugar(functions[id]);
var combinator = createCombinator(injector);
var executor = createExecutor(injector);
var valuePipe = valuePipes[id];
// link them
if (injector.injections !== null) {
replace(injector.injections, function (injection) {
return createValueProviderFromInjection(valuePipes, combinator, injection, hasInjections);
});
}
if (injector.thisInjection !== null) {
injector.thisInjection = createValueProviderFromInjection(valuePipes, combinator, injector.thisInjection, hasInjections);
}
combinator.injector = injector;
injector.executor = executor;
executor.valuePipe = valuePipe;
if (!hasInjections[0]) {
rootInjectors.push(injector);
}
});
// start execution chain
each(rootInjectors, function (injector) {
injector.execute(true, 1);
});
}
/**
* @param {Object.<String, ValuePipe>} valuePipes
* @param {AllCombinationsCombinator} combinator
* @param {Injection} injection
* @param {boolean[]} hasInjections
* @return {ValueProvider}
*/
function createValueProviderFromInjection(valuePipes, combinator, injection, hasInjections) {
var valueProvider = new ValueProvider();
if (injection instanceof Injection) {
hasInjections[0] = true;
var id = injection.id;
var valuePipe;
if (id.indexOf("|") >= 0) {
// "or injections" ( ref("a|b") )
valuePipe = new DemuxValuePipe(combinator);
var ids = id.split("|");
valuePipe.inputPipes = map(ids, function (id) {
var inputValuePipe = getValuePipe(valuePipes, id);
inputValuePipe.register(valuePipe);
return inputValuePipe;
});
} else {
valuePipe = getValuePipe(valuePipes, id);
valuePipe.register(combinator);
}
combinator.valuePipes.push(valuePipe);
combinator.valueProviders.push(valueProvider);
} else {
valueProvider.value = injection;
}
return valueProvider;
}
/**
* @param {Object.<String, ValuePipe>} valuePipes
* @param {String} id
* @return {ValuePipe}
*/
function getValuePipe(valuePipes, id) {
var valuePipe = valuePipes[id];
if (!valuePipe) {
throw new Error("Unbound identifier '" + id + "'.");
}
return valuePipe;
}
/**
* @param {*} functions
*/
function simpleChain(functions) {
var valuePipes = map(functions, function () {
return new ValuePipe();
});
var rootInjector = null;
each(functions, function (fn, i) {
var injector = desugar(fn);
injector.isSimpleChain = true;
var executor = createExecutor(injector);
var valuePipe = valuePipes[i];
injector.injections && replace(injector.injections, function (injection) {
var valueProvider = new ValueProvider();
valueProvider.value = injection;
return valueProvider;
});
if (i > 0) {
var combinator = createCombinator(injector);
var inputValuePipe = valuePipes[i - 1];
inputValuePipe.register(combinator);
var injectionValueProvider;
var thisValueProvider = new ValueProvider();
if (injector.thisInjection === void 0) {
// inject last result as "this" argument
injector.thisInjection = thisValueProvider;
injectionValueProvider = thisValueProvider;
} else {
// inject last result as last argument
thisValueProvider.value = injector.thisInjection;
injectionValueProvider = new ValueProvider();
if (injector.injections === null) {
injector.injections = [injectionValueProvider];
} else {
injector.injections.push(injectionValueProvider);
}
}
combinator.valuePipes.push(inputValuePipe);
combinator.valueProviders.push(injectionValueProvider);
injector.thisInjection = thisValueProvider;
combinator.injector = injector;
} else {
rootInjector = injector;
}
injector.executor = executor;
executor.valuePipe = valuePipe;
});
rootInjector.execute(true, 1);
}
/**
* Registers a general error handler.
* @callback catch
* @param {function(*)} errorHandler A function which accepts one argument - the error.
*/
/**
* Wraps {@link fore} with a try-catch mechanism, which registers a general error handler for all provided functions.
* Once any of these functions "returns" an error this error handler will be invoked and the propagation of the
* respective execution branch will be stopped.
*
* This error handler can be shadowed for single functions using {@link Injector.prototype.catch}.
* @example
* fore.try(
* // functions...
* ).catch(error => ...)
* @return {{catch: catch}} An object with a single function <code>catch</code> to pass the error handler to.
*/
fore.try = function () {
var args = arguments;
return {
"catch": function (errorHandler) {
var functions = args[0];
if (typeof functions === "object" && Object.getPrototypeOf(functions) === Object.prototype) {
each(Object.getOwnPropertyNames(functions), function (name) {
functions[name] = injectErrorHandler(functions[name], errorHandler);
});
fore(functions);
} else {
fore.apply(null, map(args, function (f) {
return injectErrorHandler(f, errorHandler);
}));
}
}
}
};
/**
* Successively returns all values the iterable provides. Values will be processed in parallel.
* @see fore.collect
* @see fore.reduce
* @param {Array|Iterator|Iterable|function|Injector} iterable One of the following:
* <ul>
* <li>An array of arbitrary values. Use to run a sequence of functions for multiple values.</li>
* <li>An Iterator, i.e. an object providing a <code>next</code> function which itself returns objects in the shape of
* <code>{value: value|undefined, done: true|false|undefined}</code>.</li>
* <li>An Iterable, i.e. an object providing a <code>Symbol.iterator</code> to retrieve an Iterator like above.</li>
* <li>A generator function: A function which returns an Iterator (like ES6 <code>function*</code>s do}. If this function
* takes arguments make sure to take care of the respective injections.</li>
* </ul>
* The cases 1 - 3 require <code>fore.each</code> to be in a root position (they don't depend on other functions).
* The last case is allowed in any position.
*
* Any iterable type may also provide promises as values in order to asynchronously generate values.
*
* Values generated by <code>fore.each</code> will be propagated through subsequent functions causing them to be called
* multiple times. If a function has several dependencies (in auto mode) that originate in a <code>fore.each</code>,
* it will be invoked with any possible combinations of the incoming values.
* @return {Injector} An injector which can be used to inject arguments to the iterable in case it is a generator
* function (<code>function*</code>).
*/
fore.each = function foreEach(iterable) {
var injector = (iterable instanceof Injector) ? iterable : new Injector(iterable);
injector.mode = ExecutionMode.EACH;
return injector;
};
/**
* The counterpart to {@link fore.each}. Collects all values that were generated by {@link fore.each} and modified by
* in-between functions. The results will be passed on to <code>fn</code> as array. If it depends on multiple iterables
* <code>fore.collect</code> waits for all branches to finish and each result array will be passed on as separate argument.
*
* Naturally for asynchronous code the result array will not necessarily have the same order as the input.
* @see fore.reduce
* @param {function|Injector|Array.<*>} fn
* @return {Injector} An injector which can be used to inject arguments to the function.
*/
fore.collect = function (fn) {
var injector = desugar(fn);
injector.mode = ExecutionMode.COLLECT;
return injector;
};
/**
* Another counterpart to {@link fore.each}. Behaves much like {@link fore.collect} but provides the results not as array
* but in a fashion similar to {@link Array.prototype.reduce}: <code>fn</code> will be called once for each element of
* the result. It will receive the accumulator followed by injections followed by a callback
* <code>(accumulator, injections, ..., callback)</code>. The "return value" of this call will be the new accumulator
* for the next invocation. For the first invocation the accumulation variable is <code>initialValue</code>.
*
* If there is more than one dependency (and several of these originate in a {@link fore.each}) <code>fn</code> will
* be called once for every possible combination of the incoming values.
*
* Likewise, no specific execution order can be guaranteed.
* @example
* fore(
* fore.each([1, 2, 3, 4]),
* plusOne,
* fore.reduce((accumulator, value, callback) => callback(null, accumulator * value), 1),
* console.log
* // result: 1 * 2 * 3 * 4 = 24
* )
* @param {function|Injector|Array.<*>} fn The function which will be invoked with
* <code>(accumulator, value, ..., callback)</code>
* @param {*} initialValue The value for the accumulator during the first invocation.
* @return {Injector} An injector which can be used to inject arguments to the function.
*/
fore.reduce = function (fn, initialValue) {
var injector = desugar(fn);
injector.mode = ExecutionMode.REDUCE;
if (injector.injections === null) {
injector.injections = [initialValue];
} else {
injector.injections.unshift(initialValue);
}
return injector;
};
/**
* @param {Injector|function} injector
* @param {function} errorHandler
* @return {Injector}
*/
function injectErrorHandler(injector, errorHandler) {
injector = desugar(injector);
if (!injector.errorHandler) {
injector.errorHandler = errorHandler;
}
return injector;
}
/**
* @param {Injector|undefined} injector
* @param {*} err
*/
function handleError(injector, err) {
injector.errorHandler(err);
}
/**
* @param {Array.<*>|function|Injector} fn
* @param {Injector} fn.injector
* @return {Injector}
*/
function desugar(fn) {
if (Array.isArray(fn)) {
// desugar ["a", "b", function (a, b) {...}]
var functions = [];
var injections = [];
for (var i = -1, length = fn.length; ++i < length;) {
var item = fn[i];
if (typeof item === "function" || item instanceof Injector) {
functions.push(item);
} else {
injections.push(typeof item === "string" ? fore.ref(item) : item);
}
}
var theFunction;
if (functions.length === 1) {
theFunction = functions[0];
} else {
theFunction = function () {
var lastIndex = arguments.length - 1;
// inject values from "outer" fore into first function of "inner" fore
var injector = new Injector(functions[0]);
var injections = injector.injections = new Array(lastIndex);
for (var j = -1; ++j < lastIndex;) {
injections[j] = arguments[j];
}
functions[0] = injector;
// append "outer" callback as last function
var callback = arguments[lastIndex];
functions.push(function (result) {
callback(null, result);
});
simpleChain(functions);
};
}
var injector = new Injector(theFunction);
injector.injections = injections;
return injector;
}
if (!(fn instanceof Injector)) {
return new Injector(fn);
}
return fn;
}
/**
* References the result of another function when using auto mode. To be used within {@link Injector.prototype.args} or
* {@link Injector.prototype.this}
* @param {String} id The id to reference.
*/
fore.ref = function ref(id) {
return new Injection(id);
};
/**
* @constructor
* @property {Array.<*>} values
* @property {boolean} done
*/
function ValuePipe() {
this.values = [];
this.observers = [];
this.done = false;
this.reachedLast = false;
this.expectedLength = 0;
this.failedLength = 0;
}
/**
* @param {Combinator|DemuxValuePipe} observer
*/
ValuePipe.prototype.register = function (observer) {
if (this.observers.indexOf(observer) < 0) {
this.observers.push(observer);
}
};
/**
* @param {*} value
* @param {boolean} done
* @param {number} expectedLength
*/
ValuePipe.prototype.push = function (value, done, expectedLength) {
this.values.push(value);
this.updateDone(done, expectedLength);
var sender = this;
each(this.observers, function (observer) {
observer.notify(sender);
});
};
ValuePipe.prototype.pushFailure = function (done, expectedLength) {
this.failedLength++;
this.updateDone(done, expectedLength);
var sender = this;
each(this.observers, function (observer) {
observer.notifyFailure(sender);
});
};
/**
* @param {boolean} done
* @param {number} expectedLength
* @private
*/
ValuePipe.prototype.updateDone = function (done, expectedLength) {
if (done) {
this.reachedLast = true;
}
this.expectedLength = Math.max(this.expectedLength, expectedLength);
this.done = this.reachedLast && this.values.length === this.expectedLength - this.failedLength;
};
/**
* This is neither a real {@link ValuePipe} nor a real {@link Combinator}. It de-multiplexes several ValuePipes to one
* Combinator input.
* @param {Combinator} combinator
* @constructor
*/
function DemuxValuePipe(combinator) {
this.values = [];
this.combinator = combinator;
this.inputPipes = null;
this.done = false;
}
DemuxValuePipe.prototype.notify = function (sender) {
var senderValues = sender.values;
this.values.push(senderValues[senderValues.length - 1]);
this.updateDone();
this.combinator.notify(this);
};
DemuxValuePipe.prototype.notifyFailure = function (sender) {
this.updateDone();
this.combinator.notifyFailure(this);
};
DemuxValuePipe.prototype.updateDone = function () {
this.done = every(this.inputPipes, function (valuePipe) { return valuePipe.done });
};
/**
* @constructor
* @abstract
* @property {Injector} injector
* @property {ValuePipe[]} valuePipes
* @property {ValueProvider[]} valueProviders
*/
function Combinator() {
this.injector = null;
this.valuePipes = [];
this.valueProviders = [];
}
/**
* @param {ValuePipe|DemuxValuePipe} sender
* @abstract
*/
Combinator.prototype.notify = function (sender) {
};
/**
* @param {ValuePipe|DemuxValuePipe} sender
*/
Combinator.prototype.notifyFailure = function (sender) {
};
/**
* @constructor
* @extends Combinator
*/
function SimpleCombinator() {
Combinator.call(this);
}
SimpleCombinator.prototype = Object.create(Combinator.prototype);
SimpleCombinator.prototype.notify = function (sender) {
var valuePipe = this.valuePipes[0];
var values = valuePipe.values;
this.valueProviders[0].value = values[values.length - 1];
this.injector.execute(valuePipe.done, values.length);
};
/**
* @constructor
* @extends Combinator
*/
function AllCombinationsCombinator() {
Combinator.call(this);
this.executionCounter = 0;
}
AllCombinationsCombinator.prototype = Object.create(Combinator.prototype);
AllCombinationsCombinator.prototype.notify = function (sender) {
var valuePipes = this.valuePipes;
var valueProviders = this.valueProviders;
var pipesDone = true;
var possibleCombinations = 1;
var senderIndex = -1;
var currentLengths = map(valuePipes, function (valuePipe, i) {
pipesDone &= valuePipe.done;
var length = valuePipe.values.length;
possibleCombinations *= length;
if (valuePipe === sender) {
senderIndex = i;
}
return length;
});
if (this.executionCounter >= possibleCombinations) {
// prevent double execution
return;
}
this.executionCounter = possibleCombinations;
valueProviders[senderIndex].value = valuePipes[senderIndex].values[currentLengths[senderIndex] - 1];
var currentIndices = replace(new Array(valuePipes.length), function () {
return 0;
});
var carry = false;
while (!carry) {
carry = true;
// set current values and count one step
var i = -1, length = valuePipes.length;
while (++i < length) {
if (i === senderIndex) {
continue;
}
valueProviders[i].value = valuePipes[i].values[currentIndices[i]];
if (carry) {
currentIndices[i]++;
carry = false;
if (currentIndices[i] === currentLengths[i]) {
currentIndices[i] = 0;
carry = true;
}
}
}
// emit value combination
this.injector.execute(pipesDone && carry, possibleCombinations);
}
};
/**
* @constructor
* @extends Combinator
*/
function CollectorCombinator() {
Combinator.call(this);
}
CollectorCombinator.prototype = Object.create(Combinator.prototype);
CollectorCombinator.prototype.notify = CollectorCombinator.prototype.notifyFailure = function (sender) {
var valuePipes = this.valuePipes;
if (!every(valuePipes, function (pipe) { return pipe.done })) {
return;
}
var valueProviders = this.valueProviders;
each(valuePipes, function (valuePipe, i) {
valueProviders[i].value = map(valuePipe.values, id);
});
this.injector.execute(true, 1);
};
/**
* @param {Injector} injector
* @return {*}
*/
function createCombinator(injector) {
if (injector.mode === ExecutionMode.COLLECT) {
return new CollectorCombinator();
} else if (injector.injections === null
|| (injector.injections.length <= 1 && injector.thisInjection === null)
|| (injector.thisInjection !== null && (injector.injections === null || injector.injections.length === 0))) {
return new SimpleCombinator();
}
return new AllCombinationsCombinator();
}
/**
* @constructor
* @property {*} value
*/
function ValueProvider() {
this.value = null;
}
/**
* @param {String} id
* @constructor
* @property {String} id
*/
function Injection(id) {
this.id = id;
}
/**
* @readonly
* @enum {number}
*/
var ExecutionMode = {
STANDARD: 0,
EACH: 1,
COLLECT: 2,
REDUCE: 3
};
/**
* Provides methods to inject dependencies or constant values into functions. This means that a function will be
* "partially evaluated". Additional arguments like the callback function or, in simple mode, the "return values" of the
* previous function are provided by {@link fore}.
* Retrieved by {@link inject}. Don't call this constructor by yourself.
* @constructor
*/
function Injector(fn) {
this.fn = fn;
this.injections = null;
this.thisInjection = null;
this.errorHandler = null;
this.mode = ExecutionMode.STANDARD;
this.isSimpleChain = false;
this.executor = null;
}
/**
* Injects constant values or dependencies into this function starting from the left. Use {@link fore.ref} to inject
* dependencies in auto mode. If no string constants need to be injected, <code>inject.args(...)</code> can also be
* written using a shorter array notation, which is especially handy for anonymous functions:
* @example
* ((arg1, arg2) => ...).inject.args(fore.ref("arg1"), fore.ref("arg2"))
* // shorter:
* ["arg1", "arg2", (arg1, arg2) => ...]
* @param {Injection|*} arguments The list of injections.
* @return {Injector} <code>this</code>.
* @chainable
*/
Injector.prototype.args = function args() {
this.injections = map(arguments, function (arg) {
return arg;
});
return this;
};
/**
* Injects a constant value or dependency as <code>this</code> argument. Use {@link fore.ref} to inject dependencies in
* auto mode. In chain mode, call this function without arguments in order to retrieve the "return value" of the previous
* function as <code>this</code> argument instead of as first argument. If the previous function "returns" multiple values
* only the first one will be passed, the others are ignored.
* @param {Injection|Object} object The injection.
* @return {Injector} <code>this</code>
* @chainable
*/
Injector.prototype.this = function ths(object) {
this.thisInjection = object;
return this;
};
/**
* Attaches an error handler to this function that will be called if the function invokes its callback with a non-null
* first argument. This error will be passed as first argument to the errorHandler. Once an error occurs, the propagation
* of this execution branch will be stopped.
*
* It is also possible to register a general error handler to the entire fore-block using {@link fore.try}. Error handlers
* attached directly to the function are prioritized.
*
* If an error occurs and no error handler has been registered the execution will break. So catch your errors!
* @param {function(*)} errorHandler The error handler, a function which accepts one argument.
* @return {Injector} <code>this</code>
* @chainable
*/
Injector.prototype.catch = function ctch(errorHandler) {
this.errorHandler = errorHandler;
return this;
};
/**
* @param {boolean} done
* @param {number} expectedLength
* @protected
*/
Injector.prototype.execute = function (done, expectedLength) {
var injections = this.injections;
var args;
if (this.isSimpleChain) {
args = [];
if (injections !== null) {
each(injections, function (valueProvider) {
var value = valueProvider.value;
if (value instanceof ArgumentsWrapper) {
Array.prototype.push.apply(args, value.args);
} else {
args.push(value);
}
});
}
} else {
args = injections === null ? [] : map(injections, function (valueProvider) {
var value = valueProvider.value;
return (value instanceof ArgumentsWrapper) ? value.args : value;
});
}
var thisArg;
var thisInjection = this.thisInjection;
if (thisInjection instanceof ValueProvider) {
if (thisInjection.value instanceof ArgumentsWrapper) {
thisArg = thisInjection.value.args[0];
} else {
thisArg = thisInjection.value;
}
}
this.executor.execute(thisArg, args, done, expectedLength);
};
/**
* @param {Arguments} args
* @constructor
*/
function ArgumentsWrapper(args) {
var clone = this.args = new Array(args.length - 1);
var i = 0, length = args.length;
while (++i < length) {
clone[i - 1] = args[i];
}
}
/**
* @constructor
* @abstract
* @property {ValuePipe} valuePipe
* @property {Injector} injector
*/
function Executor(injector) {
this.injector = injector;
this.valuePipe = null;
}
/**
* @abstract
* @param {Object|null} thisArg
* @param {Array.<*>} args
* @param {boolean} done
* @param {number} expectedLength
*/
Executor.prototype.execute = function (thisArg, args, done, expectedLength) {
};
/**
* @param {Injector} injector
* @constructor
* @extends Executor
*/
function AsyncExecutor(injector) {
Executor.call(this, injector);
this.fn = injector.fn;
}
AsyncExecutor.prototype = Object.create(Executor.prototype);
AsyncExecutor.prototype.execute = function (thisArg, args, done, expectedLength) {
var valuePipe = this.valuePipe;
function emit(res) {
valuePipe.push(res, done, expectedLength);
}
executeFunction(this.fn, thisArg, args, this.injector, emit);
};
/**
* @param {function} fn
* @param {*} thisArg
* @param {Array.<*>} args
* @param {Injector} injector
* @param {function(*)} emit
*/
function executeFunction(fn, thisArg, args, injector, emit) {
function callback(err, res) {
if (err !== null) {
handleError(injector, err);
} else {
if (arguments.length > 2) {
res = new ArgumentsWrapper(arguments);
}
emit(res);
}
}
args.push(callback);
var returnValue = fn.apply(thisArg, args);
if (returnValue === void 0) {
return;
}
if (returnValue instanceof Promise) {
executePromise(returnValue, injector, emit);
} else {
emit(returnValue);
}
}
/**
* @param {Injector} injector
* @param {Promise} injector.fn
* @constructor
* @extends Executor
*/
function PromiseExecutor(injector) {
Executor.call(this, injector);
this.promise = injector.fn;
}
PromiseExecutor.prototype = Object.create(Executor.prototype);
PromiseExecutor.prototype.execute = function (thisArg, args, done, expectedLength) {
var valuePipe = this.valuePipe;
executePromise(this.promise, this.injector, function (value) {
valuePipe.push(value, true, 1);
});
};
/**
* @param {Promise} promise
* @param {Injector} injector
* @param {function(*)} emit
*/
function executePromise(promise, injector, emit) {
promise
.then(emit)
.catch(function (err) {
handleError(injector, err);
});
}
/**
* @param {Iterator} iterator
* @param {Injector} injector
* @constructor
* @extends Executor
*/
function IteratorExecutor(iterator, injector) {
Executor.call(this, injector);
this.iterator = iterator;
}
IteratorExecutor.prototype = Object.create(Executor.prototype);
IteratorExecutor.prototype.execute = function (thisArg, args, done, expectedLength) {
var iterator = this.iterator;
var valuePipe = this.valuePipe;
var injector = this.injector;
for (var next = iterator.next(), length = 1; !next.done; length++) {
var value = next.value;
// retrieve next already here because we must know if the iterator is done
next = iterator.next();
if (value instanceof Promise) {
value
.then(function (done, expectedLength, value) {
valuePipe.push(value, done, expectedLength)
}.bind(null, next.done, length))
.catch(function (done, expectedLength, err) {
handleError(injector, err);
valuePipe.pushFailure(done, expectedLength);
}.bind(null, next.done, length));
} else {
valuePipe.push(value, next.done, length);
}
}
};
/**
* @param {function} generator
* @param {Injector} injector
* @constructor
* @extends Executor
*/
function GeneratorExecutor(generator, injector) {
Executor.call(this, injector);
this.generator = generator;
}
GeneratorExecutor.prototype = Object.create(Executor.prototype);
GeneratorExecutor.prototype.execute = function (thisArg, args, done, expectedLength) {
var iterator = this.generator.apply(thisArg, args);
var iteratorExecutor = new IteratorExecutor(iterator, this.injector);
iteratorExecutor.valuePipe = this.valuePipe;
iteratorExecutor.execute(thisArg, args, done, expectedLength);
};
/**
* @param {Injector} injector
* @constructor
* @extends Executor
*/
function ReduceExecutor(injector) {
Executor.call(this, injector);
this.fn = injector.fn;
this.accumulationValue = injector.injections[0];
this.pendingExecutions = [];
this.isRunnning = false;
this.executedLength = 0;
this.done = false;
}
ReduceExecutor.prototype = Object.create(Executor.prototype);
ReduceExecutor.prototype.execute = function (thisArg, args, done, expectedLength) {
var pendingExecutions = this.pendingExecutions;
// save snapshot
pendingExecutions.push([thisArg, args]);
this.done = this.done || done;
var execute = function () {
if (pendingExecutions.length === 0) {
return;
}
// restore snapshot
var parameters = pendingExecutions.pop();
parameters[1][0] = this.accumulationValue;
this.executedLength++;
executeFunction(this.fn, parameters[0], parameters[1], this.injector, function (value) {
if (this.done && pendingExecutions.length === 0) {
this.valuePipe.push(value, true, 1);
} else {
this.accumulationValue = value;
execute();
}
}.bind(this));
}.bind(this);
if (!this.isRunnning) {
this.isRunnning = true;
execute();
this.isRunnning = false;
}
};
/**
* @param {Injector} injector
* @return {Executor}
*/
function createExecutor(injector) {
switch (injector.mode) {
case ExecutionMode.STANDARD:
case ExecutionMode.COLLECT:
if (injector.fn instanceof Promise) {
return new PromiseExecutor(injector);
} else {
return new AsyncExecutor(injector);
}
case ExecutionMode.REDUCE:
return new ReduceExecutor(injector);
case ExecutionMode.EACH:
var iterable = injector.fn;
if (Array.isArray(iterable)) {
var iterator;
if (arrayValuesSupported) {
iterator = iterable.values();
} else {
iterator = {
next: (function () {
var array = iterable;
var i = 0;
return function () {
return i < array.length ? {value: array[i++]} : {done: true};
}
})()
}
}
return new IteratorExecutor(iterator, injector)
}
if (typeof iterable === "function") {
return new GeneratorExecutor(iterable, injector);
}
if (symbolsSupported && typeof iterable === "object" && typeof iterable[Symbol.iterator] === "function") {
return new IteratorExecutor(iterable[Symbol.iterator](), injector);
}
}
}
/**
* Starts the injection of values or dependencies into this function. Should be followed by one of the {@link Injector}
* methods. Use <code>inject</code> to avoid function wrappers or things like {@link Function.prototype.bind}.
* @return {Injector} The injector.
*/
function inject() {
return new Injector(this);
}
/**
* @param {function} fn
* @return {Injector}
*/
function foreInject(fn) {
return inject.call(fn);
}
// add inject to Function prototype
function attachInject() {
Object.defineProperty(Function.prototype, "inject", {
get: inject,
configurable: true,
enumerable: false
});
}
/**
* Configures foreJs.
* @param {object=} properties The configuration object.
* @param {boolean=} properties.dontHackFunctionPrototype Set <code>true</code> to keep <code>Function.prototype</code>
* clean and omit the {@link inject} getter. {@link inject} now exists as static property of {@link fore} instead:
* <code>fore.inject(myFunction).args(...)</code>. Default: <code>false</code>
*/
fore.config = function (properties) {
var config = new Config(properties);
if (config.dontHackFunctionPrototype) {
if (Object.getOwnPropertyDescriptor(Function.prototype, "inject").get === inject) {
delete Function.prototype.inject;
}
fore.inject = foreInject;
} else {
if (typeof fore.inject === "function") {
delete fore.inject;
}
attachInject();
}
};
/**
* @param {Object} config
* @constructor
* @property {boolean} dontHackFunctionPrototype
*/
function Config(config) {
config && each(Object.getOwnPropertyNames(config), function (name) {
this[name] = config[name];
}.bind(this));
}
Config.prototype.dontHackFunctionPrototype = false;
fore.config();
module.exports = fore;