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 = hash && clazz.__gcncache__[hash]; 158 if (chainback) { 159 // Reset the cached instance and re-initialize it. 160 chainback.__gcnstatus__ = 'OK'; 161 chainback._continuation = chainlink; 162 return chainback._init.apply(chainback, args); 163 } 164 args.push(chainlink); 165 var isFetus = !hash; 166 var isContinuation = !!chainlink; 167 if (isFetus && isContinuation) { 168 // TODO creating a hash form just the clazz argument is insufficient. 169 // We must also consider clazz._needsChainedHash. 170 // For example, createTag() will cause a new chanback to be created 171 // which will have a hash value of 'TagAPI:id' but it should 172 // be 'Page::id::TagAPI:id'. 173 hash = clazz._makeHash(); 174 chainback = chainlink.ancestor.__gcntempcache__[hash]; 175 if (!chainback) { 176 chainback = chainlink.ancestor.__gcntempcache__[hash] 177 = new Chainback(clazz, args); 178 } 179 return chainback; 180 } 181 return new Chainback(clazz, args); 182 }; 183 184 /** 185 * Create a class which allows for chainable callback methods. 186 * 187 * @ignore 188 * @public 189 * @param {object<string, *>} props Definition of the class to be created. 190 * All function are wrapped to allow them 191 * to be as chainable callbacks unless 192 * their name is prefixed with a "!" . 193 * @return {Chainback} 194 */ 195 GCN.defineChainback = function (props) { 196 197 /** 198 * @TODO: use named arguments 199 * 200 * @constructor 201 * @param {number|string} id 202 * @param {?function(Chainback)} success 203 * @param {?function(GCNError):boolean} error 204 * @param {?object} chainlink 205 */ 206 var chainback = function () { 207 var args = Array.prototype.slice.call(arguments); 208 209 this._continuation = args.pop(); 210 211 // Please note: We prefix and suffix these values with a double 212 // underscore because they are not to be relied on whatsoever 213 // outside of this file! Although they need to be carried around 214 // on chainback instances, they are nevertheless soley for internal 215 // wiring. 216 217 this.__gcnmutex__ = true; 218 this.__gcnstatus__ = 'OK'; 219 this.__gcncallchain__ = []; 220 this.__gcncallqueue__ = []; 221 this.__gcntempcache__ = {}; 222 223 // This is used to synchronize ajax calls with non-ajax calls in a 224 // chainback call queue. 225 // 226 // It serves as a type of countdown latch, or reverse counting 227 // semaphore (msdn.microsoft.com/en-us/magazine/cc163427.aspx). 228 // 229 // Upon each invocation of `_queueAjax()', on a chainback object, 230 // its `__gcnajaxcount__' counter will be incremented. And each 231 // time a queued ajax call completes (successfully or otherwise), 232 // the counter is decremented. Before any chainback object can 233 // move on to the next call in its call queue, it will check the 234 // value of this counter to determine whether it is permitted to do 235 // so. If the counter's value is 0, access is granted; otherwise 236 // the requesting chainback will wait until a pending ajax call 237 // completes to trigger a retry. 238 this.__gcnajaxcount__ = 0; 239 240 var obj = args[0]; 241 var ids; 242 243 switch (jQuery.type(obj)) { 244 case 'null': 245 case 'undefined': 246 break; 247 case 'object': 248 if (typeof obj.id !== 'undefined') { 249 ids = [obj.id]; 250 } 251 break; 252 case 'array': 253 ids = obj; 254 break; 255 default: 256 ids = [obj]; 257 } 258 259 // If one or more id is provided in the instantialization of this 260 // object, only then will this instance be added to its class' 261 // cache. 262 if (ids) { 263 this._setHash(ids.sort().join(',')); 264 this._addToCache(); 265 } 266 267 this._init.apply(this, args); 268 }; 269 270 /** 271 * Causes the chainback call queue to start running again once a lock 272 * has been released. 273 * 274 * @private 275 */ 276 props.__release__ = function () {}; 277 278 // inheritance 279 280 if (props._extends) { 281 var inheritance = (jQuery.type(props._extends) === 'array') ? 282 props._extends : [props._extends]; 283 var i; 284 var j = inheritance.length; 285 286 for (i = 0; i < j; ++i) { 287 jQuery.extend(chainback.prototype, inheritance[i].prototype); 288 } 289 290 delete props._extends; 291 } 292 293 // static fields and methods 294 295 jQuery.extend(chainback, { 296 297 /** 298 * @private 299 * @static 300 * @type {object<string, Chainback>} An associative array holding 301 * instances of this class. Each 302 * instance is mapped against a 303 * hash key generated through 304 * `_makehash()'. 305 */ 306 __gcncache__: {}, 307 308 /** 309 * @private 310 * @static 311 * @type {string} A string that represents this chainback's type. 312 * It is used in generating hashs for instances of 313 * this class. 314 */ 315 __chainbacktype__: props.__chainbacktype__ || 316 Math.random().toString(32), 317 318 /** 319 * @private 320 * @static 321 * @type {boolean} Whether or not we need to use the hash of this 322 * object's parent chainback object in order to 323 * generate a unique hash key when instantiating 324 * objects for this class. 325 */ 326 _needsChainedHash: false, 327 328 /** 329 * Given the arguments "one", "two", "three", will return something 330 * like: "one::two::ChainbackType:three". 331 * 332 * @private 333 * @static 334 * @param {...string} One or more strings to concatenate into the 335 * hash. 336 * @return {string} The hash string. 337 */ 338 _makeHash: function () { 339 var args = Array.prototype.slice.call(arguments); 340 var id = args.pop(); 341 args.push(chainback.__chainbacktype__ + (id ? ':' + id : '')); 342 return args.join('::'); 343 } 344 345 }); 346 347 var notToMerge = { 348 __gcnmutex__ : true, 349 __gcnstatus__ : true, 350 __gcnorigin__ : true, 351 __gcncallchain__ : true, 352 __gcncallqueue__ : true, 353 __gcnajaxcount__ : true, 354 __gcntempcache__ : true 355 }; 356 357 // Prototype chainback methods and properties 358 359 jQuery.extend(chainback.prototype, { 360 361 /** 362 * @type {Chainback} Each object holds a reference to its 363 * constructor. 364 */ 365 __gcnconstructor__: chainback, 366 367 /** 368 * Facilitates continuation from one chainback object to another. 369 * 370 * Uses "chainlink" objects to grow and internal linked list of 371 * chainback objects which make up a sort of callee chain. 372 * 373 * A link is created every time a context switch happens 374 * (ie: moving from one API to another). Consider the following: 375 * 376 * page('1').tags().tag('content').render('#content'); 377 * 378 * Accomplishing the above chain of execution will involve 3 379 * different chainable APIs, and 2 different API switches: a page 380 * API flows into a tags collection API, which in turn flows to a 381 * tag API. This method is invoked each time that the exposed API 382 * mutates in this way. 383 * 384 * @private 385 * @param {Chainback} clazz The Chainback class we want to 386 * continue with. 387 * @param {number|string|Array.<number|string>|object} settings 388 * If this argument is not defined, a random hash will be 389 * generated as the object's hash. 390 * An object can be provided instead of an id to directly 391 * instantiate it from JSON data received from the server. 392 * @param {function} success 393 * @param {function} error 394 * @return {Chainback} 395 * @throws UNKNOWN_ARGUMENT If `settings' is not a number, string, 396 * array or object. 397 */ 398 _continue: function (clazz, settings, success, error) { 399 var hashInputs = []; 400 401 // Is this a fully realized Chainback, or is it a Chainback 402 // which has yet to determine which id it is bound to, from its 403 // parent? 404 var isFetus = false; 405 var ids; 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 430 if (isFetus) { 431 hash = null; 432 } else { 433 hash = clazz._needsChainedHash 434 ? clazz._makeHash(this._getHash(), ids) 435 : clazz._makeHash(ids); 436 } 437 438 var chainlink = { 439 ancestor: this 440 }; 441 442 var chainback = GCN.getChainback(clazz, hash, chainlink, 443 [settings, success, error, {}]); 444 445 return chainback; 446 }, 447 448 /** 449 * Works backward to read this object's ancestor before continuing 450 * with the callback. 451 * 452 * This method should be overridden. 453 * 454 * @private 455 * @param {function(Chainback)} success Callback. 456 * @param {function} error Custom error handler. 457 */ 458 /* 459 _onContinue: function (success, error) { 460 if (success) { 461 success(this); 462 } 463 }, 464 */ 465 466 /** 467 * Sets the internal object status to the given code string. Any 468 * status other than "OK" will cause any futher calls in this 469 * object's call chain to be aborted. 470 * 471 * @private 472 * @param {string} code 473 * @return {Chainback} This Chainback. 474 */ 475 _die: function (code) { 476 this.__gcnstatus__ = code; 477 return this; 478 }, 479 480 /** 481 * Terminates any further exection of the functions that remain in 482 * the call queue. 483 * TODO: Kill all ajax calls. 484 * 485 * @private 486 * @return {Array.<function>} A list of functions that we in this 487 * Chainback's call queue when an abort 488 * happend. 489 */ 490 _abort: function () { 491 this.__gcnstatus__ = 'OK'; 492 this._clearCache(); 493 494 var callchain = this.__gcncallchain__ 495 .concat(this.__gcncallqueue__); 496 497 this.__gcnmutex__ = true; 498 this.__gcncallchain__ = []; 499 this.__gcncallqueue__ = []; 500 501 return callchain; 502 }, 503 504 /** 505 * Gets the chainback from which this object was `_continue'd() 506 * from. 507 * 508 * @private 509 * @param {Chainback} 510 * @return {Chainback} This Chainback's ancestor. 511 */ 512 _ancestor: function () { 513 return this._continuation && this._continuation.ancestor; 514 }, 515 516 /** 517 * Locks the semaphore. 518 * 519 * @private 520 * @return {Chainback} This Chainback. 521 */ 522 _procure: function () { 523 this.__gcnmutex__ = false; 524 return this; 525 }, 526 527 /** 528 * Unlocks the semaphore. 529 * 530 * @private 531 * @return {Chainback} This Chainback. 532 */ 533 _vacate: function () { 534 this.__gcnmutex__ = true; 535 this.__release__(); 536 return this; 537 }, 538 539 /** 540 * Halts and forks the main call chain of this chainback object. 541 * Creates a derivitive object that will be used to accomplish 542 * operations that need to complete before the main chain is 543 * permitted to proceed. Before execution on the main chainback 544 * object is restarted, the forked derivitive object is merged into 545 * the original chainback instance. 546 * 547 * @private 548 * @return {Chainback} A derivitive Chainback object forked from 549 * this Chainback instance. 550 */ 551 _fork: function () { 552 var that = this; 553 this._procure(); 554 var Fork = function ChainbackFork() { 555 var prop; 556 for (prop in that) { 557 if (that.hasOwnProperty(prop) && !notToMerge[prop]) { 558 this[prop] = that[prop]; 559 } 560 } 561 this.__gcnorigin__ = that; 562 this.__gcnmutex__ = true; 563 this.__gcnstatus__ = 'OK'; 564 this.__gcncallchain__ = []; 565 this.__gcncallqueue__ = []; 566 }; 567 Fork.prototype = this; 568 return new Fork(); 569 }, 570 571 /** 572 * Transfers the state of this derivitive into its origin. 573 */ 574 _merge: function () { 575 if (!this.__gcnorigin__) { 576 return; 577 } 578 var origin = this.__gcnorigin__; 579 var prop; 580 for (prop in this) { 581 if (this.hasOwnProperty(prop) && !notToMerge[prop]) { 582 origin[prop] = this[prop]; 583 } 584 } 585 origin._vacate(); 586 }, 587 588 /** 589 * Wraps jQuery's `ajax' method. Queues the callbacks in the chain 590 * call so that they can be invoked synchonously. Without blocking 591 * the browser thread. 592 * 593 * @private 594 * @param {object} settings 595 */ 596 _queueAjax: function (settings) { 597 if (settings.json) { 598 settings.data = JSON.stringify(settings.json); 599 delete settings.json; 600 } 601 602 settings.dataType = 'json'; 603 settings.contentType = 'application/json; charset=utf-8'; 604 settings.error = (function (onError) { 605 return function (xhr, status, error) { 606 var throwException = true; 607 608 if (onError) { 609 throwException = onError( 610 GCN.createError('HTTP_ERROR', error, xhr) 611 ); 612 } 613 614 if (throwException !== false) { 615 GCN.error('AJAX_ERROR', error, xhr); 616 } 617 }; 618 }(settings.error)); 619 620 // Duck-type the complete callback, or add one if not provided. 621 // We use complete to forward the continuation because it is 622 // the last callback to be executed the jQuery ajax callback 623 // sequence. 624 settings.complete = (function (chainback, onComplete, opts) { 625 return function () { 626 --chainback.__gcnajaxcount__; 627 onComplete.apply(chainback, arguments); 628 callNext(chainback); 629 }; 630 }(this, settings.complete || function () {}, settings)); 631 632 ++this.__gcnajaxcount__; 633 634 GCN.ajax(settings); 635 }, 636 637 /** 638 * Clears the cache for this individual object. 639 * 640 * @private 641 * @return {Chainback} This Chainback. 642 */ 643 _clearCache: function () { 644 if (chainback.__gcncache__[this._getHash()]) { 645 delete chainback.__gcncache__[this._getHash()]; 646 } 647 return this; 648 }, 649 650 /** 651 * Add this object to the cache, using its hash as the key. 652 * 653 * @private 654 * @return {Chainback} This Chainback. 655 */ 656 _addToCache: function () { 657 this.__gcnconstructor__.__gcncache__[this._getHash()] = this; 658 return this; 659 }, 660 661 /** 662 * Removes the given chainback instance from the temporary cache, 663 * usually after the chainback instance has matured from a "fetus" 664 * into a fully realized chainback object. 665 * 666 * @param {Chainback} instance The chainback instance to remove. 667 * @return {boolean} True if this chainback instance was found and 668 * removed, false if it could not be found. 669 */ 670 _removeFromTempCache: function (instance) { 671 var i; 672 for (i in this.__gcntempcache__) { 673 if (this.__gcntempcache__.hasOwnProperty(i) && 674 instance === this.__gcntempcache__[i]) { 675 delete this.__gcntempcache__[i]; 676 return true; 677 } 678 } 679 return false; 680 }, 681 682 /** 683 * @private 684 * @return {string} This object's hash key. 685 */ 686 _getHash: function () { 687 return this.__gcnhash__; 688 }, 689 690 /** 691 * @private 692 * @param {string|number} str 693 * @return {Chainback} This Chainback. 694 */ 695 _setHash: function (str) { 696 var constructor = this.__gcnconstructor__; 697 698 if (constructor._needsChainedHash && this._continuation) { 699 this.__gcnhash__ = constructor._makeHash( 700 this._continuation.ancestor._getHash(), 701 str 702 ); 703 } else { 704 this.__gcnhash__ = constructor._makeHash(str); 705 } 706 707 return this; 708 } 709 710 }); 711 712 /** 713 * Invokes the `onContinue()' method of this chainback's ancestor. 714 * 715 * @private 716 * @param {function} success Callback. 717 * @param {function} error Optional custom error handler. 718 */ 719 props._continueWith = function (success, error) { 720 if (this._continuation && 721 this._continuation.ancestor._onContinue) { 722 this._continuation.ancestor._onContinue(success, error); 723 } else { 724 success(this); 725 } 726 }; 727 728 var propName; 729 var propValue; 730 731 // Generates the chainable callback methods. Transforms all functions 732 // whose names do not start with the "!" character into chainable 733 // callback prototype methods. 734 735 for (propName in props) { 736 if (props.hasOwnProperty(propName)) { 737 propValue = props[propName]; 738 739 if (jQuery.type(propValue) === 'function' && 740 propName.charAt(0) !== '!') { 741 chainback.prototype[propName] = makeMethodChainable( 742 propValue, 743 propName 744 ); 745 } else { 746 if (propName.charAt(0) === '!') { 747 propName = propName.substring(1, propName.length); 748 } 749 750 chainback.prototype[propName] = propValue; 751 } 752 } 753 } 754 755 return chainback; 756 }; 757 758 }(GCN)); 759