constructors/request.js

/* jshint -W002 */

/**
 * @class
 * @name RequestModule 
 */

/**
 * @callback RequestModuleConstructor
 * @returns {RequestModule}
 */

/**
 * Module used for retrieving date using XMLHttpRequest
 * @example
 * Application.require('request').then(function (request) {
 *	 request()
 *		 .url('/data.json')
 *		 .response('json')
 *		 .then(function (data) {
 *			 console.log(data);
 *		 }, console.error);
 * }, console.error);
 * @returns {RequestModuleConstructor}
 * @see RequestModule
 */

 Application.require("extensions/prototype", function (epro) {
	if (typeof(Application.NodeInterface) === "function") {
		if (typeof(XMLHttpRequest) !== "undefined" && Application.NodeInterface()) {
			if (typeof(require) === "function") {
				var err;
				try {
					XMLHttpRequest = Application.NodeInterface().require("xmlhttprequest").XMLHttpRequest;
					global.XMLHttpRequest = XMLHttpRequest;
				} catch (err) {}
			}
		}
	}

	function Request() {
		var app	 = new ApplicationPrototype();
		var httpRequest = new XMLHttpRequest();


		/**
		 * @inner
		 * @typedef {Object} RequestConfig
		 * @memberof RequestModule
		 * @property {boolean} [method="GET"]
		 * @property {boolean} [url="#"]
		 * @property {boolean} [async=true]
		 * @property {boolean} [opened=false]
		 * @property {boolean} [isSent=false]
		 * @property {boolean} [isLoaded=false]
		 * @property {boolean} [isUploaded=false]
		 * @property {boolean} [ignoreStatusCode=false]
		 * @property {boolean} [ignoreStatusCode=false]
		 * @property {string|null} [basicAuthUsername]
		 * @property {string|null} [basicAuthPassword]
		 * @property {Object<string, string>} [headers]
		 */
		var config  = {
			method  : "GET",
			url	 : "#",
			async   : true,
			opened  : false,
			isSent  : false,
			isLoaded: false,
			isUploaded: false,
			ignoreStatusCode : false,
			BasicAuthUsername: null,
			BasicAuthPassword: null,
			headers: {}
		};


		/**
		 * @typedef {object} RequestModule.readyStateType
		 * @property {number} [READY_STATE_UNSENT=0]
		 * @property {number} [READY_STATE_OPENED=1]
		 * @property {number} [READY_STATE_HEADERS_RECEIVED=2]
		 * @property {number} [READY_STATE_LOADING=3]
		 * @property {number} [READY_STATE_DONE=4]
		 */

		var configurator	= {
			"ignore-status-code"  : function () {
			  config.ignoreStatusCode = true;
			},
			"check-status-code"  : function () {
			  config.ignoreStatusCode = false;
			},
			"prepare-json"  : function () {
				httpRequest.setRequestHeader('Accept', 'application/json');
				httpRequest.setRequestHeader('Content-Type', 'application/json');
			},
			"prepare-post"  : function () {
				httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
			},
			"retrieve-binary-string"	: function () {
				httpRequest.overrideMimeType("text/plain; charset=x-user-defined");
				httpRequest.responseType = "arraybuffer";
			},
			"retrieve-blob"	: function () {
				httpRequest.responseType = "blob";
			},
			"prepare-multipart" : function () {
				config.sBoundary	= config.sBoundary || epro.fn.getRandId().toHex();
				httpRequest.setRequestHeader("Content-Type", "multipart\/form-data; boundary=" + config.sBoundary);
			},
			"POST" : "prepare-post",
			"binary"	: "retrieve-binary-string",
			"blob" : "retrieve-blob",
			"multipart" : "prepare-multipart"
		};

		/**
		 * @method config
		 * @memberof RequestModule#
		 * @returns {RequestModule.RequestConfig}
		 */
		app.bind("config", function () {
			return config;
		});

		/**
		 * @method configurator
		 * @memberof RequestModule#
		 * @param {('multipart'|'blob'|'binary'|'POST'|'prepare-multipart'|'retrieve-blob'|'retrieve-binary-string'|'prepare-post'|'check-status-code'|'ignore-status-code')} template configuration name
		 * @returns {RequestModule}
		 */
		app.bind("configurator", function (template) {
			var f = function (template) {
				if (typeof(configurator[template]) === "string") {
					return f(configurator[template]);
				} else if (typeof(configurator[template]) === "function") {
					return configurator[template];
				}
			};
			var configTemplate = f(template);
			if (typeof(configTemplate) === "function") {
				configTemplate();
			}
			return app;
		});

		// events for download
		httpRequest.addEventListener("progress", function (evt) {
			if (evt.lengthComputable && evt.total) {
				var percentComplete = evt.loaded / evt.total;
				app.emit("progress", [evt, percentComplete]);
			} else {
				app.emit("progress", [evt]);
			}
		});
		httpRequest.addEventListener("load", function (evt) {
			app.emit("load", [evt]);
		});
		httpRequest.addEventListener("loadend", function (evt) {
			config.isLoaded = true;
			app.emit("loadend", [evt]);
		});
		httpRequest.addEventListener("error", function (evt) {
			app.emit("error", [evt]);
		});
		httpRequest.addEventListener("abort", function (evt) {
			app.emit("abort", [evt]);
		});

		// events for upload
		if (httpRequest.upload) {
			httpRequest.upload.addEventListener("progress", function (evt) {
				if (evt.lengthComputable && evt.total) {
					var percentComplete = evt.loaded / evt.total;
					app.emit("upload-progress", [evt, percentComplete]);
				} else {
					app.emit("upload-progress", [evt]);
				}
			});
			httpRequest.upload.addEventListener("load", function (evt) {
				app.emit("upload-load", [evt]);
			});
			httpRequest.upload.addEventListener("error", function (evt) {
				app.emit("upload-error", [evt]);
			});
			httpRequest.upload.addEventListener("abort", function (evt) {
				app.emit("upload-abort", [evt]);
			});
	
	
			httpRequest.upload.addEventListener("loadend", function (evt) {
				config.isUploaded = true;
				app.emit("upload-loadend", [evt]);
			});
		}

		/**
		 * @method request
		 * @memberof RequestModule#
		 * @returns {XMLHttpRequest}
		 */
		app.bind("request", function () { return httpRequest; }, "");

		/**
		 * @method response
		 * @memberof RequestModule#
		 * @param {(''|'request'|'blob'|'arraybuffer'|'text'|'json'|'document')} [type]
		 * @param {object} [options]
		 * @param {string} [options.type="application/octet-stream"] Blob constructor's params
		 * @returns {(RequestModule|Promise<ArrayBuffer>|Promise<Blob>|Promise<HTMLElement>|Promise<DocumentFragment>|Promise<string>|ArrayBuffer|Blob|HTMLElement|string)}
		 */
		app.bind("response", function (type, params) {
			var er, err, data, node;
			if (typeof(type) === "undefined") {
				return httpRequest.response;
			} else {
				if (!config.isSent) {
					app.send();
				}
				if (!config.isLoaded) {
					return new Application.Promise(function (resolve, reject) {
						app.on("loadend", function () {
							var result = app.response(type, params);
							result.then(function (data) {
								resolve(data);
							}, function (er) {
								er.httpRequest = app;
								reject(er);
							});
						});
						app.on("error", function (er) {
							er.httpRequest = app;
							reject(er);
						});
					});
				}
				var response = Application.Promise(), reader;
				if (!config.ignoreStatusCode && (httpRequest.status < 200 || httpRequest.status >= 300)) {
					response.reject(Error('Status ' + httpRequest.status + ': ' + httpRequest.statusText));
				} else if (type === "request") {
					response.resolve(app);
				} else if (type === "response") {
					response.resolve(httpRequest.response);
				} else if (httpRequest.responseType === "arraybuffer") {
					switch (type) {
						case "blob":
							response.resolve(new Blob(
								[httpRequest.response],
								params || {type: "application/octet-stream"}
							));
						break;
						case "json":
							data	= undefined;
							er = undefined;
							try {
								data = JSON.parse(httpRequest.response.toStringUtf8());
								response.resolve(data);
							} catch (err) { er = err; }
							if (typeof(er) !== "undefined") {
								response.reject(er);
							}
						break;
						case "document":
							node = document.createElement("div");
							node.innerHTML  = httpRequest.response.toStringUtf8();
							response.resolve(node);
						break;
						case "text":
							response.resolve(httpRequest.response.toStringUtf8());
						break;
						case "arraybuffer":
							response.resolve(httpRequest.response);
						break;
					}
				} else if (httpRequest.responseType === "blob") {
					switch (type) {
						case "blob":
							response.resolve(httpRequest.response);
						break;
						case "json":
							httpRequest.response.toArrayBuffer(function (er, data) {
								if (typeof(er) !== "undefined") {
									response.reject(er);
								} else {
									var str = data.toStringUtf8();
									data = undefined;
									er  = undefined;
									try {
										data = JSON.parse(str);
										response.resolve(data);
									} catch (err) { er = err; }
									if (typeof(er) !== "undefined") {
										response.reject(er);
									}
								}
							});
						break;
						case "document":
							httpRequest.response.toArrayBuffer(function (er, data) {
								if (typeof(er) !== "undefined") {
									response.reject(er);
								} else {
									var node = document.createElement("div");
									node.innerHTML  = data.toStringUtf8();
									response.resolve(node);
								}
							});
						break;
						case "text":
							httpRequest.response.toArrayBuffer(function (er, data) {
								if (typeof(er) !== "undefined") {
									response.reject(er);
								} else {
									response.resolve(data.toStringUtf8());
								}
							});
						break;
						case "arraybuffer":
							httpRequest.response.toArrayBuffer(function (er, data) {
								if (typeof(er) !== "undefined") {
									response.reject(er);
								} else {
									response.resolve(data);
								}
							});
						break;
					}
				} else {
					switch (type) {
						case "blob":
							response.resolve(new Blob([httpRequest.response], (params || { type: "application/octet-stream" })));
						break;
						case "json":
							var str = httpRequest.response;
							data = undefined;
							er  = undefined;
							try {
								data = JSON.parse(str);
								response.resolve(data);
							} catch (err) { er = err; }
							if (typeof(er) !== "undefined") {
								response.reject(er);
							}
						break;
						case "document":
							node = document.createElement("div");
							node.innerHTML  = httpRequest.response;
							response.resolve(node);
						break;
						case "text":
							response.resolve(httpRequest.response);
						break;
						case "arraybuffer":
							response.resolve(httpRequest.response.toArrayBufferFromUtf8());
						break;
					}
				}
				return new Application.Promise(function (resolve, reject) {
				  response.then(function (data) {
					if (httpRequest.status >= 200 && httpRequest.status < 300 || config.ignoreStatusCode) {
					  resolve(data);
					} else {
					  var er = Error(
						httpRequest.statusText ?
						('Status '+ httpRequest.status + ': ' + httpRequest.statusText) :
						("Response Status is " + httpRequest.status)
					  );
					  er.httpRequest = app;
					  er.httpResponse = data;
					  reject(er);
					}
				  }, function (er) {
					er.httpRequest = app;
					reject(er);
				  });
				});
			}
		}, "");

		/**
		 * current XMLHttpRequest timeout in seconds
		 * @method timeout
		 * @memberof RequestModule#
		 * @returns {number}
		 */

		/**
		 * update XMLHttpRequest timeout in seconds
		 * @method timeout
		 * @memberof RequestModule#
		 * @param {number} seconds set 0 to unlimited
		 * @returns {RequestModule}
		 */
		app.bind("timeout", function (seconds) {
		  if (typeof(seconds) === "number") {
			httpRequest.timeout = seconds || 0;
			return app;
		  }
		  return httpRequest.timeout || 0;
		});

		/**
		 * current XMLHttpRequest withCredentials status
		 * @method withCredentials
		 * @memberof RequestModule#
		 * @returns {boolean}
		 */
		
		/**
		 * update XMLHttpRequest withCredentials flag
		 * @method withCredentials
		 * @memberof RequestModule#
		 * @param {boolean} status
		 * @returns {RequestModule}
		 */
		app.bind("withCredentials", function (status) {
		  if (typeof(status) === "boolean") {
			httpRequest.withCredentials = !!status;
			return app;
		  }
		  return !!httpRequest.withCredentials;
		});

		/**
		 * Client has been created. open() not called yet.
		 * @alias RequestModule.READY_STATE_UNSENT
		 * @type {number}
		 * @default 0
		 */
		app.READY_STATE_UNSENT = 0;
		/**
		 * open() has been called.
		 * @alias RequestModule.READY_STATE_OPENED
		 * @type {number}
		 * @default 1
		 */
		app.READY_STATE_OPENED = 1;
		/**
		 * send() has been called, and headers and status are available.
		 * @alias RequestModule.READY_STATE_HEADERS_RECEIVED
		 * @type {number}
		 * @default 2
		 */
		app.READY_STATE_HEADERS_RECEIVED = 2;
		/**
		 * Downloading; responseText holds partial data.
		 * @alias RequestModule.READY_STATE_LOADING
		 * @type {number}
		 * @default 3
		 */
		app.READY_STATE_LOADING = 3;
		/**
		 * Downloading is done
		 * @alias RequestModule.READY_STATE_DONE
		 * @type {number}
		 * @default 4
		 */
		app.READY_STATE_DONE = 4;


		/**
		 * @method readyState
		 * @memberof RequestModule#
		 * @returns {RequestModule.readyStateType}
		 */
		app.bind("readyState", function (int) {
		  return httpRequest.readyState;
		});

		httpRequest.onreadystatechange = function () {
		  app.emit('onReadyState', [httpRequest.readyState, httpRequest.status]);
		};

		/**
		 * @method status
		 * @memberof RequestModule#
		 * @returns {number}
		 */
		app.bind("status", function (int) {
		  return httpRequest.status;
		});

		/**
		 * @method statusText
		 * @memberof RequestModule#
		 * @returns {string}
		 */
		app.bind("statusText", function (int) {
		  return httpRequest.statusText;
		});


		/**
		 * returns `RequestModule.RequestConfig["async"]`
		 * @method async
		 * @memberof RequestModule#
		 * @returns {boolean}
		 * @see RequestModule.RequestConfig
		 */
		/**
		 * @method async
		 * @memberof RequestModule#
		 * @param {boolean} status enable/disable XMLHttpRequest async mode
		 * @returns {RequestModule}
		 */
		app.bind("async", function (bool) {
			if (typeof(bool) !== "undefined") {
				config.async = !!bool;
				return app;
			}
			return config.async;
		});

		/**
		 * returns `RequestModule.RequestConfig["method"]`
		 * @method method
		 * @memberof RequestModule#
		 * @returns {string}
		 * @see RequestModule.RequestConfig
		 */
		/**
		 * @method method
		 * @memberof RequestModule#
		 * @param {string} status XMLHttpRequest method name, default is `"GET"`
		 * @returns {RequestModule}
		 */
		app.bind("method", function (method) {
			if (typeof(method) === "string") {
				config.method = method;
				return app;
			}
			return config.method;
		});

		/**
		 * returns `RequestModule.RequestConfig["url"]`
		 * @method url
		 * @memberof RequestModule#
		 * @returns {string}
		 * @see RequestModule.RequestConfig
		 *//**
		 * @memberof RequestModule#
		 * @method url
		 * @param {string} url setup the url that should be processed
		 * @returns {RequestModule}
		 */
		app.bind("url", function (url) {
			if (typeof(url) === "string") {
				config.url = url;
				return app;
			}
			return config.url;
		});


		/**
		 * @method basicAuth
		 * @memberof RequestModule#
		 * @returns {{username: string, password: string}}
		 * @see RequestModule.RequestConfig
		 *//**
		 * @memberof RequestModule#
		 * @method basicAuth
		 * @param {string|null} username
		 * @param {string|null} password
		 * @returns {RequestModule}
		 */
		 app.bind("basicAuth", function (username, password) {
			if (typeof(username) === "string") {
				config.BasicAuthUsername = username;
				config.BasicAuthPassword = password;
				return app;
			}
			if (username === null) {
				config.BasicAuthUsername = null;
				config.BasicAuthPassword = null;
				return app;
			}
			return {
				username: config.BasicAuthUsername,
				password: config.BasicAuthPassword
			};
		});

		/**
		 * @method open
		 * @memberof RequestModule#
		 * @param {string} [method="GET"]
		 * @param {string} [url]
		 * @param {boolean} [async]
		 * @param {number} [timeout] request timeout in seconds
		 * @returns {RequestModule}
		 */
		app.bind("open", function (method, url, async, timeout, username, password) {
			if (method && typeof(url) === "undefined" && typeof(async) === "undefined") {
				url = method;
				method = undefined;
			}
			app.timeout(timeout);
			config.opened   = true;
			if (typeof(username) === "string" || typeof(config.BasicAuthUsername) === "string") {
				httpRequest.open(
					method || config.method,
					url || config.url,
					async || config.async,
					username || config.BasicAuthUsername,
					password || config.BasicAuthPassword || ''
				);
			} else {
				httpRequest.open(method || config.method, url || config.url, async || config.async);
			}
			return app;
		});

		/**
		 * @method send
		 * @memberof RequestModule#
		 * @param {string|FormData|MediaStream} [data='']
		 * @param {("asFormData"|"json"|"urlencoded")} [type=null]
		 * @param {Object<string,string>} [headers]
		 * @returns {RequestModule}
		 */
		app.bind("send", function (data, type, headers) {
			if (!config.opened) {
				app.open();
			}
			if (config.isSent) {
				console.warn("Error: the request is sended twice", app, app.request());
				return app;
			}
			
			var header;
			var _headers = Object.assign({}, config.headers || {}, headers || {});
			for (header in _headers) {
				httpRequest.setRequestHeader(header, _headers[header]);
			}

			config.headers = {};

			config.isSent   = true;
			if (type === "asFormData") {
				app.configurator("multipart");
				// convert data to form data
				var fData = new FormData();
				var i;
				for (i in data) {
					fData.append(i, data[i]);
				}
				httpRequest.send(fData);
			} else if (type === "json") {
				app.configurator("prepare-json");
				httpRequest.send(JSON.stringify(data));
			} else {
				if (type === "urlencoded") {
					app.configurator("prepare-post");
				}
				httpRequest.send(typeof(data) === "undefined" ? null : data);
			}
			return app;
		});

		/**
		 * @method headers
		 * @memberof RequestModule#
		 * @returns {string}
		 */
		app.bind("headers", function () {
			return httpRequest.getAllResponseHeaders();
		});

		/**
		 * set a new header
		 * @method headers
		 * @memberof RequestModule#
		 * @param {string} name
		 * @param {string} value
		 * @returns {RequestModule}
		 */
		app.bind("header", function (name, value) {
			if (config.opened) {
				httpRequest.setRequestHeader(name, value);
			} else {
				config.headers[name] = value;
			}
			return app;
		});

		return app;
	};
	module.exports  = Request;
});