Source: Chain.js

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