1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 sc_require('system/response'); 9 10 /** 11 @class 12 13 Implements support for AJAX requests using XHR, XHR2 and other protocols. 14 15 SC.Request is essentially a client version of the request/response objects you receive when 16 implementing HTTP servers. 17 18 To send a request, you just need to create your request object, configure your options, and call 19 `send()` to initiate the request. The request will be added to the pending request queue managed 20 by `SC.Request.manager`. 21 22 For example, 23 24 // Create a simple XHR request. 25 var request = SC.Request.create(); 26 request.set('address', resourceAddress); 27 request.set('type', 'GET'); 28 request.send(); 29 30 The previous example will create an XHR GET request to `resourceAddress`. Because the `address` 31 and `type` of the request must be set on every request, there are helper methods on `SC.Request` 32 that you will likely use in every situation. 33 34 For example, the previous example can be written more concisely as, 35 36 // Create a simple XHR request. 37 var request = SC.Request.getUrl(resourceAddress); 38 39 There are four other helper methods to cover the other common HTTP method types: `POST`, `DELETE`, 40 `PUT` and `PATCH`, which are `postUrl`, `deleteUrl`, `putUrl` and `patchUrl` respectively. Since 41 you may also send a body with `POST`, `PUT` and `PATCH` requests, those helper methods also take a 42 second argument `body` (which is analogous to calling `request.set('body', body)`). 43 44 ## Responses 45 46 ### XHR Requests & Custom Communication Protocols 47 48 By default, the request will create an instance of `SC.XHRResponse` in order to complete the 49 request. As the name implies, the `SC.XHRResponse` class will make an XHR request to the server, 50 which is the typical method that the SproutCore client will communicate with remote endpoints. 51 52 In order to use a custom response type handler, you should extend `SC.Response` and set the 53 `responseClass` of the request to your custom response type. 54 55 For example, 56 57 var request = SC.Request.getUrl(resourceAddress).set('responseClass', MyApp.CustomProtocolResponse); 58 59 60 ### Handling Responses 61 62 `SC.Request` supports multiple response handlers based on the status code of the response. This 63 system is quite intelligent and allows for specific callbacks to handle specific status codes 64 (e.g. 404) or general status codes (e.g. 400) or combinations of both. Callbacks are registered 65 using the `notify` method, which accepts a `target` and a `method` which will be called when the 66 request completes. The most basic example of registering a general response handler would be like 67 so, 68 69 // The response handler target (typically a state or controller or some such SC.Object instance). 70 var targetObject; 71 72 targetObject = SC.Object.create({ 73 74 handleResponse: function (response) { 75 // Handle the various possible status codes. 76 var status = response.get('status'); 77 if (status === 200) { // 200 OK 78 // Do something. 79 } else if (status < 400) { // i.e. 3xx 80 // Do something. 81 } else ... 82 } 83 84 }); 85 86 87 // Create a simple XHR request. 88 var request; 89 90 request = SC.Request.getUrl(resourceAddress) 91 .notify(targetObject, 'handleResponse') 92 .send(); 93 94 However, this approach requires that every response handler be able to handle all of the possible 95 error codes that we may be able to handle in a more general manner. It's also more code for us 96 to write to write all of the multiple condition statements. For this reason, the `notify` method 97 accepts an optional status code argument *before* the target and method. You can use a generic 98 status code (i.e. 400) or a specific status code (i.e. 404). If you use a generic status code, all 99 statuses within that range will result in that callback being used. 100 101 For example, here is a more specific example, 102 103 // The response handler target (typically a data source or state or some such SC.Object instance). 104 var targetObject; 105 106 targetObject = SC.Object.create({ 107 108 gotOK: function (response) { // 2xx Successful 109 // Do something. 110 111 return true; // Return true to ensure that any following generic handlers don't run! 112 }, 113 114 gotForbidden: function (response) { // 403 Forbidden 115 // Do something. 116 117 return true; // Return true to ensure that any following generic handlers don't run! 118 }, 119 120 gotUnknownError: function (response) { // 3xx, 4xx (except 403), 5xx 121 // Do something. 122 } 123 124 }); 125 126 127 // Create a simple XHR request. 128 var request; 129 130 request = SC.Request.getUrl(resourceAddress) 131 .notify(200, targetObject, 'gotOK') 132 .notify(403, targetObject, 'gotForbidden') 133 .notify(targetObject, 'gotUnknownError') 134 .send(); 135 136 Please note that the notifications will fall through in the order they are added if not handled. 137 This means that the generic handler `gotUnknownError` will be called for any responses not caught 138 by the other handlers. In this example, to ensure that `gotUnknownError` doesn't get called when a 139 2xx or 403 response comes in, those handlers *return `true`*. 140 141 Please also note that this design allows us to easily re-use handler methods. For example, we may 142 choose to have `gotUnknownError` be the standard last resort fallback handler for all requests. 143 144 For more examples, including handling of XHR2 progress events, please @see SC.Request.prototype.notify. 145 146 ### Response Bodies & JSON Decoding 147 148 The body of the response is the `body` property on the response object which is passed to the 149 notify target method. For example, 150 151 gotOK: function (response) { // 2xx Successful 152 var body = response.get('body'); 153 154 // Do something. 155 156 return true; // Return true to ensure that any following generic handlers don't run! 157 }, 158 159 The type of the body will depend on what the server returns, but since it will typically be JSON, 160 we have a built-in option to have the body be decoded into a JavaScript object automatically by 161 setting `isJSON` to true on the request. 162 163 For example, 164 165 // Create a simple XHR request. 166 var request; 167 168 request = SC.Request.getUrl(resourceAddress) 169 .set('isJSON', true) 170 .notify(200, targetObject, 'gotOK') 171 .send(); 172 173 There is a helper method to achieve this as well, `json()`, 174 175 // Create a simple XHR request. 176 var request; 177 178 request = SC.Request.getUrl(resourceAddress) 179 .json() // Set `isJSON` to true. 180 .notify(200, targetObject, 'gotOK') 181 .send(); 182 183 @extends SC.Object 184 @extends SC.Copyable 185 @extends SC.Freezable 186 @since SproutCore 1.0 187 */ 188 SC.Request = SC.Object.extend(SC.Copyable, SC.Freezable, 189 /** @scope SC.Request.prototype */ { 190 191 // .......................................................... 192 // PROPERTIES 193 // 194 195 /** 196 Whether to allow credentials, such as Cookies, in the request. While this has no effect on 197 requests to the same domain, cross-domain requests require that the transport be configured to 198 allow the inclusion of credentials such as Cookies. 199 200 You can change this property using the chainable `credentials()` helper method (or set it directly). 201 202 @type Boolean 203 @default YES 204 */ 205 allowCredentials: YES, 206 207 /** 208 Sends the request asynchronously instead of blocking the browser. You 209 should almost always make requests asynchronous. You can change this 210 options with the async() helper option (or simply set it directly). 211 212 @type Boolean 213 @default YES 214 */ 215 isAsynchronous: YES, 216 217 /** 218 Processes the request and response as JSON if possible. You can change 219 this option with the json() helper method. 220 221 @type Boolean 222 @default NO 223 */ 224 isJSON: NO, 225 226 /** 227 Process the request and response as XML if possible. You can change this 228 option with the xml() helper method. 229 230 @type Boolean 231 @default NO 232 */ 233 isXML: NO, 234 235 /** 236 Specifies whether or not the request will have custom headers attached 237 to it. By default, SC.Request attaches X-Requested-With and 238 X-SproutCore-Version headers to all outgoing requests. This allows 239 you to override that behavior. 240 241 You may want to set this to NO if you are making simple CORS requests 242 in compatible browsers. See <a href="http://www.w3.org/TR/cors/">CORS 243 Spec for more information.</a> 244 245 TODO: Add unit tests for this feature 246 247 @type Boolean 248 @default YES 249 */ 250 attachIdentifyingHeaders: YES, 251 252 /** 253 Current set of headers for the request 254 255 @field 256 @type Hash 257 @default {} 258 */ 259 headers: function() { 260 var ret = this._headers; 261 if (!ret) { ret = this._headers = {}; } 262 return ret; 263 }.property().cacheable(), 264 265 /** 266 Whether the request is within the same domain or not. The response class may use this property 267 to determine specific cross domain configurations. 268 269 @field 270 @type Boolean 271 */ 272 isSameDomain: function () { 273 var address = this.get('address'), 274 urlRegex = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/, 275 location = window.location, 276 parts, originParts; 277 278 // This pattern matching strategy was taken from jQuery. 279 parts = urlRegex.exec( address.toLowerCase() ); 280 originParts = urlRegex.exec( location.href.toLowerCase() ); 281 282 return SC.none(parts) || 283 (parts[1] === originParts[1] && // protocol 284 parts[2] === originParts[2] && // domain 285 (parts[3] || (parts[1] === "http:" ? 80 : 443 ) ) === (originParts[3] || (originParts[1] === "http:" ? 80 : 443))); // port 286 }.property('address').cacheable(), 287 288 /** 289 Underlying response class to actually handle this request. Currently the 290 only supported option is SC.XHRResponse which uses a traditional 291 XHR transport. 292 293 @type SC.Response 294 @default SC.XHRResponse 295 */ 296 responseClass: SC.XHRResponse, 297 298 /** 299 The original request for copied requests. 300 301 @property SC.Request 302 @default null 303 */ 304 source: null, 305 306 /** 307 The URL this request to go to. 308 309 @type String 310 @default null 311 */ 312 address: null, 313 314 /** 315 The HTTP method to use. 316 317 @type String 318 @default 'GET' 319 */ 320 type: 'GET', 321 322 /** 323 An optional timeout value of the request, in milliseconds. The timer 324 begins when SC.Response#fire is actually invoked by the request manager 325 and not necessarily when SC.Request#send is invoked. If this timeout is 326 reached before a response is received, the equivalent of 327 SC.Request.manager#cancel() will be invoked on the SC.Response instance 328 and the didReceive() callback will be called. 329 330 An exception will be thrown if you try to invoke send() on a request that 331 has both a timeout and isAsyncronous set to NO. 332 333 @type Number 334 @default null 335 */ 336 timeout: null, 337 338 /** 339 The body of the request. May be an object if isJSON or isXML is set, 340 otherwise should be a string. 341 342 @type Object|String 343 @default null 344 */ 345 body: null, 346 347 /** 348 The body, encoded as JSON or XML if needed. 349 350 @field 351 @type Object|String 352 @default #body 353 */ 354 encodedBody: function() { 355 // TODO: support XML 356 var ret = this.get('body'); 357 if (ret && this.get('isJSON')) { ret = SC.json.encode(ret); } 358 return ret; 359 }.property('isJSON', 'isXML', 'body').cacheable(), 360 361 362 // .......................................................... 363 // CALLBACKS 364 // 365 366 /** 367 Invoked on the original request object just before a copied request is 368 frozen and then sent to the server. This gives you one last change to 369 fixup the request; possibly adding headers and other options. 370 371 If you do not want the request to actually send, call cancel(). 372 373 @param {SC.Request} request A copy of the request object, not frozen 374 @param {SC.Response} response The object that will wrap the response 375 */ 376 willSend: function(request, response) {}, 377 378 /** 379 Invoked on the original request object just after the request is sent to 380 the server. You might use this callback to update some state in your 381 application. 382 383 The passed request is a frozen copy of the request, indicating the 384 options set at the time of the request. 385 386 @param {SC.Request} request A copy of the request object, frozen 387 @param {SC.Response} response The object that will wrap the response 388 @returns {Boolean} YES on success, NO on failure 389 */ 390 didSend: function(request, response) {}, 391 392 /** 393 Invoked when a response has been received but not yet processed. This is 394 your chance to fix up the response based on the results. If you don't 395 want to continue processing the response call response.cancel(). 396 397 @param {SC.Request} request A copy of the request object, frozen 398 @param {SC.Response} response The object that will wrap the response 399 */ 400 willReceive: function(request, response) {}, 401 402 /** 403 Invoked after a response has been processed but before any listeners are 404 notified. You can do any standard processing on the request at this 405 point. If you don't want to allow notifications to continue, call 406 response.cancel() 407 408 @param {SC.Request} request A copy of the request object, frozen 409 @param {SC.Response} response The object that will wrap the response 410 */ 411 didReceive: function(request, response) {}, 412 413 414 // .......................................................... 415 // HELPER METHODS 416 // 417 418 /** @private */ 419 concatenatedProperties: 'COPY_KEYS', 420 421 /** @private */ 422 COPY_KEYS: ['attachIdentifyingHeaders', 'allowCredentials', 'isAsynchronous', 'isJSON', 'isXML', 'address', 'type', 'timeout', 'body', 'responseClass', 'willSend', 'didSend', 'willReceive', 'didReceive'], 423 424 /** 425 Returns a copy of the current request. This will only copy certain 426 properties so if you want to add additional properties to the copy you 427 will need to override copy() in a subclass. 428 429 @returns {SC.Request} new request 430 */ 431 copy: function() { 432 var ret = {}, 433 keys = this.COPY_KEYS, 434 loc = keys.length, 435 key; 436 437 while(--loc >= 0) { 438 key = keys[loc]; 439 if (this.hasOwnProperty(key)) { 440 ret[key] = this.get(key); 441 } 442 } 443 444 if (this.hasOwnProperty('listeners')) { 445 ret.listeners = SC.copy(this.get('listeners')); 446 } 447 448 if (this.hasOwnProperty('_headers')) { 449 ret._headers = SC.copy(this._headers); 450 } 451 452 ret.source = this.get('source') || this; 453 454 return this.constructor.create(ret); 455 }, 456 457 /** 458 To set headers on the request object. Pass either a single key/value 459 pair or a hash of key/value pairs. If you pass only a header name, this 460 will return the current value of the header. 461 462 @param {String|Hash} key 463 @param {String} value 464 @returns {SC.Request|Object} receiver 465 */ 466 header: function(key, value) { 467 var header, headers; 468 469 if (SC.typeOf(key) === SC.T_STRING) { 470 headers = this._headers; 471 if (arguments.length === 1) { 472 return headers ? headers[key] : null; 473 } else { 474 this.propertyWillChange('headers'); 475 if (!headers) { headers = this._headers = {}; } 476 headers[key] = value; 477 this.propertyDidChange('headers'); 478 return this; 479 } 480 481 // handle parsing hash of parameters 482 } else if (value === undefined) { 483 headers = key; 484 this.beginPropertyChanges(); 485 for(header in headers) { 486 if (!headers.hasOwnProperty(header)) { continue; } 487 this.header(header, headers[header]); 488 } 489 this.endPropertyChanges(); 490 return this; 491 } 492 493 return this; 494 }, 495 496 /** 497 Clears the list of headers that were set on this request. 498 This could be used by a subclass to blow-away any custom 499 headers that were added by the super class. 500 */ 501 clearHeaders: function() { 502 this.propertyWillChange('headers'); 503 this._headers = {}; 504 this.propertyDidChange('headers'); 505 }, 506 507 /** 508 Converts the current request to be asynchronous. 509 510 @param {Boolean} flag YES to make asynchronous, NO or undefined. Default YES. 511 @returns {SC.Request} receiver 512 */ 513 async: function(flag) { 514 if (flag === undefined) { flag = YES; } 515 return this.set('isAsynchronous', flag); 516 }, 517 518 /** 519 Converts the current request to request allowing credentials or not. 520 521 @param {Boolean} flag YES to request allowing credentials, NO to disallow credentials. Default YES. 522 @returns {SC.Request} receiver 523 */ 524 credentials: function(flag) { 525 if (flag === undefined) { flag = YES; } 526 return this.set('allowCredentials', flag); 527 }, 528 529 /** 530 Sets the maximum amount of time the request will wait for a response. 531 532 @param {Number} timeout The timeout in milliseconds. 533 @returns {SC.Request} receiver 534 */ 535 timeoutAfter: function(timeout) { 536 return this.set('timeout', timeout); 537 }, 538 539 /** 540 Converts the current request to use JSON. 541 542 @param {Boolean} flag YES to make JSON, NO or undefined. Default YES. 543 @returns {SC.Request} receiver 544 */ 545 json: function(flag) { 546 if (flag === undefined) { flag = YES; } 547 if (flag) { this.set('isXML', NO); } 548 return this.set('isJSON', flag); 549 }, 550 551 /** 552 Converts the current request to use XML. 553 554 @param {Boolean} flag YES to make XML, NO or undefined. Default YES. 555 @returns {SC.Request} recevier 556 */ 557 xml: function(flag) { 558 if (flag === undefined) { flag = YES; } 559 if (flag) { this.set('isJSON', NO); } 560 return this.set('isXML', flag); 561 }, 562 563 /** 564 Called just before a request is enqueued. This will encode the body 565 into JSON if it is not already encoded, and set identifying headers 566 */ 567 _prep: function() { 568 var hasContentType = !!this.header('Content-Type'); 569 570 if (this.get('attachIdentifyingHeaders')) { 571 this.header('X-Requested-With', 'XMLHttpRequest'); 572 this.header('X-SproutCore-Version', SC.VERSION); 573 } 574 575 // Set the Content-Type header only if not specified and the request 576 // includes a body. 577 if (!hasContentType && !!this.get('body')) { 578 if (this.get('isJSON')) { 579 this.header('Content-Type', 'application/json'); 580 } else if (this.get('isXML')) { 581 this.header('Content-Type', 'text/xml'); 582 } 583 } 584 return this; 585 }, 586 587 /** 588 Will fire the actual request. If you have set the request to use JSON 589 mode then you can pass any object that can be converted to JSON as the 590 body. Otherwise you should pass a string body. 591 592 @param {String|Object} [body] 593 @returns {SC.Response} New response object 594 */ 595 send: function(body) { 596 // Sanity-check: Be sure a timeout value was not specified if the request 597 // is synchronous (because it wouldn't work). 598 var timeout = this.get('timeout'); 599 if (timeout && !this.get('isAsynchronous')) { 600 throw new Error("Timeout values cannot be used with synchronous requests"); 601 } else if (timeout === 0) { 602 throw new Error("The timeout value must either not be specified or must be greater than 0"); 603 } 604 605 if (body) { this.set('body', body); } 606 return SC.Request.manager.sendRequest(this.copy()._prep()); 607 }, 608 609 /** 610 Resends the current request. This is more efficient than calling send() 611 for requests that have already been used in a send. Otherwise acts just 612 like send(). Does not take a body argument. 613 614 @returns {SC.Response} new response object 615 */ 616 resend: function() { 617 var req = this.get('source') ? this : this.copy()._prep(); 618 return SC.Request.manager.sendRequest(req); 619 }, 620 621 /** 622 Configures a callback to execute as a request progresses or completes. You 623 must pass at least a target and action/method to this and optionally an 624 event name or status code. 625 626 You may also pass additional arguments which will then be passed along to 627 your callback. 628 629 ## Scoping With Status Codes 630 631 If you pass a status code as the first argument to this method, the 632 accompanying notification callback will only be called if the response 633 status matches the status code. For example, if you pass 201 (or 634 SC.Request.CREATED), the accompanying method will only be called if the 635 response status from the server is also 201. 636 637 You can also pass "generic" status codes such as 200, 300, or 400, which 638 will be invoked anytime the status code is in the same range and if a more 639 specific notifier was not registered first and returned YES. 640 641 Finally, passing a status code of 0 or no status at all will cause your 642 method to be executed no matter what the resulting status is unless a 643 more specific notifier was registered first and returned YES. 644 645 For example, 646 647 var req = SC.Request.create({type: 'POST'}); 648 req.notify(201, this, this.reqWasCreated); // Handle a specific status code 649 req.notify(401, this, this.reqWasUnauthorized); // Handle a specific status code 650 req.notify(400, this, this.reqDidRedirect); // Handle any 4xx status 651 req.notify(this, function(response, arg1, arg2) { 652 // do something 653 }, arg1, arg2); // Handle any status. Also, passing additional arguments to the callback handler 654 655 ## Notifying on Progress Events 656 657 If you pass a progress event name your callback will be called each time 658 the event fires on the response. For example, the XMLHttpRequest Level 2 659 specification defines several progress events: loadstart, progress, abort, 660 error, load, timeout and loadend. Therefore, when using the default 661 SC.Request responseClass, SC.XHRResponse, you can be notified of each of 662 these events by simply providing the matching event name. 663 664 Note that many older browsers do not support XMLHttpRequest Level 2. See 665 http://caniuse.com/xhr2 for a list of supported browsers. 666 667 For example, 668 669 var req = SC.Request.create({type: 'GET'}); 670 req.notify('progress', this, this.reqDidProgress); // Handle 'progress' events 671 req.notify('abort', this, this.reqDidAbort); // Handle 'abort' events 672 req.notify('upload.progress', this, this.reqUploadDidProgress); // Handle 'progress' events on the XMLHttpRequestUpload 673 req.send(); 674 675 ## Callback Format 676 677 Your notification callback should expect to receive the Response object as 678 the first parameter for status code notifications and the Event object for 679 progress notifications; plus any additional parameters that you pass. If 680 your callback handles the notification and to prevent further handling, it 681 should return YES. 682 683 @param [statusOrEvent] {Number|String} A Number status code or String Event name. 684 @param target {Object} The target object for the callback action. 685 @param action {String|Function} The method name or function to call on the target. 686 @returns {SC.Request} The SC.Request object. 687 */ 688 notify: function(statusOrEvent, target, action) { 689 var args, 690 i, len; 691 692 //@if (debug) 693 if (statusOrEvent === 'loadend' && SC.Request.WARN_ON_LOADEND) { 694 SC.warn("Developer Warning: You have called SC.Request#notify for the 'loadend' event. Note that on certain platforms, like older iPads, loadend is not supported and this notification will fail silently. You can protect against this by checking SC.platform.get('supportsXHR2LoadEndEvent'), and attaching listeners for load, error and abort instead. (This is not done automatically because your code may need to handle event type and fire order considerations.) To suppress this warning, set SC.Request.WARN_ON_LOADEND to NO."); 695 } 696 //@endif 697 698 // Normalize arguments 699 if (SC.typeOf(statusOrEvent) !== SC.T_NUMBER && SC.typeOf(statusOrEvent) !== SC.T_STRING) { 700 // Accept multiple additional arguments (Do so before shifting the arguments!) 701 702 // Fast arguments access. 703 // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly. 704 args = new Array(arguments.length - 2); // SC.A(arguments).slice(2) 705 for (i = 0, len = args.length; i < len; i++) { args[i] = arguments[i + 2]; } 706 707 // Shift the arguments 708 action = target; 709 target = statusOrEvent; 710 statusOrEvent = 0; 711 } else { 712 // Accept multiple additional arguments. 713 714 if (arguments.length > 3) { 715 // Fast arguments access. 716 // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly. 717 args = new Array(arguments.length - 3); // SC.A(arguments).slice(3) 718 for (i = 0, len = args.length; i < len; i++) { args[i] = arguments[i + 3]; } 719 } else { 720 args = []; 721 } 722 } 723 724 // Prepare listeners for this object and notification target. 725 var listeners = this.get('listeners'); 726 if (!listeners) { this.set('listeners', listeners = {}); } 727 if(!listeners[statusOrEvent]) { listeners[statusOrEvent] = []; } 728 729 // Add another listener for the given status code or event name. 730 listeners[statusOrEvent].push({target: target, action: action, args: args}); 731 732 return this; 733 } 734 735 }); 736 737 SC.Request.mixin( 738 /** @scope SC.Request */ { 739 740 /** 741 Helper method for quickly setting up a GET request. 742 743 @param {String} address url of request 744 @returns {SC.Request} receiver 745 */ 746 getUrl: function(address) { 747 return this.create().set('address', address).set('type', 'GET'); 748 }, 749 750 /** 751 Helper method for quickly setting up a POST request. 752 753 @param {String} address url of request 754 @param {String} body 755 @returns {SC.Request} receiver 756 */ 757 postUrl: function(address, body) { 758 var req = this.create().set('address', address).set('type', 'POST'); 759 if(body) { req.set('body', body) ; } 760 return req ; 761 }, 762 763 /** 764 Helper method for quickly setting up a DELETE request. 765 766 @param {String} address url of request 767 @returns {SC.Request} receiver 768 */ 769 deleteUrl: function(address) { 770 return this.create().set('address', address).set('type', 'DELETE'); 771 }, 772 773 /** 774 Helper method for quickly setting up a PUT request. 775 776 @param {String} address url of request 777 @param {String} body 778 @returns {SC.Request} receiver 779 */ 780 putUrl: function(address, body) { 781 var req = this.create().set('address', address).set('type', 'PUT'); 782 if(body) { req.set('body', body) ; } 783 return req ; 784 }, 785 786 /** 787 Helper method for quickly setting up a PATCH request. 788 789 @param {String} address url of request 790 @param {String} body 791 @returns {SC.Request} receiver 792 */ 793 patchUrl: function(address, body) { 794 var req = this.create().set('address', address).set('type', 'PATCH'); 795 if(body) { req.set('body', body) ; } 796 return req ; 797 } 798 799 }); 800 801 /* @private Gates loadend warning. */ 802 SC.Request.WARN_ON_LOADEND = YES; 803 804 /** 805 @class 806 807 The request manager coordinates all of the active XHR requests. It will 808 only allow a certain number of requests to be active at a time; queuing 809 any others. This allows you more precise control over which requests load 810 in which order. 811 812 @since SproutCore 1.0 813 */ 814 SC.Request.manager = SC.Object.create( 815 /** @scope SC.Request.manager */{ 816 817 /** 818 Maximum number of concurrent requests allowed. 6 for all browsers. 819 820 @type Number 821 @default 6 822 */ 823 maxRequests: 6, 824 825 /** 826 Current requests that are inflight. 827 828 @type Array 829 @default [] 830 */ 831 inflight: [], 832 833 /** 834 Requests that are pending and have not been started yet. 835 836 @type Array 837 @default [] 838 */ 839 pending: [], 840 841 842 // .......................................................... 843 // METHODS 844 // 845 846 /** 847 Invoked by the send() method on a request. This will create a new low- 848 level transport object and queue it if needed. 849 850 @param {SC.Request} request the request to send 851 @returns {SC.Object} response object 852 */ 853 sendRequest: function(request) { 854 if (!request) { return null; } 855 856 // create low-level transport. copy all critical data for request over 857 // so that if the request has been reconfigured the transport will still 858 // work. 859 var response = request.get('responseClass').create({ request: request }); 860 861 // add to pending queue 862 this.get('pending').pushObject(response); 863 this.fireRequestIfNeeded(); 864 865 return response; 866 }, 867 868 /** 869 Cancels a specific request. If the request is pending it will simply 870 be removed. Otherwise it will actually be cancelled. 871 872 @param {SC.Response} response a response object 873 @returns {Boolean} YES if cancelled 874 */ 875 cancel: function(response) { 876 var pending = this.get('pending'), 877 inflight = this.get('inflight'); 878 879 if (pending.indexOf(response) >= 0) { 880 this.propertyWillChange('pending'); 881 pending.removeObject(response); 882 this.propertyDidChange('pending'); 883 return YES; 884 } else if (inflight.indexOf(response) >= 0) { 885 response.cancel(); 886 887 inflight.removeObject(response); 888 this.fireRequestIfNeeded(); 889 return YES; 890 } 891 892 return NO; 893 }, 894 895 /** 896 Cancels all inflight and pending requests. 897 898 @returns {Boolean} YES if any items were cancelled. 899 */ 900 cancelAll: function() { 901 var pendingLen = this.getPath('pending.length'), 902 inflightLen = this.getPath('inflight.length'), 903 inflight = this.get('inflight'), 904 pending = this.get('pending'); 905 906 if(pendingLen || inflightLen) { 907 // Iterate backwards. 908 for( var i = inflightLen - 1; i >= 0; i--) { 909 // This will 'eventually' try to remove the request from 910 // inflight, but it's not fast enough for us. 911 var r = inflight.objectAt(i); 912 r.cancel(); 913 } 914 915 // Manually scrub the arrays without screwing up memory pointers. 916 pending.replace(0, pendingLen); 917 inflight.replace(0, inflightLen); 918 919 return YES; 920 921 } 922 return NO; 923 }, 924 925 /** 926 Checks the inflight queue. If there is an open slot, this will move a 927 request from pending to inflight. 928 929 @returns {Object} receiver 930 */ 931 fireRequestIfNeeded: function() { 932 var pending = this.get('pending'), 933 inflight = this.get('inflight'), 934 max = this.get('maxRequests'), 935 next; 936 937 if ((pending.length>0) && (inflight.length<max)) { 938 next = pending.shiftObject(); 939 inflight.pushObject(next); 940 next.fire(); 941 } 942 }, 943 944 /** 945 Called by a response/transport object when finishes running. Removes 946 the transport from the queue and kicks off the next one. 947 */ 948 transportDidClose: function(response) { 949 this.get('pending').removeObject(response); 950 this.get('inflight').removeObject(response); 951 this.fireRequestIfNeeded(); 952 }, 953 954 /** 955 Checks if the response is in the pending queue. 956 957 @param {SC.Response} response a response object 958 @return {Boolean} is response in pending queue 959 */ 960 isPending: function(response) { 961 return this.get('pending').contains(response); 962 }, 963 964 /** 965 Checks if the response is in the inflight queue. 966 967 @param {SC.Response} response a response object 968 @return {Boolean} is response in inflight queue 969 */ 970 isInFlight: function(response) { 971 return this.get('inflight').contains(response); 972 } 973 974 }); 975