Source: utils/remote-requests/standard/standard_remote_request_utils.js

//@ts-check
import RandomNumberCharGenUtils from "../../random-number-generator/random_number_char_generator";

/**
 * @typedef ActiveRemoteRequests
 * @property {string} id
 * @property {XMLHttpRequest} XMLHttpRequest
 * 
 * @typedef RemoteStateChangeCallbackOptions
 * @property {remoteReqSuccessCb<{}>} successCallback
 * @property {overrideStateCb} overrideStateCb
 */

/**
 * 
 */
class StandardRemoteRequestUtils{

    constructor(){

        /**
         * @type {ActiveRemoteRequests[]}
         */
        this.activeRequests = [];
    };

    static get _REMOTE_REQUEST_ERROR(){

        return "REMOTE REQUEST ERROR";
    }

    static get _REMOTE_REQUEST_WARNING(){

        return "REMOTE REQUEST WARNING";
    }

    static get _REQUEST_ID_SIZE(){

        return 4;
    }

    /**
     * 
     * Used in Oats\~i builds heavily. Also exists to support use of this API in non-Oats~i implementations
     * where lifecycle is not applicable
     */
    isFragmentRunning(){

        return true;
    }

    /**
     * Run in a try catch. Throws error if fragment dead and making request
     * 
     * @type {standardizedRemoteRequestFunction<{}>}
     * 
     * @param {RequestOptions} requestOptions
     * @param {remoteReqSuccessCb<{}>} successCallback
     * @param {remoteReqErrCb} errorCallback
     * @param {XMLHttpRequest} [customXHReq] Will be deprecated
     * @param {overrideStateCb} [overrideStateCb] Might be deprecated if lifecycle non-enforceable with it. Trying some code to see
     * @param {string} [overrideID]
     * 
     * @returns {string} The ID of the request
     */
    makeRemoteRequest(requestOptions, successCallback, errorCallback, customXHReq, overrideStateCb, overrideID){

        //Run only if fragment running
        if(this.isFragmentRunning()){

            if(successCallback && overrideStateCb){

                console.warn("Success callback will not be called as an override state callback has been given. Handle API success in it");
            }
            let xhReq = customXHReq ? customXHReq : new XMLHttpRequest();
            xhReq.withCredentials = true;
            xhReq.open(requestOptions.reqMethod, requestOptions.reqAddress, true);
            if(requestOptions.contentType){
                
                xhReq.setRequestHeader("Content-Type", requestOptions.contentType);
            }
            if(requestOptions.reqHeaders){
    
                let authorizationSet = false;
                //Added last bit to handle situations where authorization value is provided differently
                if(requestOptions.reqHeaders.authorization && !requestOptions.reqHeaders.dontPrependBearer){
    
                    authorizationSet = true;
                    xhReq.setRequestHeader("Authorization", "Bearer " + requestOptions.reqHeaders.authorization);
                }
                
                for(const header in requestOptions.reqHeaders){

                    if(header === "authorization" && authorizationSet){

                    } else {

                        xhReq.setRequestHeader(header, requestOptions.reqHeaders[header]);
                    }
                }
            }

            //set reponse type
            if(requestOptions.responseType){

                xhReq.responseType = requestOptions.responseType;
            }
    
            /**
             * @type {RemoteStateChangeCallbackOptions}
             */
            const callbackOptions = {

                successCallback: successCallback,
                overrideStateCb: overrideStateCb
            }

            // xhReq.addEventListener("readystatechange", overrideStateCb ? overrideStateCb.bind(xhReq) : this.onRequestStateChange.bind(xhReq, this, requestOptions.processAbort, callbackOptions));
            xhReq.addEventListener("readystatechange", this.onRequestStateChange.bind(xhReq, this, requestOptions.processAbort, callbackOptions));
            // xhReq.addEventListener("error", errorCallback.bind(this));
            xhReq.addEventListener("error", this.onRequestError.bind(xhReq, this, errorCallback));
    
            //Get id
            const requestID = overrideID ? overrideID : RandomNumberCharGenUtils.generateRandomNumChar(StandardRemoteRequestUtils._REQUEST_ID_SIZE);

            //Abort any running with overrideID. Thus, user doesn't have to abort calls when using it. Automatically done
            //So, new calls to loadCategoriesData will abort any running calls and load afresh
            if(overrideID){

                this.abortRequest(overrideID);
            }

            //Save to active here
            this.activeRequests.push({
    
                id: requestID,
                XMLHttpRequest: xhReq
            });
    
            xhReq.send(requestOptions.reqBody);
            return requestID;
        } else {

            throw new Error(`${StandardRemoteRequestUtils._REMOTE_REQUEST_WARNING}: Fragment not running. Request denied`);
        }
    }

