/**
* (c) 2013-2016 Ibon Tolosana.
*
* @see license.txt file
*
*/
/**
* Schedule a task in the future.
* @param f {function()}
* @param delay {number=} milliseconds to schedule in the future
*/
function schedule(f:()=>void, delay?:number) {
if (!delay) {
setImmediate(f);
} else {
setTimeout(f, delay || 0);
}
}
/**
* ForEach parallel.
* Schedule each of the Array elements in the future.
* Do not use directly, it is used internally by the library.
*/
function forEachP(arr:ParallelConditionDescriptor[]) {
arr.forEach(function (elem:ParallelConditionDescriptor,index:number) {
schedule(
function () {
elem._fn(elem._condition, index);
}
);
});
}
export type SignalObserver = (...args:any[])=>void;
/**
* Signal is a string-based observer substitution. Instead of registering events by (commonly) string,
* the signal gives context to the event by creating an object which takes care of notifying its observers.
* It also takes care of all the burden of managing observers, registering, notifying, etc.
*
* It allows to registers single and multi shot observers.
*
* The signal is agnostic regarding what content will notify to its observers.
*
* Every time the signal emits (notifies observers) the observersOnce collection will be reset.
*
* @return {*}
* @constructor
*/
export class Signal {
_observers:SignalObserver[];
_observersOnce:SignalObserver[];
constructor() {
this._observers = [];
this._observersOnce = [];
}
on(observer:SignalObserver) {
return this.addListener(observer);
}
/**
* Add a multishot observer.
* @param f {function(*)} a callback function with variable parameters.
* @return {*}
*/
addListener(f:SignalObserver) {
this._observers.push(f);
return this;
}
/**
* Add a multi shot observer as the first one in the list.
* @param f {function(*)} a callback function with variable parameters.
* @return {*}
*/
addListenerInFrontOfList(f:SignalObserver) {
this._observers.unshift(f);
return this;
}
/**
* Remove a multishot observer.
* @param f {function(*)} a previously registered observer by a call to addLitener.
* @return {*}
*/
removeListener(f:SignalObserver) {
let index = this._observers.indexOf(f);
if (-1 !== index) {
this._observers.splice(index, 1);
}
return this;
}
/**
* Remove all multishot observers.
* @return {*}
*/
removeAllListeners() {
this._observers = [];
this._observersOnce = [];
return this;
}
/**
* Add a one shot observer.
* The callback function will only be called once.
* @param f {function()}
*/
addListenerOnce(f:SignalObserver) {
this._observersOnce.push(f);
return this;
}
/**
* Notify all observers, either single/multi shot.
*/
emit(...__arguments:any[]) {
this._observers.forEach(function (e) {
e.apply(e, Array.prototype.slice.call(__arguments));
});
this._observersOnce.forEach(function (e) {
e.apply(e, Array.prototype.slice.call(__arguments));
});
this._observersOnce = [];
}
}
/**
* @enum Condition possible values.
*/
export enum BOOL_OPERATOR {
NOT_SET = -1,
TRUE = 1,
FALSE = 0
}
var __conditionIndex = 0;
/**
* Condition is a wrapper for a tri-state condition.
*
* The need for a Condition object is that of statefulness. In certain situations, you want to know
* whether certain condition has happened in time, and whether it was true or false.
*
* A condition will only notify once when its value is set. It will never notify again after set, and can't
* have its internal condition value changed.
*
* Whenever the Condition changes state, it will notify any registered observers. To do so, the Condition
* holds a Signal object.
*
* Conditions can have associated a timeout. If the timer expires, the Condition is automatically set to
* false by calling setFalse().
*
* You can wait for value changes on a condition by calling
*
onTrue(callback)
* onFalse(callback)
* onChange(callback)
*
* in all three cases, the callback function will receive the Condition as parameter.
*
* A condition usage use case:
*
* var c= new Condition().onTrue( function(condition) {
* // do something here when the condition is met
* } );
*
* later in the code:
*
* c.setTrue();
*
* This is no different than setting a callback, but at any given moment, you can ask if the condition has
* ever had value:
*
* c.isNotSet()
*
* and if it has ever met, whether it was true or false:
*
* c.isTrue() or c.isFalse().
*
* @return {*}
* @constructor
*/
export class Condition {
/**
* Signal to emit state change events {Signal}
* @type {Signal}
* @private
*/
_signalConditionStateChange:Signal;
/**
* Signal to emit condition timed out.
* @type {Signal}
*/
_signalTimeout:Signal;
/**
* internal state value {Condition}
* @type {BOOL_OPERATOR}
* @private
*/
_b_condition:BOOL_OPERATOR;
/**
* Arbitrary id.
* @type {*}
*/
_id:string;
_timerId:number;
constructor() {
this._signalConditionStateChange = new Signal();
this._signalTimeout = new Signal();
this._b_condition = BOOL_OPERATOR.NOT_SET;
this._id = 'Condition' + __conditionIndex++;
this._timerId = null;
}
get id() {
return this._id;
}
set id(id:string) {
this._id = id;
}
setId( id:string ) {
this.id = id;
return this;
}
/**
* Emit condition state change events.
* @private
*/
__emit() {
this._signalConditionStateChange.emit(this);
}
/**
* Set a condition as true.
* If the condition was true, nothing happens.
* Otherwise, the internal status will be BOOL_OPERATOR.TRUE.
* Observers of this condition will be notified of the state change.
* @return {*}
*/
setTrue() {
if ( this._b_condition===BOOL_OPERATOR.TRUE ) {
return this;
}
this.__cancelTimer();
this._b_condition = BOOL_OPERATOR.TRUE;
this.__emit();
return this;
}
/**
* Set a condition as true.
* If the condition was false, nothing happens.
* Otherwise, the internal status will be a value from BOOL_OPERATOR.FALSE.
* Observers of this condition will be notified of the state change.
* @return {*}
*/
setFalse() {
if ( this._b_condition===BOOL_OPERATOR.FALSE ) {
return this;
}
this.__cancelTimer();
this._b_condition = BOOL_OPERATOR.FALSE;
this.__emit();
return this;
}
/**
* Test this condition for BOOL_OPERATOR.TRUE
* @return {Boolean}
*/
isTrue() {
return this._b_condition === BOOL_OPERATOR.TRUE;
}
/**
* Test this condition for BOOL_OPERATOR.FALSE
* @return {Boolean}
*/
isFalse() {
return this._b_condition === BOOL_OPERATOR.FALSE;
}
/**
* Test this condition for BOOL_OPERATOR.NOT_SET
* @return {Boolean}
*/
isNotSet() {
return this._b_condition === BOOL_OPERATOR.NOT_SET;
}
/**
* Register a callback function to be notified whenever the Condition changes state.
* @param callback {SignalObserver} a callback function to notify upon Condition state changes.
* @return {Condition}
*/
onStateChange(callback:SignalObserver) {
this._signalConditionStateChange.addListener(callback);
return this;
}
/**
* Register a callback function to be notified whenever the Condition gets BOOL_OPERATOR.TRUE.
* @param callback {SignalObserver} a callback function to notify upon Condition state changes.
* @return {Condition}
*/
onTrue(callback:SignalObserver) {
if (this.isTrue()) {
schedule(function () {
callback(this);
}, 0);
} else {
(function (me:Condition, callback:SignalObserver) {
me._signalConditionStateChange.addListener(function (condition) {
if (condition.isTrue()) {
callback(me);
}
});
})(this, callback);
}
return this;
}
/**
* Register a callback function to be notified whenever the Condition gets BOOL_OPERATOR.FALSE.
* @param callback {SignalObserver} a callback function to notify upon Condition state changes.
* @return {Condition}
*/
onFalse(callback:SignalObserver) {
if (this.isFalse()) {
schedule(function () {
callback(this);
}, 0);
} else {
(function (me:Condition, callback:SignalObserver) {
me._signalConditionStateChange.addListener(function (condition) {
if (condition.isFalse()) {
callback(me);
}
});
})(this, callback);
}
return this;
}
/**
* Set this condition timeout.
* When the timeout expires, setFalse is called in the condition.
* @param timeout
* @return {*}
*/
setTimeout(timeout:number) {
this._timerId = setTimeout(this.__timeout.bind(this), timeout);
return this;
}
/**
* Cancel this Condition internal timeout.
* @private
*/
__cancelTimer() {
if (this._timerId) {
clearTimeout(this._timerId);
this._timerId = null;
}
}
/**
* This function is invoked when the Condition is timed out.
* @private
*/
__timeout() {
this.setFalse();
this._timerId = null;
this._signalTimeout.emit(this);
}
/**
* Register an observer callback function for timeout events.
* @param f
* @return {*}
*/
onTimeout(f:SignalObserver) {
this._signalTimeout.addListener(f);
return this;
}
/**
* Disable this condition by removing all registered listeners.
*/
disable() {
this._signalConditionStateChange.removeAllListeners();
this._signalTimeout.removeAllListeners();
}
/**
* Return this Condition's internal value.
* @return {*}
*/
getCurrentValue() {
return this._b_condition;
}
then(success:SignalObserver, error?:SignalObserver) {
this.onTrue(success);
if (error) {
this.onFalse(error);
}
}
reset() {
this.setNotSet();
return this;
}
setNotSet() {
var prev= this._b_condition;
this._b_condition = BOOL_OPERATOR.NOT_SET;
if ( prev!==BOOL_OPERATOR.NOT_SET ) {
this.__emit();
}
return this;
}
}
export enum BOOLEAN_OPERATOR {
AND = 0,
OR = 1
}
/**
* ConditionTree is the extension of a simple Condition into a full fledged boolean condition tree.
* A ConditionTree can contain other trees as conditions to form structures like:
* A or (B and (C or D))
*
* All the base behavior of a simple Condition can be applied to ConditionTree objects.
*
* A condition tree applies fast condition short circuit, notifying as soon as possible about condition
* state changed.
*
* @return {*}
* @constructor
*/
export class ConditionTree extends Condition {
_booleanOperator:BOOLEAN_OPERATOR;
_children:Condition[];
constructor( operator? : BOOLEAN_OPERATOR ) {
super();
this._booleanOperator = typeof operator!=="undefined" ? operator : BOOLEAN_OPERATOR.AND;
this._children = [];
}
/**
* Find this tree's boolean value making a logical OR with its children.
* @return {BOOL_OPERATOR}
* @private
*/
__isTrueOr():BOOL_OPERATOR {
let i:number;
let notSet = false;
for (i = 0; i < this._children.length; i++) {
if (this._children[i].isTrue()) {
return BOOL_OPERATOR.TRUE;
} else if (this._children[i].isNotSet()) {
notSet = true;
}
}
return notSet ? BOOL_OPERATOR.NOT_SET : BOOL_OPERATOR.FALSE;
}
/**
* Find this tree's boolen value making a logical AND with its children.
* @return {(BOOL_OPERATOR)}
* @private
*/
__isTrueAnd():BOOL_OPERATOR {
let notSet = false;
for (let i = 0; i < this._children.length; i++) {
if (this._children[i].isFalse()) {
return BOOL_OPERATOR.FALSE;
} else if (this._children[i].isNotSet()) {
notSet = true;
}
}
return notSet ? BOOL_OPERATOR.NOT_SET : BOOL_OPERATOR.TRUE;
}
/**
* @return {BOOL_OPERATOR}
* @private
*/
__isTrue() {
let value:BOOL_OPERATOR;
value = ( this._booleanOperator === BOOLEAN_OPERATOR.AND ) ?
this.__isTrueAnd() :
this.__isTrueOr();
return value;
}
/**
* Add a new Condition to this ConditionTree.
* @param condition {Condition | ConditionTree}
*/
addCondition(condition:Condition) {
this._children.push(condition);
condition.onStateChange(this.__conditionChanged.bind(this));
return this;
}
/**
* Invoked when a condition in this tree changes value.
* @private
*/
__conditionChanged() {
if (this.isNotSet()) {
switch (this.__isTrue()) {
case BOOL_OPERATOR.NOT_SET:
break;
case BOOL_OPERATOR.TRUE:
this.setTrue();
break;
case BOOL_OPERATOR.FALSE:
this.setFalse();
break;
}
}
}
}
export class ParallelConditionDescriptor {
_condition:Condition;
_fn:ParallelConditionElementFunction;
constructor(fn:ParallelConditionElementFunction, condition:Condition) {
this._condition = condition;
this._fn = fn;
}
}
export type ParallelConditionElementFunction = (c:Condition, index:number)=>void;
export type ParallelConditionElement = ParallelConditionElementFunction | ParallelCondition;
/**
*
* A parallel condition object defines a ConditionTree where each condition is associated with an asynchronous
* executing function.
* It expects a list of functions or other ParallelCondition objects to be asynchronously executed.
* As a ConditionTree, it will short circuit the condition fast to allow your main execution line progress as
* soon as possible.
*
* It inherits all the behavior from ConditionTree and hence from Condition.
*
* @param array Array.<{ ( function( Condition, number ) | ParallelCondition) } > array of functions that accept a
* Condition and a number (index sequence of paralleled functions).
* @param timeout {number} millisecond to have the task completed
*/
export class ParallelCondition extends ConditionTree {
_iterableArray:ParallelConditionDescriptor[];
_timeout:number;
constructor(array:ParallelConditionElement[], timeout?:number) {
super();
this.__setIterableArray(array);
this._timeout = timeout || 0;
}
/**
* Set the internal ConditionTree object.
* @param array {Array< SignalObserver | ParallelCondition >}
* @private
*/
__setIterableArray(array:ParallelConditionElement[]) {
const me = this;
const iterableArray:ParallelConditionDescriptor[] = [];
if (array.constructor !== Array) {
throw "ParallelCondition needs an Array of functions or other ParallelConditions.";
}
array.forEach(function (element:ParallelConditionElement) {
var condition:Condition;
if (typeof element === "function") {
condition = new Condition();
me.addCondition(condition);
iterableArray.push(new ParallelConditionDescriptor(element as ParallelConditionElementFunction, condition));
} else if (element instanceof ParallelCondition) {
condition = element;
me.addCondition(condition);
iterableArray.push(new ParallelConditionDescriptor(
function () {
(onTrue
* onFalse
* onStateChange
* onTimeout
*/
execute() {
if (this._timeout > 0) {
this.setTimeout(this._timeout);
}
// element is supposed to be a function that receives as parameters:
// + Condition object
// + Number as the index sequence of the paralleled functions
forEachP(this._iterableArray);
}
}
export type FutureCallbackonValueSet.
*
* If you want to test for a valid value in the Future object, a call to isValueSet must
* be performed to know whether a value has been set, followed by a call to getValue which
* will return the actual value set in this Future object.
*
* @return {*}
* @constructor
*/
export class FutureisValueSet must be performed to know whether the Future
* has had a value set.
*
* @return {*}
*/
getValue() {
return this._value;
}
/**
* Test internal Condition object to know whether a value has been set in the Future object.
* @return {Boolean}
*/
isValueSet() {
return this._valueSetCondition.isTrue();
}
/**
* Set this Future object's value and notify any registered observers.
* @param v {Object}
*/
setValue(v:any) {
if (!this._valueSetCondition.isTrue()) {
this._value = v;
this._valueSetCondition.setTrue();
}
}
/**
* Register a callback function as observer for this Future object's set value event.
* @param callback {FutureCallback}
* @return {*}
*/
onValueSet(callback:FutureCallbackFuture object,
* but it may be a closure which behaves distinctly depending on the function called in the
* Dispatcher object.
*
* A chained call, may be interrupted by external events:
*
* + if a timeout event is generated, the function execution will stop and the WorkerTask disabled (no condition
* or callback notification). The worker will be killed, and a new one will be created.
* + if one function in the chain sets the Future's parameter to an Error instance, the chain call will stop
* and the worker will be reused.
*
*
* @param task {FutureCallback}
* @param timeout {number}
* @return {*}
* @constructor
*/
export class WorkerTask {
_timeout:number;
_future:Future