The Lonely RequestIn the programming model of a client receiving data from an HTTP source is the always present request and response model. This stays true when employing the technique of Ajax as well but just in typically smaller packages. There are instances where the client simply won't receive a response. In a typical HTTP request the browser will timeout and the user will be notified of the error and can take action to try again or go elsewhere. When using an XHR, the heart and soul of Ajax, the request can linger indefinitey without proper precautions. The XHR does have an abort method but has no internal timeout functionality. In this article we're going to take a look at the Ajax.Request class in prototype.js library and look at what would be necessary to implement timeout functionality via refactoring the Ajax.Request class. The Timeout ModelThe idea would be to allow another configuration property to set the timeout in a number of seconds, and debatebly another callback specifically for onTimeout. This is where the clarity of the functionality begins to cloud. If the request has timed out then has it not in fact failed, so should onTimeout and onFailure events both be fired? The alternative being that if the timeout duration has been exceeded the transport should be aborted and the onFailure method should be executed. This difference in behavior seems subtle but I believe it should be decided wisely. I am in favor of simply executing the onFailure method and setting a property of the object to indicate that it has timed out. in that approach we're able to maintain explicit functionality for the timeout case while avoiding bloat in the options argument. Failure with TimeoutThis approach would fire both events. The onFailure and onTimeout, I believe this approach could become sticky as they're very similar cases. If both execute there could be a lot of redunandant action as your onFailure method tries to clean up after a failed XHR. While the onTimeout will be doing more or less the same thing with perhaps a variation of notification to the user. Just a FailureI believe this approach to be the cleanest, to simply execute the onFailure event to handle the network failure and clean up the application as necessary. Although it is the minimalist approach it could quickly bloat when implementing specialized procedures for the onTimeout case. Timeout AloneKeeping it clean yet giving the timeout case a designated event. This certainly keeps things clean and specific but could be a bit of a nuisance when trying to re-use code from you onFailure handler. One More MethodI also think that there should be a convienence method for the Ajax.Response class. It is an ecapsulation class that wraps the Ajax.Request and is sent to the event handling methods such as onComplete etc. This class could do well to have a "hasTimedOut" method which would allow for a handy method at the top level for programmers using prototype that havn't navigated the depths of the Ajax mechanisms.
The CodeDifferences varying from the prototype 1.6 release are noted in red text.
Ajax.Request = Class.create(Ajax.Base, {
_complete: false,
timeout: false,
initialize: function($super, url, options) {
$super(options);
this.transport = Ajax.getTransport();
this.request(url);
},
request: function(url) {
this.url = url;
this.method = this.options.method;
var params = Object.clone(this.options.parameters);
if (!['get', 'post'].include(this.method)) {
// simulate other verbs over post
params['_method'] = this.method;
this.method = 'post';
}
this.parameters = params;
if (params = Object.toQueryString(params)) {
// when GET, append parameters to URL
if (this.method == 'get')
this.url += (this.url.include('?') ? '&' : '?') + params;
else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
params += '&_=';
}
try {
var response = new Ajax.Response(this);
if (this.options.onCreate) this.options.onCreate(response);
Ajax.Responders.dispatch('onCreate', this, response);
this.transport.open(this.method.toUpperCase(), this.url,
this.options.asynchronous);
if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
this.transport.onreadystatechange = this.onStateChange.bind(this);
this.setRequestHeaders();
this.body = this.method == 'post' ? (this.options.postBody || params) : null;
this.transport.send(this.body);
/* Force Firefox to handle ready state 4 for synchronous requests */
if (!this.options.asynchronous && this.transport.overrideMimeType)
this.onStateChange();
if(this.options.timeoutDelay && this.options.onTimeout)
this.startTimer(this.options.timeoutDelay);
}
catch (e) {
this.dispatchException(e);
}
},
onStateChange: function() {
var readyState = this.transport.readyState;
if (readyState > 1 && !((readyState == 4) && this._complete) && !this.timeout)
this.respondToReadyState(this.transport.readyState);
},
setRequestHeaders: function() {
var headers = {
'X-Requested-With': 'XMLHttpRequest',
'X-Prototype-Version': Prototype.Version,
'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
};
if (this.method == 'post') {
headers['Content-type'] = this.options.contentType +
(this.options.encoding ? '; charset=' + this.options.encoding : '');
/* Force "Connection: close" for older Mozilla browsers to work
* around a bug where XMLHttpRequest sends an incorrect
* Content-length header. See Mozilla Bugzilla #246651.
*/
if (this.transport.overrideMimeType &&
(navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
headers['Connection'] = 'close';
}
// user-defined headers
if (typeof this.options.requestHeaders == 'object') {
var extras = this.options.requestHeaders;
if (Object.isFunction(extras.push))
for (var i = 0, length = extras.length; i < length; i += 2)
headers[extras[i]] = extras[i+1];
else
$H(extras).each(function(pair) { headers[pair.key] = pair.value });
}
for (var name in headers)
this.transport.setRequestHeader(name, headers[name]);
},
success: function() {
var status = this.getStatus();
return (status >= 200 && status < 300);
},
getStatus: function() {
try {
return this.transport.status || 0;
} catch (e) { return 0 }
},
clearTimeout : function(){
clearTimeout(this.timer);
},
startTimer : function(sec){
this.timer = setTimeout(this.handleTimeout.bind(this), (sec*1000));
},
handleTimeout : function(){
try{
this.timeout = true;
this.transport.abort();
this.options.onTimeout(new Ajax.Response(this));
}
catch (e) {
this.dispatchException(e);
}
},
respondToReadyState: function(readyState) {
var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
if (state == 'Complete') {
try {
this._complete = true;
(this.options['on' + response.status]
|| this.options['on' + (this.success() ? 'Success' : 'Failure')]
|| Prototype.emptyFunction)(response, response.headerJSON);
} catch (e) {
this.dispatchException(e);
}
var contentType = response.getHeader('Content-type');
if (this.options.evalJS == 'force'
|| (this.options.evalJS && this.isSameOrigin() && contentType
&& contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
this.evalResponse();
}
try {
(this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
} catch (e) {
this.dispatchException(e);
}
if (state == 'Complete') {
this.clearTimeout();
// avoid memory leak in MSIE: clean up
this.transport.onreadystatechange = Prototype.emptyFunction;
}
},
isSameOrigin: function() {
var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
protocol: location.protocol,
domain: document.domain,
port: location.port ? ':' + location.port : ''
}));
},
getHeader: function(name) {
try {
return this.transport.getResponseHeader(name) || null;
} catch (e) { return null }
},
evalResponse: function() {
try {
return eval((this.transport.responseText || '').unfilterJSON());
} catch (e) {
this.dispatchException(e);
}
},
dispatchException: function(exception) {
(this.options.onException || Prototype.emptyFunction)(this, exception);
Ajax.Responders.dispatch('onException', this, exception);
}
});
|
||
CommentsNo comments have been posted for this page.
|
||