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