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)); 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 * Create a class which allows for chainable callback methods. 173 * 174 * @ignore 175 * @private 176 * @param {object<string, *>} props Definition of the class to be created. 177 * All function are wrapped to allow them 178 * to be as chainable callbacks unless 179 * their name is prefixed with a "!" . 180 * @return {Chainback} 181 */ 182 GCN.defineChainback = function (props) { 183 184 /** 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 the chainback call queue to start running again once a lock 258 * has been released. 259 * 260 * @private 261 */ 262 props.__release__ = function () {}; 263 264 // inheritance 265 if (props._extends) { 266 var inheritance = (jQuery.type(props._extends) === 'array') 267 ? props._extends 268 : [props._extends]; 269 var i; 270 for (i = 0; i < inheritance.length; ++i) { 271 jQuery.extend(chainback.prototype, inheritance[i].prototype); 272 } 273 delete props._extends; 274 } 275 276 // static fields and methods 277 jQuery.extend(chainback, { 278 279 /** 280 * @private 281 * @static 282 * @type {object<string, Chainback>} An associative array holding 283 * instances of this class. Each 284 * instance is mapped against a 285 * hash key generated through 286 * `_makehash()'. 287 */ 288 __gcncache__: {}, 289 290 /** 291 * @private 292 * @static 293 * @type {string} A string that represents this chainback's type. 294 * It is used in generating hashs for instances of 295 * this class. 296 */ 297 __chainbacktype__: props.__chainbacktype__ || 298 Math.random().toString(32), 299 300 /** 301 * @private 302 * @static 303 * @type {boolean} Whether or not we need to use the hash of this 304 * object's parent chainback object in order to 305 * generate a unique hash key when instantiating 306 * objects for this class. 307 */ 308 _needsChainedHash: false, 309 310 /** 311 * Given the arguments "one", "two", "three", will return something 312 * like: "one::two::ChainbackType:three". 313 * 314 * @private 315 * @static 316 * @param {...string} One or more strings to concatenate into the 317 * hash. 318 * @return {string} The hash string. 319 */ 320 _makeHash: function () { 321 var ids = Array.prototype.slice.call(arguments); 322 var id = ids.pop(); 323 ids.push(chainback.__chainbacktype__ + ':' + id); 324 return ids.join('::'); 325 } 326 327 }); 328 329 var DONT_MERGE = { 330 __gcnmutex__ : true, 331 __gcnorigin__ : true, 332 __gcncallchain__ : true, 333 __gcncallqueue__ : true, 334 __gcnajaxcount__ : true, 335 __gcntempcache__ : true 336 }; 337 338 // Prototype chainback methods and properties 339 340 jQuery.extend(chainback.prototype, { 341 342 /** 343 * @type {Chainback} Each object holds a reference to its 344 * constructor. 345 */ 346 _constructor: chainback, 347 348 /** 349 * Facilitates chaining from one chainback object to another. 350 * 351 * Uses "chainlink" objects to grow and internal linked list of 352 * chainback objects which make up a sort of callee chain. 353 * 354 * A link is created every time a context switch happens 355 * (ie: moving from one API to another). Consider the following: 356 * 357 * page('1').tags().tag('content').render('#content'); 358 * 359 * Accomplishing the above chain of execution will involve 3 360 * different chainable APIs, and 2 different API switches: a page 361 * API flows into a tags collection API, which in turn flows to a 362 * tag API. This method is invoked each time that the exposed API 363 * mutates in this way. 364 * 365 * @private 366 * @param {Chainback} ctor The Chainback class we want to continue 367 * with. 368 * @param {number|string|Array.<number|string>|object} settings 369 * If this argument is not defined, a random hash will be 370 * generated as the object's hash. 371 * An object can be provided instead of an id to directly 372 * instantiate it from JSON data received from the server. 373 * @param {function} success 374 * @param {function} error 375 * @return {Chainback} 376 * @throws UNKNOWN_ARGUMENT If `settings' is not a number, string, 377 * array or object. 378 */ 379 _continue: function (ctor, settings, success, error) { 380 // Is this a fully realized Chainback, or is it a Chainback 381 // which has yet to determine which id it is bound to, from its 382 // parent? 383 var isFetus = false; 384 var ids; 385 var hashInputs = []; 386 387 switch (jQuery.type(settings)) { 388 case 'undefined': 389 case 'null': 390 isFetus = true; 391 break; 392 case 'array': 393 ids = settings.sort().join(','); 394 break; 395 case 'number': 396 case 'string': 397 ids = settings; 398 break; 399 case 'object': 400 ids = settings.id; 401 break; 402 default: 403 GCN.error('UNKNOWN_ARGUMENT', 404 'Don\'t know what to do with the object ' + settings); 405 return; 406 } 407 408 var hash; 409 if (isFetus) { 410 hash = null; 411 } else { 412 var channel = getChannel(this); 413 hash = ctor._needsChainedHash 414 ? ctor._makeHash(this.__gcnhash__, channel + '/' + ids) 415 : ctor._makeHash(channel + '/' + ids); 416 } 417 418 var chainback = GCN.getChainback(ctor, hash, this, 419 [settings, success, error, {}]); 420 421 return chainback; 422 }, 423 424 /** 425 * Works backward to read this object's ancestor before continuing 426 * with the callback. 427 * 428 * This method should be overridden. 429 * 430 * @private 431 * @param {function(Chainback)} success Callback. 432 * @param {function} error Custom error handler. 433 */ 434 /* 435 _onContinue: function (success, error) { 436 if (success) { 437 success(this); 438 } 439 }, 440 */ 441 442 /** 443 * Terminates any further exection of the functions that remain in 444 * the call queue. 445 * TODO: Kill all ajax calls. 446 * 447 * @private 448 * @return {Array.<function>} A list of functions that we in this 449 * Chainback's call queue when an abort 450 * happend. 451 */ 452 _abort: function () { 453 this._clearCache(); 454 var callchain = this.__gcncallchain__.concat( 455 this.__gcncallqueue__ 456 ); 457 this.__gcnmutex__ = true; 458 this.__gcncallchain__ = []; 459 this.__gcncallqueue__ = []; 460 return callchain; 461 }, 462 463 /** 464 * Gets the chainback from which this object was `_continue'd() 465 * from. 466 * 467 * @private 468 * @param {Chainback} 469 * @return {Chainback} This Chainback's ancestor. 470 */ 471 _ancestor: function () { 472 return this._chain; 473 }, 474 475 /** 476 * Locks the semaphore. 477 * 478 * @private 479 * @return {Chainback} This Chainback. 480 */ 481 _procure: function () { 482 this.__gcnmutex__ = false; 483 return this; 484 }, 485 486 /** 487 * Unlocks the semaphore. 488 * 489 * @private 490 * @return {Chainback} This Chainback. 491 */ 492 _vacate: function () { 493 this.__gcnmutex__ = true; 494 this.__release__(); 495 return this; 496 }, 497 498 /** 499 * Halts and forks the main call chain of this chainback object. 500 * Creates a derivitive object that will be used to accomplish 501 * operations that need to complete before the main chain is 502 * permitted to proceed. Before execution on the main chainback 503 * object is restarted, the forked derivitive object is merged into 504 * the original chainback instance. 505 * 506 * @private 507 * @return {Chainback} A derivitive Chainback object forked from 508 * this Chainback instance. 509 */ 510 _fork: function () { 511 var that = this; 512 this._procure(); 513 var Fork = function ChainbackFork() { 514 var prop; 515 for (prop in that) { 516 if (that.hasOwnProperty(prop) && !DONT_MERGE[prop]) { 517 this[prop] = that[prop]; 518 } 519 } 520 this.__gcnorigin__ = that; 521 this.__gcnmutex__ = true; 522 this.__gcncallchain__ = []; 523 this.__gcncallqueue__ = []; 524 }; 525 Fork.prototype = new this._constructor(); 526 return new Fork(); 527 }, 528 529 /** 530 * Transfers the state of this derivitive into its origin. 531 * 532 * @private 533 */ 534 _merge: function () { 535 if (!this.__gcnorigin__) { 536 return; 537 } 538 var origin = this.__gcnorigin__; 539 var prop; 540 for (prop in this) { 541 if (this.hasOwnProperty(prop) && !DONT_MERGE[prop]) { 542 origin[prop] = this[prop]; 543 } 544 } 545 origin._vacate(); 546 return origin; 547 }, 548 549 /** 550 * Wraps jQuery's `ajax' method. Queues the callbacks in the chain 551 * call so that they can be invoked synchonously. Without blocking 552 * the browser thread. 553 * 554 * @private 555 * @param {object} settings 556 */ 557 _queueAjax: function (settings) { 558 if (settings.json) { 559 settings.data = JSON.stringify(settings.json); 560 delete settings.json; 561 } 562 563 settings.dataType = 'json'; 564 settings.contentType = 'application/json; charset=utf-8'; 565 settings.error = (function (onError) { 566 return function (xhr, status, error) { 567 var throwException = true; 568 if (onError) { 569 throwException = onError(GCN.createError('HTTP_ERROR', error, xhr)); 570 } 571 if (throwException !== false) { 572 GCN.error('AJAX_ERROR', error, xhr); 573 } 574 }; 575 }(settings.error)); 576 577 // Duck-type the complete callback, or add one if not provided. 578 // We use complete to forward the continuation because it is 579 // the last callback to be executed the jQuery ajax callback 580 // sequence. 581 settings.complete = (function (chainback, onComplete, opts) { 582 return function () { 583 --chainback.__gcnajaxcount__; 584 onComplete.apply(chainback, arguments); 585 callNext(chainback); 586 }; 587 }(this, settings.complete || function () {}, settings)); 588 589 ++this.__gcnajaxcount__; 590 591 GCN.ajax(settings); 592 }, 593 594 /** 595 * Clears the cache for this individual object. 596 * 597 * @private 598 * @return {Chainback} This Chainback. 599 */ 600 _clearCache: function () { 601 if (chainback.__gcncache__[this.__gcnhash__]) { 602 delete chainback.__gcncache__[this.__gcnhash__]; 603 } 604 return this; 605 }, 606 607 /** 608 * Add this object to the cache, using its hash as the key. 609 * 610 * @private 611 * @return {Chainback} This Chainback. 612 */ 613 _addToCache: function () { 614 this._constructor.__gcncache__[this.__gcnhash__] = this; 615 return this; 616 }, 617 618 /** 619 * Removes the given chainback instance from the temporary cache, 620 * usually after the chainback instance has matured from a "fetus" 621 * into a fully realized chainback object. 622 * 623 * @param {Chainback} instance The chainback instance to remove. 624 * @return {boolean} True if this chainback instance was found and 625 * removed, false if it could not be found. 626 */ 627 _removeFromTempCache: function (instance) { 628 var hash; 629 var cache = this.__gcntempcache__; 630 for (hash in cache) { 631 if (cache.hasOwnProperty(hash) && instance === cache[hash]) { 632 delete cache[hash]; 633 return true; 634 } 635 } 636 return false; 637 }, 638 639 /** 640 * @private 641 * @param {string|number} str 642 * @return {Chainback} This Chainback. 643 */ 644 _setHash: function (str) { 645 var ctor = this._constructor; 646 var addAncestorHash = ctor._needsChainedHash && this._chain; 647 var channel = getChannel(this); 648 var hash = addAncestorHash 649 ? ctor._makeHash(this._chain.__gcnhash__, channel + '/' + str) 650 : ctor._makeHash(channel + '/' + str); 651 this.__gcnhash__ = hash; 652 return this; 653 }, 654 655 /** 656 * Invokes the given callback function while ensuring that any 657 * exceptions that occur during the invocation of the callback, 658 * will be caught and allow the Chainback object to complete its 659 * all remaining queued calls. 660 * 661 * @param {function} callback The function to invoke. 662 * @param {Array.<*>=} args A list of object that will be passed as 663 * arguments into the callback function. 664 */ 665 _invoke: function (callback, args) { 666 if (typeof callback !== 'function') { 667 return; 668 } 669 try { 670 if (args && args.length) { 671 callback.apply(null, args); 672 } else { 673 callback(); 674 } 675 } catch (ex) { 676 setTimeout(function () { 677 throw ex; 678 }, 1); 679 } 680 } 681 682 }); 683 684 /** 685 * Causes all promises that are made by this Chainback object, as well 686 * as all derived promises, to be resolved before the given success 687 * callback is invoked to receive this object in a "fulfilled" state. 688 * If a problem is encountered at any point during the resolution of a 689 * promise in the resolution chain, the error function is called, and 690 * any further resolution is aborted. 691 * 692 * @private 693 * @param {function(Chainback)} success Callback function to be invoked 694 * when all the promises on which 695 * this object depends have been 696 * completed. 697 * @param {function(GCNError):boolean=} error Optional custom error 698 * handler. 699 */ 700 props._fulfill = function (success, error) { 701 if (this._chain) { 702 this._chain._fulfill(success, error); 703 } else { 704 success(this); 705 } 706 }; 707 708 var propName; 709 var propValue; 710 711 // Generates the chainable callback methods. Transforms all functions 712 // whose names do not start with the "!" character into chainable 713 // callback prototype methods. 714 715 for (propName in props) { 716 if (props.hasOwnProperty(propName)) { 717 propValue = props[propName]; 718 if (jQuery.type(propValue) === 'function' && 719 propName.charAt(0) !== '!') { 720 chainback.prototype[propName] = makeMethodChainable( 721 propValue, 722 propName 723 ); 724 } else { 725 if (propName.charAt(0) === '!') { 726 propName = propName.substring(1, propName.length); 727 } 728 chainback.prototype[propName] = propValue; 729 } 730 } 731 } 732 733 return chainback; 734 }; 735 736 }(GCN)); 737