(function(window)
{
/**
* Creates a chain. If an object is provided, the chain is augmented onto the object.
*
* @param object
*/
Chain = function(object, skipAutoTrap)
{
if (!object)
{
object = {};
}
// wraps the object into a proxy
var proxiedObject = Chain.proxy(object);
// the following methods get pushed onto the chained object
// methods for managing chain state
proxiedObject.__queue = (function() {
var queue = [];
return function(x) {
if (x) { if (x == 'empty') { queue = []; } else { queue.push(x); }}
return queue;
};
})();
proxiedObject.__response = (function() {
var response = null;
return function(x) {
if (!Gitana.isUndefined(x)) { if (x) { response = x; } else { response = null; } }
return response;
};
})();
proxiedObject.__waiting = (function() {
var waiting = false;
return function(x) {
if (!Gitana.isUndefined(x)) { waiting = x; }
return waiting;
};
})();
proxiedObject.__parent = (function() {
var parent = null;
return function(x) {
if (!Gitana.isUndefined(x)) { if (x) { parent = x; } else { parent = null; } }
return parent;
};
})();
proxiedObject.__id = (function() {
var id = Chain.idCount;
Chain.idCount++;
return function() {
return id;
};
})();
proxiedObject.__helper = (function() {
var helper = null;
return function(x) {
if (x) { helper = x; }
return helper;
};
})();
// marks any chain links which are placeholders for functions
proxiedObject.__transparent = (function() {
var transparent = false; // assume not transparent
return function(x) {
if (!Gitana.isUndefined(x)) { transparent = x; }
return transparent;
};
})();
// provides consume behavior for copy into (from another object into this one)
if (!proxiedObject.__copyState) {
proxiedObject.__copyState = function(other) {
Gitana.copyInto(this, other);
};
}
/**
* Queues either a callback function, an array of callback functions or a subchain.
*
* @param element
* @param [functionName] function name for debugging
*/
proxiedObject.then = function(element, functionName)
{
var self = this;
var autorun = false;
//
// ARRAY
//
// if we're given an array of functions, we'll automatically build out a function that orchestrates
// the concurrent execution of parallel chains.
//
// the function will be pushed onto the queue
//
if (Gitana.isArray(element))
{
var array = element;
// parallel function invoker
var parallelInvoker = function()
{
// counter and onComplete() method to keep track of our parallel thread completion
var count = 0;
var total = array.length;
var onComplete = function()
{
count++;
if (count == total)
{
// manually signal that we're done
self.next();
}
};
for (var i = 0; i < array.length; i++)
{
var func = array[i];
// use a closure
var x = function(func)
{
// each function gets loaded onto its own "parallel" chain
// the parallel chain contains a subchain and the onComplete method
// the parallel chain is a clone of this chain
// the subchain runs the function
// these are serial so that the subchain must complete before the onComplete method is called
var parallelChain = Chain(); // note: empty chain (parent)
parallelChain.__waiting(true); // this prevents auto-run (which would ground out the first subchain call)
parallelChain.subchain(self).then(function() { // TODO: should we self.clone() for parallel operations?
func.call(this);
});
parallelChain.then(function() {
onComplete();
});
parallelChain.__waiting(false); // switch back to normal
parallelChain.run();
};
x(func);
}
// return false so that we wait for manual self.next() signal
return false;
};
// build a subchain (transparent)
var subchain = this.subchain(null, true); // don't auto add, we'll do it ourselves
subchain.__queue(parallelInvoker);
if (functionName) { subchain.__helper(functionName); }
element = subchain;
}
//
// FUNCTION
//
// if we're given a function, then we're being asked to execute a function serially.
// to facilitate this, we'll wrap it in a subchain and push the subchain down into the queue.
// the reason is just to make things a little easier and predictive of what the end user might do with
// the chain. they probably don't expect it to just exit out if they try to to
// this.then(something)
// in other words, they should always feel like they have their own chain (which in fact they do)
else if (Gitana.isFunction(element))
{
// create the subchain
// this does a re-entrant call that adds it to the queue (as a subchain)
var subchain = this.subchain(null, true); // don't auto add, we'll do it ourselves
subchain.__queue(element);
if (functionName) { subchain.__helper(functionName); }
element = subchain;
// note: because we're given a function, we can tell this chain to try to "autorun"
autorun = true;
}
// anything that arrives this far is just a subchain
this.__queue(element);
// if we're able to autorun (meaning that we were told to then() a function)...
// we climb the parents until we find the topmost parent and tell it to run.
if (autorun && !this.__waiting())
{
var runner = this;
while (runner.__parent())
{
runner = runner.__parent();
}
if (!runner.__waiting())
{
runner.run();
}
}
// always hand back reference to ourselves
return this;
};
/**
* Run the next element in the queue
*/
proxiedObject.run = function()
{
var self = this;
// short cut, if nothing in the queue, bail
if (this.__queue().length == 0 || this.__waiting())
{
return this;
}
// mark that we're running something
this.__waiting(true);
// the element to run
var element = this.__queue().shift();
// case: callback function
if (Gitana.isFunction(element))
{
// it's a callback function
var callback = element;
// try to determine response and previous response
var response = null;
var previousResponse = null;
if (this.__parent())
{
response = this.__parent().__response();
if (this.__parent().__parent())
{
previousResponse = this.__parent().__parent().__response();
}
}
// async
window.setTimeout(function()
{
Chain.log(self, (self.__helper() ? self.__helper()+ " " : "") + "> " + element.toString());
// execute with "this = chain"
var returned = callback.call(self, response, previousResponse);
if (returned !== false)
{
self.next(returned);
}
}, 0);
}
else
{
// it's a subchain element (object)
// we make sure to copy response forward
var subchain = element;
subchain.__response(this.__response());
// pre-emptively copy forward into subchain
// only do this if the subchain is transparent
if (subchain.__transparent())
{
//Gitana.copyInto(subchain, this);
subchain.__copyState(this);
}
// BEFORE CHAIN RUN CALLBACK
// this provides a way for a chained object to adjust its method signatures and state ahead
// of actually executing, usually based on some data that was loaded (such as the type of object
// like a domain user or group)
//
if (subchain.beforeChainRun)
{
subchain.beforeChainRun.call(subchain);
}
subchain.run();
}
return this;
};
/**
* Creates a subchain and adds it to the queue.
*
* If no argument is provided, the generated subchain will be cloned from the current chain element.
*/
proxiedObject.subchain = function(object, noAutoAdd)
{
var transparent = false;
if (!object) {
transparent = true;
}
if (!object)
{
object = this;
}
var subchain = Chain(object, true);
subchain.__parent(this);
// BEFORE CHAIN RUN CALLBACK
// this provides a way for a chained object to adjust its method signatures and state ahead
// of actually executing, usually based on some data that was loaded (such as the type of object
// like a domain user or group)
//
if (subchain.beforeChainRun)
{
subchain.beforeChainRun.call(subchain);
}
if (!noAutoAdd)
{
this.then(subchain);
}
subchain.__transparent(transparent);
return subchain;
};
/**
* Completes the current element in the chain and provides the response that was generated.
*
* The response is pushed into the chain as the current response and the current response is bumped
* back as the previous response.
*
* If the response is null, nothing will be bumped.
*
* @param [Object] response
*/
proxiedObject.next = function(response)
{
// toggle responses
if (typeof response !== "undefined")
{
this.__response(response);
}
// no longer processing callback
this.__waiting(false);
// if there isn't anything left in the queue, then we're done
// if we have a parent, we can signal that we've completed
if (this.__queue().length == 0)
{
if (this.__parent())
{
// copy response up to parent
var r = this.__response();
this.__parent().__response(r);
this.__response(null);
// if the current node is transparent, then copy back to parent
//if (this.__transparent())
if (this.__transparent())
{
Gitana.deleteProperties(this.__parent());
//Gitana.copyInto(this.__parent(), this);
this.__parent().__copyState(this);
}
// inform parent that we're done
this.__parent().next();
}
// clear parent so that this chain can be relinked
this.__parent(null);
this.__queue('empty');
}
else
{
// run the next element in the queue
this.run();
}
};
/**
* Tells the chain to sleep the given number of milliseconds
*/
proxiedObject.wait = function(ms)
{
return this.then(function() {
var wake = function(chain)
{
return function()
{
chain.next();
};
}(this);
window.setTimeout(wake, ms);
return false;
});
};
/**
* Registers an error handler;
*
* @param errorHandler
*/
proxiedObject.trap = function(errorHandler)
{
this.errorHandler = errorHandler;
return this;
};
/**
* Handles the error.
*
* @param err
*/
proxiedObject.error = function(err)
{
// find the first error handler we can walking up the chain
var errorHandler = null;
var ancestor = this;
while (ancestor && !errorHandler)
{
errorHandler = ancestor.errorHandler;
if (!errorHandler)
{
ancestor = ancestor.__parent();
}
}
// clean up the chain so that it can still be used
this.__queue('empty');
this.__response(null);
// disconnect and stop the parent from processing
if (this.__parent())
{
this.__parent().__queue('empty');
this.__parent().__waiting(false);
}
// invoke error handler
if (errorHandler)
{
var code = errorHandler.call(this, err);
// finish out the chain if we didn't get "false"
if (code !== false)
{
this.next();
}
}
};
/**
* Completes a chain and hands control back up to the parent.
*/
proxiedObject.done = function()
{
return this.__parent();
};
/**
* Creates a new chain for this object
*/
proxiedObject.chain = function()
{
return Chain(this, true).then(function() {
// empty chain to kick start
});
};
// each object that gets chained provides a clone() method
// if there is already a clone property, don't override it
// this allows implementation classes to control how they get cloned
if (!proxiedObject.clone)
{
/**
* Clones this chain and resets chain properties.
*/
proxiedObject.clone = function()
{
return Chain.clone(this);
};
}
// apply auto trap?
if (!skipAutoTrap && autoTrap())
{
proxiedObject.trap(autoTrap());
}
return proxiedObject;
};
/**
* Wraps the given object into a proxy.
*
* If the object is an existing proxy, it is unpackaged and re-proxied.
* @param o
*/
Chain.proxy = function(o)
{
if (o.__original && o.__original())
{
// NOTE: we can't just unproxy since that loses all state of the current object
// unproxy back to original
//o = Chain.unproxy(o);
// for now, we can do this?
delete o.__original;
}
// clone the object using clone() method
var proxy = null;
if (o.clone) {
proxy = o.clone();
} else {
proxy = Chain.clone(o);
}
proxy.__original = function() {
return o;
};
return proxy;
};
/**
* Hands back the original object for a proxy.
*
* @param proxy
*/
Chain.unproxy = function(proxy)
{
var o = null;
if (proxy.__original && proxy.__original())
{
o = proxy.__original();
}
return o;
};
Chain.debug = false;
Chain.log = function(chain, text)
{
if (Chain.debug && !Gitana.isUndefined(console))
{
var f = function()
{
var identifier = this.__id();
if (this.__transparent()) {
identifier += "[t]";
}
if (!this.__parent())
{
return identifier;
}
return f.call(this.__parent()) + " > " + identifier;
};
var identifier = f.call(chain);
console.log("Chain[" + identifier + "] " + text);
}
};
// clone workhorse method
Chain.clone = function(object)
{
// based on Crockford's solution for clone using prototype on function
// this copies all properties and methods
// includes copies of chain functions
function F() {}
F.prototype = object;
var clone = new F();
// copy properties
Gitana.copyInto(clone, object);
return clone;
};
var autoTrapValue = null;
var autoTrap = Chain.autoTrap = function(_autoTrap)
{
if (_autoTrap)
{
autoTrapValue = _autoTrap;
}
return autoTrapValue;
};
Chain.idCount = 0;
})(window);