aurelia-http-client
Version:
A simple, restful, message-based wrapper around XMLHttpRequest.
1,350 lines (1,186 loc) • 38.4 kB
JavaScript
import {join,buildQueryString} from 'aurelia-path';
import {PLATFORM,DOM} from 'aurelia-pal';
/**
* Represents http request/response headers.
*/
export class Headers {
/**
* Creates an instance of the headers class.
* @param headers A set of key/values to initialize the headers with.
*/
constructor(headers?: Object = {}) {
this.headers = {};
// Convert object to set with case insensitive keys
for (let key in headers) {
this.headers[key.toLowerCase()] = {key, value: headers[key]};
}
}
/**
* Adds a header.
* @param key The header key.
* @param value The header value.
*/
add(key: string, value: string): void {
this.headers[key.toLowerCase()] = {key, value};
}
/**
* Gets a header value.
* @param key The header key.
* @return The header value.
*/
get(key: string): string {
let header = this.headers[key.toLowerCase()];
return header ? header.value : undefined;
}
/**
* Clears the headers.
*/
clear(): void {
this.headers = {};
}
/**
* Determines whether or not the indicated header exists in the collection.
* @param header The header key to check.
* @return True if it exists, false otherwise.
*/
has(header: string): boolean {
return this.headers.hasOwnProperty(header.toLowerCase());
}
/**
* Configures an XMR object with the headers.
* @param xhr The XHRT instance to configure.
*/
configureXHR(xhr: XHR): void {
for (let name in this.headers) {
if (this.headers.hasOwnProperty(name)) {
xhr.setRequestHeader(this.headers[name].key, this.headers[name].value);
}
}
}
/**
* XmlHttpRequest's getAllResponseHeaders() method returns a string of response
* headers according to the format described here:
* http://www.w3.org/TR/XMLHttpRequest/#the-getallresponseheaders-method
* This method parses that string into a user-friendly key/value pair object.
* @param headerStr The string from the XHR.
* @return A Headers instance containing the parsed headers.
*/
static parse(headerStr: string): Headers {
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];
// Can't use split() here because it does the wrong thing
// if the header value has the string ": " in it.
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;
}
}
/**
* Represents a request message.
*/
export class RequestMessage {
/**
* The HTTP method.
*/
method: string;
/**
* The url to submit the request to.
*/
url: string;
/**
* The content of the request.
*/
content: any;
/**
* The headers to send along with the request.
*/
headers: Headers;
/**
* Use tradional style for param serialization.
*/
traditional: boolean;
/**
* The base url that the request url is joined with.
*/
baseUrl: string;
/**
* Creates an instance of RequestMessage.
* @param method The HTTP method.
* @param url The url to submit the request to.
* @param content The content of the request.
* @param headers The headers to send along with the request.
*/
constructor(method: string, url: string, content: any, headers?: Headers) {
this.method = method;
this.url = url;
this.content = content;
this.headers = headers || new Headers();
this.baseUrl = '';
}
/**
* Builds the url to make the request from.
* @return The constructed url.
*/
buildFullUrl(): string {
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;
}
}
/**
* Represents a response message from an HTTP or JSONP request.
*/
export class HttpResponseMessage {
/**
* The request message that resulted in this response.
*/
requestMessage: RequestMessage;
/**
* The status code of the response.
*/
statusCode: number;
/**
* The raw response.
*/
response: any;
/**
* The type of the response.
*/
responseType: string;
/**
* The success status of the request based on status code.
*/
isSuccess: boolean;
/**
* The status text.
*/
statusText: string;
/**
* A reviver function to use in transforming the content.
*/
reviver: (key: string, value: any) => any;
/**
* The mime type of the response.
*/
mimeType: string;
/**
* The headers received with the response.
*/
headers: Headers;
/**
* Creates an instance of HttpResponseMessage.
* @param requestMessage The request message that resulted in this response.
* @param xhr The XHR instance that made the request.
* @param responseType The type of the response.
* @param reviver? A reviver function to use in transforming the content.
*/
constructor(requestMessage: RequestMessage, xhr: XHR, responseType: string, reviver?: (key: string, value: any) => any) {
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;
}
/**
* Gets the content of the response.
* @return the response content.
*/
get content(): any {
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;
}
}
}
/**
* MimeTypes mapped to responseTypes
*
* @type {Object}
*/
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'
};
/*eslint no-unused-vars:0*/
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);
}
}
/**
* Creates an XHR implementation.
*/
interface XHRConstructor {
//new():XHR;
}
/**
* Represents an XHR.
*/
interface XHR {
/**
* The status code of the response.
*/
status: number;
/**
* The status text.
*/
statusText: string;
/**
* The raw response.
*/
response: any;
/**
* The raw response text.
*/
responseText: string;
/**
* The load callback.
*/
onload: Function;
/**
* The timeout callback.
*/
ontimeout: Function;
/**
* The error callback.
*/
onerror: Function;
/**
* The abort callback.
*/
onabort: Function;
/**
* Aborts the request.
*/
abort(): void;
/**
* Opens the XHR channel.
*/
open(method: string, url: string, isAsync: boolean, user?: string, password?: string): void;
/**
* Sends the request.
*/
send(content? : any): void;
}
/**
* Represents an XHR transformer.
*/
interface XHRTransformer {
(client: HttpClient, processor: RequestMessageProcessor, message: RequestMessage, xhr: XHR): void;
}
/**
* Processes request messages.
*/
export class RequestMessageProcessor {
/**
* Creates an instance of RequestMessageProcessor.
*/
constructor(xhrType: XHRConstructor, xhrTransformers: XHRTransformer[]) {
this.XHRType = xhrType;
this.xhrTransformers = xhrTransformers;
this.isAborted = false;
}
/**
* Aborts the request.
*/
abort(): void {
// The logic here is if the xhr object is not set then there is nothing to abort so the intent was carried out
// Also test if the XHR is UNSENT - if not, it will be aborted in the process() phase
if (this.xhr && this.xhr.readyState !== PLATFORM.XMLHttpRequest.UNSENT) {
this.xhr.abort();
}
this.isAborted = true;
}
/**
* Processes the request.
* @param client The HttpClient making the request.
* @param requestMessage The message to process.
* @return A promise for an HttpResponseMessage.
*/
process(client: HttpClient, requestMessage: RequestMessage): Promise<HttpResponseMessage> {
let promise = new Promise((resolve, reject) => {
let rejectResponse: (resp: HttpResponseMessage) => void;
if (client.rejectPromiseWithErrorObject) {
rejectResponse = (resp: HttpResponseMessage) => {
const errorResp = new ErrorHttpResponseMessage(resp);
reject(errorResp);
};
} else {
rejectResponse = (resp: HttpResponseMessage) => {
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) {
// Some interceptors can delay sending of XHR, so when abort is called
// before XHR is actually sent we abort() instead send()
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') {
// IE serializes undefined as "undefined"
// some servers reject such requests because of unexpected payload, e.g. in case of DELETE requests
this.xhr.send();
} else {
this.xhr.send(message.content);
}
}
return promise;
};
// [ onFullfilled, onReject ] pairs
let chain = [[processRequest, undefined]];
// Apply interceptors chain from the message.interceptors
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;
});
}
}
/**
* Adds a timeout to the request.
* @param client The http client.
* @param processor The request message processor.
* @param message The request message.
* @param xhr The xhr instance.
*/
export function timeoutTransformer(client: HttpClient, processor: RequestMessageProcessor, message: RequestMessage, xhr: XHR) {
if (message.timeout !== undefined) {
xhr.timeout = message.timeout;
}
}
/**
* Adds a callback parameter name to the request.
* @param client The http client.
* @param processor The request message processor.
* @param message The request message.
* @param xhr The xhr instance.
*/
export function callbackParameterNameTransformer(client: HttpClient, processor: RequestMessageProcessor, message: RequestMessage, xhr: XHR) {
if (message.callbackParameterName !== undefined) {
xhr.callbackParameterName = message.callbackParameterName;
}
}
/**
* Sets withCredentials on the request.
* @param client The http client.
* @param processor The request message processor.
* @param message The request message.
* @param xhr The xhr instance.
*/
export function credentialsTransformer(client: HttpClient, processor: RequestMessageProcessor, message: RequestMessage, xhr: XHR) {
if (message.withCredentials !== undefined) {
xhr.withCredentials = message.withCredentials;
}
}
/**
* Adds an upload.onprogress callback to the request.
* @param client The http client.
* @param processor The request message processor.
* @param message The request message.
* @param xhr The xhr instance.
*/
export function progressTransformer(client: HttpClient, processor: RequestMessageProcessor, message: RequestMessage, xhr: XHR) {
if (message.progressCallback) {
xhr.upload.onprogress = message.progressCallback;
}
}
/**
* Adds an onprogress callback to the request.
* @param client The http client.
* @param processor The request message processor.
* @param message The request message.
* @param xhr The xhr instance.
*/
export function downloadProgressTransformer(client: HttpClient, processor: RequestMessageProcessor, message: RequestMessage, xhr: XHR) {
if (message.downloadProgressCallback) {
xhr.onprogress = message.downloadProgressCallback;
}
}
/**
* Adds a response type transformer to the request.
* @param client The http client.
* @param processor The request message processor.
* @param message The request message.
* @param xhr The xhr instance.
*/
export function responseTypeTransformer(client: HttpClient, processor: RequestMessageProcessor, message: RequestMessage, xhr: XHR) {
let responseType = message.responseType;
if (responseType === 'json') {
responseType = 'text'; //IE does not support json
}
xhr.responseType = responseType;
}
/**
* Adds headers to the request.
* @param client The http client.
* @param processor The request message processor.
* @param message The request message.
* @param xhr The xhr instance.
*/
export function headerTransformer(client: HttpClient, processor: RequestMessageProcessor, message: RequestMessage, xhr: XHR) {
message.headers.configureXHR(xhr);
}
/**
* Transforms the content of the request.
* @param client The http client.
* @param processor The request message processor.
* @param message The request message.
* @param xhr The xhr instance.
*/
export function contentTransformer(client: HttpClient, processor: RequestMessageProcessor, message: RequestMessage, xhr: 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');
}
}
/**
* Represents an JSONP request message.
*/
export class JSONPRequestMessage extends RequestMessage {
/**
* Creates an instance of JSONPRequestMessage.
* @param url The url to submit the request to.
* @param callbackParameterName The name of the callback parameter that the api expects.
*/
constructor(url: string, callbackParameterName: string) {
super('JSONP', url);
this.responseType = 'jsonp';
this.callbackParameterName = callbackParameterName;
}
}
class JSONPXHR {
open(method: string, url: string): void {
this.method = method;
this.url = url;
this.callbackName = 'jsonp_callback_' + Math.round(100000 * Math.random());
}
send(): void {
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() : void {
if (this.status === undefined) {
this.status = 0;
this.onabort(new Error('abort'));
}
}
setRequestHeader() {}
}
/**
* Creates a RequestMessageProcessor for handling JSONP request messages.
* @return A processor instance for JSONP request messages.
*/
export function createJSONPRequestMessageProcessor(): RequestMessageProcessor {
return new RequestMessageProcessor(JSONPXHR, [
timeoutTransformer,
callbackParameterNameTransformer
]);
}
/**
* Represents an HTTP request message.
*/
export class HttpRequestMessage extends RequestMessage {
/**
* A replacer function to use in transforming the content.
*/
replacer: (key: string, value: any) => any;
/**
* Creates an instance of HttpRequestMessage.
* @param method The http method.
* @param url The url to submit the request to.
* @param content The content of the request.
* @param headers The headers to send along with the request.
*/
constructor(method: string, url: string, content: any, headers?: Headers) {
super(method, url, content, headers);
this.responseType = 'json'; //text, arraybuffer, blob, document
}
}
/**
* Creates a RequestMessageProcessor for handling HTTP request messages.
* @return A processor instance for HTTP request messages.
*/
export function createHttpRequestMessageProcessor(): RequestMessageProcessor {
return new RequestMessageProcessor(PLATFORM.XMLHttpRequest, [
timeoutTransformer,
credentialsTransformer,
progressTransformer,
downloadProgressTransformer,
responseTypeTransformer,
contentTransformer,
headerTransformer
]);
}
/**
* Represents an error like object response message from an HTTP or JSONP request.
*/
export class ErrorHttpResponseMessage extends HttpResponseMessage {
/**
* Error like name
*/
name: string;
/**
* Error like message
*/
message: string;
/**
* Instanciate a new error response message
* ErrorHttpResponseMessage instanceof Error is false but with two members 'name' and 'message' we have an error like object
* @param responseMessage response message
*/
constructor(responseMessage: HttpResponseMessage) {
super(responseMessage.requestMessage, {
response: responseMessage.response,
status: responseMessage.statusCode,
statusText: responseMessage.statusText
}, responseMessage.responseType);
//error like properties : name and message
this.name = responseMessage.responseType;
this.message = `Error: ${responseMessage.statusCode} Status: ${responseMessage.statusText}`;
}
}
/**
* Intercepts requests, responses and errors.
*/
interface Interceptor {
/**
* Intercepts the response.
*/
response?: (message: HttpResponseMessage) => HttpResponseMessage | Promise<HttpResponseMessage>;
/**
* Intercepts a response error.
*/
responseError?: (error: HttpResponseMessage) => HttpResponseMessage | Promise<HttpResponseMessage>;
/**
* Intercepts the request.
*/
request?: (message: RequestMessage) => RequestMessage | Promise<RequestMessage>;
/**
* Intercepts a request error.
*/
requestError?: (error: Error) => RequestMessage | Promise<RequestMessage>;
}
/**
* Transforms a request.
*/
interface RequestTransformer {
(client: HttpClient, processor: RequestMessageProcessor, message: RequestMessage): void;
}
/**
* A builder class allowing fluent composition of HTTP requests.
*/
export class RequestBuilder {
/**
* The HttpClient instance.
*/
client: HttpClient;
/**
* Creates an instance of RequestBuilder
* @param client An instance of HttpClient
*/
constructor(client: HttpClient) {
this.client = client;
this.transformers = client.requestTransformers.slice(0);
this.useJsonp = false;
}
/**
* Makes the request a DELETE request.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
asDelete(): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.method = 'DELETE';
});
}
/**
* Makes the request a GET request.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
asGet(): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.method = 'GET';
});
}
/**
* Makes the request a HEAD request.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
asHead(): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.method = 'HEAD';
});
}
/**
* Makes the request a OPTIONS request.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
asOptions(): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.method = 'OPTIONS';
});
}
/**
* Makes the request a PATCH request.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
asPatch(): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.method = 'PATCH';
});
}
/**
* Makes the request a POST request.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
asPost(): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.method = 'POST';
});
}
/**
* Makes the request a PUT request.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
asPut(): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.method = 'PUT';
});
}
/**
* Makes the request a JSONP request.
* @param callbackParameterName The name of the callback to use.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
asJsonp(callbackParameterName: string): RequestBuilder {
this.useJsonp = true;
return this._addTransformer(function(client, processor, message) {
message.callbackParameterName = callbackParameterName;
});
}
/**
* Sets the request url.
* @param url The url to use.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withUrl(url: string): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.url = url;
});
}
/**
* Sets the request content.
* @param The content to send.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withContent(content: any): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.content = content;
});
}
/**
* Sets the base url that will be prepended to the url.
* @param baseUrl The base url to use.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withBaseUrl(baseUrl: string): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.baseUrl = baseUrl;
});
}
/**
* Sets params that will be added to the request url as a query string.
* @param params The key/value pairs to use to build the query string.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withParams(params: Object, traditional ?: boolean): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.traditional = traditional;
message.params = params;
});
}
/**
* Sets the response type.
* @param responseType The response type to expect.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withResponseType(responseType: string): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.responseType = responseType;
});
}
/**
* Sets a timeout for the request.
* @param timeout The timeout for the request.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withTimeout(timeout: number): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.timeout = timeout;
});
}
/**
* Sets a header on the request.
* @param key The header key to add.
* @param value The header value to add.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withHeader(key: string, value: string): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.headers.add(key, value);
});
}
/**
* Sets the withCredentials flag on the request.
* @param value The value of the withCredentials flag to set.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withCredentials(value: boolean): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.withCredentials = value;
});
}
/**
* Sets the user and password to use in opening the request.
* @param user The username to send.
* @param password The password to send.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withLogin(user: string, password: string): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.user = user; message.password = password;
});
}
/**
* Sets a reviver to transform the response content.
* @param reviver The reviver to use in processing the response.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withReviver(reviver: (key: string, value: any) => any): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.reviver = reviver;
});
}
/**
* Sets a replacer to transform the request content.
* @param replacer The replacer to use in preparing the request.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withReplacer(replacer: (key: string, value: any) => any): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.replacer = replacer;
});
}
/**
* Sets an upload progress callback.
* @param progressCallback The progress callback function.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withProgressCallback(progressCallback: Function): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.progressCallback = progressCallback;
});
}
/**
* Sets an download progress callback.
* @param progressCallback The progress callback function.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withDownloadProgressCallback(downloadProgressCallback: Function): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.downloadProgressCallback = downloadProgressCallback;
});
}
/**
* Sets a callback parameter name for JSONP.
* @param callbackParameterName The name of the callback parameter that the JSONP request requires.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withCallbackParameterName(callbackParameterName: string): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.callbackParameterName = callbackParameterName;
});
}
/**
* Adds an interceptor to the request.
* @param interceptor The interceptor to add.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
withInterceptor(interceptor: Interceptor): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
// NOTE: Interceptors are stored in reverse order. Inner interceptors before outer interceptors.
// This reversal is needed so that we can build up the interception chain around the
// server request.
message.interceptors = message.interceptors || [];
message.interceptors.unshift(interceptor);
});
}
/**
* Skips the request content processing transform.
* @return The chainable RequestBuilder to use in further configuration of the request.
*/
skipContentProcessing(): RequestBuilder {
return this._addTransformer(function(client, processor, message) {
message.skipContentProcessing = true;
});
}
_addTransformer(fn) {
this.transformers.push(fn);
return this;
}
/**
* Adds a user-defined request transformer to the RequestBuilder.
* @param name The name of the helper to add.
* @param fn The helper function.
*/
static addHelper(name: string, fn: () => RequestTransformer): void {
RequestBuilder.prototype[name] = function() {
return this._addTransformer(fn.apply(this, arguments));
};
}
/**
* Sends the request.
* @return {Promise} A cancellable promise object.
*/
send(): Promise<HttpResponseMessage> {
let message = this.useJsonp ? new JSONPRequestMessage() : new HttpRequestMessage();
return this.client.send(message, this.transformers);
}
}
/*eslint no-unused-vars:0*/
function trackRequestStart(client: HttpClient, processor: RequestMessageProcessor) {
client.pendingRequests.push(processor);
client.isRequesting = true;
}
function trackRequestEnd(client: HttpClient, processor: RequestMessageProcessor) {
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);
}
}
/**
* The main HTTP client object.
*/
export class HttpClient {
/**
* Return true if promises are rejected with an error like object. Default false
*/
rejectPromiseWithErrorObject: boolean;
/**
* Indicates whether or not the client is in the process of requesting resources.
*/
isRequesting: boolean = false;
/**
* Creates an instance of HttpClient.
*/
constructor() {
this.rejectPromiseWithErrorObject = false;
this.requestTransformers = [];
this.requestProcessorFactories = new Map();
this.requestProcessorFactories.set(HttpRequestMessage, createHttpRequestMessageProcessor);
this.requestProcessorFactories.set(JSONPRequestMessage, createJSONPRequestMessageProcessor);
this.pendingRequests = [];
}
/**
* Configure this HttpClient with default settings to be used by all requests.
* @param fn A function that takes a RequestBuilder as an argument.
*/
configure(fn: (builder: RequestBuilder) => void): HttpClient {
let builder = new RequestBuilder(this);
fn(builder);
this.requestTransformers = builder.transformers;
return this;
}
/**
* Returns a new RequestBuilder for this HttpClient instance that can be used to build and send HTTP requests.
* @param url The target URL.
*/
createRequest(url: string): RequestBuilder {
let builder = new RequestBuilder(this);
if (url) {
builder.withUrl(url);
}
return builder;
}
/**
* Sends a message using the underlying networking stack.
* @param message A configured HttpRequestMessage or JSONPRequestMessage.
* @param transformers A collection of transformers to apply to the HTTP request.
* @return A cancellable promise object.
*/
send(requestMessage: RequestMessage, transformers: Array<RequestTransformer>): Promise<HttpResponseMessage> {
let createProcessor = this.requestProcessorFactories.get(requestMessage.constructor);
let processor: RequestMessageProcessor;
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 => {
// First apply transformers passed to the client.send()
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;
}
/**
* Sends an HTTP DELETE request.
* @param url The target URL.
* @return A cancellable promise object.
*/
delete(url: string): Promise<HttpResponseMessage> {
return this.createRequest(url).asDelete().send();
}
/**
* Sends an HTTP GET request.
* @param url The target URL.
* @return {Promise} A cancellable promise object.
*/
get(url: string, params?: Object, traditional?: boolean): Promise<HttpResponseMessage> {
let req = this.createRequest(url).asGet();
if (params) {
return req.withParams(params, traditional).send();
}
return req.send();
}
/**
* Sends an HTTP HEAD request.
* @param url The target URL.
* @return A cancellable promise object.
*/
head(url: string): Promise<HttpResponseMessage> {
return this.createRequest(url).asHead().send();
}
/**
* Sends a JSONP request.
* @param url The target URL.
* @return A cancellable promise object.
*/
jsonp(url: string, callbackParameterName: string = 'jsoncallback'): Promise<HttpResponseMessage> {
return this.createRequest(url).asJsonp(callbackParameterName).send();
}
/**
* Sends an HTTP OPTIONS request.
* @param url The target URL.
* @return A cancellable promise object.
*/
options(url: string): Promise<HttpResponseMessage> {
return this.createRequest(url).asOptions().send();
}
/**
* Sends an HTTP PUT request.
* @param url The target URL.
* @param content The request payload.
* @return A cancellable promise object.
*/
put(url: string, content: any): Promise<HttpResponseMessage> {
return this.createRequest(url).asPut().withContent(content).send();
}
/**
* Sends an HTTP PATCH request.
* @param url The target URL.
* @param content The request payload.
* @return A cancellable promise object.
*/
patch(url: string, content: any): Promise<HttpResponseMessage> {
return this.createRequest(url).asPatch().withContent(content).send();
}
/**
* Sends an HTTP POST request.
* @param url The target URL.
* @param content The request payload.
* @return A cancellable promise object.
*/
post(url: string, content: any): Promise<HttpResponseMessage> {
return this.createRequest(url).asPost().withContent(content).send();
}
}