UNPKG

mithril

Version:

A framework for building brilliant applications

200 lines (176 loc) 6.8 kB
"use strict" var buildPathname = require("../pathname/build") var hasOwn = require("../util/hasOwn") module.exports = function($window, oncompletion) { function PromiseProxy(executor) { return new Promise(executor) } function makeRequest(url, args) { return new Promise(function(resolve, reject) { url = buildPathname(url, args.params) var method = args.method != null ? args.method.toUpperCase() : "GET" var body = args.body var assumeJSON = (args.serialize == null || args.serialize === JSON.serialize) && !(body instanceof $window.FormData || body instanceof $window.URLSearchParams) var responseType = args.responseType || (typeof args.extract === "function" ? "" : "json") var xhr = new $window.XMLHttpRequest(), aborted = false, isTimeout = false var original = xhr, replacedAbort var abort = xhr.abort xhr.abort = function() { aborted = true abort.call(this) } xhr.open(method, url, args.async !== false, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined) if (assumeJSON && body != null && !hasHeader(args, "content-type")) { xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8") } if (typeof args.deserialize !== "function" && !hasHeader(args, "accept")) { xhr.setRequestHeader("Accept", "application/json, text/*") } if (args.withCredentials) xhr.withCredentials = args.withCredentials if (args.timeout) xhr.timeout = args.timeout xhr.responseType = responseType for (var key in args.headers) { if (hasOwn.call(args.headers, key)) { xhr.setRequestHeader(key, args.headers[key]) } } xhr.onreadystatechange = function(ev) { // Don't throw errors on xhr.abort(). if (aborted) return if (ev.target.readyState === 4) { try { var success = (ev.target.status >= 200 && ev.target.status < 300) || ev.target.status === 304 || (/^file:\/\//i).test(url) // When the response type isn't "" or "text", // `xhr.responseText` is the wrong thing to use. // Browsers do the right thing and throw here, and we // should honor that and do the right thing by // preferring `xhr.response` where possible/practical. var response = ev.target.response, message if (responseType === "json") { // For IE and Edge, which don't implement // `responseType: "json"`. if (!ev.target.responseType && typeof args.extract !== "function") { // Handle no-content which will not parse. try { response = JSON.parse(ev.target.responseText) } catch (e) { response = null } } } else if (!responseType || responseType === "text") { // Only use this default if it's text. If a parsed // document is needed on old IE and friends (all // unsupported), the user should use a custom // `config` instead. They're already using this at // their own risk. if (response == null) response = ev.target.responseText } if (typeof args.extract === "function") { response = args.extract(ev.target, args) success = true } else if (typeof args.deserialize === "function") { response = args.deserialize(response) } if (success) { if (typeof args.type === "function") { if (Array.isArray(response)) { for (var i = 0; i < response.length; i++) { response[i] = new args.type(response[i]) } } else response = new args.type(response) } resolve(response) } else { var completeErrorResponse = function() { try { message = ev.target.responseText } catch (e) { message = response } var error = new Error(message) error.code = ev.target.status error.response = response reject(error) } if (xhr.status === 0) { // Use setTimeout to push this code block onto the event queue // This allows `xhr.ontimeout` to run in the case that there is a timeout // Without this setTimeout, `xhr.ontimeout` doesn't have a chance to reject // as `xhr.onreadystatechange` will run before it setTimeout(function() { if (isTimeout) return completeErrorResponse() }) } else completeErrorResponse() } } catch (e) { reject(e) } } } xhr.ontimeout = function (ev) { isTimeout = true var error = new Error("Request timed out") error.code = ev.target.status reject(error) } if (typeof args.config === "function") { xhr = args.config(xhr, args, url) || xhr // Propagate the `abort` to any replacement XHR as well. if (xhr !== original) { replacedAbort = xhr.abort xhr.abort = function() { aborted = true replacedAbort.call(this) } } } if (body == null) xhr.send() else if (typeof args.serialize === "function") xhr.send(args.serialize(body)) else if (body instanceof $window.FormData || body instanceof $window.URLSearchParams) xhr.send(body) else xhr.send(JSON.stringify(body)) }) } // In case the global Promise is some userland library's where they rely on // `foo instanceof this.constructor`, `this.constructor.resolve(value)`, or // similar. Let's *not* break them. PromiseProxy.prototype = Promise.prototype PromiseProxy.__proto__ = Promise // eslint-disable-line no-proto function hasHeader(args, name) { for (var key in args.headers) { if (hasOwn.call(args.headers, key) && key.toLowerCase() === name) return true } return false } return { request: function(url, args) { if (typeof url !== "string") { args = url; url = url.url } else if (args == null) args = {} var promise = makeRequest(url, args) if (args.background === true) return promise var count = 0 function complete() { if (--count === 0 && typeof oncompletion === "function") oncompletion() } return wrap(promise) function wrap(promise) { var then = promise.then // Set the constructor, so engines know to not await or resolve // this as a native promise. At the time of writing, this is // only necessary for V8, but their behavior is the correct // behavior per spec. See this spec issue for more details: // https://github.com/tc39/ecma262/issues/1577. Also, see the // corresponding comment in `request/tests/test-request.js` for // a bit more background on the issue at hand. promise.constructor = PromiseProxy promise.then = function() { count++ var next = then.apply(promise, arguments) next.then(complete, function(e) { complete() if (count === 0) throw e }) return wrap(next) } return promise } } } }