UNPKG

qwest

Version:

Ajax library with XHR2, promises and request limit

503 lines (483 loc) 19.5 kB
/*! qwest 4.5.0 (https://github.com/pyrsmk/qwest) */ module.exports = function() { var global = typeof window != 'undefined' ? window : self, pinkyswear = require('pinkyswear'), jparam = require('jquery-param'), defaultOptions = {}, // Default response type for XDR in auto mode defaultXdrResponseType = 'json', // Default data type defaultDataType = 'post', // Variables for limit mechanism limit = null, requests = 0, request_stack = [], // Get XMLHttpRequest object getXHR = global.XMLHttpRequest? function(){ return new global.XMLHttpRequest(); }: function(){ return new ActiveXObject('Microsoft.XMLHTTP'); }, // Guess XHR version xhr2 = (getXHR().responseType===''), // Core function qwest = function(method, url, data, options, before) { // Format method = method.toUpperCase(); data = data === undefined ? null : data; options = options || {}; for(var name in defaultOptions) { if(!(name in options)) { if(typeof defaultOptions[name] == 'object' && typeof options[name] == 'object') { for(var name2 in defaultOptions[name]) { options[name][name2] = defaultOptions[name][name2]; } } else { options[name] = defaultOptions[name]; } } } // Define variables var nativeResponseParsing = false, crossOrigin, xhr, xdr = false, timeout, aborted = false, attempts = 0, headers = {}, mimeTypes = { text: '*/*', xml: 'text/xml', json: 'application/json', post: 'application/x-www-form-urlencoded', document: 'text/html' }, accept = { text: '*/*', xml: 'application/xml; q=1.0, text/xml; q=0.8, */*; q=0.1', json: 'application/json; q=1.0, text/*; q=0.8, */*; q=0.1' }, i, j, response, sending = false, // Create the promise promise = pinkyswear(function(pinky) { pinky.abort = function() { if(!aborted) { if(xhr && xhr.readyState != 4) { // https://stackoverflow.com/questions/7287706/ie-9-javascript-error-c00c023f xhr.abort(); } if(sending) { --requests; sending = false; } aborted = true; } }; pinky.send = function() { // Prevent further send() calls if(sending) { return; } // Reached request limit, get out! if(requests == limit) { request_stack.push(pinky); return; } // Verify if the request has not been previously aborted if(aborted) { if(request_stack.length) { request_stack.shift().send(); } return; } // The sending is running ++requests; sending = true; // Get XHR object xhr = getXHR(); if(crossOrigin) { if(!('withCredentials' in xhr) && global.XDomainRequest) { xhr = new XDomainRequest(); // CORS with IE8/9 xdr = true; if(method != 'GET' && method != 'POST') { method = 'POST'; } } } // Open connection if(xdr) { xhr.open(method, url); } else { xhr.open(method, url, options.async, options.user, options.password); if(xhr2 && options.async) { xhr.withCredentials = options.withCredentials; } } // Set headers if(!xdr) { for(var i in headers) { if(headers[i]) { xhr.setRequestHeader(i, headers[i]); } } } // Verify if the response type is supported by the current browser if(xhr2 && options.responseType != 'auto') { try { xhr.responseType = options.responseType; nativeResponseParsing = (xhr.responseType == options.responseType); } catch(e) {} } // Plug response handler if(xhr2 || xdr) { xhr.onload = handleResponse; xhr.onerror = handleError; // http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/ if(xdr) { xhr.onprogress = function() {}; } } else { xhr.onreadystatechange = function() { if(xhr.readyState == 4) { handleResponse(); } }; } // Plug timeout if(options.async) { if('timeout' in xhr) { xhr.timeout = options.timeout; xhr.ontimeout = handleTimeout; } else { timeout = setTimeout(handleTimeout, options.timeout); } } // http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/ else if(xdr) { xhr.ontimeout = function() {}; } // Override mime type to ensure the response is well parsed if(options.responseType != 'auto' && 'overrideMimeType' in xhr) { xhr.overrideMimeType(mimeTypes[options.responseType]); } // Run 'before' callback if(before) { before(xhr); } // Send request if(xdr) { // https://developer.mozilla.org/en-US/docs/Web/API/XDomainRequest setTimeout(function() { xhr.send(method != 'GET'? data : null); }, 0); } else { xhr.send(method != 'GET' ? data : null); } }; return pinky; }), // Handle the response handleResponse = function() { var i, responseType; // Stop sending state sending = false; clearTimeout(timeout); // Launch next stacked request if(request_stack.length) { request_stack.shift().send(); } // Verify if the request has not been previously aborted if(aborted) { return; } // Decrease the number of requests --requests; // Handle response try{ // Process response if(nativeResponseParsing) { if('response' in xhr && xhr.response === null) { throw 'The request response is empty'; } response = xhr.response; } else { // Guess response type responseType = options.responseType; if(responseType == 'auto') { if(xdr) { responseType = defaultXdrResponseType; } else { var ct = xhr.getResponseHeader('Content-Type') || ''; if(ct.indexOf(mimeTypes.json)>-1) { responseType = 'json'; } else if(ct.indexOf(mimeTypes.xml) > -1) { responseType = 'xml'; } else { responseType = 'text'; } } } // Handle response type switch(responseType) { case 'json': if(xhr.responseText.length) { try { if('JSON' in global) { response = JSON.parse(xhr.responseText); } else { response = new Function('return (' + xhr.responseText + ')')(); } } catch(e) { throw "Error while parsing JSON body : "+e; } } break; case 'xml': // Based on jQuery's parseXML() function try { // Standard if(global.DOMParser) { response = (new DOMParser()).parseFromString(xhr.responseText,'text/xml'); } // IE<9 else { response = new ActiveXObject('Microsoft.XMLDOM'); response.async = 'false'; response.loadXML(xhr.responseText); } } catch(e) { response = undefined; } if(!response || !response.documentElement || response.getElementsByTagName('parsererror').length) { throw 'Invalid XML'; } break; default: response = xhr.responseText; } } // Late status code verification to allow passing data when, per example, a 409 is returned // --- https://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request if('status' in xhr && !/^2|1223/.test(xhr.status)) { throw xhr.status + ' (' + xhr.statusText + ')'; } // Fulfilled promise(true, [xhr, response]); } catch(e) { // Rejected promise(false, [e, xhr, response]); } }, // Handle errors handleError = function(message) { if(!aborted) { message = typeof message == 'string' ? message : 'Connection aborted'; promise.abort(); promise(false, [new Error(message), xhr, null]); } }, // Handle timeouts handleTimeout = function() { if(!aborted) { if(!options.attempts || ++attempts != options.attempts) { xhr.abort(); sending = false; promise.send(); } else { handleError('Timeout (' + url + ')'); } } }; // Normalize options options.async = 'async' in options ? !!options.async : true; options.cache = 'cache' in options ? !!options.cache : false; options.dataType = 'dataType' in options ? options.dataType.toLowerCase() : defaultDataType; options.responseType = 'responseType' in options ? options.responseType.toLowerCase() : 'auto'; options.user = options.user || ''; options.password = options.password || ''; options.withCredentials = !!options.withCredentials; options.timeout = 'timeout' in options ? parseInt(options.timeout, 10) : 30000; options.attempts = 'attempts' in options ? parseInt(options.attempts, 10) : 1; // Guess if we're dealing with a cross-origin request i = url.match(/\/\/(.+?)\//); crossOrigin = i && (i[1] ? i[1] != location.host : false); // Prepare data if('ArrayBuffer' in global && data instanceof ArrayBuffer) { options.dataType = 'arraybuffer'; } else if('Blob' in global && data instanceof Blob) { options.dataType = 'blob'; } else if('Document' in global && data instanceof Document) { options.dataType = 'document'; } else if('FormData' in global && data instanceof FormData) { options.dataType = 'formdata'; } if(data !== null) { switch(options.dataType) { case 'json': data = JSON.stringify(data); break; case 'post': case 'queryString': data = jparam(data); } } // Prepare headers if(options.headers) { var format = function(match,p1,p2) { return p1 + p2.toUpperCase(); }; for(i in options.headers) { headers[i.replace(/(^|-)([^-])/g,format)] = options.headers[i]; } } if(!('Content-Type' in headers) && method!='GET') { if(options.dataType in mimeTypes) { if(mimeTypes[options.dataType]) { headers['Content-Type'] = mimeTypes[options.dataType]; } } } if(!headers.Accept) { headers.Accept = (options.responseType in accept) ? accept[options.responseType] : '*/*'; } if(!crossOrigin && !('X-Requested-With' in headers)) { // (that header breaks in legacy browsers with CORS) headers['X-Requested-With'] = 'XMLHttpRequest'; } if(!options.cache && !('Cache-Control' in headers)) { headers['Cache-Control'] = 'no-cache'; } // Prepare URL if((method == 'GET' || options.dataType == 'queryString') && data && typeof data == 'string') { url += (/\?/.test(url)?'&':'?') + data; } // Start the request if(options.async) { promise.send(); } // Return promise return promise; }; // Define external qwest object var getNewPromise = function(q) { // Prepare var promises = [], loading = 0, values = []; // Create a new promise to handle all requests return pinkyswear(function(pinky) { // Basic request method var method_index = -1, createMethod = function(method) { return function(url, data, options, before) { var index = ++method_index; ++loading; promises.push(qwest(method, pinky.base + url, data, options, before).then(function(xhr, response) { values[index] = arguments; if(!--loading) { pinky(true, values.length == 1 ? values[0] : [values]); } }, function() { pinky(false, arguments); })); return pinky; }; }; // Define external API pinky.get = createMethod('GET'); pinky.post = createMethod('POST'); pinky.put = createMethod('PUT'); pinky['delete'] = createMethod('DELETE'); pinky['catch'] = function(f) { return pinky.then(null, f); }; pinky.complete = function(f) { var func = function() { f(); // otherwise arguments will be passed to the callback }; return pinky.then(func, func); }; pinky.map = function(type, url, data, options, before) { return createMethod(type.toUpperCase()).call(this, url, data, options, before); }; // Populate methods from external object for(var prop in q) { if(!(prop in pinky)) { pinky[prop] = q[prop]; } } // Set last methods pinky.send = function() { for(var i=0, j=promises.length; i<j; ++i) { promises[i].send(); } return pinky; }; pinky.abort = function() { for(var i=0, j=promises.length; i<j; ++i) { promises[i].abort(); } return pinky; }; return pinky; }); }, q = { base: '', get: function() { return getNewPromise(q).get.apply(this, arguments); }, post: function() { return getNewPromise(q).post.apply(this, arguments); }, put: function() { return getNewPromise(q).put.apply(this, arguments); }, 'delete': function() { return getNewPromise(q)['delete'].apply(this, arguments); }, map: function() { return getNewPromise(q).map.apply(this, arguments); }, xhr2: xhr2, limit: function(by) { limit = by; return q; }, setDefaultOptions: function(options) { defaultOptions = options; return q; }, setDefaultXdrResponseType: function(type) { defaultXdrResponseType = type.toLowerCase(); return q; }, setDefaultDataType: function(type) { defaultDataType = type.toLowerCase(); return q; }, getOpenRequests: function() { return requests; } }; return q; }();