1. //################################################################################################
  2. /** @file XHR Networking mixin for ish.js
  3. * @mixin ish.io.net
  4. * @author Nick Campbell
  5. * @license MIT
  6. * @copyright 2014-2023, Nick Campbell
  7. * @ignore
  8. */ //############################################################################################
  9. /*global module, define, require, XMLHttpRequest, ActiveXObject, Promise */ //# Enable Node globals for JSHint
  10. /*jshint maxcomplexity:9 */ //# Enable max complexity warnings for JSHint
  11. (function () {
  12. 'use strict'; //<MIXIN>
  13. function init(core, XHRConstructor) {
  14. //################################################################################################
  15. /** Collection of XHR Networking-based functionality.
  16. * @namespace ish.io.net
  17. * @ignore
  18. */ //############################################################################################
  19. core.oop.partial(core.io.net, function (oProtected) {
  20. var _undefined /*= undefined*/,
  21. //fnBaseVerbs = oProtected.verbs,
  22. oXHROptions = {
  23. async: true,
  24. cache: true,
  25. useCache: false
  26. //beforeSend: undefined,
  27. //onreadystatechange: undefined
  28. }
  29. ;
  30. //# Processes the verb into the correct response type (fetch, xhr or xhrAsync)
  31. function processVerb(sVerb, sUrl, oBody, oOptions) {
  32. return (oOptions.hasOwnProperty("fn") ? //# If this is an xhr request
  33. (core.type.fn.is(oOptions.fn) ? //# If this is a sync xhr request
  34. doXhr(sVerb, sUrl, oBody, core.extend({}, oXHROptions, oOptions)).send(oBody) :
  35. doXhrPromise(sVerb, sUrl, oBody, core.extend({}, oXHROptions, oOptions))
  36. ) : //# Else this is a fetch request
  37. oProtected.doFetch(sVerb, sUrl, oBody, oOptions)
  38. );
  39. } //# processVerb
  40. //# Safely returns a new xhr instance via the XHRConstructor
  41. function getXhr() {
  42. //# IE5.5+ (ActiveXObject IE5.5-9), based on http://toddmotto.com/writing-a-standalone-ajax-xhr-javascript-micro-library/
  43. try {
  44. return new XHRConstructor('MSXML2.XMLHTTP.3.0');
  45. } catch (e) {
  46. core.type.is.ish.expectedErrorHandler(e);
  47. //return undefined;
  48. }
  49. } //# getXhr
  50. //# Wrapper for an XHR call
  51. function doXhr(sVerb, sUrl, oBody, oOptions) {
  52. var oReturnVal, iMS,
  53. bResponseTypeText = (!oOptions.responseType || (core.type.str.is(oOptions.responseType, true) && oOptions.responseType.trim().toLowerCase() === "text")),
  54. bAsync = !!oOptions.async,
  55. $xhr = getXhr(),
  56. bValidRequest = ($xhr && core.type.str.is(sUrl, true)),
  57. bAbort = false,
  58. oData = core.resolve(core.io.net.cache(), [sVerb.toLowerCase(), sUrl])
  59. ;
  60. //# If we are supposed to .useCache and we were able to find the oData in the .cache
  61. if (oOptions.useCache && core.type.obj.is(oData)) {
  62. //#
  63. core.resolve($xhr, 'fromCache', true); //# $xhr.fromCache = true;
  64. oOptions.fn( /* bSuccess, oData, vArg, $xhr */
  65. oData.ok,
  66. oData,
  67. oOptions.arg,
  68. $xhr
  69. );
  70. }
  71. //# Else if we were able to collect an $xhr object and we have an sUrl
  72. else if (bValidRequest) {
  73. //# Setup the $xhr callback
  74. //$xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
  75. $xhr.onreadystatechange = function () {
  76. //# If the request is finished and the .responseText is ready
  77. if ($xhr.readyState === 4) {
  78. oData = {
  79. ok: (($xhr.status >= 200 && $xhr.status <= 299) || ($xhr.status === 0 && sUrl.substr(0, 7) === "file://")),
  80. status: $xhr.status,
  81. url: sUrl,
  82. verb: sVerb,
  83. async: bAsync,
  84. aborted: bAbort,
  85. response: $xhr[bResponseTypeText ? 'responseText' : 'response'],
  86. text: bResponseTypeText ? $xhr.responseText : null,
  87. json: bResponseTypeText ? core.type.fn.tryCatch(JSON.parse)($xhr.responseText) : null
  88. };
  89. oData.loaded = oData.ok; //# TODO: Remove
  90. oData.data = oData.json; //# TODO: Remove
  91. //#
  92. if (oOptions.cache) {
  93. core.resolve(true, core.io.net.cache(), [sVerb.toLowerCase(), sUrl], oData);
  94. }
  95. //# 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
  96. if (
  97. !oData.ok &&
  98. !bAbort &&
  99. core.type.fn.is(oOptions.retry) &&
  100. core.type.int.is(iMS = oOptions.retry(oOptions.attempts++))
  101. ) {
  102. setTimeout(function () {
  103. doXhr(sVerb, sUrl, oBody, oOptions)
  104. .send(oBody)
  105. ;
  106. }, iMS);
  107. }
  108. //#
  109. else {
  110. oOptions.fn(/* bSuccess, oData, vArg, $xhr */
  111. !bAbort && oData.ok,
  112. oData,
  113. oOptions.arg,
  114. $xhr
  115. );
  116. }
  117. }
  118. };
  119. }
  120. //# Else we were unable to collect the $xhr, so signal a failure to the oOptions.fn
  121. else {
  122. core.type.fn.call(oOptions.fn, [false, null, oOptions.arg, $xhr]);
  123. }
  124. //# Build then return our (mostly) chainable oReturnVal
  125. oReturnVal = {
  126. xhr: $xhr,
  127. send: function (vBody, fnHook) {
  128. var a_sKeys, i,
  129. sBody = ""
  130. ;
  131. //# If we were able to collect an $xhr object and we have an sUrl, .open and .send the request now
  132. if (bValidRequest) {
  133. $xhr.open(sVerb, sUrl, bAsync);
  134. //#
  135. if (core.type.str.is(vBody)) {
  136. sBody = vBody;
  137. }
  138. else if (core.type.obj.is(vBody)) {
  139. sBody = JSON.stringify(vBody);
  140. }
  141. else if (core.type.is.value(vBody)) {
  142. sBody = core.type.str.mk(vBody);
  143. }
  144. //#
  145. if (core.type.fn.is(fnHook)) {
  146. fnHook($xhr);
  147. }
  148. //#
  149. else {
  150. if (core.type.obj.is(oOptions.headers)) {
  151. a_sKeys = core.type.obj.ownKeys(oOptions.headers);
  152. for (i = 0; i < a_sKeys.length; i++) {
  153. $xhr.setRequestHeader(a_sKeys[i], oOptions.headers[a_sKeys[i]]);
  154. }
  155. }
  156. if (core.type.str.is(oOptions.mimeType, true)) {
  157. $xhr.overrideMimeType(oOptions.mimeType); // 'application/json; charset=utf-8' 'text/plain'
  158. }
  159. if (core.type.str.is(oOptions.contentType, true)) {
  160. $xhr.setRequestHeader('Content-Type', oOptions.contentType); //# 'text/plain'
  161. }
  162. if (core.type.str.is(oOptions.responseType, true)) {
  163. $xhr.responseType = oOptions.responseType; //# 'text'
  164. }
  165. if (!oOptions.useCache) {
  166. $xhr.setRequestHeader("Cache-Control", "no-cache, max-age=0");
  167. }
  168. }
  169. $xhr.send(sBody || null);
  170. }
  171. return oReturnVal;
  172. },
  173. abort: function () {
  174. bAbort = true;
  175. $xhr.abort();
  176. core.io.console.warn("ish.io.net: Aborted " + sVerb + " " + sUrl);
  177. return oReturnVal;
  178. }
  179. };
  180. return oReturnVal;
  181. } //# xhr
  182. doXhr.options = function (oOptions) {
  183. core.extend(oXHROptions, oOptions);
  184. return oXHROptions;
  185. }; //# doXhr.options
  186. //# Wrapper for a Promise-ified XHR call
  187. function doXhrPromise(sVerb, sUrl, oBody, oOptions) {
  188. var bResponseTypeText;
  189. //#
  190. function setupXhr(fnPromiseResolve, fnPromiseReject) {
  191. var a_sKeys, oData, iMS, i,
  192. $xhr = getXhr()
  193. ;
  194. //# Setup the $xhr callback
  195. $xhr.onreadystatechange = function () {
  196. //# If the request is finished and the .responseType is ready
  197. if ($xhr.readyState === 4) {
  198. oData = {
  199. ok: (($xhr.status >= 200 && $xhr.status <= 299) || ($xhr.status === 0 && sUrl.substr(0, 7) === "file://")),
  200. status: $xhr.status,
  201. url: sUrl,
  202. verb: sVerb,
  203. async: true,
  204. aborted: false,
  205. response: $xhr[bResponseTypeText ? 'responseText' : 'response'],
  206. text: (bResponseTypeText ? $xhr.responseText : null),
  207. json: (bResponseTypeText ? core.type.fn.tryCatch(JSON.parse)($xhr.responseText) : null)
  208. };
  209. //oData.loaded = oData.ok;
  210. //oData.data = oData.json;
  211. //#
  212. if (oXHROptions.cache) {
  213. core.resolve(true, core.io.net.cache(), [sVerb.toLowerCase(), sUrl], oData);
  214. }
  215. //# 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)
  216. if (
  217. !oData.ok &&
  218. core.type.fn.is(oOptions.retry) &&
  219. core.type.int.is(iMS = oOptions.retry(oOptions.attempts++))
  220. ) {
  221. setTimeout(function () {
  222. $xhr = setupXhr(fnPromiseResolve, fnPromiseReject);
  223. }, iMS);
  224. }
  225. //# If the oData was .ok, fnPromiseResolve
  226. else if (oData.ok) {
  227. fnPromiseResolve(oData);
  228. }
  229. //# Else the oData isn't .ok, so fnPromiseReject
  230. else {
  231. //fnPromiseReject(oData);
  232. fnPromiseResolve(oData);
  233. }
  234. }
  235. };
  236. //#
  237. $xhr.open(sVerb, sUrl, true);
  238. if (core.type.obj.is(oOptions.headers)) {
  239. a_sKeys = core.type.obj.ownKeys(oOptions.headers);
  240. for (i = 0; i < a_sKeys.length; i++) {
  241. $xhr.setRequestHeader(a_sKeys[i], oOptions.headers[a_sKeys[i]]);
  242. }
  243. }
  244. if (core.type.str.is(oOptions.mimeType, true)) {
  245. $xhr.overrideMimeType(oOptions.mimeType); // 'application/json; charset=utf-8' 'text/plain'
  246. }
  247. if (core.type.str.is(oOptions.contentType, true)) {
  248. $xhr.setRequestHeader('Content-Type', oOptions.contentType); //# 'text/plain'
  249. }
  250. if (core.type.str.is(oOptions.responseType, true)) {
  251. $xhr.responseType = oOptions.responseType; //# 'text'
  252. }
  253. if (!oOptions.useCache) {
  254. $xhr.setRequestHeader("Cache-Control", "no-cache, max-age=0");
  255. }
  256. return $xhr;
  257. } //# setupXhr
  258. //# Ensure the passed oOptions .is an .obj then set bResponseTypeText
  259. oOptions = core.type.obj.mk(oOptions);
  260. bResponseTypeText = (!oOptions.responseType || (core.type.str.is(oOptions.responseType, true) && oOptions.responseType.trim().toLowerCase() === "text"));
  261. //# Wrap the new Promise() call, returning undefined if it's unavailable
  262. try {
  263. return new Promise(function (resolve, reject) {
  264. var $xhr = setupXhr(resolve, reject);
  265. $xhr.send(oBody);
  266. });
  267. } catch (e) {
  268. //return undefined;
  269. }
  270. } //# doXhrPromise
  271. //# Processes the vCallOptions for XHR calls
  272. function processXHROptions(vCallOptions) {
  273. return (core.type.fn.is(vCallOptions) ?
  274. { fn: vCallOptions, arg: null /*, cache: oOptions.cache, useCache: oOptions.useCache */ } :
  275. vCallOptions
  276. );
  277. } //# processXHROptions
  278. //# Override oProtected's .verbs to wire in .processVerb
  279. oProtected.verbs = { //# GET, POST, PUT, PATCH, DELETE, HEAD + TRACE, CONNECT, OPTIONS - https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods
  280. get: function (vBaseOptions) {
  281. return function (sUrl, vCallOptions) {
  282. return processVerb("GET", sUrl, _undefined, oProtected.processOptions(vBaseOptions, processXHROptions(vCallOptions)));
  283. };
  284. },
  285. post: function (vBaseOptions) {
  286. return function (sUrl, oBody, vCallOptions) {
  287. return processVerb("POST", sUrl, oBody, oProtected.processOptions(vBaseOptions, processXHROptions(vCallOptions)));
  288. };
  289. },
  290. put: function (vBaseOptions) {
  291. return function (sUrl, oBody, vCallOptions) {
  292. return processVerb("PUT", sUrl, oBody, oProtected.processOptions(vBaseOptions, processXHROptions(vCallOptions)));
  293. };
  294. },
  295. "delete": function (vBaseOptions) {
  296. return function (sUrl, vCallOptions) {
  297. return processVerb("DELETE", sUrl, _undefined, oProtected.processOptions(vBaseOptions, processXHROptions(vCallOptions)));
  298. };
  299. },
  300. head: function (vBaseOptions) {
  301. return function (sUrl, vCallOptions) {
  302. return processVerb("HEAD", sUrl, _undefined, oProtected.processOptions(vBaseOptions, processXHROptions(vCallOptions)));
  303. };
  304. }
  305. }; //# oProtected.verbs
  306. return core.extend(
  307. //# Override the default io.net.* interfaces with the updated oProtected reference
  308. oProtected.netInterfaceFactory(/*undefined*/),
  309. {
  310. //#########
  311. /** XMLHttpRequest (XHR) management function.
  312. * @function ish.io.net.xhr
  313. * @param {string} sVerb Value representing the HTTP Verb.
  314. * @param {boolean} bAsync Value representing if the HTTP request is to be asynchronous.
  315. * @param {string} sUrl Value representing the URL to interrogate.
  316. * @param {fnIshIoNetCallback|object} [vCallback] Value representing the function to be called when the request returns or the desired options:
  317. * @param {fnIshIoNetCallback} [vCallback.fn] Value representing the function to be called when the request returns; <code>vCallback.fn(bSuccess, oResponse, vArg, $xhr)</code>.
  318. * @param {variant} [vCallback.arg] Value representing the argument that will be passed to the callback function.
  319. * @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}).
  320. * @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}).
  321. * @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>
  322. * @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}).
  323. * @param {boolean} [vCallback.cache=false] Value representing if the response is to be cached.
  324. * @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>
  325. * @param {function} [fnRetry] Value representing the function to be called when the request is to be retried.
  326. * @param {object} [oBody] Value representing the body of the request.
  327. * @returns {object} =interface Value representing the following properties:
  328. * @returns {function} =interface.send Sends the request; <code>send(vBody, fnHook)</code>:
  329. * <table class="params">
  330. * <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>
  331. * <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>
  332. * </table>
  333. * @returns {function} =interface.abort Aborts the request; <code>abort()</code>.
  334. * @returns {object} =interface.xhr Value representing the underlying <code>XMLHttpRequest</code> management object.
  335. */ //#####
  336. xhr: function (sVerb, bAsync, sUrl, vCallback, fnRetry, oBody) {
  337. return doXhr(sVerb, sUrl, oBody,
  338. core.extend({},
  339. processXHROptions(vCallback),
  340. { retry: fnRetry, async: bAsync }
  341. )
  342. );
  343. }
  344. //xhr.options = xhr.options
  345. }
  346. );
  347. }); //# core.io.net
  348. //# .fire the plugin's loaded event
  349. core.io.event.fire("ish.io.net-xhr");
  350. //# Return core to allow for chaining
  351. return core;
  352. }
  353. //# If we are running server-side
  354. //# NOTE: Compliant with UMD, see: https://github.com/umdjs/umd/blob/master/templates/returnExports.js
  355. //# NOTE: Does not work with strict CommonJS, but only CommonJS-like environments that support module.exports, like Node.
  356. if (typeof module === 'object' && module.exports) { //if (typeof module !== 'undefined' && this.module !== module && module.exports) {
  357. module.exports = function (core) {
  358. return init(core, require("xmlhttprequest").XMLHttpRequest);
  359. };
  360. }
  361. //# Else if we are running in an .amd environment, register as an anonymous module
  362. else if (typeof define === 'function' && define.amd) {
  363. define([], function (core) {
  364. return init(core, XMLHttpRequest || ActiveXObject);
  365. });
  366. }
  367. //# Else we are running in the browser, so we need to setup the _document-based features
  368. else {
  369. /*global ActiveXObject: false */ //# JSHint "ActiveXObject variable undefined" error suppressor
  370. return init(window.head.ish || document.querySelector("SCRIPT[ish]").ish, XMLHttpRequest || ActiveXObject);
  371. }
  372. //</MIXIN>
  373. }());