1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-23 22:10:55 +01:00
phorge-phorge/webroot/rsrc/externals/javelin/lib/Request.js
epriestley 2b4c551b0e Provide a global router for Ajax requests
Summary:
Fixes T430. Fixes T4834. Obsoletes D7641. Currently, we do some things less-well than we could:

  - We just let the browser queue and prioritize requests, so if you load a revision with 50 changes and then click "Award Token", the action blocks until the changes load in most/all browsers. It would be better to prioritize this action and queue it immediately.
  - Similarly, changes tend to load in order, even if the user has clicked to a specific file. When the user expresses a preference for a specific file, we should prioritize it.
  - We show a spinning GIF when waiting on requests. This is appropriate for some types of reuqests, but distracting for others.

To fix this:

  - Queue all (or, at least, most) requests into a new queue in JX.Router.
  - JX.Router handles prioritizing the requests. Principally:
    - You can submit a request with a specific priority (500 = general content loading, 1000 = default, 2000 = explicit user action) and JX.Router will get the higher stuff fired off sooner.
    - You can name requests and then adjust their prorities later, if the user expresses an interest in specific results.
  - Only use the spinner gif for "workflow" requests, which is bascially when the user clicked something and we're waiting on the server. I think it's useful and not-annoying in this case.
  - Don't show any status for draft requests.
  - For content requests, show a subtle hipster-style top loading bar.

Test Plan:
  - Viewed a diff with 93 changes, and clicked award token.
    - Prior to this patch, the action took many many seconds to resolve.
    - After this patch, it resolves quickly.
  - Viewed a diff with 93 changes and saw a pleasant subtle hipster-style loading bar.
  - Viewed a diff with 93 changes and typed some draft text. Previews populated fairly quickly and there was no spinner.
  - Viewed a diff with 93 changes and clicked something with workflow, saw a spinner after a moment.
  - Viewed a diff with 93 changes and clicked a file in the table of contents near the end of the list.
    - Prior to this patch, it took a long time to show up.
    - After this patch, it loads directly.

Reviewers: chad, btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T430, T4834

Differential Revision: https://secure.phabricator.com/D8979
2014-05-05 10:57:42 -07:00

484 lines
13 KiB
JavaScript

