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 * @param {Boolean} release Whether or not to vacate the lock on 536 * this fork's origin object after merging. 537 * Defaults to true. 538 */ 539 _merge: function (release) { 540 if (!this.__gcnorigin__) { 541 return; 542 } 543 var origin = this.__gcnorigin__; 544 var prop; 545 for (prop in this) { 546 if (this.hasOwnProperty(prop) && !DONT_MERGE[prop]) { 547 origin[prop] = this[prop]; 548 } 549 } 550 if (false !== release) { 551 origin._vacate(); 552 } 553 return origin; 554 }, 555 556 /** 557 * Wraps jQuery's `ajax' method. Queues the callbacks in the chain 558 * call so that they can be invoked synchonously. Without blocking 559 * the browser thread. 560 * @TODO: The onError callbacks should always return a value. 561 * 'undefined' should be treated like false. 562 * 563 * @private 564 * @param {object} settings 565 */ 566 _queueAjax: function (settings) { 567 if (settings.json) { 568 settings.data = JSON.stringify(settings.json); 569 delete settings.json; 570 } 571 572 settings.dataType = 'json'; 573 settings.contentType = 'application/json; charset=utf-8'; 574 settings.error = (function (onError) { 575 return function (xhr, status, error) { 576 // Check if the error message and the response headers 577 // are empty. This means that the ajax request was aborted. 578 // This usually happens when another page is loaded. 579 // jQuery doesn't provide a meaningful error message in this case. 580 if (!error 581 && typeof xhr === 'object' 582 && typeof xhr.getAllResponseHeaders === 'function' 583 && !xhr.getAllResponseHeaders()) { 584 return; 585 } 586 587 var throwException = true; 588 if (onError) { 589 throwException = onError(GCN.createError('HTTP_ERROR', error, xhr)); 590 } 591 if (throwException !== false) { 592 GCN.error('AJAX_ERROR', error, xhr); 593 } 594 }; 595 }(settings.error)); 596 597 // Duck-type the complete callback, or add one if not provided. 598 // We use complete to forward the continuation because it is 599 // the last callback to be executed the jQuery ajax callback 600 // sequence. 601 settings.complete = (function (chainback, onComplete, opts) { 602 return function () { 603 --chainback.__gcnajaxcount__; 604 onComplete.apply(chainback, arguments); 605 callNext(chainback); 606 }; 607 }(this, settings.complete || function () {}, settings)); 608 609 ++this.__gcnajaxcount__; 610 611 GCN.ajax(settings); 612 }, 613 614 /** 615 * Clears the cache for this individual object. 616 * 617 * @private 618 * @return {Chainback} This Chainback. 619 */ 620 _clearCache: function () { 621 if (chainback.__gcncache__[this.__gcnhash__]) { 622 delete chainback.__gcncache__[this.__gcnhash__]; 623 } 624 return this; 625 }, 626 627 /** 628 * Add this object to the cache, using its hash as the key. 629 * 630 * @private 631 * @return {Chainback} This Chainback. 632 */ 633 _addToCache: function () { 634 this._constructor.__gcncache__[this.__gcnhash__] = this; 635 return this; 636 }, 637 638 /** 639 * Removes the given chainback instance from the temporary cache, 640 * usually after the chainback instance has matured from a "fetus" 641 * into a fully realized chainback object. 642 * 643 * @param {Chainback} instance The chainback instance to remove. 644 * @return {boolean} True if this chainback instance was found and 645 * removed, false if it could not be found. 646 */ 647 _removeFromTempCache: function (instance) { 648 var hash; 649 var cache = instance._ancestor().__gcntempcache__; 650 for (hash in cache) { 651 if (cache.hasOwnProperty(hash) && instance === cache[hash]) { 652 delete cache[hash]; 653 return true; 654 } 655 } 656 return false; 657 }, 658 659 /** 660 * @private 661 * @param {string|number} str 662 * @return {Chainback} This Chainback. 663 */ 664 _setHash: function (str) { 665 var ctor = this._constructor; 666 var addAncestorHash = ctor._needsChainedHash && this._chain; 667 var channel = getChannel(this); 668 var hash = addAncestorHash 669 ? ctor._makeHash(this._chain.__gcnhash__, channel + '/' + str) 670 : ctor._makeHash(channel + '/' + str); 671 this.__gcnhash__ = hash; 672 return this; 673 }, 674 675 /** 676 * Invokes the given callback function while ensuring that any 677 * exceptions that occur during the invocation of the callback, 678 * will be caught and allow the Chainback object to complete its 679 * all remaining queued calls. 680 * 681 * @param {function} callback The function to invoke. 682 * @param {Array.<*>=} args A list of object that will be passed as 683 * arguments into the callback function. 684 */ 685 _invoke: function (callback, args) { 686 if (typeof callback !== 'function') { 687 return; 688 } 689 try { 690 if (args && args.length) { 691 callback.apply(null, args); 692 } else { 693 callback(); 694 } 695 } catch (ex) { 696 setTimeout(function () { 697 throw ex; 698 }, 1); 699 } 700 } 701 702 }); 703 704 /** 705 * Causes the chainback call queue to start running again once a lock 706 * has been released. This function is defined here because it needs 707 * to be made chainable. 708 * 709 * @private 710 */ 711 props.__release__ = function () {}; 712 713 var propName; 714 var propValue; 715 716 // Generates the chainable callback methods. Transforms all functions 717 // whose names do not start with the "!" character into chainable 718 // callback prototype methods. 719 for (propName in props) { 720 if (props.hasOwnProperty(propName)) { 721 propValue = props[propName]; 722 if (jQuery.type(propValue) === 'function' && 723 propName.charAt(0) !== '!') { 724 chainback.prototype[propName] = 725 makeMethodChainable(propValue, propName); 726 } else { 727 if (propName.charAt(0) === '!') { 728 propName = propName.substring(1, propName.length); 729 } 730 chainback.prototype[propName] = propValue; 731 } 732 } 733 } 734 735 return chainback; 736 }; 737 738 }(GCN)); 739