fetch-dns
Version:
A drop-in replacement of Node's 'dns' module using 'fetch' and DNS-over-HTTPS
196 lines • 6.52 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const isFQDN_1 = tslib_1.__importDefault(require("validator/lib/isFQDN"));
const events_1 = require("events");
const CLEAN_PERIOD_MILLIS = 600 * 1000;
const PERIOD_BETWEEN_CLEANS = 1000;
const MAX_LISTENERS_FOR_COUNTER = 4096;
const MILLIS_PER_SECOND = 1000;
function itIsEmpty(it) {
if (!it || lodash_1.default.isNil(it))
return false;
return it.isEmpty();
}
function itIsNotEmpty(it) {
if (!it || lodash_1.default.isNil(it))
return false;
return !it.isEmpty();
}
const counter = new (class Counter extends events_1.EventEmitter {
constructor() {
super(...arguments);
this.pit = Number.MIN_SAFE_INTEGER;
this.horizon = Number.MIN_SAFE_INTEGER;
this.lastCleaned = Date.now();
}
shouldFire() {
return (this.lastUsedValue() % PERIOD_BETWEEN_CLEANS === 0 ||
this.lastCleaned + CLEAN_PERIOD_MILLIS < Date.now());
}
lastUsedValue() {
return this.pit;
}
runClean() {
lodash_1.default.defer(this.emit.bind(this), "clean", Math.max(this.lastUsedValue() - PERIOD_BETWEEN_CLEANS, this.horizon));
}
nextValue() {
if (this.lastUsedValue() === Number.MAX_SAFE_INTEGER) {
this.horizon = Number.MAX_SAFE_INTEGER;
this.runClean();
this.horizon = this.pit = Number.MIN_SAFE_INTEGER;
}
else if (this.shouldFire()) {
this.runClean();
this.horizon = this.lastUsedValue();
this.lastCleaned = Date.now();
}
return this.pit++;
}
})();
counter.setMaxListeners(MAX_LISTENERS_FOR_COUNTER);
function reverseHostname(hostname) {
if (!isFQDN_1.default(hostname)) {
throw new Error(`Hostname to cache is not a fully qualified domain name (FQDN): ${JSON.stringify(hostname)} (${typeof hostname})`);
}
return lodash_1.default.reject(lodash_1.default.reverse(hostname.split(".")), lodash_1.default.isEmpty);
}
class RecordCache {
constructor(records) {
this._setAt = Date.now();
this._setPit = counter.nextValue();
this._records = lodash_1.default.cloneDeep(records);
this._listenForClean();
}
_listenForClean() {
if (lodash_1.default.isEmpty(this._records))
return;
counter.once("clean", this.clean.bind(this));
}
get records() {
const now = Date.now();
return lodash_1.default.cloneDeep(lodash_1.default.filter(this._records, (record) => {
if (lodash_1.default.isEmpty(record) || lodash_1.default.isEmpty(record.record))
return false;
const ttlSeconds = record.ttl;
return ttlSeconds * MILLIS_PER_SECOND + this._setAt >= now;
}));
}
clean(horizon = Number.MIN_SAFE_INTEGER) {
const { _setPit, _records } = this;
if (lodash_1.default.isEmpty(_records))
return;
if (_setPit <= horizon) {
_records.length = 0;
}
else {
this._listenForClean();
}
}
isEmpty() {
this.clean();
return lodash_1.default.isEmpty(this._records);
}
}
class ResultCache {
constructor() {
this._mapping = {};
this._subdomains = {};
this._listenForClean();
}
_listenForClean() {
counter.once("clean", this.clean.bind(this));
}
isEmpty() {
this.clean();
return (lodash_1.default.every(this._mapping, itIsEmpty) && lodash_1.default.every(this._subdomains, itIsEmpty));
}
clean() {
lodash_1.default.forOwn(this._subdomains, (results, sub) => {
if (itIsEmpty(results)) {
lodash_1.default.unset(this._subdomains, sub);
}
});
lodash_1.default.forOwn(this._mapping, (records, rrtype) => {
if (itIsEmpty(records)) {
lodash_1.default.unset(this._mapping, rrtype);
}
});
this._listenForClean();
}
getRecordCache(rrtype) {
const cache = this._mapping[rrtype];
if (itIsNotEmpty(cache)) {
return cache;
}
else {
lodash_1.default.unset(this._mapping, rrtype);
return false;
}
}
getSubdomain(sub) {
const oldSub = this._subdomains[sub];
if (!oldSub || lodash_1.default.isEmpty(oldSub)) {
return (this._subdomains[sub] = new ResultCache());
}
else {
return oldSub;
}
}
setRecordCache(rrtype, records) {
if (lodash_1.default.isEmpty(lodash_1.default.compact(records))) {
lodash_1.default.unset(this._mapping, rrtype);
}
else {
this._mapping[rrtype] = new RecordCache(records);
}
}
setResult(domainParts, rrtype, records) {
if (lodash_1.default.isEmpty(domainParts)) {
this.setRecordCache(rrtype, records);
}
else {
const [head, ...rest] = domainParts;
this.getSubdomain(head).setResult(rest, rrtype, records);
}
}
getResult(domainParts, rrtype) {
if (lodash_1.default.isEmpty(domainParts)) {
const result = this.getRecordCache(rrtype);
if (result !== false && itIsNotEmpty(result)) {
return lodash_1.default.map(result.records, "record");
}
else {
return false;
}
}
else {
const [head, ...rest] = domainParts;
return this.getSubdomain(head).getResult(rest, rrtype);
}
}
}
exports.default = new (class DnsCache {
constructor() {
this.root = new ResultCache();
}
put(hostname, rrtype, records) {
records = lodash_1.default.compact(records);
if (lodash_1.default.isEmpty(hostname) || lodash_1.default.isEmpty(records))
return;
this.root.setResult(reverseHostname(hostname), rrtype, records);
}
check(hostname, rrtype) {
if (lodash_1.default.isEmpty(hostname))
return false;
const result = this.root.getResult(reverseHostname(hostname), rrtype);
if (result && !lodash_1.default.isEmpty(result)) {
return result;
}
else {
return false;
}
}
})();
//# sourceMappingURL=Cache.js.map