"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpTransport = void 0;
const axios_1 = require("axios");
const cache_1 = require("../cache");
const metrics_1 = require("../metrics");
const util_1 = require("../util");
const error_1 = require("../validation/error");
const subscription_1 = require("./abstract/subscription");
const WARMUP_BATCH_REQUEST_ID = '9002';
const logger = (0, util_1.makeLogger)('HttpTransport');
/**
 * Transport implementation that takes incoming batches of requests and keeps a warm cache of values.
 * Within the setup function, adapter params are added to a set that also keeps track and expires values.
 * In the background execute, the list of non-expired items in the set is fetched.
 * Then, the list is passed through the `prepareRequest` function, that returns an AxiosRequestConfig.
 * The Data Provider response is then passed through the `parseResponse` function to create a [[CacheEntry]] list.
 * Finally, the items in that [[CacheEntry]] list are set in the Cache so the Adapter can fetch values from there.
 *
 * @typeParam T - all types related to the [[Transport]]
 */
class HttpTransport extends subscription_1.SubscriptionTransport {
    constructor(config) {
        super();
        this.config = config;
        // Flag used to track whether the warmer has moved from having no entries to having some and vice versa
        // Used for recording the cache warmer active metrics accurately
        this.WARMER_ACTIVE = false;
    }
    async initialize(dependencies, adapterSettings, endpointName, transportName) {
        await super.initialize(dependencies, adapterSettings, endpointName, transportName);
        this.requester = dependencies.requester;
    }
    getSubscriptionTtlFromConfig(adapterSettings) {
        return adapterSettings.WARMUP_SUBSCRIPTION_TTL;
    }
    async backgroundHandler(context, entries) {
        if (!entries.length) {
            logger.debug(`No entries in subscription set, sleeping for ${context.adapterSettings.BACKGROUND_EXECUTE_MS_HTTP}ms...`);
            if (this.WARMER_ACTIVE) {
                // Decrement count when warmer changed from having entries to having none
                metrics_1.metrics.get('cacheWarmerCount').labels({ isBatched: 'true' }).dec();
                this.WARMER_ACTIVE = false;
            }
            await (0, util_1.sleep)(context.adapterSettings.BACKGROUND_EXECUTE_MS_HTTP);
            return;
        }
        else if (this.WARMER_ACTIVE === false) {
            // Increment count when warmer changed from having no entries to having some
            metrics_1.metrics.get('cacheWarmerCount').labels({ isBatched: 'true' }).inc();
            this.WARMER_ACTIVE = true;
        }
        logger.trace(`Have ${entries.length} entries in batch, preparing requests...`);
        const rawRequests = this.config.prepareRequests(entries, context.adapterSettings);
        const requests = Array.isArray(rawRequests) ? rawRequests : [rawRequests];
        metrics_1.metrics
            .get('httpRequestsPerBgExecute')
            .labels({ adapter_endpoint: context.endpointName })
            .set(requests.length);
        // We're awaiting these promises because although we have request coalescing, new entries
        // could be added to the subscription set if not blocking this operation, so the next time the
        // background execute is triggered if the request is for a fully batched endpoint, we could end up
        // with the full combination of possible params within the request queue
        logger.trace(`Sending ${requests.length} requests...`);
        const start = Date.now();
        await Promise.all(requests.map((r) => this.handleRequest(r, context)));
        const duration = Date.now() - start;
        logger.trace(`All requests in the background execute were completed`);
        // These logs will surface warnings that operators should take action on, in case the execution of all
        // requests is taking too long so that entries could have expired within this timeframe
        if (duration > context.adapterSettings.WARMUP_SUBSCRIPTION_TTL) {
            logger.warn(`Background execution of all HTTP requests in a batch took ${duration},\
         which is longer than the subscription TTL (${context.adapterSettings.WARMUP_SUBSCRIPTION_TTL}).\
         This might be due to insufficient speed on the selected API tier, please check metrics and logs to confirm and consider moving to a faster tier.`);
        }
        if (duration > context.adapterSettings.CACHE_MAX_AGE) {
            logger.warn(`Background execution of all HTTP requests in a batch took ${duration},\
         which is longer than the max cache age (${context.adapterSettings.CACHE_MAX_AGE}).\
         This might be due to insufficient speed on the selected API tier, please check metrics and logs to confirm and consider moving to a faster tier.`);
        }
        // We're not sleeping here on purpose. We sleep when there are no entries in the subscription set to avoid polling too
        // frequently, but if we have entries we want the background execute to be re-run ASAP so we can prepare the next batch
        // of requests, and the sleep to rate-limit will be performed by the rate-limiter in the Requester.
        return;
    }
    async handleRequest(requestConfig, context) {
        const { results, msUntilNextExecution } = await this.makeRequest(requestConfig, context);
        if (!results.length) {
            logger.trace('Got no results from the request.');
            return;
        }
        logger.debug('Setting adapter responses in cache');
        await this.responseCache.write(this.name, results);
        if (msUntilNextExecution) {
            // If we got this, it means that the queue was unable to accomomdate this request.
            // We want to sleep here for a bit, to avoid running into constant queue overflow replacements in competing threads.
            logger.info(`Request queue has overflowed, sleeping for ${msUntilNextExecution}ms until reprocessing...`);
            await (0, util_1.sleep)(msUntilNextExecution);
        }
    }
    async makeRequest(requestConfig, context) {
        try {
            // The key generated here that we pass to the requester is potentially very long, but we're not considering it an issue given that:
            //   - the requester will store values in memory, so we're not sending the string anywhere
            //   - there's no problems using very large strings as object keys
            //   - there should be a limit on the amount of subscriptions in the set
            // Use cache key to avoid coalescing requests across different endpoints
            const requesterResult = await this.requester.request((0, cache_1.calculateHttpRequestKey)({
                context: {
                    ...context,
                    endpointName: requestConfig.endpointNameOverride ?? context.endpointName,
                },
                data: requestConfig.params,
                transportName: this.name,
            }), requestConfig.request, requestConfig.cost);
            // Parse responses and apply timestamps
            const results = this.config
                .parseResponse(requestConfig.params, requesterResult.response, context.adapterSettings)
                .map((r) => {
                const result = r;
                const partialResponse = r.response;
                result.response.timestamps = {
                    ...requesterResult.timestamps,
                    providerIndicatedTimeUnixMs: partialResponse.timestamps?.providerIndicatedTimeUnixMs,
                };
                return result;
            });
            // Record cost of data provider call
            const cost = (0, metrics_1.retrieveCost)(requesterResult.response.data);
            metrics_1.metrics
                .get('rateLimitCreditsSpentTotal')
                .labels({ feed_id: 'N/A', participant_id: WARMUP_BATCH_REQUEST_ID })
                .inc(cost);
            logger.trace('Storing successful response');
            return { results };
        }
        catch (e) {
            if (e instanceof error_1.AdapterDataProviderError && e.cause instanceof axios_1.AxiosError) {
                const err = e;
                const cause = err.cause;
                const errorMessage = `Provider request failed with status ${cause.status}: ${JSON.stringify(cause.response?.data)}`;
                (0, util_1.censorLogs)(() => logger.info(errorMessage));
                return {
                    results: requestConfig.params.map((entry) => ({
                        params: entry,
                        response: {
                            errorMessage,
                            statusCode: 502,
                            timestamps: err.timestamps,
                        },
                    })),
                };
            }
            else if (e instanceof error_1.AdapterRateLimitError) {
                const err = e;
                (0, util_1.censorLogs)(() => logger.info(err.message));
                return {
                    results: requestConfig.params.map((entry) => ({
                        params: entry,
                        response: {
                            errorMessage: err.message,
                            statusCode: 429,
                            timestamps: {
                                providerDataReceivedUnixMs: 0,
                                providerDataRequestedUnixMs: 0,
                                providerIndicatedTimeUnixMs: undefined,
                            },
                        },
                    })),
                    msUntilNextExecution: err.msUntilNextExecution,
                };
            }
            else {
                (0, util_1.censorLogs)(() => logger.error(e));
                return { results: [] };
            }
        }
    }
}
exports.HttpTransport = HttpTransport;
//# sourceMappingURL=http.js.map