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