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