http/client.js

/**
 * Asynchronous Http Client using XHR
 * @module http/client
 * @memberOf http
 */

const http = require('./methods');
const {startsWith, endsWith, filter, forEach} = require('../core/collections');


class InterceptorStore {
    all = []
    use(interceptor) {
        this.all.push(interceptor);
    }
}

/**
 * Async Http Client using XHR
 * @class
 */
class XHttpClient {
    /**
     *
     * @param {String} host - Server host address
     * @param {Number} [ratePerMinute] - Maximum requests allowed per minute (default 300)
     */
    constructor(host = '' ,{ratePerMinute = 300}={}) {
        if (endsWith(host, '/')) {
            host = host.split('').splice(host.length-1).join()
        }
        /**
         * Interceptors
         * @type {{request: InterceptorStore, response: InterceptorStore}}
         */
        this.interceptors = {
            request : new InterceptorStore(),
            response: new InterceptorStore()
        }
        this.host = host
        this.__queue = []
        this.__sending = []
        this.__interval = undefined
        this.__ratePerMinute = ratePerMinute
        this.__timeBetween = 60000/ratePerMinute
        this.__lastRequestTime = new Date().getTime() - this.__timeBetween;
    }

    /**
     *
     * @param client
     * @private
     */
    _intervalSend(client) {
        if (client.__queue.length === 0) {
            clearInterval(client.__interval);
            client.__interval = undefined;
            return;
        }
        let now = new Date().getTime();
        if (now - client.__lastRequestTime > client.__timeBetween) {
            let ajax = client.__queue.pop().send();
            client.__sending.push(ajax);
            client.__lastRequestTime = now;
        }
    }

    /**
     *
     * @param ajax
     * @param responseType
     * @param cancelToken
     * @return {Promise<Ajax>}
     * @private
     */
    _addRequest(ajax, {responseType, cancelToken}) {
        this.__queue.push(ajax);
        if (!this.__interval) {
            this.__interval = setInterval(this._intervalSend, 1, this);
        }
        ajax.cancelToken = cancelToken;
        if (responseType) ajax.xhr.responseType = responseType;
        return http.makePromise(ajax, ({client, request}) => {
            let rqi = client.__sending.indexOf(request);
            if (rqi >= 0) {
                client.__sending.splice(rqi);
            }
        }, {client: this, request: ajax});
    }

    /**
     * Add ajax request to queue
     * @param {Ajax} ajax
     */
    send(ajax) {
        this._addRequest(ajax)
    }

    /**
     *
     * @param method
     * @param route
     * @param params
     * @param headers
     * @param content
     * @param responseType
     * @param cancelToken
     * @return {Promise<Ajax>}
     * @private
     */
    _contentRequest(method, route, {params, headers, content,responseType, cancelToken}) {
        if (!startsWith(route, '/') && route.length > 1) {
            route = '/' + route;
        }
        return this._addRequest(
            method(this.host+route, params || {})
                .headers(headers || {})
                .withContent(content || {type: '', data: ''})
        , {responseType, cancelToken})
    }

    /**
     * Equeue http GET request
     *
     * @param {String} route - request route - appends to host address
     * @param {Object} [params] - request params(args)
     * @param {Object} [headers] - request headers
     * @param {String} [responseType] - "text"|"json"|"xml"|"document"|"arraybuffer"|"blob"|"ms-stream"|""
     * @param {String} [cancelToken] - A token used to cancel a group of requests
     * @return {Promise<Ajax>}
     */
    get(route, {params, headers, responseType, cancelToken}={}) {
        if (!startsWith(route, '/') && route.length > 1) {
            route = '/' + route;
        }
        return this._addRequest(http.Get(this.host+route, params).headers(headers), {responseType, cancelToken});
    }

    /**
     * Equeue http POST request
     *
     * @param {String} route - request route - appends to host address
     * @param {Object} [params] - request params(args)
     * @param {Object} [headers] - request headers
     * @param {http.HttpContent} [content] - request content. example: {type:'json', data={count: 13}}
     * @param {String} [responseType] - "text"|"json"|"xml"|"document"|"arraybuffer"|"blob"|"ms-stream"|""
     * @param {String} [cancelToken] - A token used to cancel a group of requests
     * @return {Promise<Ajax>}
     */
    post(route, {params, headers, content,responseType, cancelToken}={}) {
        return this._contentRequest(http.Post, route, {params, headers, content,responseType, cancelToken});
    }

    /**
     * Equeue http PUT request
     *
     * @param {String} route - request route - appends to host address
     * @param {Object} [params] - request params(args)
     * @param {Object} [headers] - request headers
     * @param {http.HttpContent} [content] - request content. example: {type:'json', data={count: 13}}
     * @param {String} [responseType] - "text"|"json"|"xml"|"document"|"arraybuffer"|"blob"|"ms-stream"|""
     * @param {String} [cancelToken] - A token used to cancel a group of requests
     * @return {Promise<Ajax>}
     */
    put(route, {params, headers, content,responseType, cancelToken}={}) {
        return this._contentRequest(http.Put, route, {params, headers, content,responseType, cancelToken});
    }

    /**
     * Equeue http PATCH request
     *
     * @param {String} route - request route - appends to host address
     * @param {Object} [params] - request params(args)
     * @param {Object} [headers] - request headers
     * @param {http.HttpContent} [content] - request content. example: {type:'json', data={count: 13}}
     * @param {String} [responseType] - "text"|"json"|"xml"|"document"|"arraybuffer"|"blob"|"ms-stream"|""
     * @param {String} [cancelToken] - A token used to cancel a group of requests
     * @return {Promise<Ajax>}
     */
    patch(route, {params, headers, content,responseType, cancelToken}={}) {
        return this._contentRequest(http.Patch, route, {params, headers, content,responseType, cancelToken});
    }

    /**
     * Equeue http DELETE request
     *
     * @param {String} route - request route - appends to host address
     * @param {Object} [params] - request params(args)
     * @param {Object} [headers] - request headers
     * @param {http.HttpContent} [content] - request content. example: {type:'json', data={count: 13}}
     * @param {String} [responseType] - "text"|"json"|"xml"|"document"|"arraybuffer"|"blob"|"ms-stream"|""
     * @param {String} [cancelToken] - A token used to cancel a group of requests
     * @return {Promise<Ajax>}
     */
    delete(route, {params, headers, content,responseType, cancelToken}={}) {
        return this._contentRequest(http.Delete, route, {params, headers, content,responseType, cancelToken});
    }

    /**
     * Cancel all requests(sending or enqueued) with given token
     * @param {String} token
     */
    cancel(token) {
        this.__queue = filter(this.__queue, (a)=>a.cancelToken !== token);
        let sending = filter(this.__sending, (a)=>a.cancelToken === token);
        forEach(sending, (ajax)=>{
            try {
                ajax.xhr.abort();
            } catch (e) {
                console.log(e)
            }
        });

        this.__sending = filter(this.__sending, (a)=>a.cancelToken !== token);
    }
}

module.exports = {XHttpClient}