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