"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AsyncCaller = void 0;
const p_retry_1 = __importDefault(require("p-retry"));
const p_queue_1 = __importDefault(require("p-queue"));
const fetch_js_1 = require("../singletons/fetch.cjs");
const STATUS_NO_RETRY = [
    400, // Bad Request
    401, // Unauthorized
    402, // Payment required
    403, // Forbidden
    404, // Not Found
    405, // Method Not Allowed
    406, // Not Acceptable
    407, // Proxy Authentication Required
    408, // Request Timeout
    409, // Conflict
    422, // Unprocessable Entity
];
/**
 * Do not rely on globalThis.Response, rather just
 * do duck typing
 */
function isResponse(x) {
    if (x == null || typeof x !== "object")
        return false;
    return "status" in x && "statusText" in x && "text" in x;
}
/**
 * Utility error to properly handle failed requests
 */
class HTTPError extends Error {
    constructor(status, message, response) {
        super(`HTTP ${status}: ${message}`);
        Object.defineProperty(this, "status", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "text", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "response", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        this.status = status;
        this.text = message;
        this.response = response;
    }
    static async fromResponse(response, options) {
        try {
            return new HTTPError(response.status, await response.text(), options?.includeResponse ? response : undefined);
        }
        catch {
            return new HTTPError(response.status, response.statusText, options?.includeResponse ? response : undefined);
        }
    }
}
/**
 * A class that can be used to make async calls with concurrency and retry logic.
 *
 * This is useful for making calls to any kind of "expensive" external resource,
 * be it because it's rate-limited, subject to network issues, etc.
 *
 * Concurrent calls are limited by the `maxConcurrency` parameter, which defaults
 * to `Infinity`. This means that by default, all calls will be made in parallel.
 *
 * Retries are limited by the `maxRetries` parameter, which defaults to 5. This
 * means that by default, each call will be retried up to 5 times, with an
 * exponential backoff between each attempt.
 */
class AsyncCaller {
    constructor(params) {
        Object.defineProperty(this, "maxConcurrency", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "maxRetries", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "queue", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "onFailedResponseHook", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "customFetch", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        this.maxConcurrency = params.maxConcurrency ?? Infinity;
        this.maxRetries = params.maxRetries ?? 4;
        if ("default" in p_queue_1.default) {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            this.queue = new p_queue_1.default.default({
                concurrency: this.maxConcurrency,
            });
        }
        else {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            this.queue = new p_queue_1.default({ concurrency: this.maxConcurrency });
        }
        this.onFailedResponseHook = params?.onFailedResponseHook;
        this.customFetch = params.fetch;
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    call(callable, ...args) {
        const onFailedResponseHook = this.onFailedResponseHook;
        return this.queue.add(() => (0, p_retry_1.default)(() => callable(...args).catch(async (error) => {
            // eslint-disable-next-line no-instanceof/no-instanceof
            if (error instanceof Error) {
                throw error;
            }
            else if (isResponse(error)) {
                throw await HTTPError.fromResponse(error, {
                    includeResponse: !!onFailedResponseHook,
                });
            }
            else {
                throw new Error(error);
            }
        }), {
            async onFailedAttempt(error) {
                if (error.message.startsWith("Cancel") ||
                    error.message.startsWith("TimeoutError") ||
                    error.message.startsWith("AbortError")) {
                    throw error;
                }
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                if (error?.code === "ECONNABORTED") {
                    throw error;
                }
                if (error instanceof HTTPError) {
                    if (STATUS_NO_RETRY.includes(error.status)) {
                        throw error;
                    }
                    if (onFailedResponseHook && error.response) {
                        await onFailedResponseHook(error.response);
                    }
                }
            },
            // If needed we can change some of the defaults here,
            // but they're quite sensible.
            retries: this.maxRetries,
            randomize: true,
        }), { throwOnTimeout: true });
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    callWithOptions(options, callable, ...args) {
        // Note this doesn't cancel the underlying request,
        // when available prefer to use the signal option of the underlying call
        if (options.signal) {
            return Promise.race([
                this.call(callable, ...args),
                new Promise((_, reject) => {
                    options.signal?.addEventListener("abort", () => {
                        reject(new Error("AbortError"));
                    });
                }),
            ]);
        }
        return this.call(callable, ...args);
    }
    fetch(...args) {
        const fetchFn = this.customFetch ?? (0, fetch_js_1._getFetchImplementation)();
        return this.call(() => fetchFn(...args).then((res) => (res.ok ? res : Promise.reject(res))));
    }
}
exports.AsyncCaller = AsyncCaller;
