Source: Http.js

// Copyright (c) 2021-2022 BuiltByBit (Mick Capital Pty. Ltd.)
// MIT License (https://github.com/BuiltByBit/js-api-wrapper/blob/main/LICENSE)

const { SortOptions } = require("./SortOptions.js");
const { APIError } = require("./APIError.js");

/** A type that handles raw HTTP requests to the API. */
class Http {
    /** The maximum number of objects returned by a list endpoint for a single request. */
    static #PER_PAGE = 20;

    /** The content type used for WRITE operations with bodies (ie. POST/PATCH). */
    static #WRITE_HEADERS = {headers: {"Content-Type": "application/json"}};

    #client;
    #throttler;

    constructor(client, throtter) {
        this.#client = client;
        this.#throttler = throtter;
    }

    /** Schedules a GET request for a specific endpoint.
     * 
     * @param {string} endpoint The path of the endpoint (incl. any path parameters).
     * @param {SortOptions | undefined} sort The optional set of sort options.
     * 
     * @return {*} The response data on success.
     */
    async get(endpoint, sort = new SortOptions()) {
        if (sort.isSet()) endpoint += sort.toQueryString();
        await this.#throttler.stallIfRequired(false);

        try {
            let response = await this.#client.get(endpoint);
            this.#throttler.resetRead();
            return response.data.data;
        } catch (error) {
            await this.#handleError(error, false);
            return this.get(endpoint);
        }
    }

    /** Schedules a POST request for a specific endpoint.
     * 
     * @param {string} endpoint The path of the endpoint (incl. any path parameters).
     * @param {object} body The request body options.
     * 
     * @return {number} The response data on success (ie. a content identifier).
     */
    async post(endpoint, body) {
        await this.#throttler.stallIfRequired(true);

        try {
            let response = await this.#client.post(endpoint, body, Http.#WRITE_HEADERS);
            this.#throttler.resetWrite();
            return response.data.data;
        } catch (error) {
            await this.#handleError(error, true);
            return this.post(endpoint, body);
        }
    }

    /** Schedules a PATCH request for a specific endpoint.
     * 
     * @param {string} endpoint The path of the endpoint (incl. any path parameters).
     * @param {object} body The request body options.
     */
    async patch(endpoint, body) {
        await this.#throttler.stallIfRequired(true);

        try {
            await this.#client.patch(endpoint, body, Http.#WRITE_HEADERS);
            this.#throttler.resetWrite();
            return;
        } catch (error) {
            await this.#handleError(error, true);
            return this.patch(endpoint, body);
        }
    }

    /** Schedules a DELETE request for a specific endpoint.
     * 
     * @param {string} endpoint The path of the endpoint (incl. any path parameters).
     */
    async delete(endpoint) {
        await this.#throttler.stallIfRequired(true);

        try {
            await this.#client.delete(endpoint);
            this.#throttler.resetWrite();
            return;
        } catch (error) {
            await this.#handleError(error, true);
            return this.delete(endpoint);
        }
    }

    /** A raw function returning a compiled list of objects from all available pages or until we decide to stop.
     * 
     * <br><br>
     * 
     * 'shouldContinue' expects a function with a single parameter and should return a boolean representing if we
     * should continue to add the current (and future) objects to the final list (and thus, if we should continue
     * to make requests). This function is called for every single object as a parameter within each request's
     * returned list.
     * 
     * <br><br>
     * 
     * This function continuously makes requests to a specific endpoint with a set of sort options, and increments the
     * sort option page count after each request.
     * 
     * @param {string} endpoint The path of the endpoint (incl. any path parameters).
     * @param {function(object):boolean} shouldContinue A function which determines if further pages are requested.
     * @param {SortOptions | undefined} sort An optional set of sort options.
     * 
     * @return {Array<object>} An array of raw objects.
     */
    async listUntil(endpoint, shouldContinue, sort = new SortOptions()) {
        sort.page = 1;

        let allData = [];
        let continueFor = true;
        
        while (continueFor) {
            let data = await this.get(endpoint, sort);

            for (const index in data) {
                if (shouldContinue(data[index])) {
                    allData.push(data[index]);
                } else {
                    continueFor = false;
                    break;
                }
            }

            if (data.length != Http.#PER_PAGE) continueFor = false;
            sort.page++;
        }

        return allData;
    }

    /** An internal function which throws an error if the provided error indicates the request is not retryable (ie.
     * a non-rate-limiting error).
     * 
     * @param {*} error The provided error.
     * @param {boolean} write Whether or not the request was of the WRITE type (ie. POST/PATCH/DELETE).
     */
    async #handleError(error, write) {
        if (!error.response) {
            throw APIError.internal(error.message);
        }
        
        if (error.response.status === 429) {
            let retryAfter = error.response.headers["retry-after"];

            if (write) {
                this.#throttler.setWrite(retryAfter);
            } else {
                this.#throttler.setRead(retryAfter);
            }

            return;
        }

        throw new APIError(error.response.data.error);
    }
}

exports.Http = Http;