cacheable-lookup
Advanced tools
Comparing version 2.0.0 to 3.0.0
import Keyv = require('keyv'); | ||
import {Resolver, LookupAddress} from 'dns'; | ||
import {Resolver, LookupAddress, promises as dnsPromises} from 'dns'; | ||
import {Agent} from 'http'; | ||
type AsyncResolver = dnsPromises.Resolver; | ||
type IPFamily = 4 | 6; | ||
@@ -9,7 +11,2 @@ | ||
/** | ||
* A Keyv adapter which stores the cache. | ||
* @default new Map() | ||
*/ | ||
cacheAdapter?: Keyv; | ||
/** | ||
* Limits the cache time (TTL). If set to `0`, it will make a new DNS query each time. | ||
@@ -21,5 +18,5 @@ * @default Infinity | ||
* DNS Resolver used to make DNS queries. | ||
* @default new dns.Resolver() | ||
* @default new dns.promises.Resolver() | ||
*/ | ||
resolver?: Resolver; | ||
resolver?: Resolver | AsyncResolver; | ||
} | ||
@@ -60,2 +57,5 @@ | ||
all?: boolean; | ||
} | ||
interface AsyncLookupOptions extends LookupOptions { | ||
/** | ||
@@ -84,4 +84,4 @@ * Throw when there's no match. If set to `false` and it gets no match, it will return `undefined`. | ||
*/ | ||
lookupAsync(hostname: string, options: LookupOptions & {all: true}): Promise<ReadonlyArray<EntryObject>>; | ||
lookupAsync(hostname: string, options: LookupOptions): Promise<EntryObject>; | ||
lookupAsync(hostname: string, options: AsyncLookupOptions & {all: true}): Promise<ReadonlyArray<EntryObject>>; | ||
lookupAsync(hostname: string, options: AsyncLookupOptions): Promise<EntryObject>; | ||
lookupAsync(hostname: string): Promise<EntryObject>; | ||
@@ -92,7 +92,7 @@ lookupAsync(hostname: string, family: IPFamily): Promise<EntryObject>; | ||
*/ | ||
query(hostname: string, family: IPFamily): Promise<ReadonlyArray<EntryObject>>; | ||
query(hostname: string): Promise<ReadonlyArray<EntryObject>>; | ||
/** | ||
* An asynchronous function which makes a new DNS lookup query and updates the database. This is used by `query(hostname, family)` if no entry in the database is present. Returns an array of objects with `address`, `family`, `ttl` and `expires` properties. | ||
*/ | ||
queryAndCache(hostname: string, family: IPFamily): Promise<ReadonlyArray<EntryObject>>; | ||
queryAndCache(hostname: string): Promise<ReadonlyArray<EntryObject>>; | ||
/** | ||
@@ -110,2 +110,6 @@ * Attaches itself to an Agent instance. | ||
updateInterfaceInfo(): void; | ||
/** | ||
* Clears the cache. | ||
*/ | ||
clear(): void; | ||
} |
131
index.js
'use strict'; | ||
const {Resolver, V4MAPPED, ADDRCONFIG} = require('dns'); | ||
const {V4MAPPED, ADDRCONFIG, promises: dnsPromises} = require('dns'); | ||
const {promisify} = require('util'); | ||
const os = require('os'); | ||
const Keyv = require('keyv'); | ||
const {Resolver: AsyncResolver} = dnsPromises; | ||
const kCacheableLookupData = Symbol('cacheableLookupData'); | ||
@@ -48,16 +49,55 @@ const kCacheableLookupInstance = Symbol('cacheableLookupInstance'); | ||
class TTLMap { | ||
constructor() { | ||
this.values = new Map(); | ||
this.expiries = new Map(); | ||
} | ||
set(key, value, ttl) { | ||
this.values.set(key, value); | ||
this.expiries.set(key, ttl && (ttl + Date.now())); | ||
} | ||
get(key) { | ||
const expiry = this.expiries.get(key); | ||
if (typeof expiry === 'number') { | ||
if (Date.now() > expiry) { | ||
this.values.delete(key); | ||
this.expiries.delete(key); | ||
return; | ||
} | ||
} | ||
return this.values.get(key); | ||
} | ||
clear() { | ||
this.values.clear(); | ||
this.expiries.clear(); | ||
} | ||
get size() { | ||
return this.values.size; | ||
} | ||
} | ||
const ttl = {ttl: true}; | ||
class CacheableLookup { | ||
constructor({cacheAdapter, maxTtl = Infinity, resolver} = {}) { | ||
this.cache = new Keyv({ | ||
uri: typeof cacheAdapter === 'string' && cacheAdapter, | ||
store: typeof cacheAdapter !== 'string' && cacheAdapter, | ||
namespace: 'cached-lookup' | ||
}); | ||
constructor({maxTtl = Infinity, resolver} = {}) { | ||
this.maxTtl = maxTtl; | ||
this._resolver = resolver || new Resolver(); | ||
this._resolve4 = promisify(this._resolver.resolve4.bind(this._resolver)); | ||
this._resolve6 = promisify(this._resolver.resolve6.bind(this._resolver)); | ||
this._cache = new TTLMap(); | ||
this._resolver = resolver || new AsyncResolver(); | ||
if (this._resolver instanceof AsyncResolver) { | ||
this._resolve4 = this._resolver.resolve4.bind(this._resolver); | ||
this._resolve6 = this._resolver.resolve6.bind(this._resolver); | ||
} else { | ||
this._resolve4 = promisify(this._resolver.resolve4.bind(this._resolver)); | ||
this._resolve6 = promisify(this._resolver.resolve6.bind(this._resolver)); | ||
} | ||
this._iface = getIfaceInfo(); | ||
@@ -94,13 +134,14 @@ | ||
async lookupAsync(hostname, options = {}) { | ||
let cached; | ||
if (!options.family && options.all) { | ||
const [cached4, cached6] = await Promise.all([this.lookupAsync(hostname, {all: true, family: 4}), this.lookupAsync(hostname, {all: true, family: 6})]); | ||
cached = [...cached4, ...cached6]; | ||
} else { | ||
cached = await this.query(hostname, options.family || 4); | ||
let cached = await this.query(hostname); | ||
if (cached.length === 0 && options.family === 6 && options.hints & V4MAPPED) { | ||
cached = await this.query(hostname, 4); | ||
if (options.family === 6) { | ||
const filtered = cached.filter(entry => entry.family === 6); | ||
if (filtered.length === 0 && options.hints & V4MAPPED) { | ||
map4to6(cached); | ||
} else { | ||
cached = filtered; | ||
} | ||
} else if (!options.all || options.family === 4) { | ||
cached = cached.filter(entry => entry.family === 4); | ||
} | ||
@@ -139,32 +180,45 @@ | ||
async query(hostname, family) { | ||
let cached = await this.cache.get(`${hostname}:${family}`); | ||
async query(hostname) { | ||
let cached = this._cache.get(hostname); | ||
if (!cached) { | ||
cached = await this.queryAndCache(hostname, family); | ||
cached = await this.queryAndCache(hostname); | ||
} | ||
cached = cached.map(entry => { | ||
return {...entry}; | ||
}); | ||
return cached; | ||
} | ||
async queryAndCache(hostname, family) { | ||
const resolve = family === 4 ? this._resolve4 : this._resolve6; | ||
const entries = await resolve(hostname, {ttl: true}); | ||
async queryAndCache(hostname) { | ||
const [As, AAAAs] = await Promise.all([this._resolve4(hostname, ttl).catch(() => []), this._resolve6(hostname, ttl).catch(() => [])]); | ||
if (entries === undefined) { | ||
return []; | ||
let cacheTtl = 0; | ||
const now = Date.now(); | ||
if (As) { | ||
for (const entry of As) { | ||
entry.family = 4; | ||
entry.expires = now + (entry.ttl * 1000); | ||
cacheTtl = Math.max(cacheTtl, entry.ttl); | ||
} | ||
} | ||
const now = Date.now(); | ||
if (AAAAs) { | ||
for (const entry of AAAAs) { | ||
entry.family = 6; | ||
entry.expires = now + (entry.ttl * 1000); | ||
let cacheTtl = 0; | ||
for (const entry of entries) { | ||
cacheTtl = Math.max(cacheTtl, entry.ttl); | ||
entry.family = family; | ||
entry.expires = now + (entry.ttl * 1000); | ||
cacheTtl = Math.max(cacheTtl, entry.ttl); | ||
} | ||
} | ||
const entries = [...(As || []), ...(AAAAs || [])]; | ||
cacheTtl = Math.min(this.maxTtl, cacheTtl) * 1000; | ||
if (this.maxTtl !== 0 && cacheTtl !== 0) { | ||
await this.cache.set(`${hostname}:${family}`, entries, cacheTtl); | ||
if (this.maxTtl > 0 && cacheTtl > 0) { | ||
this._cache.set(hostname, entries, cacheTtl); | ||
} | ||
@@ -215,3 +269,8 @@ | ||
this._iface = getIfaceInfo(); | ||
this._cache.clear(); | ||
} | ||
clear() { | ||
this._cache.clear(); | ||
} | ||
} | ||
@@ -218,0 +277,0 @@ |
{ | ||
"name": "cacheable-lookup", | ||
"version": "2.0.0", | ||
"version": "3.0.0", | ||
"description": "A cacheable dns.lookup(…) that respects the TTL", | ||
@@ -41,6 +41,3 @@ "engines": { | ||
"xo": "^0.25.3" | ||
}, | ||
"dependencies": { | ||
"keyv": "^4.0.0" | ||
} | ||
} |
@@ -10,4 +10,3 @@ # cacheable-lookup | ||
Making lots of HTTP requests? You can save some time by caching DNS lookups.<br> | ||
Don't worry, this package respects TTL :smiley: | ||
Making lots of HTTP requests? You can save some time by caching DNS lookups :zap: | ||
@@ -50,3 +49,3 @@ ## Usage | ||
Type: `Object`<br> | ||
Type: `object`<br> | ||
Default: `{}` | ||
@@ -56,9 +55,2 @@ | ||
##### options.cacheAdapter | ||
Type: [Keyv adapter instance](https://github.com/lukechilds/keyv)<br> | ||
Default: `new Map()` | ||
A Keyv adapter which stores the cache. | ||
##### options.maxTtl | ||
@@ -75,4 +67,4 @@ | ||
Type: `Function`<br> | ||
Default: [`new dns.Resolver()`](https://nodejs.org/api/dns.html#dns_class_dns_resolver) | ||
Type: `dns.Resolver | dns.promises.Resolver`<br> | ||
Default: [`new dns.promises.Resolver()`](https://nodejs.org/api/dns.html#dns_class_dns_resolver) | ||
@@ -83,3 +75,3 @@ An instance of [DNS Resolver](https://nodejs.org/api/dns.html#dns_class_dns_resolver) used to make DNS queries. | ||
Type: `Object` | ||
Type: `object` | ||
@@ -134,3 +126,3 @@ #### address | ||
**Note**: If entry(ies) were not found, it will return `undefined`. | ||
**Note**: If entry(ies) were not found, it will return `undefined` by default. | ||
@@ -143,3 +135,3 @@ ##### hostname | ||
Type: `Object` | ||
Type: `object` | ||
@@ -153,11 +145,11 @@ The same as the [`dns.lookup(…)`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) options. | ||
Throw when there's no match. | ||
If set to `false` and it gets no match, it will return `undefined`. | ||
If set to `true` and it gets no match, it will throw `ENOTFOUND` error. | ||
**Note**: This option is meant **only** for the asynchronous implementation! The synchronous version will always give an error if no match found. | ||
**Note**: This option is meant **only** for the asynchronous implementation! The callback version will always pass an error if no match found. | ||
#### query(hostname, family) | ||
#### query(hostname) | ||
An asynchronous function which returns cached DNS lookup entries. This is the base for `lookupAsync(hostname, options)` and `lookup(hostname, options, callback)`. | ||
An asynchronous function which returns cached DNS lookup entries.<br> | ||
This is the base for `lookupAsync(hostname, options)` and `lookup(hostname, options, callback)`. | ||
@@ -168,5 +160,6 @@ **Note**: This function has no options. | ||
#### queryAndCache(hostname, family) | ||
#### queryAndCache(hostname) | ||
An asynchronous function which makes a new DNS lookup query and updates the database. This is used by `query(hostname, family)` if no entry in the database is present. | ||
An asynchronous function which makes two DNS queries: A and AAAA. The result is cached.<br> | ||
This is used by `query(hostname)` if no entry in the database is present. | ||
@@ -179,2 +172,8 @@ Returns an array of objects with `address`, `family`, `ttl` and `expires` properties. | ||
**Note:** Running `updateInterfaceInfo()` will also trigger `clear()`! | ||
#### clear() | ||
Clears the cache. | ||
## High performance | ||
@@ -185,15 +184,15 @@ | ||
``` | ||
CacheableLookup#lookupAsync x 265,390 ops/sec ±0.65% (89 runs sampled) | ||
CacheableLookup#lookupAsync.all x 119,187 ops/sec ±2.57% (87 runs sampled) | ||
CacheableLookup#lookupAsync.all.ADDRCONFIG x 119,666 ops/sec ±0.75% (89 runs sampled) | ||
CacheableLookup#lookup x 116,604 ops/sec ±0.68% (88 runs sampled) | ||
CacheableLookup#lookup.all x 115,627 ops/sec ±0.72% (89 runs sampled) | ||
CacheableLookup#lookup.all.ADDRCONFIG x 115,578 ops/sec ±0.90% (88 runs sampled) | ||
CacheableLookup#lookupAsync - zero TTL x 60.83 ops/sec ±7.43% (51 runs sampled) | ||
CacheableLookup#lookup - zero TTL x 49.22 ops/sec ±20.58% (49 runs sampled) | ||
dns#resolve4 x 63.00 ops/sec ±5.88% (51 runs sampled) | ||
dns#lookup x 21,303 ops/sec ±29.06% (35 runs sampled) | ||
dns#lookup.all x 22,283 ops/sec ±21.96% (38 runs sampled) | ||
dns#lookup.all.ADDRCONFIG x 5,922 ops/sec ±10.18% (38 runs sampled) | ||
Fastest is CacheableLookup#lookupAsync | ||
CacheableLookup#lookupAsync x 2,525,219 ops/sec ±1.66% (86 runs sampled) | ||
CacheableLookup#lookupAsync.all x 2,648,106 ops/sec ±0.59% (88 runs sampled) | ||
CacheableLookup#lookupAsync.all.ADDRCONFIG x 2,263,173 ops/sec ±0.95% (88 runs sampled) | ||
CacheableLookup#lookup x 2,108,952 ops/sec ±0.97% (89 runs sampled) | ||
CacheableLookup#lookup.all x 2,081,357 ops/sec ±1.19% (83 runs sampled) | ||
CacheableLookup#lookup.all.ADDRCONFIG x 1,913,955 ops/sec ±0.60% (89 runs sampled) | ||
CacheableLookup#lookupAsync - zero TTL x 36.50 ops/sec ±11.21% (39 runs sampled) | ||
CacheableLookup#lookup - zero TTL x 33.66 ops/sec ±7.57% (47 runs sampled) | ||
dns#resolve4 x 40.31 ops/sec ±16.10% (49 runs sampled) | ||
dns#lookup x 13,722 ops/sec ±20.69% (37 runs sampled) | ||
dns#lookup.all x 30,343 ops/sec ±28.97% (47 runs sampled) | ||
dns#lookup.all.ADDRCONFIG x 7,023 ops/sec ±15.86% (31 runs sampled) | ||
Fastest is CacheableLookup#lookupAsync.all | ||
``` | ||
@@ -200,0 +199,0 @@ |
18031
0
313
202
- Removedkeyv@^4.0.0
- Removedjson-buffer@3.0.1(transitive)
- Removedkeyv@4.5.4(transitive)