| Index: remoting/webapp/crd/js/xhr.js
|
| diff --git a/remoting/webapp/crd/js/xhr.js b/remoting/webapp/crd/js/xhr.js
|
| index 0529838ddfa88730b521a4e5d457d3546d98ca5c..cdefb0aa4d81d45d2dee8763a2d4f19c3404c651 100644
|
| --- a/remoting/webapp/crd/js/xhr.js
|
| +++ b/remoting/webapp/crd/js/xhr.js
|
| @@ -4,7 +4,7 @@
|
|
|
| /**
|
| * @fileoverview
|
| - * Simple utilities for making XHRs more pleasant.
|
| + * Utility class for making XHRs more pleasant.
|
| */
|
|
|
| 'use strict';
|
| @@ -12,26 +12,83 @@
|
| /** @suppress {duplicate} */
|
| var remoting = remoting || {};
|
|
|
| -/** Namespace for XHR functions */
|
| -/** @type {Object} */
|
| -remoting.xhr = remoting.xhr || {};
|
| -
|
| /**
|
| - * Takes an associative array of parameters and urlencodes it.
|
| - *
|
| - * @param {Object<string,string>} paramHash The parameter key/value pairs.
|
| - * @return {string} URLEncoded version of paramHash.
|
| + * @constructor
|
| + * @param {remoting.Xhr.Params} params
|
| */
|
| -remoting.xhr.urlencodeParamHash = function(paramHash) {
|
| - var paramArray = [];
|
| - for (var key in paramHash) {
|
| - paramArray.push(encodeURIComponent(key) +
|
| - '=' + encodeURIComponent(paramHash[key]));
|
| +remoting.Xhr = function(params) {
|
| + /** @private @const {!XMLHttpRequest} */
|
| + this.nativeXhr_ = new XMLHttpRequest();
|
| + this.nativeXhr_.onreadystatechange = this.onReadyStateChange_.bind(this);
|
| + this.nativeXhr_.withCredentials = params.withCredentials || false;
|
| +
|
| + /** @private @const */
|
| + this.responseType_ = params.responseType || remoting.Xhr.ResponseType.TEXT;
|
| +
|
| + // Apply URL parameters.
|
| + var url = params.url;
|
| + var parameterString = '';
|
| + if (typeof(params.urlParams) === 'string') {
|
| + parameterString = params.urlParams;
|
| + } else if (typeof(params.urlParams) === 'object') {
|
| + parameterString = remoting.Xhr.urlencodeParamHash(
|
| + remoting.Xhr.removeNullFields_(params.urlParams));
|
| }
|
| - if (paramArray.length > 0) {
|
| - return paramArray.join('&');
|
| + if (parameterString) {
|
| + base.debug.assert(url.indexOf('?') == -1);
|
| + url += '?' + parameterString;
|
| }
|
| - return '';
|
| +
|
| + // Check that the content spec is consistent.
|
| + if ((Number(params.textContent !== undefined) +
|
| + Number(params.formContent !== undefined) +
|
| + Number(params.jsonContent !== undefined)) > 1) {
|
| + throw new Error(
|
| + 'may only specify one of textContent, formContent, and jsonContent');
|
| + }
|
| +
|
| + // Prepare the build modified headers.
|
| + var headers = remoting.Xhr.removeNullFields_(params.headers);
|
| +
|
| + // Convert the content fields to a single text content variable.
|
| + /** @private {?string} */
|
| + this.content_ = null;
|
| + if (params.textContent !== undefined) {
|
| + this.content_ = params.textContent;
|
| + } else if (params.formContent !== undefined) {
|
| + if (!('Content-type' in headers)) {
|
| + headers['Content-type'] = 'application/x-www-form-urlencoded';
|
| + }
|
| + this.content_ = remoting.Xhr.urlencodeParamHash(params.formContent);
|
| + } else if (params.jsonContent !== undefined) {
|
| + if (!('Content-type' in headers)) {
|
| + headers['Content-type'] = 'application/json; charset=UTF-8';
|
| + }
|
| + this.content_ = JSON.stringify(params.jsonContent);
|
| + }
|
| +
|
| + // Apply the oauthToken field.
|
| + if (params.oauthToken !== undefined) {
|
| + base.debug.assert(!('Authorization' in headers));
|
| + headers['Authorization'] = 'Bearer ' + params.oauthToken;
|
| + }
|
| +
|
| + this.nativeXhr_.open(params.method, url, true);
|
| + for (var key in headers) {
|
| + this.nativeXhr_.setRequestHeader(key, headers[key]);
|
| + }
|
| +
|
| + /** @private {base.Deferred<!remoting.Xhr.Response>} */
|
| + this.deferred_ = null;
|
| +};
|
| +
|
| +/**
|
| + * @enum {string}
|
| + */
|
| +remoting.Xhr.ResponseType = {
|
| + TEXT: 'TEXT', // Request a plain text response (default).
|
| + JSON: 'JSON', // Request a JSON response.
|
| + NONE: 'NONE' // Don't request any response.
|
| };
|
|
|
| /**
|
| @@ -41,8 +98,6 @@ remoting.xhr.urlencodeParamHash = function(paramHash) {
|
| *
|
| * url: The URL to request.
|
| *
|
| - * onDone: Function to call when the XHR finishes.
|
| -
|
| * urlParams: (optional) Parameters to be appended to the URL.
|
| * Null-valued parameters are omitted.
|
| *
|
| @@ -64,20 +119,118 @@ remoting.xhr.urlencodeParamHash = function(paramHash) {
|
| * oauthToken: (optional) An OAuth2 token used to construct an
|
| * Authentication header.
|
| *
|
| + * responseType: (optional) Request a response of a specific
|
| + * type. Default: TEXT.
|
| + *
|
| * @typedef {{
|
| * method: string,
|
| * url:string,
|
| - * onDone:(function(XMLHttpRequest):void),
|
| * urlParams:(string|Object<string,?string>|undefined),
|
| * textContent:(string|undefined),
|
| * formContent:(Object|undefined),
|
| * jsonContent:(*|undefined),
|
| * headers:(Object<string,?string>|undefined),
|
| * withCredentials:(boolean|undefined),
|
| - * oauthToken:(string|undefined)
|
| + * oauthToken:(string|undefined),
|
| + * responseType:(remoting.Xhr.ResponseType|undefined)
|
| * }}
|
| */
|
| -remoting.XhrParams;
|
| +remoting.Xhr.Params;
|
| +
|
| +/**
|
| + * Aborts the HTTP request. Does nothing is the request has finished
|
| + * already.
|
| + */
|
| +remoting.Xhr.prototype.abort = function() {
|
| + this.nativeXhr_.abort();
|
| +};
|
| +
|
| +/**
|
| + * Starts and HTTP request and gets a promise that is resolved when
|
| + * the request completes.
|
| + *
|
| + * Any error that prevents receiving an HTTP status
|
| + * code causes this promise to be rejected.
|
| + *
|
| + * NOTE: Calling this method more than once will return the same
|
| + * promise and not start a new request, despite what the name
|
| + * suggests.
|
| + *
|
| + * @return {!Promise<!remoting.Xhr.Response>}
|
| + */
|
| +remoting.Xhr.prototype.start = function() {
|
| + if (this.deferred_ == null) {
|
| + var xhr = this.nativeXhr_;
|
| + xhr.send(this.content_);
|
| + this.content_ = null; // for gc
|
| + this.deferred_ = new base.Deferred();
|
| + }
|
| + return this.deferred_.promise();
|
| +};
|
| +
|
| +/**
|
| + * @private
|
| + */
|
| +remoting.Xhr.prototype.onReadyStateChange_ = function() {
|
| + var xhr = this.nativeXhr_;
|
| + if (xhr.readyState == 4) {
|
| + // See comments at remoting.Xhr.Response.
|
| + this.deferred_.resolve(new remoting.Xhr.Response(xhr, this.responseType_));
|
| + }
|
| +};
|
| +
|
| +/**
|
| + * The response-related parts of an XMLHttpRequest. Note that this
|
| + * class is not just a facade for XMLHttpRequest; it saves the value
|
| + * of the |responseText| field becuase once onReadyStateChange_
|
| + * (above) returns, the value of |responseText| is reset to the empty
|
| + * string! This is a documented anti-feature of the XMLHttpRequest
|
| + * API.
|
| + *
|
| + * @constructor
|
| + * @param {!XMLHttpRequest} xhr
|
| + * @param {remoting.Xhr.ResponseType} type
|
| + */
|
| +remoting.Xhr.Response = function(xhr, type) {
|
| + /** @private @const */
|
| + this.type_ = type;
|
| +
|
| + /**
|
| + * The HTTP status code.
|
| + * @const {number}
|
| + */
|
| + this.status = xhr.status;
|
| +
|
| + /**
|
| + * The HTTP status description.
|
| + * @const {string}
|
| + */
|
| + this.statusText = xhr.statusText;
|
| +
|
| + /**
|
| + * The response URL, if any.
|
| + * @const {?string}
|
| + */
|
| + this.url = xhr.responseURL;
|
| +
|
| + /** @private {string} */
|
| + this.text_ = xhr.responseText || '';
|
| +};
|
| +
|
| +/**
|
| + * @return {string} The text content of the response.
|
| + */
|
| +remoting.Xhr.Response.prototype.getText = function() {
|
| + return this.text_;
|
| +};
|
| +
|
| +/**
|
| + * @return {*} The parsed JSON content of the response.
|
| + */
|
| +remoting.Xhr.Response.prototype.getJson = function() {
|
| + base.debug.assert(this.type_ == remoting.Xhr.ResponseType.JSON);
|
| + return JSON.parse(this.text_);
|
| +};
|
|
|
| /**
|
| * Returns a copy of the input object with all null or undefined
|
| @@ -87,7 +240,7 @@ remoting.XhrParams;
|
| * @return {!Object<string,string>}
|
| * @private
|
| */
|
| -remoting.xhr.removeNullFields_ = function(input) {
|
| +remoting.Xhr.removeNullFields_ = function(input) {
|
| /** @type {!Object<string,string>} */
|
| var result = {};
|
| if (input) {
|
| @@ -102,112 +255,38 @@ remoting.xhr.removeNullFields_ = function(input) {
|
| };
|
|
|
| /**
|
| - * Executes an arbitrary HTTP method asynchronously.
|
| + * Takes an associative array of parameters and urlencodes it.
|
| *
|
| - * @param {remoting.XhrParams} params
|
| - * @return {XMLHttpRequest} The XMLHttpRequest object.
|
| + * @param {Object<string,string>} paramHash The parameter key/value pairs.
|
| + * @return {string} URLEncoded version of paramHash.
|
| */
|
| -remoting.xhr.start = function(params) {
|
| - // Extract fields that can be used more or less as-is.
|
| - var method = params.method;
|
| - var url = params.url;
|
| - var onDone = params.onDone;
|
| - var headers = remoting.xhr.removeNullFields_(params.headers);
|
| - var withCredentials = params.withCredentials || false;
|
| -
|
| - // Apply URL parameters.
|
| - var parameterString = '';
|
| - if (typeof(params.urlParams) === 'string') {
|
| - parameterString = params.urlParams;
|
| - } else if (typeof(params.urlParams) === 'object') {
|
| - parameterString = remoting.xhr.urlencodeParamHash(
|
| - remoting.xhr.removeNullFields_(params.urlParams));
|
| - }
|
| - if (parameterString) {
|
| - base.debug.assert(url.indexOf('?') == -1);
|
| - url += '?' + parameterString;
|
| - }
|
| -
|
| - // Check that the content spec is consistent.
|
| - if ((Number(params.textContent !== undefined) +
|
| - Number(params.formContent !== undefined) +
|
| - Number(params.jsonContent !== undefined)) > 1) {
|
| - throw new Error(
|
| - 'may only specify one of textContent, formContent, and jsonContent');
|
| - }
|
| -
|
| - // Convert the content fields to a single text content variable.
|
| - /** @type {?string} */
|
| - var content = null;
|
| - if (params.textContent !== undefined) {
|
| - content = params.textContent;
|
| - } else if (params.formContent !== undefined) {
|
| - if (!('Content-type' in headers)) {
|
| - headers['Content-type'] = 'application/x-www-form-urlencoded';
|
| - }
|
| - content = remoting.xhr.urlencodeParamHash(params.formContent);
|
| - } else if (params.jsonContent !== undefined) {
|
| - if (!('Content-type' in headers)) {
|
| - headers['Content-type'] = 'application/json; charset=UTF-8';
|
| - }
|
| - content = JSON.stringify(params.jsonContent);
|
| - }
|
| -
|
| - // Apply the oauthToken field.
|
| - if (params.oauthToken !== undefined) {
|
| - base.debug.assert(!('Authorization' in headers));
|
| - headers['Authorization'] = 'Bearer ' + params.oauthToken;
|
| +remoting.Xhr.urlencodeParamHash = function(paramHash) {
|
| + var paramArray = [];
|
| + for (var key in paramHash) {
|
| + paramArray.push(encodeURIComponent(key) +
|
| + '=' + encodeURIComponent(paramHash[key]));
|
| }
|
| -
|
| - return remoting.xhr.startInternal_(
|
| - method, url, onDone, content, headers, withCredentials);
|
| -};
|
| -
|
| -/**
|
| - * Executes an arbitrary HTTP method asynchronously.
|
| - *
|
| - * @param {string} method
|
| - * @param {string} url
|
| - * @param {function(XMLHttpRequest):void} onDone
|
| - * @param {?string} content
|
| - * @param {!Object<string,string>} headers
|
| - * @param {boolean} withCredentials
|
| - * @return {XMLHttpRequest} The XMLHttpRequest object.
|
| - * @private
|
| - */
|
| -remoting.xhr.startInternal_ = function(
|
| - method, url, onDone, content, headers, withCredentials) {
|
| - /** @type {XMLHttpRequest} */
|
| - var xhr = new XMLHttpRequest();
|
| - xhr.onreadystatechange = function() {
|
| - if (xhr.readyState != 4) {
|
| - return;
|
| - }
|
| - onDone(xhr);
|
| - };
|
| -
|
| - xhr.open(method, url, true);
|
| - for (var key in headers) {
|
| - xhr.setRequestHeader(key, headers[key]);
|
| + if (paramArray.length > 0) {
|
| + return paramArray.join('&');
|
| }
|
| - xhr.withCredentials = withCredentials;
|
| - xhr.send(content);
|
| - return xhr;
|
| + return '';
|
| };
|
|
|
| /**
|
| * Generic success/failure response proxy.
|
| *
|
| + * TODO(jrw): Stop using this and move default error handling directly
|
| + * into Xhr class.
|
| + *
|
| * @param {function():void} onDone
|
| * @param {function(!remoting.Error):void} onError
|
| * @param {Array<remoting.Error.Tag>=} opt_ignoreErrors
|
| - * @return {function(XMLHttpRequest):void}
|
| + * @return {function(!remoting.Xhr.Response):void}
|
| */
|
| -remoting.xhr.defaultResponse = function(onDone, onError, opt_ignoreErrors) {
|
| - /** @param {XMLHttpRequest} xhr */
|
| - var result = function(xhr) {
|
| - var error =
|
| - remoting.Error.fromHttpStatus(/** @type {number} */ (xhr.status));
|
| +remoting.Xhr.defaultResponse = function(onDone, onError, opt_ignoreErrors) {
|
| + /** @param {!remoting.Xhr.Response} response */
|
| + var result = function(response) {
|
| + var error = remoting.Error.fromHttpStatus(response.status);
|
| if (error.isNone()) {
|
| onDone();
|
| return;
|
|
|