Home Identifier Source Repository

src/ajax.js

import Rx from "rx";

/**
 *
 * Derived from RxJS-DOM, Copyright (c) Microsoft Open Technologies, Inc:
 *
 * https://github.com/Reactive-Extensions/RxJS-DOM
 *
 * The original source file can be viewed here:
 *
 * https://github.com/Reactive-Extensions/RxJS-DOM/blob/fdb169c8bd1612318d530e6e54074b1c9e537906/src/ajax.js
 *
 * Licensed under the Apache License, Version 2.0:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Modifications from original:
 *
 * - extracted from "Rx.DOM" namespace
 * - minor eslinter cleanup ("var" to "let" etc)
 * - addition of "auto" responseType
 *
 */

var root = (typeof window !== "undefined" && window) || this;

// Gets the proper XMLHttpRequest for support for older IE
function getXMLHttpRequest() {
  if (root.XMLHttpRequest) {
    return new root.XMLHttpRequest();
  } else {
    let progId;
    try {
      let progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'];
      for(let i = 0; i < 3; i++) {
        try {
          progId = progIds[i];
          if (new root.ActiveXObject(progId)) {
            break;
          }
        } catch(e) { }
      }
      return new root.ActiveXObject(progId);
    } catch (e) {
      throw new Error('XMLHttpRequest is not supported by your browser');
    }
  }
}

// Get CORS support even for older IE
function getCORSRequest() {
  var xhr = new root.XMLHttpRequest();
  if ('withCredentials' in xhr) {
    return xhr;
  } else if (!!root.XDomainRequest) {
    return new XDomainRequest();
  } else {
    throw new Error('CORS is not supported by your browser');
  }
}

function normalizeAjaxSuccessEvent(e, xhr, settings) {
  var response = ('response' in xhr) ? xhr.response : xhr.responseText;
  if (settings.responseType === 'auto') {
    try {
      response = JSON.parse(response);
    } catch (e) {}
  } else {
    response = settings.responseType === 'json' ? JSON.parse(response) : response;
  }
  return {
    response: response,
    status: xhr.status,
    responseType: xhr.responseType,
    xhr: xhr,
    originalEvent: e
  };
}

function normalizeAjaxErrorEvent(e, xhr, type) {
  return {
    type: type,
    status: xhr.status,
    xhr: xhr,
    originalEvent: e
  };
}

/**
 * Creates an observable for an Ajax request with either a settings object with url, headers, etc or a string for a URL.
 *
 * @example
 *   source = ajax('/products');
 *   source = ajax({ url: 'products', method: 'GET' });
 *
 * @param {Object} settings Can be one of the following:
 *
 *  A string of the URL to make the Ajax call.
 *  An object with the following properties
 *   - url: URL of the request
 *   - body: The body of the request
 *   - method: Method of the request, such as GET, POST, PUT, PATCH, DELETE
 *   - async: Whether the request is async
 *   - headers: Optional headers
 *   - crossDomain: true if a cross domain request, else false
 *   - responseType: "text" (default), "json" or "auto"
 *
 * @returns {Observable} An observable sequence containing the XMLHttpRequest.
*/
export default function (options) {
  var settings = {
    method: 'GET',
    crossDomain: false,
    async: true,
    headers: {},
    responseType: 'text',
    createXHR: function(){
      return this.crossDomain ? getCORSRequest() : getXMLHttpRequest();
    },
    normalizeError: normalizeAjaxErrorEvent,
    normalizeSuccess: normalizeAjaxSuccessEvent
  };

  if(typeof options === 'string') {
    settings.url = options;
  } else {
    for(let prop in options) {
      if(hasOwnProperty.call(options, prop)) {
        settings[prop] = options[prop];
      }
    }
  }

  let normalizeError = settings.normalizeError;
  let normalizeSuccess = settings.normalizeSuccess;

  if (!settings.crossDomain && !settings.headers['X-Requested-With']) {
    settings.headers['X-Requested-With'] = 'XMLHttpRequest';
  }
  settings.hasContent = settings.body !== undefined;

  return new Rx.AnonymousObservable(function (observer) {
    var isDone = false;
    var xhr;

    var processResponse = function(xhr, e){
      var status = xhr.status === 1223 ? 204 : xhr.status;
      if ((status >= 200 && status <= 300) || status === 0 || status === '') {
        observer.onNext(normalizeSuccess(e, xhr, settings));
        observer.onCompleted();
      } else {
        observer.onError(normalizeError(e, xhr, 'error'));
      }
      isDone = true;
    };

    try {
      xhr = settings.createXHR();;
    } catch (err) {
      observer.onError(err);
    }

    try {
      if (settings.user) {
        xhr.open(settings.method, settings.url, settings.async, settings.user, settings.password);
      } else {
        xhr.open(settings.method, settings.url, settings.async);
      }

      let headers = settings.headers;
      for (let header in headers) {
        if (hasOwnProperty.call(headers, header)) {
          xhr.setRequestHeader(header, headers[header]);
        }
      }

      if(!!xhr.upload || (!('withCredentials' in xhr) && !!root.XDomainRequest)) {
        xhr.onload = function(e) {
          if(settings.progressObserver) {
            settings.progressObserver.onNext(e);
            settings.progressObserver.onCompleted();
          }
          processResponse(xhr, e);
        };

        if(settings.progressObserver) {
          xhr.onprogress = function(e) {
            settings.progressObserver.onNext(e);
          };
        }

        xhr.onerror = function(e) {
          if (settings.progressObserver) {
            settings.progressObserver.onError(e);
          }
          observer.onError(normalizeError(e, xhr, 'error'));
          isDone = true;
        };

        xhr.onabort = function(e) {
          if (settings.progressObserver) {
            settings.progressObserver.onError(e);
          }
          observer.onError(normalizeError(e, xhr, 'abort'));
          isDone = true;
        };
      } else {

        xhr.onreadystatechange = function (e) {
          if (xhr.readyState === 4) {
            processResponse(xhr, e);
          }
        };
      }

      xhr.send(settings.hasContent && settings.body || null);
    } catch (e) {
      observer.onError(e);
    }

    return function () {
      if (!isDone && xhr.readyState !== 4) { xhr.abort(); }
    };
  });
};