aurelia-http-client
Version:
A simple, restful, message-based wrapper around XMLHttpRequest.
748 lines (610 loc) • 19.8 kB
JavaScript
import { join, buildQueryString } from 'aurelia-path';
import { PLATFORM, DOM } from 'aurelia-pal';
export let Headers = class Headers {
constructor(headers = {}) {
this.headers = {};
for (let key in headers) {
this.headers[key.toLowerCase()] = { key, value: headers[key] };
}
}
add(key, value) {
this.headers[key.toLowerCase()] = { key, value };
}
get(key) {
let header = this.headers[key.toLowerCase()];
return header ? header.value : undefined;
}
clear() {
this.headers = {};
}
has(header) {
return this.headers.hasOwnProperty(header.toLowerCase());
}
configureXHR(xhr) {
for (let name in this.headers) {
if (this.headers.hasOwnProperty(name)) {
xhr.setRequestHeader(this.headers[name].key, this.headers[name].value);
}
}
}
static parse(headerStr) {
let headers = new Headers();
if (!headerStr) {
return headers;
}
let headerPairs = headerStr.split('\u000d\u000a');
for (let i = 0; i < headerPairs.length; i++) {
let headerPair = headerPairs[i];
let index = headerPair.indexOf('\u003a\u0020');
if (index > 0) {
let key = headerPair.substring(0, index);
let val = headerPair.substring(index + 2);
headers.add(key, val);
}
}
return headers;
}
};
export let RequestMessage = class RequestMessage {
constructor(method, url, content, headers) {
this.method = method;
this.url = url;
this.content = content;
this.headers = headers || new Headers();
this.baseUrl = '';
}
buildFullUrl() {
let absoluteUrl = /^([a-z][a-z0-9+\-.]*:)?\/\//i;
let url = absoluteUrl.test(this.url) ? this.url : join(this.baseUrl, this.url);
if (this.params) {
let qs = buildQueryString(this.params, this.traditional);
url = qs ? url + (this.url.indexOf('?') < 0 ? '?' : '&') + qs : url;
}
return url;
}
};
export let HttpResponseMessage = class HttpResponseMessage {
constructor(requestMessage, xhr, responseType, reviver) {
this.requestMessage = requestMessage;
this.statusCode = xhr.status;
this.response = xhr.response || xhr.responseText;
this.isSuccess = xhr.status >= 200 && xhr.status < 400;
this.statusText = xhr.statusText;
this.reviver = reviver;
this.mimeType = null;
if (xhr.getAllResponseHeaders) {
this.headers = Headers.parse(xhr.getAllResponseHeaders());
} else {
this.headers = new Headers();
}
let contentType;
if (this.headers && this.headers.headers) {
contentType = this.headers.get('Content-Type');
}
if (contentType) {
this.mimeType = responseType = contentType.split(';')[0].trim();
if (mimeTypes.hasOwnProperty(this.mimeType)) responseType = mimeTypes[this.mimeType];
}
this.responseType = responseType;
}
get content() {
try {
if (this._content !== undefined) {
return this._content;
}
if (this.response === undefined || this.response === null || this.response === '') {
this._content = this.response;
return this._content;
}
if (this.responseType === 'json') {
this._content = JSON.parse(this.response, this.reviver);
return this._content;
}
if (this.reviver) {
this._content = this.reviver(this.response);
return this._content;
}
this._content = this.response;
return this._content;
} catch (e) {
if (this.isSuccess) {
throw e;
}
this._content = null;
return this._content;
}
}
};
export let mimeTypes = {
'text/html': 'html',
'text/javascript': 'js',
'application/javascript': 'js',
'text/json': 'json',
'application/json': 'json',
'application/rss+xml': 'rss',
'application/atom+xml': 'atom',
'application/xhtml+xml': 'xhtml',
'text/markdown': 'md',
'text/xml': 'xml',
'text/mathml': 'mml',
'application/xml': 'xml',
'text/yml': 'yml',
'text/csv': 'csv',
'text/css': 'css',
'text/less': 'less',
'text/stylus': 'styl',
'text/scss': 'scss',
'text/sass': 'sass',
'text/plain': 'txt'
};
function applyXhrTransformers(xhrTransformers, client, processor, message, xhr) {
let i;
let ii;
for (i = 0, ii = xhrTransformers.length; i < ii; ++i) {
xhrTransformers[i](client, processor, message, xhr);
}
}
export let RequestMessageProcessor = class RequestMessageProcessor {
constructor(xhrType, xhrTransformers) {
this.XHRType = xhrType;
this.xhrTransformers = xhrTransformers;
this.isAborted = false;
}
abort() {
if (this.xhr && this.xhr.readyState !== PLATFORM.XMLHttpRequest.UNSENT) {
this.xhr.abort();
}
this.isAborted = true;
}
process(client, requestMessage) {
let promise = new Promise((resolve, reject) => {
let rejectResponse;
if (client.rejectPromiseWithErrorObject) {
rejectResponse = resp => {
const errorResp = new ErrorHttpResponseMessage(resp);
reject(errorResp);
};
} else {
rejectResponse = resp => {
reject(resp);
};
}
let xhr = this.xhr = new this.XHRType();
xhr.onload = e => {
let response = new HttpResponseMessage(requestMessage, xhr, requestMessage.responseType, requestMessage.reviver);
if (response.isSuccess) {
resolve(response);
} else {
rejectResponse(response);
}
};
xhr.ontimeout = e => {
rejectResponse(new HttpResponseMessage(requestMessage, {
response: e,
status: xhr.status,
statusText: xhr.statusText
}, 'timeout'));
};
xhr.onerror = e => {
rejectResponse(new HttpResponseMessage(requestMessage, {
response: e,
status: xhr.status,
statusText: xhr.statusText
}, 'error'));
};
xhr.onabort = e => {
rejectResponse(new HttpResponseMessage(requestMessage, {
response: e,
status: xhr.status,
statusText: xhr.statusText
}, 'abort'));
};
});
return Promise.resolve(requestMessage).then(message => {
let processRequest = () => {
if (this.isAborted) {
this.xhr.abort();
} else {
this.xhr.open(message.method, message.buildFullUrl(), true, message.user, message.password);
applyXhrTransformers(this.xhrTransformers, client, this, message, this.xhr);
if (typeof message.content === 'undefined') {
this.xhr.send();
} else {
this.xhr.send(message.content);
}
}
return promise;
};
let chain = [[processRequest, undefined]];
let interceptors = message.interceptors || [];
interceptors.forEach(function (interceptor) {
if (interceptor.request || interceptor.requestError) {
chain.unshift([interceptor.request ? interceptor.request.bind(interceptor) : undefined, interceptor.requestError ? interceptor.requestError.bind(interceptor) : undefined]);
}
if (interceptor.response || interceptor.responseError) {
chain.push([interceptor.response ? interceptor.response.bind(interceptor) : undefined, interceptor.responseError ? interceptor.responseError.bind(interceptor) : undefined]);
}
});
let interceptorsPromise = Promise.resolve(message);
while (chain.length) {
interceptorsPromise = interceptorsPromise.then(...chain.shift());
}
return interceptorsPromise;
});
}
};
export function timeoutTransformer(client, processor, message, xhr) {
if (message.timeout !== undefined) {
xhr.timeout = message.timeout;
}
}
export function callbackParameterNameTransformer(client, processor, message, xhr) {
if (message.callbackParameterName !== undefined) {
xhr.callbackParameterName = message.callbackParameterName;
}
}
export function credentialsTransformer(client, processor, message, xhr) {
if (message.withCredentials !== undefined) {
xhr.withCredentials = message.withCredentials;
}
}
export function progressTransformer(client, processor, message, xhr) {
if (message.progressCallback) {
xhr.upload.onprogress = message.progressCallback;
}
}
export function downloadProgressTransformer(client, processor, message, xhr) {
if (message.downloadProgressCallback) {
xhr.onprogress = message.downloadProgressCallback;
}
}
export function responseTypeTransformer(client, processor, message, xhr) {
let responseType = message.responseType;
if (responseType === 'json') {
responseType = 'text';
}
xhr.responseType = responseType;
}
export function headerTransformer(client, processor, message, xhr) {
message.headers.configureXHR(xhr);
}
export function contentTransformer(client, processor, message, xhr) {
if (message.skipContentProcessing) {
return;
}
if (PLATFORM.global.FormData && message.content instanceof FormData) {
return;
}
if (PLATFORM.global.Blob && message.content instanceof Blob) {
return;
}
if (PLATFORM.global.ArrayBuffer && message.content instanceof ArrayBuffer) {
return;
}
if (message.content instanceof Document) {
return;
}
if (typeof message.content === 'string') {
return;
}
if (message.content === null || message.content === undefined) {
return;
}
message.content = JSON.stringify(message.content, message.replacer);
if (!message.headers.has('Content-Type')) {
message.headers.add('Content-Type', 'application/json');
}
}
export let JSONPRequestMessage = class JSONPRequestMessage extends RequestMessage {
constructor(url, callbackParameterName) {
super('JSONP', url);
this.responseType = 'jsonp';
this.callbackParameterName = callbackParameterName;
}
};
let JSONPXHR = class JSONPXHR {
open(method, url) {
this.method = method;
this.url = url;
this.callbackName = 'jsonp_callback_' + Math.round(100000 * Math.random());
}
send() {
let url = this.url + (this.url.indexOf('?') >= 0 ? '&' : '?') + encodeURIComponent(this.callbackParameterName) + '=' + this.callbackName;
let script = DOM.createElement('script');
script.src = url;
script.onerror = e => {
cleanUp();
this.status = 0;
this.onerror(new Error('error'));
};
let cleanUp = () => {
delete PLATFORM.global[this.callbackName];
DOM.removeNode(script);
};
PLATFORM.global[this.callbackName] = data => {
cleanUp();
if (this.status === undefined) {
this.status = 200;
this.statusText = 'OK';
this.response = data;
this.onload(this);
}
};
DOM.appendNode(script);
if (this.timeout !== undefined) {
setTimeout(() => {
if (this.status === undefined) {
this.status = 0;
this.ontimeout(new Error('timeout'));
}
}, this.timeout);
}
}
abort() {
if (this.status === undefined) {
this.status = 0;
this.onabort(new Error('abort'));
}
}
setRequestHeader() {}
};
export function createJSONPRequestMessageProcessor() {
return new RequestMessageProcessor(JSONPXHR, [timeoutTransformer, callbackParameterNameTransformer]);
}
export let HttpRequestMessage = class HttpRequestMessage extends RequestMessage {
constructor(method, url, content, headers) {
super(method, url, content, headers);
this.responseType = 'json';
}
};
export function createHttpRequestMessageProcessor() {
return new RequestMessageProcessor(PLATFORM.XMLHttpRequest, [timeoutTransformer, credentialsTransformer, progressTransformer, downloadProgressTransformer, responseTypeTransformer, contentTransformer, headerTransformer]);
}
export let ErrorHttpResponseMessage = class ErrorHttpResponseMessage extends HttpResponseMessage {
constructor(responseMessage) {
super(responseMessage.requestMessage, {
response: responseMessage.response,
status: responseMessage.statusCode,
statusText: responseMessage.statusText
}, responseMessage.responseType);
this.name = responseMessage.responseType;
this.message = `Error: ${responseMessage.statusCode} Status: ${responseMessage.statusText}`;
}
};
export let RequestBuilder = class RequestBuilder {
constructor(client) {
this.client = client;
this.transformers = client.requestTransformers.slice(0);
this.useJsonp = false;
}
asDelete() {
return this._addTransformer(function (client, processor, message) {
message.method = 'DELETE';
});
}
asGet() {
return this._addTransformer(function (client, processor, message) {
message.method = 'GET';
});
}
asHead() {
return this._addTransformer(function (client, processor, message) {
message.method = 'HEAD';
});
}
asOptions() {
return this._addTransformer(function (client, processor, message) {
message.method = 'OPTIONS';
});
}
asPatch() {
return this._addTransformer(function (client, processor, message) {
message.method = 'PATCH';
});
}
asPost() {
return this._addTransformer(function (client, processor, message) {
message.method = 'POST';
});
}
asPut() {
return this._addTransformer(function (client, processor, message) {
message.method = 'PUT';
});
}
asJsonp(callbackParameterName) {
this.useJsonp = true;
return this._addTransformer(function (client, processor, message) {
message.callbackParameterName = callbackParameterName;
});
}
withUrl(url) {
return this._addTransformer(function (client, processor, message) {
message.url = url;
});
}
withContent(content) {
return this._addTransformer(function (client, processor, message) {
message.content = content;
});
}
withBaseUrl(baseUrl) {
return this._addTransformer(function (client, processor, message) {
message.baseUrl = baseUrl;
});
}
withParams(params, traditional) {
return this._addTransformer(function (client, processor, message) {
message.traditional = traditional;
message.params = params;
});
}
withResponseType(responseType) {
return this._addTransformer(function (client, processor, message) {
message.responseType = responseType;
});
}
withTimeout(timeout) {
return this._addTransformer(function (client, processor, message) {
message.timeout = timeout;
});
}
withHeader(key, value) {
return this._addTransformer(function (client, processor, message) {
message.headers.add(key, value);
});
}
withCredentials(value) {
return this._addTransformer(function (client, processor, message) {
message.withCredentials = value;
});
}
withLogin(user, password) {
return this._addTransformer(function (client, processor, message) {
message.user = user;message.password = password;
});
}
withReviver(reviver) {
return this._addTransformer(function (client, processor, message) {
message.reviver = reviver;
});
}
withReplacer(replacer) {
return this._addTransformer(function (client, processor, message) {
message.replacer = replacer;
});
}
withProgressCallback(progressCallback) {
return this._addTransformer(function (client, processor, message) {
message.progressCallback = progressCallback;
});
}
withDownloadProgressCallback(downloadProgressCallback) {
return this._addTransformer(function (client, processor, message) {
message.downloadProgressCallback = downloadProgressCallback;
});
}
withCallbackParameterName(callbackParameterName) {
return this._addTransformer(function (client, processor, message) {
message.callbackParameterName = callbackParameterName;
});
}
withInterceptor(interceptor) {
return this._addTransformer(function (client, processor, message) {
message.interceptors = message.interceptors || [];
message.interceptors.unshift(interceptor);
});
}
skipContentProcessing() {
return this._addTransformer(function (client, processor, message) {
message.skipContentProcessing = true;
});
}
_addTransformer(fn) {
this.transformers.push(fn);
return this;
}
static addHelper(name, fn) {
RequestBuilder.prototype[name] = function () {
return this._addTransformer(fn.apply(this, arguments));
};
}
send() {
let message = this.useJsonp ? new JSONPRequestMessage() : new HttpRequestMessage();
return this.client.send(message, this.transformers);
}
};
function trackRequestStart(client, processor) {
client.pendingRequests.push(processor);
client.isRequesting = true;
}
function trackRequestEnd(client, processor) {
let index = client.pendingRequests.indexOf(processor);
client.pendingRequests.splice(index, 1);
client.isRequesting = client.pendingRequests.length > 0;
if (!client.isRequesting) {
let evt = DOM.createCustomEvent('aurelia-http-client-requests-drained', { bubbles: true, cancelable: true });
setTimeout(() => DOM.dispatchEvent(evt), 1);
}
}
export let HttpClient = class HttpClient {
constructor() {
this.isRequesting = false;
this.rejectPromiseWithErrorObject = false;
this.requestTransformers = [];
this.requestProcessorFactories = new Map();
this.requestProcessorFactories.set(HttpRequestMessage, createHttpRequestMessageProcessor);
this.requestProcessorFactories.set(JSONPRequestMessage, createJSONPRequestMessageProcessor);
this.pendingRequests = [];
}
configure(fn) {
let builder = new RequestBuilder(this);
fn(builder);
this.requestTransformers = builder.transformers;
return this;
}
createRequest(url) {
let builder = new RequestBuilder(this);
if (url) {
builder.withUrl(url);
}
return builder;
}
send(requestMessage, transformers) {
let createProcessor = this.requestProcessorFactories.get(requestMessage.constructor);
let processor;
let promise;
let i;
let ii;
if (!createProcessor) {
throw new Error(`No request message processor factory for ${requestMessage.constructor}.`);
}
processor = createProcessor();
trackRequestStart(this, processor);
transformers = transformers || this.requestTransformers;
promise = Promise.resolve(requestMessage).then(message => {
for (i = 0, ii = transformers.length; i < ii; ++i) {
transformers[i](this, processor, message);
}
return processor.process(this, message).then(response => {
trackRequestEnd(this, processor);
return response;
}).catch(response => {
trackRequestEnd(this, processor);
throw response;
});
});
promise.abort = promise.cancel = function () {
processor.abort();
};
return promise;
}
delete(url) {
return this.createRequest(url).asDelete().send();
}
get(url, params, traditional) {
let req = this.createRequest(url).asGet();
if (params) {
return req.withParams(params, traditional).send();
}
return req.send();
}
head(url) {
return this.createRequest(url).asHead().send();
}
jsonp(url, callbackParameterName = 'jsoncallback') {
return this.createRequest(url).asJsonp(callbackParameterName).send();
}
options(url) {
return this.createRequest(url).asOptions().send();
}
put(url, content) {
return this.createRequest(url).asPut().withContent(content).send();
}
patch(url, content) {
return this.createRequest(url).asPatch().withContent(content).send();
}
post(url, content) {
return this.createRequest(url).asPost().withContent(content).send();
}
};