|
|
/** * Module requirements. */
var XMLHttpRequest = require('xmlhttprequest'); var Polling = require('./polling'); var Emitter = require('component-emitter'); var inherit = require('component-inherit'); var debug = require('debug')('engine.io-client:polling-xhr');
/** * Module exports. */
module.exports = XHR; module.exports.Request = Request;
/** * Empty function */
function empty(){}
/** * XHR Polling constructor. * * @param {Object} opts * @api public */
function XHR(opts){ Polling.call(this, opts);
if (global.location) { var isSSL = 'https:' == location.protocol; var port = location.port;
// some user agents have empty `location.port`
if (!port) { port = isSSL ? 443 : 80; }
this.xd = opts.hostname != global.location.hostname || port != opts.port; this.xs = opts.secure != isSSL; } }
/** * Inherits from Polling. */
inherit(XHR, Polling);
/** * XHR supports binary */
XHR.prototype.supportsBinary = true;
/** * Creates a request. * * @param {String} method * @api private */
XHR.prototype.request = function(opts){ opts = opts || {}; opts.uri = this.uri(); opts.xd = this.xd; opts.xs = this.xs; opts.agent = this.agent || false; opts.supportsBinary = this.supportsBinary; opts.enablesXDR = this.enablesXDR; return new Request(opts); };
/** * Sends data. * * @param {String} data to send. * @param {Function} called upon flush. * @api private */
XHR.prototype.doWrite = function(data, fn){ var isBinary = typeof data !== 'string' && data !== undefined; var req = this.request({ method: 'POST', data: data, isBinary: isBinary }); var self = this; req.on('success', fn); req.on('error', function(err){ self.onError('xhr post error', err); }); this.sendXhr = req; };
/** * Starts a poll cycle. * * @api private */
XHR.prototype.doPoll = function(){ debug('xhr poll'); var req = this.request(); var self = this; req.on('data', function(data){ self.onData(data); }); req.on('error', function(err){ self.onError('xhr poll error', err); }); this.pollXhr = req; };
/** * Request constructor * * @param {Object} options * @api public */
function Request(opts){ this.method = opts.method || 'GET'; this.uri = opts.uri; this.xd = !!opts.xd; this.xs = !!opts.xs; this.async = false !== opts.async; this.data = undefined != opts.data ? opts.data : null; this.agent = opts.agent; this.isBinary = opts.isBinary; this.supportsBinary = opts.supportsBinary; this.enablesXDR = opts.enablesXDR; this.create(); }
/** * Mix in `Emitter`. */
Emitter(Request.prototype);
/** * Creates the XHR object and sends the request. * * @api private */
Request.prototype.create = function(){ var xhr = this.xhr = new XMLHttpRequest({ agent: this.agent, xdomain: this.xd, xscheme: this.xs, enablesXDR: this.enablesXDR }); var self = this;
try { debug('xhr open %s: %s', this.method, this.uri); xhr.open(this.method, this.uri, this.async); if (this.supportsBinary) { // This has to be done after open because Firefox is stupid
// http://stackoverflow.com/questions/13216903/get-binary-data-with-xmlhttprequest-in-a-firefox-extension
xhr.responseType = 'arraybuffer'; }
if ('POST' == this.method) { try { if (this.isBinary) { xhr.setRequestHeader('Content-type', 'application/octet-stream'); } else { xhr.setRequestHeader('Content-type', 'text/plain;charset=UTF-8'); } } catch (e) {} }
// ie6 check
if ('withCredentials' in xhr) { xhr.withCredentials = true; }
if (this.hasXDR()) { xhr.onload = function(){ self.onLoad(); }; xhr.onerror = function(){ self.onError(xhr.responseText); }; } else { xhr.onreadystatechange = function(){ if (4 != xhr.readyState) return; if (200 == xhr.status || 1223 == xhr.status) { self.onLoad(); } else { // make sure the `error` event handler that's user-set
// does not throw in the same tick and gets caught here
setTimeout(function(){ self.onError(xhr.status); }, 0); } }; }
debug('xhr data %s', this.data); xhr.send(this.data); } catch (e) { // Need to defer since .create() is called directly fhrom the constructor
// and thus the 'error' event can only be only bound *after* this exception
// occurs. Therefore, also, we cannot throw here at all.
setTimeout(function() { self.onError(e); }, 0); return; }
if (global.document) { this.index = Request.requestsCount++; Request.requests[this.index] = this; } };
/** * Called upon successful response. * * @api private */
Request.prototype.onSuccess = function(){ this.emit('success'); this.cleanup(); };
/** * Called if we have data. * * @api private */
Request.prototype.onData = function(data){ this.emit('data', data); this.onSuccess(); };
/** * Called upon error. * * @api private */
Request.prototype.onError = function(err){ this.emit('error', err); this.cleanup(); };
/** * Cleans up house. * * @api private */
Request.prototype.cleanup = function(){ if ('undefined' == typeof this.xhr || null === this.xhr) { return; } // xmlhttprequest
if (this.hasXDR()) { this.xhr.onload = this.xhr.onerror = empty; } else { this.xhr.onreadystatechange = empty; }
try { this.xhr.abort(); } catch(e) {}
if (global.document) { delete Request.requests[this.index]; }
this.xhr = null; };
/** * Called upon load. * * @api private */
Request.prototype.onLoad = function(){ var data; try { var contentType; try { contentType = this.xhr.getResponseHeader('Content-Type').split(';')[0]; } catch (e) {} if (contentType === 'application/octet-stream') { data = this.xhr.response; } else { if (!this.supportsBinary) { data = this.xhr.responseText; } else { data = 'ok'; } } } catch (e) { this.onError(e); } if (null != data) { this.onData(data); } };
/** * Check if it has XDomainRequest. * * @api private */
Request.prototype.hasXDR = function(){ return 'undefined' !== typeof global.XDomainRequest && !this.xs && this.enablesXDR; };
/** * Aborts the request. * * @api public */
Request.prototype.abort = function(){ this.cleanup(); };
/** * Aborts pending requests when unloading the window. This is needed to prevent * memory leaks (e.g. when using IE) and to ensure that no spurious error is * emitted. */
if (global.document) { Request.requestsCount = 0; Request.requests = {}; if (global.attachEvent) { global.attachEvent('onunload', unloadHandler); } else if (global.addEventListener) { global.addEventListener('beforeunload', unloadHandler); } }
function unloadHandler() { for (var i in Request.requests) { if (Request.requests.hasOwnProperty(i)) { Request.requests[i].abort(); } } }
|