"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SUPPORTED_SPEC_VERSION = void 0;
const events_1 = require("events");
const request_1 = require("../request");
const url_utils_1 = require("../url-utils");
const events_2 = require("../events");
exports.SUPPORTED_SPEC_VERSION = '4.3.0';
class Repository extends events_1.EventEmitter {
    constructor({ url, appName, instanceId, projectName, refreshInterval = 15000, timeout, headers, customHeadersFunction, httpOptions, namePrefix, tags, bootstrapProvider, bootstrapOverride = true, storageProvider, }) {
        super();
        this.failures = 0;
        this.stopped = false;
        this.ready = false;
        this.connected = false;
        this.data = {};
        this.enhanceStrategies = (strategies) => {
            return strategies === null || strategies === void 0 ? void 0 : strategies.map((strategy) => {
                const { segments, ...restOfStrategy } = strategy;
                const enhancedSegments = segments === null || segments === void 0 ? void 0 : segments.map((segment) => this.getSegment(segment));
                return { ...restOfStrategy, segments: enhancedSegments };
            });
        };
        this.url = url;
        this.refreshInterval = refreshInterval;
        this.instanceId = instanceId;
        this.appName = appName;
        this.projectName = projectName;
        this.headers = headers;
        this.timeout = timeout;
        this.customHeadersFunction = customHeadersFunction;
        this.httpOptions = httpOptions;
        this.namePrefix = namePrefix;
        this.tags = tags;
        this.bootstrapProvider = bootstrapProvider;
        this.bootstrapOverride = bootstrapOverride;
        this.storageProvider = storageProvider;
        this.segments = new Map();
    }
    timedFetch(interval) {
        if (interval > 0) {
            this.timer = setTimeout(() => this.fetch(), interval);
            if (process.env.NODE_ENV !== 'test' && typeof this.timer.unref === 'function') {
                this.timer.unref();
            }
        }
    }
    validateFeature(feature) {
        const errors = [];
        if (!Array.isArray(feature.strategies)) {
            errors.push(`feature.strategies should be an array, but was ${typeof feature.strategies}`);
        }
        if (feature.variants && !Array.isArray(feature.variants)) {
            errors.push(`feature.variants should be an array, but was ${typeof feature.variants}`);
        }
        if (typeof feature.enabled !== 'boolean') {
            errors.push(`feature.enabled should be an boolean, but was ${typeof feature.enabled}`);
        }
        if (errors.length > 0) {
            const err = new Error(errors.join(', '));
            this.emit(events_2.UnleashEvents.Error, err);
        }
    }
    async start() {
        await Promise.all([this.fetch(), this.loadBackup(), this.loadBootstrap()]);
    }
    async loadBackup() {
        try {
            const content = await this.storageProvider.get(this.appName);
            if (this.ready) {
                return;
            }
            if (content && this.notEmpty(content)) {
                this.data = this.convertToMap(content.features);
                this.segments = this.createSegmentLookup(content.segments);
                this.setReady();
            }
        }
        catch (err) {
            this.emit(events_2.UnleashEvents.Warn, err);
        }
    }
    setReady() {
        const doEmitReady = this.ready === false;
        this.ready = true;
        if (doEmitReady) {
            process.nextTick(() => {
                this.emit(events_2.UnleashEvents.Ready);
            });
        }
    }
    createSegmentLookup(segments) {
        if (!segments) {
            return new Map();
        }
        return new Map(segments.map((segment) => [segment.id, segment]));
    }
    async save(response, fromApi) {
        if (fromApi) {
            this.connected = true;
            this.data = this.convertToMap(response.features);
            this.segments = this.createSegmentLookup(response.segments);
        }
        else if (!this.connected) {
            // Only allow bootstrap if not connected
            this.data = this.convertToMap(response.features);
            this.segments = this.createSegmentLookup(response.segments);
        }
        this.setReady();
        this.emit(events_2.UnleashEvents.Changed, [...response.features]);
        await this.storageProvider.set(this.appName, response);
    }
    notEmpty(content) {
        return content.features.length > 0;
    }
    async loadBootstrap() {
        try {
            const content = await this.bootstrapProvider.readBootstrap();
            if (!this.bootstrapOverride && this.ready) {
                // early exit if we already have backup data and should not override it.
                return;
            }
            if (content && this.notEmpty(content)) {
                await this.save(content, false);
            }
        }
        catch (err) {
            this.emit(events_2.UnleashEvents.Warn, `Unleash SDK was unable to load bootstrap.
Message: ${err.message}`);
        }
    }
    convertToMap(features) {
        const obj = features.reduce((o, feature) => {
            const a = { ...o };
            this.validateFeature(feature);
            a[feature.name] = feature;
            return a;
        }, {});
        return obj;
    }
    getFailures() {
        return this.failures;
    }
    nextFetch() {
        return this.refreshInterval + this.failures * this.refreshInterval;
    }
    backoff() {
        this.failures = Math.min(this.failures + 1, 10);
        return this.nextFetch();
    }
    countSuccess() {
        this.failures = Math.max(this.failures - 1, 0);
        return this.nextFetch();
    }
    // Emits correct error message based on what failed,
    // and returns 0 as the next fetch interval (stop polling)
    configurationError(url, statusCode) {
        this.failures += 1;
        if (statusCode === 401 || statusCode === 403) {
            this.emit(events_2.UnleashEvents.Error, new Error(
            // eslint-disable-next-line max-len
            `${url} responded ${statusCode} which means your API key is not allowed to connect. Stopping refresh of toggles`));
        }
        return 0;
    }
    // We got a status code we know what to do with, so will log correct message
    // and return the new interval.
    recoverableError(url, statusCode) {
        let nextFetch = this.backoff();
        if (statusCode === 429) {
            this.emit(events_2.UnleashEvents.Warn, 
            // eslint-disable-next-line max-len
            `${url} responded TOO_MANY_CONNECTIONS (429). Backing off`);
        }
        else if (statusCode === 404) {
            this.emit(events_2.UnleashEvents.Warn, 
            // eslint-disable-next-line max-len
            `${url} responded FILE_NOT_FOUND (404). Backing off`);
        }
        else if (statusCode === 500 ||
            statusCode === 502 ||
            statusCode === 503 ||
            statusCode === 504) {
            this.emit(events_2.UnleashEvents.Warn, `${url} responded ${statusCode}. Backing off`);
        }
        return nextFetch;
    }
    handleErrorCases(url, statusCode) {
        if (statusCode === 401 || statusCode === 403) {
            return this.configurationError(url, statusCode);
        }
        else if (statusCode === 404 ||
            statusCode === 429 ||
            statusCode === 500 ||
            statusCode === 502 ||
            statusCode === 503 ||
            statusCode === 504) {
            return this.recoverableError(url, statusCode);
        }
        else {
            const error = new Error(`Response was not statusCode 2XX, but was ${statusCode}`);
            this.emit(events_2.UnleashEvents.Error, error);
            return this.refreshInterval;
        }
    }
    async fetch() {
        if (this.stopped || !(this.refreshInterval > 0)) {
            return;
        }
        let nextFetch = this.refreshInterval;
        try {
            let mergedTags;
            if (this.tags) {
                mergedTags = this.mergeTagsToStringArray(this.tags);
            }
            const url = (0, url_utils_1.default)(this.url, this.projectName, this.namePrefix, mergedTags);
            const headers = this.customHeadersFunction
                ? await this.customHeadersFunction()
                : this.headers;
            const res = await (0, request_1.get)({
                url,
                etag: this.etag,
                appName: this.appName,
                timeout: this.timeout,
                instanceId: this.instanceId,
                headers,
                httpOptions: this.httpOptions,
                supportedSpecVersion: exports.SUPPORTED_SPEC_VERSION,
            });
            if (res.status === 304) {
                // No new data
                this.emit(events_2.UnleashEvents.Unchanged);
            }
            else if (res.ok) {
                nextFetch = this.countSuccess();
                try {
                    const data = await res.json();
                    if (res.headers.get('etag') !== null) {
                        this.etag = res.headers.get('etag');
                    }
                    else {
                        this.etag = undefined;
                    }
                    await this.save(data, true);
                }
                catch (err) {
                    this.emit(events_2.UnleashEvents.Error, err);
                }
            }
            else {
                nextFetch = this.handleErrorCases(url, res.status);
            }
        }
        catch (err) {
            const e = err;
            if (e.code === 'ECONNRESET') {
                nextFetch = Math.max(Math.floor(this.refreshInterval / 2), 1000);
                this.emit(events_2.UnleashEvents.Warn, `Socket keep alive error, retrying in ${nextFetch}ms`);
            }
            else {
                this.emit(events_2.UnleashEvents.Error, err);
            }
        }
        finally {
            this.timedFetch(nextFetch);
        }
    }
    mergeTagsToStringArray(tags) {
        return tags.map((tag) => `${tag.name}:${tag.value}`);
    }
    stop() {
        this.stopped = true;
        if (this.timer) {
            clearTimeout(this.timer);
        }
        this.removeAllListeners();
    }
    getSegment(segmentId) {
        return this.segments.get(segmentId);
    }
    getToggle(name) {
        return this.data[name];
    }
    getToggles() {
        return Object.keys(this.data).map((key) => this.data[key]);
    }
    getTogglesWithSegmentData() {
        const toggles = this.getToggles();
        return toggles.map((toggle) => {
            const { strategies, ...restOfToggle } = toggle;
            return { ...restOfToggle, strategies: this.enhanceStrategies(strategies) };
        });
    }
}
exports.default = Repository;//# sourceMappingURL=https://main.vscode-cdn.net/sourcemaps/bfcd46e04becbf1670511e8c1e9cb0f1c1d62983/node_modules/unleash-client/lib/repository/index.js.map