/**
* @requires javelin-install
* javelin-stratcom
* javelin-util
* javelin-behavior
* javelin-json
* javelin-dom
* javelin-resource
* javelin-routable
* @provides javelin-request
* @javelin
*/
/**
* Make basic AJAX XMLHTTPRequests.
*
* @group workflow
*/
JX.install('Request', {
construct : function(uri, handler) {
this.setURI(uri);
if (handler) {
this.listen('done', handler);
}
},
events : ['start', 'open', 'send', 'statechange', 'done', 'error', 'finally',
'uploadprogress'],
members : {
_xhrkey : null,
_transport : null,
_sent : false,
_finished : false,
_block : null,
_data : null,
_getSameOriginTransport : function() {
try {
try {
return new XMLHttpRequest();
} catch (x) {
return new ActiveXObject("Msxml2.XMLHTTP");
}
} catch (x) {
return new ActiveXObject("Microsoft.XMLHTTP");
}
},
_getCORSTransport : function() {
try {
var xport = new XMLHttpRequest();
if ('withCredentials' in xport) {
// XHR supports CORS
} else if (typeof XDomainRequest != 'undefined') {
xport = new XDomainRequest();
}
return xport;
} catch (x) {
return new XDomainRequest();
}
},
getTransport : function() {
if (!this._transport) {
this._transport = this.getCORS() ? this._getCORSTransport() :
this._getSameOriginTransport();
}
return this._transport;
},
getRoutable: function() {
var routable = new JX.Routable();
routable.listen('start', JX.bind(this, function() {
// Pass the event to allow other listeners to "start" to configure this
// request before it fires.
JX.Stratcom.pass(JX.Stratcom.context());
this.send();
}));
this.listen('finally', JX.bind(routable, routable.done));
return routable;
},
send : function() {
if (this._sent || this._finished) {
if (__DEV__) {
if (this._sent) {
JX.$E(
'JX.Request.send(): ' +
'attempting to send a Request that has already been sent.');
}
if (this._finished) {
JX.$E(
'JX.Request.send(): ' +
'attempting to send a Request that has finished or aborted.');
}
}
return;
}
// Fire the "start" event before doing anything. A listener may
// perform pre-processing or validation on this request
this.invoke('start', this);
if (this._finished) {
return;
}
var xport = this.getTransport();
xport.onreadystatechange = JX.bind(this, this._onreadystatechange);
if (xport.upload) {
xport.upload.onprogress = JX.bind(this, this._onuploadprogress);
}
var method = this.getMethod().toUpperCase();
if (__DEV__) {
if (this.getRawData()) {
if (method != 'POST') {
JX.$E(
'JX.Request.send(): ' +
'attempting to send post data over GET. You must use POST.');
}
}
}
var list_of_pairs = this._data || [];
list_of_pairs.push(['__ajax__', true]);
this._block = JX.Stratcom.allocateMetadataBlock();
list_of_pairs.push(['__metablock__', this._block]);
var q = (this.getDataSerializer() ||
JX.Request.defaultDataSerializer)(list_of_pairs);
var uri = this.getURI();
// If we're sending a file, submit the metadata via the URI instead of
// via the request body, because the entire post body will be consumed by
// the file content.
if (method == 'GET' || this.getRawData()) {
uri += ((uri.indexOf('?') === -1) ? '?' : '&') + q;
}
if (this.getTimeout()) {
this._timer = setTimeout(
JX.bind(
this,
this._fail,
JX.Request.ERROR_TIMEOUT),
this.getTimeout());
}
xport.open(method, uri, true);
// Must happen after xport.open so that listeners can modify the transport
// Some transport properties can only be set after the transport is open
this.invoke('open', this);
if (this._finished) {
return;
}
this.invoke('send', this);
if (this._finished) {
return;
}
if (method == 'POST') {
if (this.getRawData()) {
xport.send(this.getRawData());
} else {
xport.setRequestHeader(
'Content-Type',
'application/x-www-form-urlencoded');
xport.send(q);
}
} else {
xport.send(null);
}
this._sent = true;
},
abort : function() {
this._cleanup();
},
_onuploadprogress : function(progress) {
this.invoke('uploadprogress', progress);
},
_onreadystatechange : function() {
var xport = this.getTransport();
var response;
try {
this.invoke('statechange', this);
if (this._finished) {
return;
}
if (xport.readyState != 4) {
return;
}
// XHR requests to 'file:///' domains return 0 for success, which is why
// we treat it as a good result in addition to HTTP 2XX responses.
if (xport.status !== 0 && (xport.status < 200 || xport.status >= 300)) {
this._fail();
return;
}
if (__DEV__) {
var expect_guard = this.getExpectCSRFGuard();
if (!xport.responseText.length) {
JX.$E(
'JX.Request("'+this.getURI()+'", ...): '+
'server returned an empty response.');
}
if (expect_guard && xport.responseText.indexOf('for (;;);') !== 0) {
JX.$E(
'JX.Request("'+this.getURI()+'", ...): '+
'server returned an invalid response.');
}
if (expect_guard && xport.responseText == 'for (;;);') {
JX.$E(
'JX.Request("'+this.getURI()+'", ...): '+
'server returned an empty response.');
}
}
response = this._extractResponse(xport);
if (!response) {
JX.$E(
'JX.Request("'+this.getURI()+'", ...): '+
'server returned an invalid response.');
}
} catch (exception) {
if (__DEV__) {
JX.log(
'JX.Request("'+this.getURI()+'", ...): '+
'caught exception processing response: '+exception);
}
this._fail();
return;
}
try {
this._handleResponse(response);
this._cleanup();
} catch (exception) {
// In Firefox+Firebug, at least, something eats these. :/
setTimeout(function() {
throw exception;
}, 0);
}
},
_extractResponse : function(xport) {
var text = xport.responseText;
if (this.getExpectCSRFGuard()) {
text = text.substring('for (;;);'.length);
}
var type = this.getResponseType().toUpperCase();
if (type == 'TEXT') {
return text;
} else if (type == 'JSON' || type == 'JAVELIN') {
return JX.JSON.parse(text);
} else if (type == 'XML') {
var doc;
try {
if (typeof DOMParser != 'undefined') {
var parser = new DOMParser();
doc = parser.parseFromString(text, "text/xml");
} else { // IE
// an XDomainRequest
doc = new ActiveXObject("Microsoft.XMLDOM");
doc.async = false;
doc.loadXML(xport.responseText);
}
return doc.documentElement;
} catch (exception) {
if (__DEV__) {
JX.log(
'JX.Request("'+this.getURI()+'", ...): '+
'caught exception extracting response: '+exception);
}
this._fail();
return null;
}
}
if (__DEV__) {
JX.$E(
'JX.Request("'+this.getURI()+'", ...): '+
'unrecognized response type.');
}
return null;
},
_fail : function(error) {
this._cleanup();
this.invoke('error', error, this);
this.invoke('finally');
},
_done : function(response) {
this._cleanup();
if (response.onload) {
for (var ii = 0; ii < response.onload.length; ii++) {
(new Function(response.onload[ii]))();
}
}
var payload;
if (this.getRaw()) {
payload = response;
} else {
payload = response.payload;
JX.Request._parseResponsePayload(payload);
}
this.invoke('done', payload, this);
this.invoke('finally');
},
_cleanup : function() {
this._finished = true;
clearTimeout(this._timer);
this._timer = null;
// Should not abort the transport request if it has already completed
// Otherwise, we may see an "HTTP request aborted" error in the console
// despite it possibly having succeeded.
if (this._transport && this._transport.readyState != 4) {
this._transport.abort();
}
},
setData : function(dictionary) {
this._data = null;
this.addData(dictionary);
return this;
},
addData : function(dictionary) {
if (!this._data) {
this._data = [];
}
for (var k in dictionary) {
this._data.push([k, dictionary[k]]);
}
return this;
},
setDataWithListOfPairs : function(list_of_pairs) {
this._data = list_of_pairs;
return this;
},
_handleResponse : function(response) {
if (this.getResponseType().toUpperCase() == 'JAVELIN') {
if (response.error) {
this._fail(response.error);
} else {
JX.Stratcom.mergeData(
this._block,
response.javelin_metadata || {});
var when_complete = JX.bind(this, function() {
this._done(response);
JX.initBehaviors(response.javelin_behaviors || {});
});
if (response.javelin_resources) {
JX.Resource.load(response.javelin_resources, when_complete);
} else {
when_complete();
}
}
} else {
this._cleanup();
this.invoke('done', response, this);
this.invoke('finally');
}
}
},
statics : {
ERROR_TIMEOUT : -9000,
defaultDataSerializer : function(list_of_pairs) {
var uri = [];
for (var ii = 0; ii < list_of_pairs.length; ii++) {
var pair = list_of_pairs[ii];
var name = encodeURIComponent(pair[0]);
var value = encodeURIComponent(pair[1]);
uri.push(name + '=' + value);
}
return uri.join('&');
},
/**
* When we receive a JSON blob, parse it to introduce meaningful objects
* where there are magic keys for placeholders.
*
* Objects with the magic key '__html' are translated into JX.HTML objects.
*
* This function destructively modifies its input.
*/
_parseResponsePayload: function(parent, index) {
var recurse = JX.Request._parseResponsePayload;
var obj = (typeof index !== 'undefined') ? parent[index] : parent;
if (JX.isArray(obj)) {
for (var ii = 0; ii < obj.length; ii++) {
recurse(obj, ii);
}
} else if (obj && typeof obj == 'object') {
if (('__html' in obj) && (obj.__html !== null)) {
parent[index] = JX.$H(obj.__html);
} else {
for (var key in obj) {
recurse(obj, key);
}
}
}
}
},
properties : {
URI : null,
dataSerializer : null,
/**
* Configure which HTTP method to use for the request. Permissible values
* are "POST" (default) or "GET".
*
* @param string HTTP method, one of "POST" or "GET".
*/
method : 'POST',
/**
* Set the data parameter of transport.send. Useful if you want to send a
* file or FormData. Not that you cannot send raw data and data at the same
* time.
*
* @param Data, argument to transport.send
*/
rawData: null,
raw : false,
/**
* Configure a timeout, in milliseconds. If the request has not resolved
* (either with success or with an error) within the provided timeframe,
* it will automatically fail with error JX.Request.ERROR_TIMEOUT.
*
* @param int Timeout, in milliseconds (e.g. 3000 = 3 seconds).
*/
timeout : null,
/**
* Whether or not we should expect the CSRF guard in the response.
*
* @param bool
*/
expectCSRFGuard : true,
/**
* Whether it should be a CORS (Cross-Origin Resource Sharing) request to
* a third party domain other than the current site.
*
* @param bool
*/
CORS : false,
/**
* Type of the response.
*
* @param enum 'JAVELIN', 'JSON', 'XML', 'TEXT'
*/
responseType : 'JAVELIN'
}
});