(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);