diff options
| author | Vojta Jina | 2011-08-05 01:24:41 +0200 | 
|---|---|---|
| committer | Igor Minar | 2011-11-30 11:12:14 -0500 | 
| commit | 59adadca086853c5de6867ae853f6f27a3af4bbe (patch) | |
| tree | f56e4501975a7e53475f6e0d7bb606e530983a7b /src/service/http.js | |
| parent | 497839f583ca3dd75583fb996bb764cbd6d7c4ac (diff) | |
| download | angular.js-59adadca086853c5de6867ae853f6f27a3af4bbe.tar.bz2 | |
feat($http): new $http service, removing $xhr.*
Features:
- aborting requests
- more flexible callbacks (per status code)
- custom request headers (per request)
- access to response headers
- custom transform functions (both request, response)
- caching
- shortcut methods (get, head, post, put, delete, patch, jsonp)
- exposing pendingCount()
- setting timeout
Breaks Renaming $xhr to $http
Breaks Takes one parameter now - configuration object
Breaks $xhr.cache removed - use configuration cache: true instead
Breaks $xhr.error, $xhr.bulk removed
Breaks Callback functions get parameters: response, status, headers
Closes #38
Closes #80
Closes #180
Closes #299
Closes #342
Closes #395
Closes #413
Closes #414
Closes #507
Diffstat (limited to 'src/service/http.js')
| -rw-r--r-- | src/service/http.js | 428 | 
1 files changed, 428 insertions, 0 deletions
diff --git a/src/service/http.js b/src/service/http.js new file mode 100644 index 00000000..13621f90 --- /dev/null +++ b/src/service/http.js @@ -0,0 +1,428 @@ +'use strict'; + +/** + * Parse headers into key value object + * + * @param {string} headers Raw headers as a string + * @returns {Object} Parsed headers as key valu object + */ +function parseHeaders(headers) { +  var parsed = {}, key, val, i; + +  forEach(headers.split('\n'), function(line) { +    i = line.indexOf(':'); +    key = lowercase(trim(line.substr(0, i))); +    val = trim(line.substr(i + 1)); + +    if (key) { +      if (parsed[key]) { +        parsed[key] += ', ' + val; +      } else { +        parsed[key] = val; +      } +    } +  }); + +  return parsed; +} + +/** + * Chain all given functions + * + * This function is used for both request and response transforming + * + * @param {*} data Data to transform. + * @param {function|Array.<function>} fns Function or an array of functions. + * @param {*=} param Optional parameter to be passed to all transform functions. + * @returns {*} Transformed data. + */ +function transform(data, fns, param) { +  if (isFunction(fns)) +    return fns(data); + +  forEach(fns, function(fn) { +    data = fn(data, param); +  }); + +  return data; +} + + +/** + * @ngdoc object + * @name angular.module.ng.$http + * @requires $browser + * @requires $exceptionHandler + * @requires $cacheFactory + * + * @description + */ +function $HttpProvider() { +  var $config = this.defaults = { +    // transform in-coming reponse data +    transformResponse: function(data) { +      if (isString(data)) { +        if (/^\)\]\}',\n/.test(data)) data = data.substr(6); +        if (/^\s*[\[\{]/.test(data) && /[\}\]]\s*$/.test(data)) +          data = fromJson(data, true); +      } +      return data; +    }, + +    // transform out-going request data +    transformRequest: function(d) { +      return isObject(d) ? toJson(d) : d; +    }, + +    // default headers +    headers: { +      common: { +        'Accept': 'application/json, text/plain, */*', +        'X-Requested-With': 'XMLHttpRequest' +      }, +      post: {'Content-Type': 'application/json'}, +      put:  {'Content-Type': 'application/json'} +    } +  }; + +  this.$get = ['$browser', '$exceptionHandler', '$cacheFactory', '$rootScope', +      function($browser, $exceptionHandler, $cacheFactory, $rootScope) { + +  var cache = $cacheFactory('$http'), +      pendingRequestsCount = 0; + +  // the actual service +  function $http(config) { +    return new XhrFuture().retry(config); +  } + +  /** +   * @workInProgress +   * @ngdoc method +   * @name angular.service.$http#pendingCount +   * @methodOf angular.service.$http +   * +   * @description +   * Return number of pending requests +   * +   * @returns {number} Number of pending requests +   */ +  $http.pendingCount = function() { +    return pendingRequestsCount; +  }; + +  /** +   * @ngdoc method +   * @name angular.module.ng.$http#get +   * @methodOf angular.module.ng.$http +   * +   * @description +   * Shortcut method to perform `GET` request +   * +   * @param {string} url Relative or absolute URL specifying the destination of the request +   * @param {Object=} config Optional configuration object +   * @returns {XhrFuture} Future object +   */ + +  /** +   * @ngdoc method +   * @name angular.module.ng.$http#delete +   * @methodOf angular.module.ng.$http +   * +   * @description +   * Shortcut method to perform `DELETE` request +   * +   * @param {string} url Relative or absolute URL specifying the destination of the request +   * @param {Object=} config Optional configuration object +   * @returns {XhrFuture} Future object +   */ + +  /** +   * @ngdoc method +   * @name angular.module.ng.$http#head +   * @methodOf angular.module.ng.$http +   * +   * @description +   * Shortcut method to perform `HEAD` request +   * +   * @param {string} url Relative or absolute URL specifying the destination of the request +   * @param {Object=} config Optional configuration object +   * @returns {XhrFuture} Future object +   */ + +  /** +   * @ngdoc method +   * @name angular.module.ng.$http#patch +   * @methodOf angular.module.ng.$http +   * +   * @description +   * Shortcut method to perform `PATCH` request +   * +   * @param {string} url Relative or absolute URL specifying the destination of the request +   * @param {Object=} config Optional configuration object +   * @returns {XhrFuture} Future object +   */ + +  /** +   * @ngdoc method +   * @name angular.module.ng.$http#jsonp +   * @methodOf angular.module.ng.$http +   * +   * @description +   * Shortcut method to perform `JSONP` request +   * +   * @param {string} url Relative or absolute URL specifying the destination of the request. +   *                     Should contain `JSON_CALLBACK` string. +   * @param {Object=} config Optional configuration object +   * @returns {XhrFuture} Future object +   */ +  createShortMethods('get', 'delete', 'head', 'patch', 'jsonp'); + +  /** +   * @ngdoc method +   * @name angular.module.ng.$http#post +   * @methodOf angular.module.ng.$http +   * +   * @description +   * Shortcut method to perform `POST` request +   * +   * @param {string} url Relative or absolute URL specifying the destination of the request +   * @param {*} data Request content +   * @param {Object=} config Optional configuration object +   * @returns {XhrFuture} Future object +   */ + +  /** +   * @ngdoc method +   * @name angular.module.ng.$http#put +   * @methodOf angular.module.ng.$http +   * +   * @description +   * Shortcut method to perform `PUT` request +   * +   * @param {string} url Relative or absolute URL specifying the destination of the request +   * @param {*} data Request content +   * @param {Object=} config Optional configuration object +   * @returns {XhrFuture} Future object +   */ +  createShortMethodsWithData('post', 'put'); + +  return $http; + +  function createShortMethods(names) { +    forEach(arguments, function(name) { +      $http[name] = function(url, config) { +        return $http(extend(config || {}, { +          method: name, +          url: url +        })); +      }; +    }); +  } + +  function createShortMethodsWithData(name) { +    forEach(arguments, function(name) { +      $http[name] = function(url, data, config) { +        return $http(extend(config || {}, { +          method: name, +          url: url, +          data: data +        })); +      }; +    }); +  } + +  /** +   * Represents Request object, returned by $http() +   * +   * !!! ACCESS CLOSURE VARS: $browser, $config, $log, $rootScope, cache, pendingRequestsCount +   */ +  function XhrFuture() { +    var rawRequest, cfg = {}, callbacks = [], +        defHeaders = $config.headers, +        parsedHeaders; + +    /** +     * Callback registered to $browser.xhr: +     *  - caches the response if desired +     *  - calls fireCallbacks() +     *  - clears the reference to raw request object +     */ +    function done(status, response) { +      // aborted request or jsonp +      if (!rawRequest) parsedHeaders = {}; + +      if (cfg.cache && cfg.method == 'GET' && 200 <= status && status < 300) { +        parsedHeaders = parsedHeaders || parseHeaders(rawRequest.getAllResponseHeaders()); +        cache.put(cfg.url, [status, response, parsedHeaders]); +      } + +      fireCallbacks(response, status); +      rawRequest = null; +    } + +    /** +     * Fire all registered callbacks for given status code +     * +     * This method when: +     *  - serving response from real request ($browser.xhr callback) +     *  - serving response from cache +     * +     * It does: +     *  - transform the response +     *  - call proper callbacks +     *  - log errors +     *  - apply the $scope +     *  - clear parsed headers +     */ +    function fireCallbacks(response, status) { +      // transform the response +      response = transform(response, cfg.transformResponse || $config.transformResponse, rawRequest); + +      var regexp = statusToRegexp(status), +          pattern, callback; + +      pendingRequestsCount--; + +      // normalize internal statuses to 0 +      status = Math.max(status, 0); +      for (var i = 0; i < callbacks.length; i += 2) { +        pattern = callbacks[i]; +        callback = callbacks[i + 1]; +        if (regexp.test(pattern)) { +          try { +            callback(response, status, headers); +          } catch(e) { +            $exceptionHandler(e); +          } +        } +      } + +      $rootScope.$apply(); +      parsedHeaders = null; +    } + +    /** +     * Convert given status code number into regexp +     * +     * It would be much easier to convert registered statuses (e.g. "2xx") into regexps, +     * but this has an advantage of creating just one regexp, instead of one regexp per +     * registered callback. Anyway, probably not big deal. +     * +     * @param status +     * @returns {RegExp} +     */ +    function statusToRegexp(status) { +      var strStatus = status + '', +          regexp = ''; + +      for (var i = Math.min(0, strStatus.length - 3); i < strStatus.length; i++) { +        regexp += '(' + (strStatus.charAt(i) || 0) + '|x)'; +      } + +      return new RegExp(regexp); +    } + +    /** +     * This is the third argument in any user callback +     * @see parseHeaders +     * +     * Return single header value or all headers parsed as object. +     * Headers all lazy parsed when first requested. +     * +     * @param {string=} name Name of header +     * @returns {string|Object} +     */ +    function headers(name) { +      if (name) { +        return parsedHeaders +          ? parsedHeaders[lowercase(name)] || null +          : rawRequest.getResponseHeader(name); +      } + +      parsedHeaders = parsedHeaders || parseHeaders(rawRequest.getAllResponseHeaders()); + +      return parsedHeaders; +    } + +    /** +     * Retry the request +     * +     * @param {Object=} config Optional config object to extend the original configuration +     * @returns {XhrFuture} +     */ +    this.retry = function(config) { +      if (rawRequest) throw 'Can not retry request. Abort pending request first.'; + +      extend(cfg, config); +      cfg.method = uppercase(cfg.method); + +      var data = transform(cfg.data, cfg.transformRequest || $config.transformRequest), +          headers = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, +                           defHeaders.common, defHeaders[lowercase(cfg.method)], cfg.headers); + +      var fromCache; +      if (cfg.cache && cfg.method == 'GET' && (fromCache = cache.get(cfg.url))) { +        $browser.defer(function() { +          parsedHeaders = fromCache[2]; +          fireCallbacks(fromCache[1], fromCache[0]); +        }); +      } else { +        rawRequest = $browser.xhr(cfg.method, cfg.url, data, done, headers, cfg.timeout); +      } + +      pendingRequestsCount++; +      return this; +    }; + +    /** +     * Abort the request +     */ +    this.abort = function() { +      if (rawRequest) { +        rawRequest.abort(); +      } +      return this; +    }; + +    /** +     * Register a callback function based on status code +     * Note: all matched callbacks will be called, preserving registered order ! +     * +     * Internal statuses: +     *  `-2` = jsonp error +     *  `-1` = timeout +     *   `0` = aborted +     * +     * @example +     *   .on('2xx', function(){}); +     *   .on('2x1', function(){}); +     *   .on('404', function(){}); +     *   .on('xxx', function(){}); +     *   .on('20x,3xx', function(){}); +     *   .on('success', function(){}); +     *   .on('error', function(){}); +     *   .on('always', function(){}); +     * +     * @param {string} pattern Status code pattern with "x" for any number +     * @param {function(*, number, Object)} callback Function to be called when response arrives +     * @returns {XhrFuture} +     */ +    this.on = function(pattern, callback) { +      var alias = { +        success: '2xx', +        error: '0-2,0-1,000,4xx,5xx', +        always: 'xxx', +        timeout: '0-1', +        abort: '000' +      }; + +      callbacks.push(alias[pattern] || pattern); +      callbacks.push(callback); + +      return this; +    }; +  } +}]; +} +  | 
