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