Plugins_http_fetch.js

import { Serialize } from './serialize.js'
import {
    camelCase,
    extend,
    each,
    isArrayish,
    isObject,
    isFunction,
    jsonToFormdata,
    not,
    url,
    empty
} from '../../Utils/Utils.js'
/**
 * Opciones de Configuración para FT
 * @const
 * @memberOf module:Plugins.HTTP.fetch
 * @namespace OptionsFT
 * @type {Object}
 */
export const OptionsFT = {
    /**
     * Metodo de la llamada
     * @memberOf module:Plugins.HTTP.fetch.OptionsFT
     * @type {String}
     */
    method: null,
    /**
     * Encabezado de la Peticion
     * @memberOf module:Plugins.HTTP.fetch.OptionsFT
     * @type {(Object|Headers)}
     */
    headers: null,
    /**
     * Cuerpo de la Solicitud, <cite>Tenga en cuenta que una solicitud que utiliza el método GET o HEAD no puede tener un cuerpo.</cite>
     * @memberOf module:Plugins.HTTP.fetch.OptionsFT
     * @type {(Blob|BufferSource|FormData|URLSearchParams|USVString|ReadableStream|Object)}
     */
    body:null,
    /**
     * Modo para la Solicitud
     * @memberOf module:Plugins.HTTP.fetch.OptionsFT
     * @type {String}
     */
    mode:null,
    /**
     * Controla lo que hacen los navegadores con las credenciales (cookies, entradas de autenticación HTTP y certificados de cliente TLS).<br>
     * para saber mas consulte la [guia]{@link https://developer.mozilla.org/en-US/docs/web/api/fetch#parameters}
     * @memberOf module:Plugins.HTTP.fetch.OptionsFT
     * @type {String}
     */
    credentials:null,
    /**
     * Indica cómo se debe comportar la solicitud con el cache del navegador.
     * @memberOf module:Plugins.HTTP.fetch.OptionsFT
     * @type {String}
     */
    cache: null,
    /**
     * Indica cómo debe actuar si la respuesta devuelve una redirección.
     * @memberOf module:Plugins.HTTP.fetch.OptionsFT
     * @type {(Object|String)}
     */
    redirect: null,
    /**
     * Indica el tipo de respuesta a recibir
     * @memberOf module:Plugins.HTTP.fetch.OptionsFT
     * @type {String}
     */
    typeResponse: "json",
    /**
     * Si el recurso solicitado es del tipo multimedia especifique aqui el mime type del recurso adquirido
     * @memberOf module:Plugins.HTTP.fetch.OptionsFT
     * @type {String}
     */
    mime: null
}

function getResource (resource) {
    if (resource instanceof Request ){
        return resource
    }else if (isArrayish(resource)) {
        let ul = resource.shift();
        if(resource.length > 0) {
            return url(ul, resource[0])
        } else {
            return url(ul)
        }
    } else if(isObject(resource)) {
        return url(resource.url, resource.params)
    }

    return resource
}

function getSettingsOfForm(form, options = {}){
    var d = {},  
        url = getResource(form.getAttribute("action")),
        otherData = ['mode', 'credentials', 'cache', 'redirect', 'type-response', 'mime']

    d.method = form.getAttribute('method').toUpperCase()
    if (form.hasAttribute('enctype')) {
        d.headers = {}
        d.headers['Content-Type'] = form.getAttribute('enctype')
    }

    d.body = new FormData(form)

    each(otherData, (n) => {
        if (form.hasAttribute(`data-${n}`)) {
            d[camelCase(n)] = form.getAttribute(`data-${n}`)
        }
    })

    d = _$.extend({}, d, options)

    return {
        url,
        obj: getSettings(_$.extend({}, OptionsFT, d))
    }

}

