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