//################################################################################################
/** @file XHR Networking mixin for ish.js
* @mixin ish.io.net
* @author Nick Campbell
* @license MIT
* @copyright 2014-2023, Nick Campbell
* @ignore
*/ //############################################################################################
/*global module, define, require, XMLHttpRequest, ActiveXObject, Promise */ //# Enable Node globals for JSHint
/*jshint maxcomplexity:9 */ //# Enable max complexity warnings for JSHint
(function () {
'use strict'; //<MIXIN>
function init(core, XHRConstructor) {
//################################################################################################
/** Collection of XHR Networking-based functionality.
* @namespace ish.io.net
* @ignore
*/ //############################################################################################
core.oop.partial(core.io.net, function (oProtected) {
var _undefined /*= undefined*/,
//fnBaseVerbs = oProtected.verbs,
oXHROptions = {
async: true,
cache: true,
useCache: false
//beforeSend: undefined,
//onreadystatechange: undefined
}
;
//# Processes the verb into the correct response type (fetch, xhr or xhrAsync)
function processVerb(sVerb, sUrl, oBody, oOptions) {
return (oOptions.hasOwnProperty("fn") ? //# If this is an xhr request
(core.type.fn.is(oOptions.fn) ? //# If this is a sync xhr request
doXhr(sVerb, sUrl, oBody, core.extend({}, oXHROptions, oOptions)).send(oBody) :
doXhrPromise(sVerb, sUrl, oBody, core.extend({}, oXHROptions, oOptions))
) : //# Else this is a fetch request
oProtected.doFetch(sVerb, sUrl, oBody, oOptions)
);
} //# processVerb
//# Safely returns a new xhr instance via the XHRConstructor
function getXhr() {
//# IE5.5+ (ActiveXObject IE5.5-9), based on http://toddmotto.com/writing-a-standalone-ajax-xhr-javascript-micro-library/
try {
return new XHRConstructor('MSXML2.XMLHTTP.3.0');
} catch (e) {
core.type.is.ish.expectedErrorHandler(e);
//return undefined;
}
} //# getXhr
//# Wrapper for an XHR call
function doXhr(sVerb, sUrl, oBody, oOptions) {
var oReturnVal, iMS,
bResponseTypeText = (!oOptions.responseType || (core.type.str.is(oOptions.responseType, true) && oOptions.responseType.trim().toLowerCase() === "text")),
bAsync = !!oOptions.async,
$xhr = getXhr(),
bValidRequest = ($xhr && core.type.str.is(sUrl, true)),
bAbort = false,
oData = core.resolve(core.io.net.cache(), [sVerb.toLowerCase(), sUrl])
;
//# If we are supposed to .useCache and we were able to find the oData in the .cache
if (oOptions.useCache && core.type.obj.is(oData)) {
//#
core.resolve($xhr, 'fromCache', true); //# $xhr.fromCache = true;
oOptions.fn( /* bSuccess, oData, vArg, $xhr */
oData.ok,
oData,
oOptions.arg,
$xhr
);
}
//# Else if we were able to collect an $xhr object and we have an sUrl
else if (bValidRequest) {
//# Setup the $xhr callback
//$xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
$xhr.onreadystatechange = function () {
//# If the request is finished and the .responseText is ready
if ($xhr.readyState === 4) {
oData = {
ok: (($xhr.status >= 200 && $xhr.status <= 299) || ($xhr.status === 0 && sUrl.substr(0, 7) === "file://")),
status: $xhr.status,
url: sUrl,
verb: sVerb,
async: bAsync,
aborted: bAbort,
response: $xhr[bResponseTypeText ? 'responseText' : 'response'],
text: bResponseTypeText ? $xhr.responseText : null,
json: bResponseTypeText ? core.type.fn.tryCatch(JSON.parse)($xhr.responseText) : null
};
oData.loaded = oData.ok; //# TODO: Remove
oData.data = oData.json; //# TODO: Remove
//#
if (oOptions.cache) {
core.resolve(true, core.io.net.cache(), [sVerb.toLowerCase(), sUrl], oData);
}
//# If the oData isn't .ok, we haven't bAbort'ed and we have a .retry function, recurse via setTimeout to run another $xhr instance
if (
!oData.ok &&
!bAbort &&
core.type.fn.is(oOptions.retry) &&
core.type.int.is(iMS = oOptions.retry(oOptions.attempts++))
) {
setTimeout(function () {
doXhr(sVerb, sUrl, oBody, oOptions)
.send(oBody)
;
}, iMS);
}
//#
else {
oOptions.fn(/* bSuccess, oData, vArg, $xhr */
!bAbort && oData.ok,
oData,
oOptions.arg,
$xhr
);
}
}
};
}
//# Else we were unable to collect the $xhr, so signal a failure to the oOptions.fn
else {
core.type.fn.call(oOptions.fn, [false, null, oOptions.arg, $xhr]);
}
//# Build then return our (mostly) chainable oReturnVal
oReturnVal = {
xhr: $xhr,
send: function (vBody, fnHook) {
var a_sKeys, i,
sBody = ""
;
//# If we were able to collect an $xhr object and we have an sUrl, .open and .send the request now
if (bValidRequest) {
$xhr.open(sVerb, sUrl, bAsync);
//#
if (core.type.str.is(vBody)) {
sBody = vBody;
}
else if (core.type.obj.is(vBody)) {
sBody = JSON.stringify(vBody);
}
else if (core.type.is.value(vBody)) {
sBody = core.type.str.mk(vBody);
}
//#
if (core.type.fn.is(fnHook)) {
fnHook($xhr);
}
//#
else {
if (core.type.obj.is(oOptions.headers)) {
a_sKeys = core.type.obj.ownKeys(oOptions.headers);
for (i = 0; i < a_sKeys.length; i++) {
$xhr.setRequestHeader(a_sKeys[i], oOptions.headers[a_sKeys[i]]);
}
}
if (core.type.str.is(oOptions.mimeType, true)) {
$xhr.overrideMimeType(oOptions.mimeType); // 'application/json; charset=utf-8' 'text/plain'
}
if (core.type.str.is(oOptions.contentType, true)) {
$xhr.setRequestHeader('Content-Type', oOptions.contentType); //# 'text/plain'
}
if (core.type.str.is(oOptions.responseType, true)) {
$xhr.responseType = oOptions.responseType; //# 'text'
}
if (!oOptions.useCache) {
$xhr.setRequestHeader("Cache-Control", "no-cache, max-age=0");
}
}
$xhr.send(sBody || null);
}
return oReturnVal;
},
abort: function () {
bAbort = true;
$xhr.abort();
core.io.console.warn("ish.io.net: Aborted " + sVerb + " " + sUrl);
return oReturnVal;
}
};
return oReturnVal;
} //# xhr
doXhr.options = function (oOptions) {
core.extend(oXHROptions, oOptions);
return oXHROptions;
}; //# doXhr.options
//# Wrapper for a Promise-ified XHR call
function doXhrPromise(sVerb, sUrl, oBody, oOptions) {
var bResponseTypeText;
//#
function setupXhr(fnPromiseResolve, fnPromiseReject) {
var a_sKeys, oData, iMS, i,
$xhr = getXhr()
;
//# Setup the $xhr callback
$xhr.onreadystatechange = function () {
//# If the request is finished and the .responseType is ready
if ($xhr.readyState === 4) {
oData = {
ok: (($xhr.status >= 200 && $xhr.status <= 299) || ($xhr.status === 0 && sUrl.substr(0, 7) === "file://")),
status: $xhr.status,
url: sUrl,
verb: sVerb,
async: true,
aborted: false,
response: $xhr[bResponseTypeText ? 'responseText' : 'response'],
text: (bResponseTypeText ? $xhr.responseText : null),
json: (bResponseTypeText ? core.type.fn.tryCatch(JSON.parse)($xhr.responseText) : null)
};
//oData.loaded = oData.ok;
//oData.data = oData.json;
//#
if (oXHROptions.cache) {
core.resolve(true, core.io.net.cache(), [sVerb.toLowerCase(), sUrl], oData);
}
//# If the oData was not .ok and we have a oOptions.retry, recurse via setTimeout to run another $xhr instance (calculating the iMS as we go)
if (
!oData.ok &&
core.type.fn.is(oOptions.retry) &&
core.type.int.is(iMS = oOptions.retry(oOptions.attempts++))
) {
setTimeout(function () {
$xhr = setupXhr(fnPromiseResolve, fnPromiseReject);
}, iMS);
}
//# If the oData was .ok, fnPromiseResolve
else if (oData.ok) {
fnPromiseResolve(oData);
}
//# Else the oData isn't .ok, so fnPromiseReject
else {
//fnPromiseReject(oData);
fnPromiseResolve(oData);
}
}
};
//#
$xhr.open(sVerb, sUrl, true);
if (core.type.obj.is(oOptions.headers)) {
a_sKeys = core.type.obj.ownKeys(oOptions.headers);
for (i = 0; i < a_sKeys.length; i++) {
$xhr.setRequestHeader(a_sKeys[i], oOptions.headers[a_sKeys[i]]);
}
}
if (core.type.str.is(oOptions.mimeType, true)) {
$xhr.overrideMimeType(oOptions.mimeType); // 'application/json; charset=utf-8' 'text/plain'
}
if (core.type.str.is(oOptions.contentType, true)) {
$xhr.setRequestHeader('Content-Type', oOptions.contentType); //# 'text/plain'
}
if (core.type.str.is(oOptions.responseType, true)) {
$xhr.responseType = oOptions.responseType; //# 'text'
}
if (!oOptions.useCache) {
$xhr.setRequestHeader("Cache-Control", "no-cache, max-age=0");
}
return $xhr;
} //# setupXhr
//# Ensure the passed oOptions .is an .obj then set bResponseTypeText
oOptions = core.type.obj.mk(oOptions);
bResponseTypeText = (!oOptions.responseType || (core.type.str.is(oOptions.responseType, true) && oOptions.responseType.trim().toLowerCase() === "text"));
//# Wrap the new Promise() call, returning undefined if it's unavailable
try {
return new Promise(function (resolve, reject) {
var $xhr = setupXhr(resolve, reject);
$xhr.send(oBody);
});
} catch (e) {
//return undefined;
}
} //# doXhrPromise
//# Processes the vCallOptions for XHR calls
function processXHROptions(vCallOptions) {
return (core.type.fn.is(vCallOptions) ?
{ fn: vCallOptions, arg: null /*, cache: oOptions.cache, useCache: oOptions.useCache */ } :
vCallOptions
);
} //# processXHROptions
//# Override oProtected's .verbs to wire in .processVerb
oProtected.verbs = { //# GET, POST, PUT, PATCH, DELETE, HEAD + TRACE, CONNECT, OPTIONS - https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods
get: function (vBaseOptions) {
return function (sUrl, vCallOptions) {
return processVerb("GET", sUrl, _undefined, oProtected.processOptions(vBaseOptions, processXHROptions(vCallOptions)));
};
},
post: function (vBaseOptions) {
return function (sUrl, oBody, vCallOptions) {
return processVerb("POST", sUrl, oBody, oProtected.processOptions(vBaseOptions, processXHROptions(vCallOptions)));
};
},
put: function (vBaseOptions) {
return function (sUrl, oBody, vCallOptions) {
return processVerb("PUT", sUrl, oBody, oProtected.processOptions(vBaseOptions, processXHROptions(vCallOptions)));
};
},
"delete": function (vBaseOptions) {
return function (sUrl, vCallOptions) {
return processVerb("DELETE", sUrl, _undefined, oProtected.processOptions(vBaseOptions, processXHROptions(vCallOptions)));
};
},
head: function (vBaseOptions) {
return function (sUrl, vCallOptions) {
return processVerb("HEAD", sUrl, _undefined, oProtected.processOptions(vBaseOptions, processXHROptions(vCallOptions)));
};
}
}; //# oProtected.verbs
return core.extend(
//# Override the default io.net.* interfaces with the updated oProtected reference
oProtected.netInterfaceFactory(/*undefined*/),
{
//#########
/** XMLHttpRequest (XHR) management function.
* @function ish.io.net.xhr
* @param {string} sVerb Value representing the HTTP Verb.
* @param {boolean} bAsync Value representing if the HTTP request is to be asynchronous.
* @param {string} sUrl Value representing the URL to interrogate.
* @param {fnIshIoNetCallback|object} [vCallback] Value representing the function to be called when the request returns or the desired options:
* @param {fnIshIoNetCallback} [vCallback.fn] Value representing the function to be called when the request returns; <code>vCallback.fn(bSuccess, oResponse, vArg, $xhr)</code>.
* @param {variant} [vCallback.arg] Value representing the argument that will be passed to the callback function.
* @param {object} [vCallback.headers] Value representing the HTTP headers of the request (see: {@link: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader|Mozilla.org}).
* @param {string} [vCallback.mimeType] Value representing the MIME Type of the request (see: {@link: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/overrideMimeType|Mozilla.org}).
* @param {string} [vCallback.contentType] Value representing the Content Type HTTP Header of the request (see: {@link: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader|Mozilla.org}).<note>When <code>vCallback.contentType</code> is set, its value will override any value set in <code>vCallback.headers['content-type']</code>.</note>
* @param {string} [vCallback.responseType='text'] Value representing the type of data contained in the response (see: {@link: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType|Mozilla.org}).
* @param {boolean} [vCallback.cache=false] Value representing if the response is to be cached.
* @param {boolean} [vCallback.useCache=false] Value representing if the response is to be sourced from the cache if it's available.<note>When <code>!vCallback.useCache</code>, the HTTP Header <code>Cache-Control</code> is set to <code>no-cache, max-age=0</code>.</note>
* @param {function} [fnRetry] Value representing the function to be called when the request is to be retried.
* @param {object} [oBody] Value representing the body of the request.
* @returns {object} =interface Value representing the following properties:
* @returns {function} =interface.send Sends the request; <code>send(vBody, fnHook)</code>:
* <table class="params">
* <tr><td class="name"><code>vBody</code><td><td class="type param-type">variant | object<td><td class="description last">Value representing the body of the call.</td></tr>
* <tr><td class="name"><code>fnHook</code><td><td class="type param-type">function<td><td class="description last">Value representing the function to be called before the request is sent via the underlying <code>XMLHttpRequest</code> management object; <code>fnHook($xhr)</code>.</td></tr>
* </table>
* @returns {function} =interface.abort Aborts the request; <code>abort()</code>.
* @returns {object} =interface.xhr Value representing the underlying <code>XMLHttpRequest</code> management object.
*/ //#####
xhr: function (sVerb, bAsync, sUrl, vCallback, fnRetry, oBody) {
return doXhr(sVerb, sUrl, oBody,
core.extend({},
processXHROptions(vCallback),
{ retry: fnRetry, async: bAsync }
)
);
}
//xhr.options = xhr.options
}
);
}); //# core.io.net
//# .fire the plugin's loaded event
core.io.event.fire("ish.io.net-xhr");
//# Return core to allow for chaining
return core;
}
//# If we are running server-side
//# NOTE: Compliant with UMD, see: https://github.com/umdjs/umd/blob/master/templates/returnExports.js
//# NOTE: Does not work with strict CommonJS, but only CommonJS-like environments that support module.exports, like Node.
if (typeof module === 'object' && module.exports) { //if (typeof module !== 'undefined' && this.module !== module && module.exports) {
module.exports = function (core) {
return init(core, require("xmlhttprequest").XMLHttpRequest);
};
}
//# Else if we are running in an .amd environment, register as an anonymous module
else if (typeof define === 'function' && define.amd) {
define([], function (core) {
return init(core, XMLHttpRequest || ActiveXObject);
});
}
//# Else we are running in the browser, so we need to setup the _document-based features
else {
/*global ActiveXObject: false */ //# JSHint "ActiveXObject variable undefined" error suppressor
return init(window.head.ish || document.querySelector("SCRIPT[ish]").ish, XMLHttpRequest || ActiveXObject);
}
//</MIXIN>
}());