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