diff --git a/README.md b/README.md index 5cac255..108a053 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,8 @@ const resolver = dns({ // will only be used to resolve `.com` addresses 'com.': dnsJsonOverHttps('https://cloudflare-dns.com/dns-query'), - // this can also be an array, resolvers will be tried in series + // this can also be an array, resolvers will be shuffled and tried in + // series 'net.': [ dnsJsonOverHttps('https://dns.google/resolve'), dnsJsonOverHttps('https://dns.pub/dns-query') @@ -70,7 +71,7 @@ import { dns, RecordType } from '@multiformats/dns' const resolver = dns() -// resolve TXT records +// resolve only TXT records const result = await dns.query('google.com', { types: [ RecordType.TXT @@ -78,10 +79,6 @@ const result = await dns.query('google.com', { }) ``` -N.b if multiple record types are specified, most resolvers will throw if answers -are not available for all of them, so if you consider some record types optional -it's better to make multiple queries. - ## Caching Individual Aanswers are cached so. If you make a request, for which all diff --git a/src/dns.ts b/src/dns.ts index c7e1f97..67ffec1 100644 --- a/src/dns.ts +++ b/src/dns.ts @@ -4,12 +4,17 @@ import { cache } from './utils/cache.js' import { getTypes } from './utils/get-types.js' import type { DNS as DNSInterface, DNSInit, DNSResponse, QueryOptions } from './index.js' import type { DNSResolver } from './resolvers/index.js' +import type { AnswerCache } from './utils/cache.js' + +const DEFAULT_ANSWER_CACHE_SIZE = 1000 export class DNS implements DNSInterface { - private resolvers: Record + private readonly resolvers: Record + private readonly cache: AnswerCache constructor (init: DNSInit) { this.resolvers = {} + this.cache = cache(init.cacheSize ?? DEFAULT_ANSWER_CACHE_SIZE) Object.entries(init.resolvers ?? {}).forEach(([tld, resolver]) => { if (!Array.isArray(resolver)) { @@ -40,7 +45,7 @@ export class DNS implements DNSInterface { */ async query (domain: string, options: QueryOptions = {}): Promise { const types = getTypes(options.types) - const cached = options.cached !== false ? cache.get(domain, types) : undefined + const cached = options.cached !== false ? this.cache.get(domain, types) : undefined if (cached != null) { options.onProgress?.(new CustomProgressEvent('dns:cache', { detail: cached })) @@ -68,7 +73,7 @@ export class DNS implements DNSInterface { }) for (const answer of result.Answer) { - cache.add(domain, answer) + this.cache.add(domain, answer) } return result diff --git a/src/index.ts b/src/index.ts index 56c15b6..6349ff9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ * * const resolver = dns() * - * // resolve A and AAAA records with a 5s timeout + * // resolve A records with a 5s timeout * const result = await dns.query('google.com', { * signal: AbortSignal.timeout(5000) * }) @@ -217,7 +217,44 @@ export type ResolveDnsProgressEvents = export type DNSResolvers = Record export interface DNSInit { + /** + * A set of resolvers used to answer DNS queries + * + * String keys control which resolvers are used for which TLDs. + * + * @example + * + * ```TypeScript + * import { dns } from '@multiformats/dns' + * import { dnsOverHttps } from '@multiformats/dns' + * + * const resolver = dns({ + * resolvers: { + * // only used for .com domains + * 'com.': dnsOverHttps('https://example-1.com'), + * + * // only used for .net domains, can be an array + * 'net.': [ + * dnsOverHttps('https://example-2.com'), + * dnsOverHttps('https://example-3.com'), + * ], + * + * // used for everything else (can be an array) + * '.': dnsOverHttps('https://example-4.com') + * } + * }) + * ``` + */ resolvers?: DNSResolvers + + /** + * To avoid repeating DNS lookups, successful answers are cached according to + * their TTL. To avoid exhausting memory, this option controls how many + * answers to cache. + * + * @default 1000 + */ + cacheSize?: number } export function dns (init: DNSInit = {}): DNS { diff --git a/src/utils/cache.ts b/src/utils/cache.ts index e8be3ee..cc75375 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -8,12 +8,19 @@ interface CachedAnswer { value: Answer } +export interface AnswerCache { + get (fqdn: string, types: RecordType[]): DNSResponse | undefined + add (domain: string, answer: Answer): void + remove (domain: string, type: ResponseType): void + clear (): void +} + /** * Time Aware Least Recent Used Cache * * @see https://arxiv.org/pdf/1801.00390 */ -export class AnswerCache { +class CachedAnswers { private readonly lru: ReturnType constructor (maxSize: number) { @@ -92,4 +99,6 @@ export class AnswerCache { /** * Avoid sending multiple queries for the same hostname by caching results */ -export const cache = new AnswerCache(1000) +export function cache (size: number): AnswerCache { + return new CachedAnswers(size) +} diff --git a/test/index.spec.ts b/test/index.spec.ts index 2f6858b..290c796 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -69,6 +69,37 @@ describe('dns', () => { expect(defaultResolver.calledOnce).to.be.true() }) + it('should use separate caches', async () => { + const defaultResolver = Sinon.stub() + + const answerA = { + name: 'another.com', + data: '123.123.123.123', + type: RecordType.A + } + + defaultResolver.withArgs('another.com').resolves({ + Answer: [answerA] + }) + + const resolverA = dns({ + resolvers: { + '.': defaultResolver + } + }) + const resolverB = dns({ + resolvers: { + '.': defaultResolver + } + }) + await resolverA.query('another.com') + await resolverA.query('another.com') + await resolverB.query('another.com') + await resolverB.query('another.com') + + expect(defaultResolver.calledTwice).to.be.true() + }) + it('should ignore cache results', async () => { const defaultResolver = Sinon.stub()