import QuickLRU from 'quick-lru';
import debug from 'weald';
import * as utils from './utils.js';
const log = Object.assign(debug('dns-over-http-resolver'), {
    error: debug('dns-over-http-resolver:error')
});
/**
 * DNS over HTTP resolver.
 * Uses a list of servers to resolve DNS records with HTTP requests.
 */
class Resolver {
    _cache;
    _TXTcache;
    _servers;
    _request;
    _abortControllers;
    /**
     * @class
     * @param {object} [options]
     * @param {number} [options.maxCache = 100] - maximum number of cached dns records
     * @param {Request} [options.request] - function to return DNSJSON
     */
    constructor(options = {}) {
        this._cache = new QuickLRU({ maxSize: options?.maxCache ?? 100 });
        this._TXTcache = new QuickLRU({ maxSize: options?.maxCache ?? 100 });
        this._servers = [
            'https://cloudflare-dns.com/dns-query',
            'https://dns.google/resolve'
        ];
        this._request = options.request ?? utils.request;
        this._abortControllers = [];
    }
    /**
     * Cancel all outstanding DNS queries made by this resolver. Any outstanding
     * requests will be aborted and promises rejected.
     */
    cancel() {
        this._abortControllers.forEach(controller => { controller.abort(); });
    }
    /**
     * Get an array of the IP addresses currently configured for DNS resolution.
     * These addresses are formatted according to RFC 5952. It can include a custom port.
     */
    getServers() {
        return this._servers;
    }
    /**
     * Get a shuffled array of the IP addresses currently configured for DNS resolution.
     * These addresses are formatted according to RFC 5952. It can include a custom port.
     */
    _getShuffledServers() {
        const newServers = [...this._servers];
        for (let i = newServers.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * i);
            const temp = newServers[i];
            newServers[i] = newServers[j];
            newServers[j] = temp;
        }
        return newServers;
    }
    /**
     * Sets the IP address and port of servers to be used when performing DNS resolution.
     *
     * @param {string[]} servers - array of RFC 5952 formatted addresses.
     */
    setServers(servers) {
        this._servers = servers;
    }
    async resolve(hostname, rrType = 'A') {
        switch (rrType) {
            case 'A':
                return this.resolve4(hostname);
            case 'AAAA':
                return this.resolve6(hostname);
            case 'TXT':
                return this.resolveTxt(hostname);
            default:
                throw new Error(`${rrType} is not supported`);
        }
    }
    /**
     * Uses the DNS protocol to resolve the given host name into IPv4 addresses
     *
     * @param {string} hostname - host name to resolve
     */
    async resolve4(hostname) {
        const recordType = 'A';
        const cached = this._cache.get(utils.getCacheKey(hostname, recordType));
        if (cached != null) {
            return cached;
        }
        let aborted = false;
        for (const server of this._getShuffledServers()) {
            const controller = new AbortController();
            this._abortControllers.push(controller);
            try {
                const response = await this._request(utils.buildResource(server, hostname, recordType), controller.signal);
                const data = response.Answer.map(a => a.data);
                const ttl = Math.min(...response.Answer.map(a => a.TTL));
                this._cache.set(utils.getCacheKey(hostname, recordType), data, { maxAge: ttl });
                return data;
            }
            catch (err) {
                if (controller.signal.aborted) {
                    aborted = true;
                }
                log.error(`${server} could not resolve ${hostname} record ${recordType}`);
            }
            finally {
                this._abortControllers = this._abortControllers.filter(c => c !== controller);
            }
        }
        if (aborted) {
            throw Object.assign(new Error('queryA ECANCELLED'), {
                code: 'ECANCELLED'
            });
        }
        throw new Error(`Could not resolve ${hostname} record ${recordType}`);
    }
    /**
     * Uses the DNS protocol to resolve the given host name into IPv6 addresses
     *
     * @param {string} hostname - host name to resolve
     */
    async resolve6(hostname) {
        const recordType = 'AAAA';
        const cached = this._cache.get(utils.getCacheKey(hostname, recordType));
        if (cached != null) {
            return cached;
        }
        let aborted = false;
        for (const server of this._getShuffledServers()) {
            const controller = new AbortController();
            this._abortControllers.push(controller);
            try {
                const response = await this._request(utils.buildResource(server, hostname, recordType), controller.signal);
                const data = response.Answer.map(a => a.data);
                const ttl = Math.min(...response.Answer.map(a => a.TTL));
                this._cache.set(utils.getCacheKey(hostname, recordType), data, { maxAge: ttl });
                return data;
            }
            catch (err) {
                if (controller.signal.aborted) {
                    aborted = true;
                }
                log.error(`${server} could not resolve ${hostname} record ${recordType}`);
            }
            finally {
                this._abortControllers = this._abortControllers.filter(c => c !== controller);
            }
        }
        if (aborted) {
            throw Object.assign(new Error('queryAaaa ECANCELLED'), {
                code: 'ECANCELLED'
            });
        }
        throw new Error(`Could not resolve ${hostname} record ${recordType}`);
    }
    /**
     * Uses the DNS protocol to resolve the given host name into a Text record
     *
     * @param {string} hostname - host name to resolve
     */
    async resolveTxt(hostname) {
        const recordType = 'TXT';
        const cached = this._TXTcache.get(utils.getCacheKey(hostname, recordType));
        if (cached != null) {
            return cached;
        }
        let aborted = false;
        for (const server of this._getShuffledServers()) {
            const controller = new AbortController();
            this._abortControllers.push(controller);
            try {
                const response = await this._request(utils.buildResource(server, hostname, recordType), controller.signal);
                const data = response.Answer.map(a => [a.data.replace(/['"]+/g, '')]);
                const ttl = Math.min(...response.Answer.map(a => a.TTL));
                this._TXTcache.set(utils.getCacheKey(hostname, recordType), data, { maxAge: ttl });
                return data;
            }
            catch (err) {
                if (controller.signal.aborted) {
                    aborted = true;
                }
                log.error(`${server} could not resolve ${hostname} record ${recordType}`);
            }
            finally {
                this._abortControllers = this._abortControllers.filter(c => c !== controller);
            }
        }
        if (aborted) {
            throw Object.assign(new Error('queryTxt ECANCELLED'), {
                code: 'ECANCELLED'
            });
        }
        throw new Error(`Could not resolve ${hostname} record ${recordType}`);
    }
    clearCache() {
        this._cache.clear();
        this._TXTcache.clear();
    }
}
export default Resolver;
//# sourceMappingURL=index.js.map