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