blossom
Version:
Modern, Cross-Platform Application Framework
547 lines (437 loc) • 15.5 kB
JavaScript
// ==========================================================================
// Project: SproutCore - JavaScript Application Framework
// Copyright: ©2006-2011 Strobe Inc. and contributors.
// Portions ©2008-2010 Apple Inc. All rights reserved.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
/*global ActiveXObject sc_assert */
sc_require('system/object');
var SC = global.SC; // Required to allow foundation to be re-namespaced as BT
// when loaded by the buildtools.
/**
A response represents a single response from a server request. An instance
of this class is returned whenever you call SC.Request.send().
TODO: Add more info
@extend SC.Object
@since SproutCore 1.0
*/
SC.Response = SC.Object.extend(
/** @scope SC.Response.prototype */ {
/**
Becomes true if there was a failure. Makes this into an error object.
@property {Boolean}
*/
isError: false,
/**
Always the current response
@property {SC.Response}
*/
errorValue: function() {
return this;
}.property().cacheable(),
/**
The error object generated when this becomes an error
@property {SC.Error}
*/
errorObject: null,
/**
Request used to generate this response. This is a copy of the original
request object as you may have modified the original request object since
then.
To retrieve the original request object use originalRequest.
@property {SC.Request}
*/
request: null,
/**
The request object that originated this request series. Mostly this is
useful if you are looking for a reference to the original request. To
inspect actual properties you should use request instead.
@property {SC.Request}
*/
originalRequest: function() {
var ret = this.get('request');
while (ret.get('source')) ret = ret.get('source');
return ret ;
}.property('request').cacheable(),
/**
Type of request. Must be an HTTP method. Based on the request
@property {String}
*/
type: function() {
return this.getPath('request.type');
}.property('request').cacheable(),
/**
URL of request.
@property {String}
*/
address: function() {
return this.getPath('request.address');
}.property('request').cacheable(),
/**
If set then will attempt to automatically parse response as JSON
regardless of headers.
@property {Boolean}
*/
isJSON: function() {
return this.getPath('request.isJSON') || false;
}.property('request').cacheable(),
/**
If set, then will attempt to automatically parse response as XML
regarldess of headers.
@property {Boolean}
*/
isXML: function() {
return this.getPath('request.isXML') || false ;
}.property('request').cacheable(),
/**
Returns the hash of listeners set on the request.
@property {Hash}
*/
listeners: function() {
return this.getPath('request.listeners');
}.property('request').cacheable(),
/**
The response status code.
@property {Number}
*/
status: -100, // READY
/**
Headers from the response. Computed on-demand
@property {Hash}
*/
headers: null,
/**
Response body. If isJSON was set, will be parsed automatically.
@response {Hash|String|SC.Error} the response body or the parsed JSON.
Returns a SC.Error instance if there is a JSON parsing error.
*/
body: function() {
// TODO: support XML
var ret = this.get('encodedBody');
if (ret && this.get('isJSON')) {
try {
ret = SC.json.decode(ret);
} catch(e) {
return SC.Error.create({
message: e.name + ': ' + e.message,
label: 'Response',
errorValue: this });
}
}
return ret;
}.property('encodedBody').cacheable(),
/**
@private
@deprecated
Alias for body. Provides compatibility with older code.
@property {Hash|String}
*/
response: function() {
return this.get('body');
}.property('body').cacheable(),
/**
Set to true if response is cancelled
*/
isCancelled: false,
/**
Set to true if the request timed out. Set to false if the request has
completed before the timeout value. Set to null if the timeout timer is
still ticking.
*/
timedOut: null,
/**
The timer tracking the timeout
*/
timeoutTimer: null,
// ..........................................................
// METHODS
//
/**
Called by the request manager when its time to actually run. This will
invoke any callbacks on the source request then invoke transport() to
begin the actual request.
*/
fire: function() {
var req = this.get('request'),
source = req ? req.get('source') : null;
// first give the source a chance to fixup the request and response
// then freeze req so no more changes can happen.
if (source && source.willSend) source.willSend(req, this);
req.freeze();
// if the source did not cancel the request, then invoke the transport
// to actually trigger the request. This might receive a response
// immediately if it is synchronous.
if (!this.get('isCancelled')) this.invokeTransport();
// If the request specified a timeout value, then set a timer for it now.
var timeout = req.get('timeout');
if (timeout) {
var timer = SC.Timer.schedule({
target: this,
action: 'timeoutReached',
interval: timeout,
repeats: false
});
this.set('timeoutTimer', timer);
}
// if the transport did not cancel the request for some reason, let the
// source know that the request was sent
if (!this.get('isCancelled') && source && source.didSend) {
source.didSend(req, this);
}
},
invokeTransport: function() {
this.receive(function(proceed) { this.set('status', 200); }, this);
},
/**
Invoked by the transport when it receives a response. The passed-in
callback will be invoked to actually process the response. If cancelled
we will pass false. You should clean up instead.
Invokes callbacks on the source request also.
@param {Function} callback the function to receive
@param {Object} context context to execute the callback in
@returns {SC.Response} receiver
*/
receive: function(callback, context) {
// If we timed out, we should ignore this response.
if (!this.get('timedOut')) {
// If we had a timeout timer scheduled, invalidate it now.
var timer = this.get('timeoutTimer');
if (timer) timer.invalidate();
this.set('timedOut', false);
var req = this.get('request');
var source = req ? req.get('source') : null;
SC.run(function() {
// invoke the source, giving a chance to fixup the reponse or (more
// likely) cancel the request.
if (source && source.willReceive) source.willReceive(req, this);
// invoke the callback. note if the response was cancelled or not
callback.call(context, !this.get('isCancelled'));
// if we weren't cancelled, then give the source first crack at handling
// the response. if the source doesn't want listeners to be notified,
// it will cancel the response.
if (!this.get('isCancelled') && source && source.didReceive) {
source.didReceive(req, this);
}
// notify listeners if we weren't cancelled.
if (!this.get('isCancelled')) this.notify();
}, this);
}
// no matter what, remove from inflight queue
SC.Request.manager.transportDidClose(this) ;
return this;
},
/**
Default method just closes the connection. It will also mark the request
as cancelled, which will not call any listeners.
*/
cancel: function() {
if (!this.get('isCancelled')) {
this.set('isCancelled', true) ;
this.cancelTransport() ;
SC.Request.manager.transportDidClose(this) ;
}
},
/**
Default method just closes the connection.
*/
timeoutReached: function() {
// If we already received a response yet the timer still fired for some
// reason, do nothing.
if (this.get('timedOut') === null) {
this.set('timedOut', true);
this.cancelTransport();
SC.Request.manager.transportDidClose(this);
// Set our value to an error.
var error = SC.$error("HTTP Request timed out", "Request", 408) ;
error.set("errorValue", this) ;
this.set('isError', true);
this.set('errorObject', error);
// Invoke the didTimeout callback.
var req = this.get('request');
var source = req ? req.get('source') : null;
if (!this.get('isCancelled') && source && source.didTimeout) {
source.didTimeout(req, this);
}
}
},
/**
Override with concrete implementation to actually cancel the transport.
*/
cancelTransport: function() {},
/** @private
Will notify each listener.
*/
_notifyListener: function(listeners, status) {
var info = listeners[status], params, target, action;
if (!info) return false ;
params = (info.params || []).copy();
params.unshift(this);
target = info.target;
action = info.action;
if (SC.typeOf(action) === SC.T_STRING) action = target[action];
return action.apply(target, params);
},
/**
Notifies any saved target/action. Call whenever you cancel, or end.
@returns {SC.Response} receiver
*/
notify: function() {
var listeners = this.get('listeners'),
status = this.get('status'),
baseStat = Math.floor(status / 100) * 100,
handled = false ;
if (!listeners) return this ; // nothing to do
handled = this._notifyListener(listeners, status);
if (!handled) handled = this._notifyListener(listeners, baseStat);
if (!handled) handled = this._notifyListener(listeners, 0);
return this ;
},
/**
String representation of the response object
*/
toString: function() {
var ret = arguments.callee.base.apply(this, arguments);
return "%@<%@ %@, status=%@".fmt(ret, this.get('type'), this.get('address'), this.get('status'));
}
});
/**
Concrete implementation of SC.Response that implements support for using
XHR requests.
@extends SC.Response
@since SproutCore 1.0
*/
SC.XHRResponse = SC.Response.extend({
/**
Implement transport-specific support for fetching all headers
*/
headers: function() {
var xhr = this.get('rawRequest'),
str = xhr ? xhr.getAllResponseHeaders() : null,
ret = {};
if (!str) return ret;
str.split("\n").forEach(function(header) {
var idx = header.indexOf(':'),
key, value;
if (idx>=0) {
key = header.slice(0,idx);
value = header.slice(idx+1).trim();
ret[key] = value ;
}
}, this);
return ret ;
}.property('status').cacheable(),
// returns a header value if found...
header: function(key) {
var xhr = this.get('rawRequest');
return xhr ? xhr.getResponseHeader(key) : null;
},
/**
Implement transport-specific support for fetching tasks
*/
encodedBody: function() {
var xhr = this.get('rawRequest'), ret ;
if (!xhr) ret = null;
else if (this.get('isXML')) ret = xhr.responseXML;
else ret = xhr.responseText;
return ret ;
}.property('status').cacheable(),
cancelTransport: function() {
var rawRequest = this.get('rawRequest');
if (rawRequest) rawRequest.abort();
this.set('rawRequest', null);
},
invokeTransport: function() {
var rawRequest, transport, handleReadyStateChange, async, headers;
// Get an XHR object
function tryThese() {
for (var i=0; i < arguments.length; i++) {
try {
var item = arguments[i]() ;
return item ;
} catch (e) {}
}
return false;
}
rawRequest = tryThese(
function() { return new XMLHttpRequest(); },
function() { return new ActiveXObject('Msxml2.XMLHTTP'); },
function() { return new ActiveXObject('Microsoft.XMLHTTP'); }
);
// save it
this.set('rawRequest', rawRequest);
// configure async callback - differs per browser...
async = !!this.getPath('request.isAsynchronous') ;
if (async) {
transport=this;
handleReadyStateChange = function() {
if (!transport) return null ;
var ret = transport.finishRequest(null, handleReadyStateChange);
if (ret) transport = null ; // cleanup memory
return ret ;
};
if (!SC.isNode && (SC.browser && !SC.browser.msie && !SC.browser.opera)) {
rawRequest.addEventListener('readystatechange', handleReadyStateChange, false);
// SC.Event.add(rawRequest, 'readystatechange', this,
// this.finishRequest, rawRequest) ;
} else {
rawRequest.onreadystatechange = handleReadyStateChange;
}
}
// initiate request.
rawRequest.open(this.get('type'), this.get('address'), async ) ;
// headers need to be set *after* the open call.
headers = this.getPath('request.headers') ;
for (var headerKey in headers) {
rawRequest.setRequestHeader(headerKey, headers[headerKey]) ;
}
// now send the actual request body - for sync requests browser will
// block here
rawRequest.send(this.getPath('request.encodedBody')) ;
if (!async) this.finishRequest() ; // not async
return rawRequest ;
},
/** @private
Called by the XHR when it responds with some final results.
@param {XMLHttpRequest} rawRequest the actual request
@returns {SC.XHRRequestTransport} receiver
*/
finishRequest: function(evt, listener) {
var rawRequest = this.get('rawRequest'),
readyState = rawRequest.readyState,
error, status, msg;
if (readyState === 4) {
this.receive(function(proceed) {
if (!proceed) return ; // skip receiving...
// collect the status and decide if we're in an error state or not
status = -1 ;
try {
status = rawRequest.status || 0;
} catch (e) {}
// if there was an error - setup error and save it
if ((status < 200) || (status >= 300)) {
try {
msg = rawRequest.statusText || '';
} catch(e2) {
msg = '';
}
error = SC.$error(msg || "HTTP Request failed", "Request", status) ;
error.set("errorValue", this) ;
this.set('isError', true);
this.set('errorObject', error);
}
// set the status - this will trigger changes on relatedp properties
this.set('status', status);
}, this);
// Avoid memory leaks
if (!SC.isNode && (SC.browser && !SC.browser.msie && !SC.browser.opera)) {
sc_assert(listener);
rawRequest.removeEventListener('readystatechange', listener, false);
// SC.Event.remove(rawRequest, 'readystatechange', this, this.finishRequest);
} else {
rawRequest.onreadystatechange = null;
}
return true;
}
return false;
}
});