1 (function (GCN) {
  2 
  3 	'use strict';
  4 
  5 	/**
  6 	 * Enqueue a method into the given chainback objects's call chain. If a
  7 	 * mutex is locking this chain, then place the call in the queued calls
  8 	 * instead.
  9 	 *
 10 	 * @private
 11 	 * @param {Chainback} chainback The object whose queue we want to push
 12 	 *                                  the given method to.
 13 	 * @param {function} method The method to chain.
 14 	 */
 15 	function addCallToQueue(chainback, method) {
 16 		if (!chainback.__gcnmutex__) {
 17 			chainback.__gcncallqueue__.push(method);
 18 		} else {
 19 			chainback.__gcncallchain__.push(method);
 20 			if (0 === chainback.__gcnajaxcount__
 21 					&& 1 === chainback.__gcncallchain__.length) {
 22 				method.call(chainback);
 23 			}
 24 		}
 25 	}
 26 
 27 	/**
 28 	 * Dequeue the function at the top of the given chainback's call chain and
 29 	 * invoke the next function in the queue.
 30 	 *
 31 	 * @private
 32 	 * @param {Chainback} chainback
 33 	 */
 34 	function callNext(chainback) {
 35 		// Waiting for an ajax call to complete.  Go away, and try again when
 36 		// another call completes.
 37 		if (chainback.__gcnajaxcount__ > 0) {
 38 			return;
 39 		}
 40 
 41 		if (0 === chainback.__gcncallchain__.length
 42 				&& 0 === chainback.__gcncallqueue__.length) {
 43 			return; // We should never reach here. Just so you know...
 44 		}
 45 
 46 		// Discard the empty shell...
 47 		chainback.__gcncallchain__.shift();
 48 
 49 		// Load and fire the next bullet...
 50 		if (chainback.__gcncallchain__.length) {
 51 			chainback.__gcncallchain__[0].call(chainback);
 52 		} else if (chainback.__gcncallqueue__.length) {
 53 			chainback.__gcncallqueue__.shift().call(chainback);
 54 		}
 55 	}
 56 
 57 	/**
 58 	 * Wraps the given method in a closure that provides scaffolding to chain
 59 	 * invocations of the method correctly.
 60 	 *
 61 	 * @private
 62 	 * @param {function} method The original function we want to wrap.
 63 	 * @param {string} name The method name as it was defined in its object.
 64 	 * @return {function} A function that wraps the original function.
 65 	 */
 66 	function makeMethodChainable(method, name) {
 67 		return function () {
 68 			var args = arguments;
 69 			var that = this;
 70 			var func = function () {
 71 				method.apply(that, args);
 72 				callNext(that);
 73 			};
 74 			func.__gcncallname__ = name; // For debugging
 75 			addCallToQueue(this, func);
 76 			return this;
 77 		};
 78 	}
 79 
 80 	/**
 81 	 * The Chainback constructor.
 82 	 *
 83 	 * Surfaces the chainback constructor in such a way as to be able to use
 84 	 * call apply() on it.
 85 	 *
 86 	 * http://stackoverflow.com/questions/1606797/use-of-apply-with-new-operator-is-this-possible
 87 	 *
 88 	 * @private
 89 	 * @param {Chainback} ctor The Chainback class we wish to initialize.
 90 	 * @param {Array} args
 91 	 * @param {object} continuation
 92 	 * @return {Chainback}
 93 	 */
 94 	var Chainback = (function () {
 95 		var Chainback = function (ctor, args) {
 96 			return ctor.apply(this, args);
 97 		};
 98 		return function (ctor, args, continuation) {
 99 			Chainback.prototype = ctor.prototype;
100 			return new Chainback(ctor, args);
101 		};
102 	}());
103 
104 	/**
105 	 * Get a chainback instance's current channel.
106 	 *
107 	 * @param {Chainback} chainback
108 	 * @return {number} The channel id or 0 if no channel is set.
109 	 */
110 	function getChannel(chainback) {
111 		return chainback._channel || 0;
112 	}
113 
114 	/**
115 	 * Get an instance of the given chainback class from its constructor's
116 	 * cache.  If the object for this hash does not exist in the cache, then
117 	 * instantiate a new object, place into the cache using the hash as the
118 	 * cache key.
119 	 *
120 	 * If no hash is passed to this function, then the Chainback instance that
121 	 * is returned will not be fully realized.  It will be a "fetus" instance
122 	 * that has yet to be bound to an id. Once it receives an id it will keep
123 	 * it for the remainder of its life.  These "fetus" instances are not be
124 	 * placed in the cache until they have aquired an id.
125 	 *
126 	 * @ignore
127 	 * @private
128 	 * @param {Chainback} ctor A chainback constructor.
129 	 * @param {string} hash A hash string that represents this chainback.
130 	 * @param {Chainback} callee The Chainback instance from which this
131 	 *                           invocation originated.
132 	 * @param {Array.<*>} args Arguments that will be applied to the chainback
133 	 *                         when (re-) initializing it.  This array should
134 	 *                         contain the following elements in the following
135 	 *                         order:
136 	 *                              id : string|array
137 	 *                         success : function|null
138 	 *                           error : function|null
139 	 *                         setting : object
140 	 * @return {Chainback}
141 	 */
142 	GCN.getChainback = function (ctor, hash, callee, args) {
143 		var chainback = hash && ctor.__gcncache__[hash];
144 		if (chainback) {
145 			// Reset the cached instance and re-initialize it.
146 			chainback._chain = callee;
147 			return chainback._init.apply(chainback, args);
148 		}
149 		args.push(callee);
150 		var isFetus = !hash;
151 		var hasCallee = !!callee;
152 		if (isFetus && hasCallee) {
153 			// TODO: Creating a hash form just the ctor argument is
154 			//       insufficient.  We must also consider
155 			//       ctor._needsChainedHash.  For example, createTag() will
156 			//       cause a new chanback to be created which will have a hash
157 			//       value of 'TagAPI:id' but it should be
158 			//       'Page::id::TagAPI:id'.
159 			hash = ctor._makeHash(getChannel(callee) + '/' + GCN.uniqueId(ctor.__chainbacktype__ + '-unique-'));
160 			chainback = callee.__gcntempcache__[hash];
161 			if (!chainback) {
162 				chainback =
163 					callee.__gcntempcache__[hash] =
164 						new Chainback(ctor, args);
165 			}
166 			return chainback;
167 		}
168 		return new Chainback(ctor, args);
169 	};
170 
171 	/**
172 	 * @ignore
173 	 * Create a class which allows for chainable callback methods.
174 	 * @private
175 	 * @param {object<string, *>} props Definition of the class to be created.
176 	 *                                  All function are wrapped to allow them
177 	 *                                  to be as chainable callbacks unless
178 	 *                                  their name is prefixed with a "!" .
179 	 * @return {Chainback}
180 	 */
181 	GCN.defineChainback = function (props) {
182 
183 		/**
184 		 * @ignore
185 		 * @TODO: use named arguments
186 		 *
187 		 * @constructor
188 		 * @param {number|string} id
189 		 * @param {?function(Chainback)} success
190 		 * @param {?function(GCNError):boolean} error
191 		 * @param {?object} chainlink
192 		 */
193 		var chainback = function () {
194 			var args = Array.prototype.slice.call(arguments);
195 
196 			this._chain = args.pop();
197 			this._channel = this._chain ? this._chain._channel : GCN.channel();
198 
199 			// Please note: We prefix and suffix these values with a double
200 			// underscore because they are not to be relied on whatsoever
201 			// outside of this file!  Although they need to be carried around
202 			// on chainback instances, they are nevertheless soley for internal
203 			// wiring.
204 			this.__gcnmutex__     = true;
205 			this.__gcncallchain__ = [];
206 			this.__gcncallqueue__ = [];
207 			this.__gcntempcache__ = {};
208 
209 			// This is used to synchronize ajax calls with non-ajax calls in a
210 			// chainback call queue.
211 			//
212 			// It serves as a type of countdown latch, or reverse counting
213 			// semaphore (msdn.microsoft.com/en-us/magazine/cc163427.aspx).
214 			//
215 			// Upon each invocation of `_queueAjax()', on a chainback object,
216 			// its `__gcnajaxcount__' counter will be incremented.  And each
217 			// time a queued ajax call completes (successfully or otherwise),
218 			// the counter is decremented.  Before any chainback object can
219 			// move on to the next call in its call queue, it will check the
220 			// value of this counter to determine whether it is permitted to do
221 			// so.  If the counter's value is 0, access is granted;  otherwise
222 			// the requesting chainback will wait until a pending ajax call
223 			// completes to trigger a retry.
224 			this.__gcnajaxcount__ = 0;
225 
226 			var obj = args[0];
227 			var ids;
228 
229 			switch (jQuery.type(obj)) {
230 			case 'null':
231 			case 'undefined':
232 				break;
233 			case 'object':
234 				if (typeof obj.id !== 'undefined') {
235 					ids = [obj.id];
236 				}
237 				break;
238 			case 'array':
239 				ids = obj;
240 				break;
241 			default:
242 				ids = [obj];
243 			}
244 
245 			// If one or more id is provided in the instantialization of this
246 			// object, only then will this instance be added to its class'
247 			// cache.
248 			if (ids) {
249 				this._setHash(ids.sort().join(','));
250 				this._addToCache();
251 			}
252 
253 			this._init.apply(this, args);
254 		};
255 
256 		/**
257 		 * Causes all promises that are made by this Chainback object, as well
258 		 * as all derived promises, to be resolved before the given success
259 		 * callback is invoked to receive this object in a "fulfilled" state.
260 		 * If a problem is encountered at any point during the resolution of a
261 		 * promise in the resolution chain, the error function is called, and
262 		 * further resolution is aborted.
263 		 *
264 		 * @private
265 		 * @param {function(Chainback)} success Callback function to be invoked
266 		 *                                      when all the promises on which
267 		 *                                      this object depends have been
268 		 *                                      completed.
269 		 * @param {function(GCNError):boolean=} error Optional custom error
270 		 *                                      handler.
271 		 */
272 		chainback.prototype._fulfill = function (success, error) {
273 			if (this._chain) {
274 				this._chain._fulfill(success, error);
275 			} else {
276 				success(this);
277 			}
278 		};
279 
280 		// inheritance
281 		if (props._extends) {
282 			var inheritance = (jQuery.type(props._extends) === 'array')
283 			                ? props._extends
284 			                : [props._extends];
285 			var i;
286 			for (i = 0; i < inheritance.length; i++) {
287 				jQuery.extend(chainback.prototype, inheritance[i].prototype);
288 			}
289 			delete props._extends;
290 		}
291 
292 		// static fields and methods
293 		jQuery.extend(chainback, {
294 
295 			/**
296 			 * @private
297 			 * @static
298 			 * @type {object<string, Chainback>} An associative array holding
299 			 *                                   instances of this class.  Each
300 			 *                                   instance is mapped against a
301 			 *                                   hash key generated through
302 			 *                                   `_makehash()'.
303 			 */
304 			__gcncache__: {},
305 
306 			/**
307 			 * @private
308 			 * @static
309 			 * @type {string} A string that represents this chainback's type.
310 			 *                It is used in generating hashs for instances of
311 			 *                this class.
312 			 */
313 			__chainbacktype__: props.__chainbacktype__ ||
314 				Math.random().toString(32),
315 
316 			/**
317 			 * @private
318 			 * @static
319 			 * @type {boolean} Whether or not we need to use the hash of this
320 			 *                 object's parent chainback object in order to
321 			 *                 generate a unique hash key when instantiating
322 			 *                 objects for this class.
323 			 */
324 			_needsChainedHash: false,
325 
326 			/**
327 			 * Given the arguments "one", "two", "three", will return something
328 			 * like: "one::two::ChainbackType:three".
329 			 *
330 			 * @private
331 			 * @static
332 			 * @param {...string} One or more strings to concatenate into the
333 			 *                    hash.
334 			 * @return {string} The hash string.
335 			 */
336 			_makeHash: function () {
337 				var ids = Array.prototype.slice.call(arguments);
338 				var id = ids.pop();
339 				ids.push(chainback.__chainbacktype__ + ':' + id);
340 				return ids.join('::');
341 			}
342 		});
343 
344 		var DONT_MERGE = {
345 			__gcnmutex__     : true,
346 			__gcnorigin__    : true,
347 			__gcncallchain__ : true,
348 			__gcncallqueue__ : true,
349 			__gcnajaxcount__ : true,
350 			__gcntempcache__ : true
351 		};
352 
353 		// Prototype chainback methods and properties
354 
355 		jQuery.extend(chainback.prototype, {
356 
357 			/**
358 			 * @type {Chainback} Each object holds a reference to its
359 			 *                   constructor.
360 			 */
361 			_constructor: chainback,
362 
363 			/**
364 			 * Facilitates chaining from one chainback object to another.
365 			 *
366 			 * Uses "chainlink" objects to grow and internal linked list of
367 			 * chainback objects which make up a sort of callee chain.
368 			 *
369 			 * A link is created every time a context switch happens
370 			 * (ie: moving from one API to another).  Consider the following:
371 			 *
372 			 * page('1').tags().tag('content').render('#content');
373 			 *
374 			 * Accomplishing the above chain of execution will involve 3
375 			 * different chainable APIs, and 2 different API switches: a page
376 			 * API flows into a tags collection API, which in turn flows to a
377 			 * tag API.  This method is invoked each time that the exposed API
378 			 * mutates in this way.
379 			 *
380 			 * @private
381 			 * @param {Chainback} ctor The Chainback class we want to continue
382 			 *                         with.
383 			 * @param {number|string|Array.<number|string>|object} settings
384 			 *      If this argument is not defined, a random hash will be
385 			 *      generated as the object's hash.
386 			 *      An object can be provided instead of an id to directly
387 			 *      instantiate it from JSON data received from the server.
388 			 * @param {function} success
389 			 * @param {function} error
390 			 * @return {Chainback}
391 			 * @throws UNKNOWN_ARGUMENT If `settings' is not a number, string,
392 			 *                          array or object.
393 			 */
394 			_continue: function (ctor, settings, success, error) {
395 				// Is this a fully realized Chainback, or is it a Chainback
396 				// which has yet to determine which id it is bound to, from its
397 				// parent?
398 				var isFetus = false;
399 				var ids;
400 				var hashInputs = [];
401 
402 				switch (jQuery.type(settings)) {
403 				case 'undefined':
404 				case 'null':
405 					isFetus = true;
406 					break;
407 				case 'array':
408 					ids = settings.sort().join(',');
409 					break;
410 				case 'number':
411 				case 'string':
412 					ids = settings;
413 					break;
414 				case 'object':
415 					ids = settings.id;
416 					break;
417 				default:
418 					GCN.error('UNKNOWN_ARGUMENT',
419 						'Don\'t know what to do with the object ' + settings);
420 					return;
421 				}
422 
423 				var hash;
424 				if (isFetus) {
425 					hash = null;
426 				} else {
427 					var channel = getChannel(this);
428 					hash = ctor._needsChainedHash
429 					     ? ctor._makeHash(this.__gcnhash__, channel + '/' + ids)
430 					     : ctor._makeHash(channel + '/' + ids);
431 				}
432 
433 				var chainback = GCN.getChainback(ctor, hash, this,
434 					[settings, success, error, {}]);
435 
436 				return chainback;
437 			},
438 
439 			/**
440 			 * Terminates any further exection of the functions that remain in
441 			 * the call queue.
442 			 *
443 			 * TODO: Kill all ajax calls.
444 			 *
445 			 * @private
446 			 * @return {Array.<function>} A list of functions that we in this
447 			 *                            Chainback's call queue when an abort
448 			 *                            happend.
449 			 */
450 			_abort: function () {
451 				this._clearCache();
452 				var callchain =
453 						this.__gcncallchain__.concat(this.__gcncallqueue__);
454 				this.__gcnmutex__ = true;
455 				this.__gcncallchain__ = [];
456 				this.__gcncallqueue__ = [];
457 				return callchain;
458 			},
459 
460 			/**
461 			 * Gets the chainback from which this object was `_continue'd()
462 			 * from.
463 			 *
464 			 * @private
465 			 * @param {Chainback}
466 			 * @return {Chainback} This Chainback's ancestor.
467 			 */
468 			_ancestor: function () {
469 				return this._chain;
470 			},
471 
472 			/**
473 			 * Locks the semaphore.
474 			 *
475 			 * @private
476 			 * @return {Chainback} This Chainback.
477 			 */
478 			_procure: function () {
479 				this.__gcnmutex__ = false;
480 				return this;
481 			},
482 
483 			/**
484 			 * Unlocks the semaphore.
485 			 *
486 			 * @private
487 			 * @return {Chainback} This Chainback.
488 			 */
489 			_vacate: function () {
490 				this.__gcnmutex__ = true;
491 				this.__release__();
492 				return this;
493 			},
494 
495 			/**
496 			 * Halts and forks the main call chain of this chainback object.
497 			 * Creates a derivitive object that will be used to accomplish
498 			 * operations that need to complete before the main chain is
499 			 * permitted to proceed.  Before execution on the main chainback
500 			 * object is restarted, the forked derivitive object is merged into
501 			 * the original chainback instance.
502 			 *
503 			 * @private
504 			 * @return {Chainback} A derivitive Chainback object forked from
505 			 *                     this Chainback instance.
506 			 */
507 			_fork: function () {
508 				var that = this;
509 				this._procure();
510 				var Fork = function ChainbackFork() {
511 					var prop;
512 					for (prop in that) {
513 						if (that.hasOwnProperty(prop) && !DONT_MERGE[prop]) {
514 							this[prop] = that[prop];
515 						}
516 					}
517 					this.__gcnorigin__    = that;
518 					this.__gcnmutex__     = true;
519 					this.__gcncallchain__ = [];
520 					this.__gcncallqueue__ = [];
521 				};
522 				Fork.prototype = new this._constructor();
523 				return new Fork();
524 			},
525 
526 			/**
527 			 * Transfers the state of this derivitive into its origin.
528 			 *
529 			 * @private
530 			 */
531 			_merge: function () {
532 				if (!this.__gcnorigin__) {
533 					return;
534 				}
535 				var origin = this.__gcnorigin__;
536 				var prop;
537 				for (prop in this) {
538 					if (this.hasOwnProperty(prop) && !DONT_MERGE[prop]) {
539 						origin[prop] = this[prop];
540 					}
541 				}
542 				origin._vacate();
543 				return origin;
544 			},
545 
546 			/**
547 			 * Wraps jQuery's `ajax' method.  Queues the callbacks in the chain
548 			 * call so that they can be invoked synchonously.  Without blocking
549 			 * the browser thread.
550 			 *
551 			 * @private
552 			 * @param {object} settings
553 			 */
554 			_queueAjax: function (settings) {
555 				if (settings.json) {
556 					settings.data = JSON.stringify(settings.json);
557 					delete settings.json;
558 				}
559 
560 				settings.dataType = 'json';
561 				settings.contentType = 'application/json; charset=utf-8';
562 				settings.error = (function (onError) {
563 					return function (xhr, status, error) {
564 						var throwException = true;
565 						if (onError) {
566 							throwException = onError(GCN.createError('HTTP_ERROR', error, xhr));
567 						}
568 						if (throwException !== false) {
569 							GCN.error('AJAX_ERROR', error, xhr);
570 						}
571 					};
572 				}(settings.error));
573 
574 				// Duck-type the complete callback, or add one if not provided.
575 				// We use complete to forward the continuation because it is
576 				// the last callback to be executed the jQuery ajax callback
577 				// sequence.
578 				settings.complete = (function (chainback, onComplete, opts) {
579 					return function () {
580 						--chainback.__gcnajaxcount__;
581 						onComplete.apply(chainback, arguments);
582 						callNext(chainback);
583 					};
584 				}(this, settings.complete || function () {}, settings));
585 
586 				++this.__gcnajaxcount__;
587 
588 				GCN.ajax(settings);
589 			},
590 
591 			/**
592 			 * Clears the cache for this individual object.
593 			 *
594 			 * @private
595 			 * @return {Chainback} This Chainback.
596 			 */
597 			_clearCache: function () {
598 				if (chainback.__gcncache__[this.__gcnhash__]) {
599 					delete chainback.__gcncache__[this.__gcnhash__];
600 				}
601 				return this;
602 			},
603 
604 			/**
605 			 * Add this object to the cache, using its hash as the key.
606 			 *
607 			 * @private
608 			 * @return {Chainback} This Chainback.
609 			 */
610 			_addToCache: function () {
611 				this._constructor.__gcncache__[this.__gcnhash__] = this;
612 				return this;
613 			},
614 
615 			/**
616 			 * Removes the given chainback instance from the temporary cache,
617 			 * usually after the chainback instance has matured from a "fetus"
618 			 * into a fully realized chainback object.
619 			 *
620 			 * @param {Chainback} instance The chainback instance to remove.
621 			 * @return {boolean} True if this chainback instance was found and
622 			 *                   removed, false if it could not be found.
623 			 */
624 			_removeFromTempCache: function (instance) {
625 				var hash;
626 				var cache = instance._ancestor().__gcntempcache__;
627 				for (hash in cache) {
628 					if (cache.hasOwnProperty(hash) && instance === cache[hash]) {
629 						delete cache[hash];
630 						return true;
631 					}
632 				}
633 				return false;
634 			},
635 
636 			/**
637 			 * @private
638 			 * @param {string|number} str
639 			 * @return {Chainback} This Chainback.
640 			 */
641 			_setHash: function (str) {
642 				var ctor = this._constructor;
643 				var addAncestorHash = ctor._needsChainedHash && this._chain;
644 				var channel = getChannel(this);
645 				var hash = addAncestorHash
646 				         ? ctor._makeHash(this._chain.__gcnhash__, channel + '/' + str)
647 				         : ctor._makeHash(channel + '/' + str);
648 				this.__gcnhash__ = hash;
649 				return this;
650 			},
651 
652 			/**
653 			 * Invokes the given callback function while ensuring that any
654 			 * exceptions that occur during the invocation of the callback,
655 			 * will be caught and allow the Chainback object to complete its
656 			 * all remaining queued calls.
657 			 *
658 			 * @param {function} callback The function to invoke.
659 			 * @param {Array.<*>=} args A list of object that will be passed as
660 			 *                          arguments into the callback function.
661 			 */
662 			_invoke: function (callback, args) {
663 				if (typeof callback !== 'function') {
664 					return;
665 				}
666 				try {
667 					if (args && args.length) {
668 						callback.apply(null, args);
669 					} else {
670 						callback();
671 					}
672 				} catch (ex) {
673 					setTimeout(function () {
674 						throw ex;
675 					}, 1);
676 				}
677 			}
678 
679 		});
680 
681 		/**
682 		 * Causes the chainback call queue to start running again once a lock
683 		 * has been released.  This function is defined here because it needs
684 		 * to be made chainable.
685 		 *
686 		 * @private
687 		 */
688 		props.__release__ = function () {};
689 
690 		var propName;
691 		var propValue;
692 
693 		// Generates the chainable callback methods.  Transforms all functions
694 		// whose names do not start with the "!" character into chainable
695 		// callback prototype methods.
696 		for (propName in props) {
697 			if (props.hasOwnProperty(propName)) {
698 				propValue = props[propName];
699 				if (jQuery.type(propValue) === 'function' &&
700 						propName.charAt(0) !== '!') {
701 					chainback.prototype[propName] =
702 							makeMethodChainable(propValue, propName);
703 				} else {
704 					if (propName.charAt(0) === '!') {
705 						propName = propName.substring(1, propName.length);
706 					}
707 					chainback.prototype[propName] = propValue;
708 				}
709 			}
710 		}
711 
712 		return chainback;
713 	};
714 
715 }(GCN));
716