function getSettings (s) {
    if (!isObject(s)) {
        return false
    }
    
    let o = extend({}, OptionsFT, s),
        op = {}

    if (o.method.toLowerCase() !== 'post' && not(o.headers)) {
        o.headers = new Headers({
            'Content-Type': 'application/x-www-form-urlencoded'
        })
    }


    each(o, (v,n) => {
        if (!not(v)) {
            if(n == 'headers') {
                if (v instanceof Headers) {
                    op[n] = v
                } else if(isObject(v)) {
                    op[n] = new Headers(v)
                }
            } else if(n == 'body') {
               op[n] =  v
            } else if (n == 'credentials') {
                if (["omit", "same-origin", "include"].indexOf(v) > -1) {
                    op[n] = v
                }
            } else if (n == 'cache') {
                if (["default","no-store","reload","no-cache","force-cache","only-if-cached"].indexOf(v) > -1) {
                    op[n] = v
                }
            } else {
              op[n] = v  
            }            
        }
    })

    return !not(op) ? op : false
}
/**
 * Nueva API fetch la evolución de XHR<br>
 * Antes de Usar verifique la [Compatibilidad]{@link https://caniuse.com/fetch}
 * @function fetch
 * @memberOf module:Plugins.HTTP
 * @param  {(String|URL|Request|Array|Object|HTMLFormElement)} resource recurso de la url o un formulario de la instancia de HTMLFormElement
 * @param  {Object} settings Opciones de configuraciones
 *
 * @return {Object}
 */
export default function http(resource, settings = {}) {
    let loading = false,
        chunks = [],
        results = null,
        error = null,
        controller = null,
        u, s, tr, ml 

    if (resource instanceof HTMLFormElement) {
        let arg = getSettingsOfForm(resource, settings)
        u = arg.url
        s = arg.obj
    } else {
        u = getResource(resource) 
        s = getSettings(settings)
    }

    tr = s.typeResponse
    ml = s.mime

    delete s.typeResponse
    delete s.mime

    const _resetLocals = () => {
        loading = false;

        chunks = [];
        results = null;
        error = null;

        controller = new AbortController();
    }


    const _readBody = async (response, typeResponse = 'json', mimeType = null) => { 
        const reader = response.body.getReader(),
              length = +response.headers.get('content-length'); 

        let received = 0

        while (loading) {
            const { done, value } = await reader.read(),
                payload = { detail: { received, length, loading } },
                onProgress = new CustomEvent('fetch-progress', payload),
                onFinished = new CustomEvent('fetch-finished', payload)

            if (done) {
                // Finish loading 
                loading = false;
                window.dispatchEvent(onFinished)

            } else {
                // Push values to the chunk array
                chunks.push(value);
                received += value.length;

                 window.dispatchEvent(onProgress)
            }
        }

        let body = new Uint8Array(received),
            position = 0

        for (let chunk of chunks) {
            body.set(chunk, position)
            position += chunk.length
        }
        if (typeResponse.toLowerCase() === "json") {
            var n = new TextDecoder('utf-8').decode(body)
            try{
                return JSON.parse(n)
            } catch(e) {
                return n
            }
        } else if(typeResponse.toLowerCase() === "blob") {
            if (!not(mimeType)) {
                return new Blob([body], {type: mimeType})
            } else {
                return new Blob([body])
            }
            
        } else {
            return new TextDecoder('utf-8').decode(body)
        }
    }
    /**
     * Incia la llamada fetch
     * @memberOf module:Plugins.HTTP.fetch
     * @function start
     * @return {Promise}
     */
    const start = async () => {
        _resetLocals()
        loading = true 
        let signal = controller.signal

        try{
            const  response = await fetch(u, {signal, ...s})
            
            if (response.status >= 200 && response.status < 300) {
                return await _readBody(response, tr, ml)
            } else {
                throw new Error(response.statusText)
            }
        } catch(err) {
            error = err
            results = null
            return error
        } finally {
            loading = false
        }        
    }
    /**
     * Cancela petición en curso
     * @memberOf module:Plugins.HTTP.fetch
     * @function cancel
     * @return {Promise}
     */
    const cancel = () => {
        let onAbort = new CustomEvent('fetch-abort', { 
            detail: { 
               status: "abourt" 
            } 
        })
        
        controller.abort()
        _resetLocals()
        window.dispatchEvent(onAbort)
    }

    return {
        start,
        cancel
    }
}