    /**
     * @private
     * 
     * Using JS # syntax for better privatization
     * 
     * @this XMLHttpRequest
     * Runs in context of running XMLHttpRequest object
     * 
     * @param {StandardRemoteRequestUtils} objRef
     * @param {boolean} processAbort
     * @param {RemoteStateChangeCallbackOptions} callbackOptions
     */
    onRequestStateChange(objRef, processAbort, callbackOptions) {

        if(objRef.isFragmentRunning()){

            if(callbackOptions.overrideStateCb){

                //Using call because we can successfully bind the xhReq instance as the context (this keyword)
                //Failed with a bind before adding it to the callbackOptions object. Will work if we save it as var after binding. Otherwise fails for a weird reason
                callbackOptions.overrideStateCb.call(this);
                if(this.readyState === XMLHttpRequest.DONE){

                    //No longer active
                    objRef.popRemoteReqEntryFromActive(this);
                }
            } else {

                if(this.readyState === XMLHttpRequest.DONE){

                    //No longer active
                    objRef.popRemoteReqEntryFromActive(this);
                    //First checks when aborting, second when not aborting
                    if((this.status === 0 && processAbort && objRef.canProcessRequest(this.status)) || (this.status !== 0 && objRef.canProcessRequest(this.status))){
        
                        callbackOptions.successCallback(this.status, this.response);
                    }
                }
            }
        }
    }

    /**
     * @private
     * 
     * @this XMLHttpRequest
     * 
     * @param {StandardRemoteRequestUtils} objRef
     * @param {remoteReqErrCb} cb 
     */
    onRequestError(objRef, cb){ //e, - had this, but not binding to event correctly

        if(objRef.isFragmentRunning()){

            objRef.popRemoteReqEntryFromActive(this);
            cb();
        }
    }

    /**
     * @private
     * 
     * @param {XMLHttpRequest} request 
     */
    popRemoteReqEntryFromActive(request){

        this.stopRequestInIndex(this.activeRequests.findIndex((req) => req.XMLHttpRequest === request), true);
    }

    /**
     * @private
     * 
     * @param {number} index 
     * @param {boolean} [notAborting]
     */
    stopRequestInIndex(index, notAborting){

        let id = "undefined ID";
        if(index !== -1){

            id = this.activeRequests[index].id;

            if(!notAborting){

                this.activeRequests[index].XMLHttpRequest.abort();
            }
            //Remove from active requests
            this.activeRequests.splice(index, 1);
        } else {

            // console.log(`${StandardRemoteRequestUtils._REMOTE_REQUEST_ERROR}: Cannot abort request with id ${id}. Not found`);
        }
    }

    /**
     * Override in children
     * 
     * Called when the request has been completed
     * Override and check whether request processing can proceed based on response
     * This involves checking the status and determining whether the request has been approved by the API
     * 
     * Returning true allows the 
     * 
     * @param {number} status
     * @returns {boolean} Whether the request is completed as per API requirements
     */
    canProcessRequest(status){

        return true;
    }

    /**
     * Use internally and carefully
     */
    abortRunningRequests(){

        this.activeRequests.forEach((request) => {

            request.XMLHttpRequest.abort();
        });

        this.activeRequests.splice(0, this.activeRequests.length);
    }

    /**
     * Consoles an error if failed to abort request
     * 
     * @param {string} id 
     */
    abortRequest(id){

        //Throw error if fail
        this.stopRequestInIndex(this.activeRequests.findIndex((request) => request.id == id));
    }
}

if(false){

    /**
     * @type {import("StandardRemoteRequestUtils").StandardRemoteRequestUtilsConstructor}
     */
    const check = StandardRemoteRequestUtils;
}

export default StandardRemoteRequestUtils;