@esm2cjs/cacheable-lookup
Version:
A cacheable dns.lookup(…) that respects TTL. This is a fork of szmarczak/cacheable-lookup, but with CommonJS support.
377 lines (376 loc) • 11.6 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all2) => {
for (var name in all2)
__defProp(target, name, { get: all2[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var esm_exports = {};
__export(esm_exports, {
default: () => CacheableLookup
});
module.exports = __toCommonJS(esm_exports);
var import_node_dns = require("node:dns");
var import_node_util = require("node:util");
var import_node_os = __toESM(require("node:os"));
const { Resolver: AsyncResolver } = import_node_dns.promises;
const kCacheableLookupCreateConnection = Symbol("cacheableLookupCreateConnection");
const kCacheableLookupInstance = Symbol("cacheableLookupInstance");
const kExpires = Symbol("expires");
const supportsALL = typeof import_node_dns.ALL === "number";
const verifyAgent = (agent) => {
if (!(agent && typeof agent.createConnection === "function")) {
throw new Error("Expected an Agent instance as the first argument");
}
};
const map4to6 = (entries) => {
for (const entry of entries) {
if (entry.family === 6) {
continue;
}
entry.address = `::ffff:${entry.address}`;
entry.family = 6;
}
};
const getIfaceInfo = () => {
let has4 = false;
let has6 = false;
for (const device of Object.values(import_node_os.default.networkInterfaces())) {
for (const iface of device) {
if (iface.internal) {
continue;
}
if (iface.family === "IPv6") {
has6 = true;
} else {
has4 = true;
}
if (has4 && has6) {
return { has4, has6 };
}
}
}
return { has4, has6 };
};
const isIterable = (map) => {
return Symbol.iterator in map;
};
const ignoreNoResultErrors = (dnsPromise) => {
return dnsPromise.catch((error) => {
if (error.code === "ENODATA" || error.code === "ENOTFOUND" || error.code === "ENOENT") {
return [];
}
throw error;
});
};
const ttl = { ttl: true };
const all = { all: true };
const all4 = { all: true, family: 4 };
const all6 = { all: true, family: 6 };
class CacheableLookup {
constructor({
cache = /* @__PURE__ */ new Map(),
maxTtl = Infinity,
fallbackDuration = 3600,
errorTtl = 0.15,
resolver = new AsyncResolver(),
lookup = import_node_dns.lookup
} = {}) {
this.maxTtl = maxTtl;
this.errorTtl = errorTtl;
this._cache = cache;
this._resolver = resolver;
this._dnsLookup = lookup && (0, import_node_util.promisify)(lookup);
this.stats = {
cache: 0,
query: 0
};
if (this._resolver instanceof AsyncResolver) {
this._resolve4 = this._resolver.resolve4.bind(this._resolver);
this._resolve6 = this._resolver.resolve6.bind(this._resolver);
} else {
this._resolve4 = (0, import_node_util.promisify)(this._resolver.resolve4.bind(this._resolver));
this._resolve6 = (0, import_node_util.promisify)(this._resolver.resolve6.bind(this._resolver));
}
this._iface = getIfaceInfo();
this._pending = {};
this._nextRemovalTime = false;
this._hostnamesToFallback = /* @__PURE__ */ new Set();
this.fallbackDuration = fallbackDuration;
if (fallbackDuration > 0) {
const interval = setInterval(() => {
this._hostnamesToFallback.clear();
}, fallbackDuration * 1e3);
if (interval.unref) {
interval.unref();
}
this._fallbackInterval = interval;
}
this.lookup = this.lookup.bind(this);
this.lookupAsync = this.lookupAsync.bind(this);
}
set servers(servers) {
this.clear();
this._resolver.setServers(servers);
}
get servers() {
return this._resolver.getServers();
}
lookup(hostname, options, callback) {
if (typeof options === "function") {
callback = options;
options = {};
} else if (typeof options === "number") {
options = {
family: options
};
}
if (!callback) {
throw new Error("Callback must be a function.");
}
this.lookupAsync(hostname, options).then((result) => {
if (options.all) {
callback(null, result);
} else {
callback(null, result.address, result.family, result.expires, result.ttl, result.source);
}
}, callback);
}
async lookupAsync(hostname, options = {}) {
if (typeof options === "number") {
options = {
family: options
};
}
let cached = await this.query(hostname);
if (options.family === 6) {
const filtered = cached.filter((entry) => entry.family === 6);
if (options.hints & import_node_dns.V4MAPPED) {
if (supportsALL && options.hints & import_node_dns.ALL || filtered.length === 0) {
map4to6(cached);
} else {
cached = filtered;
}
} else {
cached = filtered;
}
} else if (options.family === 4) {
cached = cached.filter((entry) => entry.family === 4);
}
if (options.hints & import_node_dns.ADDRCONFIG) {
const { _iface } = this;
cached = cached.filter((entry) => entry.family === 6 ? _iface.has6 : _iface.has4);
}
if (cached.length === 0) {
const error = new Error(`cacheableLookup ENOTFOUND ${hostname}`);
error.code = "ENOTFOUND";
error.hostname = hostname;
throw error;
}
if (options.all) {
return cached;
}
return cached[0];
}
async query(hostname) {
let source = "cache";
let cached = await this._cache.get(hostname);
if (cached) {
this.stats.cache++;
}
if (!cached) {
const pending = this._pending[hostname];
if (pending) {
this.stats.cache++;
cached = await pending;
} else {
source = "query";
const newPromise = this.queryAndCache(hostname);
this._pending[hostname] = newPromise;
this.stats.query++;
try {
cached = await newPromise;
} finally {
delete this._pending[hostname];
}
}
}
cached = cached.map((entry) => {
return { ...entry, source };
});
return cached;
}
async _resolve(hostname) {
const [A, AAAA] = await Promise.all([
ignoreNoResultErrors(this._resolve4(hostname, ttl)),
ignoreNoResultErrors(this._resolve6(hostname, ttl))
]);
let aTtl = 0;
let aaaaTtl = 0;
let cacheTtl = 0;
const now = Date.now();
for (const entry of A) {
entry.family = 4;
entry.expires = now + entry.ttl * 1e3;
aTtl = Math.max(aTtl, entry.ttl);
}
for (const entry of AAAA) {
entry.family = 6;
entry.expires = now + entry.ttl * 1e3;
aaaaTtl = Math.max(aaaaTtl, entry.ttl);
}
if (A.length > 0) {
if (AAAA.length > 0) {
cacheTtl = Math.min(aTtl, aaaaTtl);
} else {
cacheTtl = aTtl;
}
} else {
cacheTtl = aaaaTtl;
}
return {
entries: [
...A,
...AAAA
],
cacheTtl
};
}
async _lookup(hostname) {
try {
const [A, AAAA] = await Promise.all([
ignoreNoResultErrors(this._dnsLookup(hostname, all4)),
ignoreNoResultErrors(this._dnsLookup(hostname, all6))
]);
return {
entries: [
...A,
...AAAA
],
cacheTtl: 0
};
} catch {
return {
entries: [],
cacheTtl: 0
};
}
}
async _set(hostname, data, cacheTtl) {
if (this.maxTtl > 0 && cacheTtl > 0) {
cacheTtl = Math.min(cacheTtl, this.maxTtl) * 1e3;
data[kExpires] = Date.now() + cacheTtl;
try {
await this._cache.set(hostname, data, cacheTtl);
} catch (error) {
this.lookupAsync = async () => {
const cacheError = new Error("Cache Error. Please recreate the CacheableLookup instance.");
cacheError.cause = error;
throw cacheError;
};
}
if (isIterable(this._cache)) {
this._tick(cacheTtl);
}
}
}
async queryAndCache(hostname) {
if (this._hostnamesToFallback.has(hostname)) {
return this._dnsLookup(hostname, all);
}
let query = await this._resolve(hostname);
if (query.entries.length === 0 && this._dnsLookup) {
query = await this._lookup(hostname);
if (query.entries.length !== 0 && this.fallbackDuration > 0) {
this._hostnamesToFallback.add(hostname);
}
}
const cacheTtl = query.entries.length === 0 ? this.errorTtl : query.cacheTtl;
await this._set(hostname, query.entries, cacheTtl);
return query.entries;
}
_tick(ms) {
const nextRemovalTime = this._nextRemovalTime;
if (!nextRemovalTime || ms < nextRemovalTime) {
clearTimeout(this._removalTimeout);
this._nextRemovalTime = ms;
this._removalTimeout = setTimeout(() => {
this._nextRemovalTime = false;
let nextExpiry = Infinity;
const now = Date.now();
for (const [hostname, entries] of this._cache) {
const expires = entries[kExpires];
if (now >= expires) {
this._cache.delete(hostname);
} else if (expires < nextExpiry) {
nextExpiry = expires;
}
}
if (nextExpiry !== Infinity) {
this._tick(nextExpiry - now);
}
}, ms);
if (this._removalTimeout.unref) {
this._removalTimeout.unref();
}
}
}
install(agent) {
verifyAgent(agent);
if (kCacheableLookupCreateConnection in agent) {
throw new Error("CacheableLookup has been already installed");
}
agent[kCacheableLookupCreateConnection] = agent.createConnection;
agent[kCacheableLookupInstance] = this;
agent.createConnection = (options, callback) => {
if (!("lookup" in options)) {
options.lookup = this.lookup;
}
return agent[kCacheableLookupCreateConnection](options, callback);
};
}
uninstall(agent) {
verifyAgent(agent);
if (agent[kCacheableLookupCreateConnection]) {
if (agent[kCacheableLookupInstance] !== this) {
throw new Error("The agent is not owned by this CacheableLookup instance");
}
agent.createConnection = agent[kCacheableLookupCreateConnection];
delete agent[kCacheableLookupCreateConnection];
delete agent[kCacheableLookupInstance];
}
}
updateInterfaceInfo() {
const { _iface } = this;
this._iface = getIfaceInfo();
if (_iface.has4 && !this._iface.has4 || _iface.has6 && !this._iface.has6) {
this._cache.clear();
}
}
clear(hostname) {
if (hostname) {
this._cache.delete(hostname);
return;
}
this._cache.clear();
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {});
//# sourceMappingURL=index.js.map