"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CdmHttpClient = void 0;
const https = require("../../Storage/request");
const Logger_1 = require("../Logging/Logger");
const CdmTimedOutException_1 = require("./CdmTimedOutException");
const CdmNumberOfRetriesExceededException_1 = require("./CdmNumberOfRetriesExceededException");
const REQUEST_TIMEOUT = 'Request timeout.';
/**
 * CDM Http Client is an HTTP client which implements retry logic to execute retries in the case of failed requests.
 * A user can specify API endpoint when creating the client and additional path in the CDM HTTP request.
 * Alternatively, if a user doesn't specify API endpoint in the client, it has to specify the full path in the request.
 * The client also expects a user to specify callback function which will be used in the case of a failure
 * (4xx or 5xx HTTP standard status codes).
 */
class CdmHttpClient {
    /**
     * Initializes a new instance of the CdmHttpClient.
     * @param {string} apiEndpoint The API endpoint
     * @param {HttpRequestCallback} httpHandler The HTTP handler which implements the interface to support injection of
     * different system's http request methods.
     */
    constructor(apiEndpoint, httpHandler) {
        this.TAG = CdmHttpClient.name;
        this.headers = new Map();
        this.apiEndpoint = apiEndpoint;
        if (httpHandler !== undefined) {
            this.httpHandler = httpHandler;
        }
        else {
            // No handler specified, use the default one to make real requests.
            this.httpHandler = https.requestUrl;
        }
    }
    /**
     * Executes a promise that will wait for some specified amount of time by using timeout.
     * @param {number} ms The wait time in milliseconds.
     * @return {Promise}
     */
    static sleep(ms) {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve => setTimeout(resolve, ms)));
        });
    }
    /**
     * Combine the base URL with the URL's suffix.
     * @param {string} baseUrl The base URL.
     * @param {string} suffix The suffix.
     * @return {string} representing the concatenated URL.
     */
    static Combine(baseUrl, suffix) {
        baseUrl = baseUrl.replace(/^[\/]+|[\/]+$/g, '');
        suffix = suffix.replace(/^[\/]+|[\/]+$/g, '');
        return `${baseUrl}/${suffix}`;
    }
    /**
     * @internal
     * Sends a CDM request with the retry logic.
     * @param {CdmHttpRequest} cdmRequest The CDM Http request.
     * @param {StorageAdapterConfigCallback} callback An optional parameter which specifies a callback function that gets
     * executed after we try to execute the HTTP request.
     * @return {Promise}, representing the CDM HTTP response.
     */
    SendAsync(cdmRequest, callback, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            // Merge headers first.
            this.headers.forEach((value, key) => {
                cdmRequest.headers.set(key, value);
            });
            // Start counting the time for this request.
            cdmRequest.start();
            try {
                return yield this.SendAsyncHelper(cdmRequest, callback, ctx);
            }
            catch (e) {
                if (e instanceof CdmTimedOutException_1.CdmTimedOutException && cdmRequest.maximumTimeoutExceeded) {
                    throw 'Maximum timeout exceeded.';
                }
                throw e;
            }
        });
    }
    /**
     * Sends a CDM request with the retry logic helper function.
     * @param {CdmHttpRequest} cdmRequest The CDM Http request.
     * @param {StorageAdapterConfigCallbackequest} callback An optional parameter which specifies the adapter which
     * contains the callback function that gets executed after we try to execute the HTTP request.
     * @return {Promise}, representing the CDM HTTP response.
     */
    SendAsyncHelper(cdmRequest, callback, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            let fullUrl;
            if (this.apiEndpoint !== undefined) {
                fullUrl = CdmHttpClient.Combine(this.apiEndpoint, cdmRequest.stripSasSig());
            }
            else {
                fullUrl = cdmRequest.requestedUrl;
            }
            // Specific to TS, we have no other way of specifying the content type.
            if (cdmRequest.contentType !== undefined) {
                cdmRequest.headers.set('Content-Type', cdmRequest.contentType);
                cdmRequest.headers.set('Content-Length', Buffer.from(cdmRequest.content).length.toString());
            }
            const outgoingHeaders = {};
            if (cdmRequest.headers !== undefined) {
                cdmRequest.headers.forEach((value, key) => {
                    outgoingHeaders[key] = value;
                });
            }
            // If the number of retries is 0, we only try once, otherwise we retry the specified number of times.
            for (let retryNumber = 0; retryNumber <= cdmRequest.numberOfRetries; retryNumber++) {
                let hasFailed = false;
                let response;
                const startTime = new Date();
                try {
                    if (ctx !== undefined) {
                        Logger_1.Logger.debug(ctx, this.TAG, this.SendAsyncHelper.name, undefined, `Sending request ${cdmRequest.requestId}, request type: ${cdmRequest.method}, request url: ${cdmRequest.stripSasSig()}, retry number: ${retryNumber}.`);
                    }
                    if (cdmRequest.maximumTimeoutExceeded) {
                        throw new CdmTimedOutException_1.CdmTimedOutException(REQUEST_TIMEOUT);
                    }
                    // Calculate how much longer we have before hitting the maximum timout.
                    const maxTimeout = cdmRequest.timeForMaximumTimeout;
                    // The request should timeout either for its own timeout or if maximum timeout is reached.
                    const timeout = Math.min(maxTimeout, cdmRequest.timeout);
                    response = yield this.httpHandler(fullUrl, cdmRequest.method, timeout, cdmRequest.content, outgoingHeaders);
                    if (ctx !== undefined) {
                        const endTime = new Date();
                        Logger_1.Logger.debug(ctx, this.TAG, this.SendAsyncHelper.name, undefined, `Response for request ${cdmRequest.requestId} received, elapsed time: ${endTime.valueOf() - startTime.valueOf()} ms.`);
                    }
                }
                catch (err) {
                    hasFailed = true;
                    const endTime = new Date();
                    if (err.code === 'ECONNRESET' && ctx) {
                        Logger_1.Logger.debug(ctx, this.TAG, this.SendAsyncHelper.name, undefined, `Request ${cdmRequest.requestId} timeout after ${endTime.valueOf() - startTime.valueOf()} ms.`);
                    }
                    // Only throw an exception if another retry is not expected anymore.
                    if (callback === undefined || retryNumber === cdmRequest.numberOfRetries) {
                        if (retryNumber !== 0 && !cdmRequest.maximumTimeoutExceeded) {
                            throw new CdmNumberOfRetriesExceededException_1.CdmNumberOfRetriesExceededException('The number of retries has exceeded the maximum number allowed by the client.');
                        }
                        else if (err.code === 'ECONNRESET') {
                            throw new CdmTimedOutException_1.CdmTimedOutException(REQUEST_TIMEOUT);
                        }
                        else {
                            throw err;
                        }
                    }
                }
                // Check whether we have a callback function set and whether this is not our last retry.
                if (callback && retryNumber !== cdmRequest.numberOfRetries && !cdmRequest.maximumTimeoutExceeded) {
                    // Call the callback function with the retry numbers starting from 1.
                    const waitTime = yield callback(response, hasFailed, retryNumber + 1);
                    // Callback has returned that we do not want to retry anymore (probably successful request,
                    // client can set up what they want here).
                    if (waitTime === undefined) {
                        return response;
                    }
                    else {
                        // Sleep time specified by the callback.
                        yield CdmHttpClient.sleep(waitTime);
                    }
                }
                else {
                    // CDM Http Response exists, could be successful or bad (e.g. 403/404), it is up to caller to deal with it.
                    if (response !== undefined) {
                        return response;
                    }
                    else if (retryNumber < cdmRequest.numberOfRetries || cdmRequest.maximumTimeoutExceeded) {
                        throw new CdmTimedOutException_1.CdmTimedOutException(REQUEST_TIMEOUT);
                    }
                    else {
                        // If response doesn't exist repeatedly, just throw that the number of retries has exceeded
                        // (we don't have any other information).
                        throw new CdmNumberOfRetriesExceededException_1.CdmNumberOfRetriesExceededException('The number of retries has exceeded the maximum number allowed by the client.');
                    }
                }
            }
        });
    }
}
exports.CdmHttpClient = CdmHttpClient;
//# sourceMappingURL=CdmHttpClient.js.map