tangerine 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +223 -213
  2. package/index.js +73 -61
  3. package/package.json +6 -4
package/README.md CHANGED
@@ -11,11 +11,11 @@
11
11
  </div>
12
12
  <br />
13
13
  <div align="center">
14
- 🍊 <a href="https://github.com/forwardemail/tangerine" target="_blank">Tangerine</a> is the best <a href="https://nodejs.org" target="_blank">Node.js</a> drop-in replacement for <a href="https://nodejs.org/api/dns.html#resolveroptions" target="_blank">dns.promises.Resolver</a> using <a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank">DNS over HTTPS</a> ("DoH") via <a href="https://github.com/nodejs/undici" target="_blank">undici</a> with built-in retries, timeouts, smart server rotation, <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController" target="_blank">AbortControllers</a>, and caching support for multiple backends via <a href="https://github.com/jaredwray/keyv" target="_blank">Keyv</a>.
14
+ 🍊 <a href="https://github.com/forwardemail/tangerine" target="_blank">Tangerine</a> is the best <a href="https://nodejs.org" target="_blank">Node.js</a> drop-in replacement for <a href="https://nodejs.org/api/dns.html#resolveroptions" target="_blank">dns.promises.Resolver</a> using <a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank">DNS over HTTPS</a> ("DoH") via <a href="https://github.com/nodejs/undici" target="_blank">undici</a> with built-in retries, timeouts, smart server rotation, <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController" target="_blank">AbortControllers</a>, and caching support for multiple backends (with TTL support).
15
15
  </div>
16
16
  <hr />
17
17
  <div align="center">
18
- ⚡ <mark><a href="#tangerine-benchmarks"><i><u><strong>FASTER</strong></u></i></a></mark> than <a href="https://nodejs.org/api/dns.html" target="_blank">Node.js <code>dns</code></a>! 🚀 &bull; Supports Node v16+ with ESM/CJS &bull; Made for <a href="https://forwardemail.net" target="_blank"><strong>Forward Email</strong></a>.
18
+ ⚡ <a href="#tangerine-benchmarks"><i><u><strong>AS FAST AS</strong></u></i></a> native <a href="https://nodejs.org/api/dns.html" target="_blank">Node.js <code>dns</code></a>! 🚀 &bull; Supports Node v16+ with ESM/CJS &bull; Made for <a href="https://forwardemail.net" target="_blank"><strong>Forward Email</strong></a>.
19
19
  </div>
20
20
  <hr />
21
21
 
@@ -55,6 +55,7 @@
55
55
  * [`tangerine.setDefaultResultOrder(order)`](#tangerinesetdefaultresultorderorder)
56
56
  * [`tangerine.setServers(servers)`](#tangerinesetserversservers)
57
57
  * [Options](#options)
58
+ * [Cache](#cache)
58
59
  * [Debugging](#debugging)
59
60
  * [Benchmarks](#benchmarks)
60
61
  * [Tangerine Benchmarks](#tangerine-benchmarks)
@@ -96,7 +97,7 @@ Our team at [Forward Email](https://forwardemail.net) (100% open-source and priv
96
97
  * Once popular packages such as [native-dns](https://github.com/tjfontaine/node-dns/issues/111) and [dnscached](https://github.com/yahoo/dnscache/issues/28) are archived or deprecated.
97
98
  * [Other packages](https://www.npmjs.com/search?q=dns%20cache) only provide `lookup` functions, have a limited sub-set of methods such as [@zeit/dns-cached-resolver](https://github.com/vercel/dns-cached-resolve), or are unmaintained.
98
99
  * Act as a 1:1 drop-in replacement for `dns.promises.Resolver` with DNS over HTTPS ("DoH").
99
- * Support caching for multiple backends with [Keyv](https://github.com/jaredwray/keyv) (with respect to DNS answer TTL too!), retries, smart server rotation, and [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) usage.
100
+ * Support caching for multiple backends (with TTL support), retries, smart server rotation, and [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) usage.
100
101
  * Provide out of the box support for both ECMAScript modules (ESM) **and** CommonJS (CJS) (see discussions [for](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) and [against](https://gist.github.com/joepie91/bca2fda868c1e8b2c2caf76af7dfcad3)).
101
102
  * The native Node.js `dns` module does not support caching out of the box – which is a [highly requested feature](https://github.com/nodejs/node/issues/5893) (but belongs in userland).
102
103
  * Writing tests against DNS-related infrastructure requires either hacky DNS mocking or a DNS server (manipulating cache is much easier).
@@ -276,27 +277,86 @@ Tangerine supports a new `ecsSubnet` property in the `options` Object argument.
276
277
 
277
278
  Similar to the `options` argument from `new dns.promises.Resolver(options)` invocation – :tangerine: Tangerine also has its own options with default `dns` behavior mirrored. See [index.js](https://github.com/forwardemail/tangerine/blob/main/index.js) for more insight into how these options work.
278
279
 
279
- | Property | Type | Default Value | Description |
280
- | ------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
281
- | `timeout` | `Number` | `5000` | Number of milliseconds for requests to timeout. |
282
- | `tries` | `Number` | `4` | Number of tries per `server` in `servers` to attempt. |
283
- | `servers` | `Set` or `Array` | `new Set(['1.1.1.1', '1.0.0.1'])` | A Set or Array of [RFC 5952](https://tools.ietf.org/html/rfc5952#section-6) formatted addresses for DNS queries (matches default Node.js dns module behavior). Duplicates will be removed as this is converted to a `Set` internally. Defaults to Cloudflare's of `1.1.1.1` and `1.0.0.1`. If an `Array` is passed, then it will be converted to a `Set`. |
284
- | `requestOptions` | `Object` | Defaults to an Object with `requestOptions.method` and `requestOptions.headers` properties and values below | Default options to pass to [undici](https://github.com/nodejs/undici) (or your custom HTTP library function passed as `request`). |
285
- | `requestOptions.method` | `String` | Defaults to `"GET"` (must be either `"GET"` or `"POST"`, case-insensitive depending on library you use). | Default HTTP method to use for DNS over HTTP ("DoH") requests. |
286
- | `requestOptions.headers` | `Object` | Defaults to `{ 'content-type': 'application/dns-message', 'user-agent': pkg.name + "/" + pkg.version, accept: 'application/dns-message', bodyTimeout: timeout }`. | Default HTTP headers to use for DNS over HTTP ("DoH") requests. |
287
- | `protocol` | `String` | Defaults to `"https"`. | Default HTTP protocol to use for DNS over HTTPS ("DoH") requests. |
288
- | `dnsOrder` | `String` | Defaults to `"verbatim"` for Node.js v17.0.0+ and `"ipv4first"` for older versions. | Sets the default result order of `lookup` invocations (see [dns.setDefaultResultOrder](https://nodejs.org/api/dns.html#dnssetdefaultresultorderorder) for more insight). |
289
- | `logger` | `Object` | `false` | This is the default logger. We recommend using [Cabin](https://github.com/cabinjs) instead of using `console` as your default logger. Set this value to `false` to disable logging entirely (uses noop function). |
290
- | `id` | `Number` or `Function` | `0` | Default `id` to be passed for DNS packet creation. This could alternatively be a synchronous or asynchronous function that returns a `Number` (e.g. `id: () => Tangerine.getRandomInt(1, 65534)`). |
291
- | `concurrency` | `Number` | `os.cpus().length` | Default concurrency to use for `resolveAny` lookup via [p-map](https://github.com/sindresorhus/p-map). The default value is the number of CPU's available to the system using the Node.js `os` module [os.cpus()](https://nodejs.org/api/os.html#oscpus) method. |
292
- | `ipv4` | `String` | `"0.0.0.0"` | Default IPv4 address to use for HTTP agent `localAddress` if DNS `server` was an IPv4 address. |
293
- | `ipv6` | `String` | `"::0"` | Default IPv6 address to use for HTTP agent `localAddress` if DNS `server` was an IPv6 address. |
294
- | `ipv4Port` | `Number` | `undefined` | Default port to use for HTTP agent `localPort` if DNS `server` was an IPv4 address. |
295
- | `ipv6Port` | `Number` | `undefined` | Default port to use for HTTP agent `localPort` if DNS `server` was an IPv6 address. |
296
- | `cache` | `Map` or `Boolean` | `new Map()` | Set this to `false` in order to disable caching. Default `Map` instance to use for caching. Entries are by type, e.g. `map.set('TXT', new Keyv({})`). If cache set values are not provided, then they will default to a new instance of `Keyv`. See cache setup and usage in [index.js](https://github.com/forwardemail/tangerine/blob/main/index.js) for more insight. You can iterate over `Tangerine.TYPES` if necessary to create a similar cache setup. |
297
- | `returnHTTPErrors` | `Boolean` | `false` | Whether to return HTTP errors instead of mapping them to corresponding DNS errors. |
298
- | `smartRotate` | `Boolean` | `true` | Whether to do smart server rotation if servers fail. |
299
- | `defaultHTTPErrorMessage` | `String` | `"Unsuccessful HTTP response"` | Default fallback message if `statusCode` returned from HTTP request was not found in [http.STATUS_CODES](https://nodejs.org/api/http.html#httpstatus_codes). |
280
+ | Property | Type | Default Value | Description |
281
+ | ------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
282
+ | `timeout` | `Number` | `5000` | Number of milliseconds for requests to timeout. |
283
+ | `tries` | `Number` | `4` | Number of tries per `server` in `servers` to attempt. |
284
+ | `servers` | `Set` or `Array` | `new Set(['1.1.1.1', '1.0.0.1'])` | A Set or Array of [RFC 5952](https://tools.ietf.org/html/rfc5952#section-6) formatted addresses for DNS queries (matches default Node.js dns module behavior). Duplicates will be removed as this is converted to a `Set` internally. Defaults to Cloudflare's of `1.1.1.1` and `1.0.0.1`. If an `Array` is passed, then it will be converted to a `Set`. |
285
+ | `requestOptions` | `Object` | Defaults to an Object with `requestOptions.method` and `requestOptions.headers` properties and values below | Default options to pass to [undici](https://github.com/nodejs/undici) (or your custom HTTP library function passed as `request`). |
286
+ | `requestOptions.method` | `String` | Defaults to `"GET"` (must be either `"GET"` or `"POST"`, case-insensitive depending on library you use). | Default HTTP method to use for DNS over HTTP ("DoH") requests. |
287
+ | `requestOptions.headers` | `Object` | Defaults to `{ 'content-type': 'application/dns-message', 'user-agent': pkg.name + "/" + pkg.version, accept: 'application/dns-message' }`. | Default HTTP headers to use for DNS over HTTP ("DoH") requests. |
288
+ | `protocol` | `String` | Defaults to `"https"`. | Default HTTP protocol to use for DNS over HTTPS ("DoH") requests. |
289
+ | `dnsOrder` | `String` | Defaults to `"verbatim"` for Node.js v17.0.0+ and `"ipv4first"` for older versions. | Sets the default result order of `lookup` invocations (see [dns.setDefaultResultOrder](https://nodejs.org/api/dns.html#dnssetdefaultresultorderorder) for more insight). |
290
+ | `logger` | `Object` | `false` | This is the default logger. We recommend using [Cabin](https://github.com/cabinjs) instead of using `console` as your default logger. Set this value to `false` to disable logging entirely (uses noop function). |
291
+ | `id` | `Number` or `Function` | `0` | Default `id` to be passed for DNS packet creation. This could alternatively be a synchronous or asynchronous function that returns a `Number` (e.g. `id: () => Tangerine.getRandomInt(1, 65534)`). |
292
+ | `concurrency` | `Number` | `os.cpus().length` | Default concurrency to use for `resolveAny` lookup via [p-map](https://github.com/sindresorhus/p-map). The default value is the number of CPU's available to the system using the Node.js `os` module [os.cpus()](https://nodejs.org/api/os.html#oscpus) method. |
293
+ | `ipv4` | `String` | `"0.0.0.0"` | Default IPv4 address to use for HTTP agent `localAddress` if DNS `server` was an IPv4 address. |
294
+ | `ipv6` | `String` | `"::0"` | Default IPv6 address to use for HTTP agent `localAddress` if DNS `server` was an IPv6 address. |
295
+ | `ipv4Port` | `Number` | `undefined` | Default port to use for HTTP agent `localPort` if DNS `server` was an IPv4 address. |
296
+ | `ipv6Port` | `Number` | `undefined` | Default port to use for HTTP agent `localPort` if DNS `server` was an IPv6 address. |
297
+ | `cache` | `Map`, `Boolean`, or custom cache implementation with `get` and `set` methods | `new Map()` | Set this to `false` in order to disable caching. By default or if you pass `cache: true`, it will use a new `Map` instance for caching. See [Cache](#cache) documentation and the options `defaultTTLSeconds`, `maxTTLSeconds`, and `setCacheArgs` below. |
298
+ | `defaultTTLSeconds` | `Number` (seconds) | `300` | The default number of seconds to use for storing results in cache (defaults to [Cloudflare's recommendation](https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl/) of 300 seconds – 5 minutes). |
299
+ | `maxTTLSeconds` | `Number` (seconds) | `86400` | The maximum number of seconds to use for storing results in cache (defaults to [Cloudflare's recommendation](https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl/) of 86,400 seconds – 24 hours – 1 day). |
300
+ | `setCacheArgs` | `Function` | `(key, result) => []` | This is a helper function used for cache store providers such as [ioredis](https://github.com/luin/ioredis) or [lru-cache](https://github.com/isaacs/node-lru-cache) which support more than two arguments to `cache.set()` function. See [Cache](#cache) documentation below for more insight and examples into how this works. You may want to set this to something such as `(key, result) => [ 'PX', Math.round(result.ttl * 1000) ]` if you are using `ioredis`. |
301
+ | `returnHTTPErrors` | `Boolean` | `false` | Whether to return HTTP errors instead of mapping them to corresponding DNS errors. |
302
+ | `smartRotate` | `Boolean` | `true` | Whether to do smart server rotation if servers fail. |
303
+ | `defaultHTTPErrorMessage` | `String` | `"Unsuccessful HTTP response"` | Default fallback message if `statusCode` returned from HTTP request was not found in [http.STATUS_CODES](https://nodejs.org/api/http.html#httpstatus_codes). |
304
+
305
+
306
+ ## Cache
307
+
308
+ :tangerine: Tangerine supports custom cache implementations, such as with [ioredis](https://github.com/luin/ioredis) or any other cache store that has a Map-like implementation with `set(key, value)` and `get(key)` methods. If your cache implementation allows a third argument to `set()`, such as `set(key, value, ttl)` or `set(key, value, { maxAge })`, then you must set the `setCacheArgs` option respectively (see below examples). A third argument with TTL argument support is optional as it is already built-in to :tangerine: Tangerine out of the box (cached results store their TTL and expiration time on the objects themselves – view source code for insight).
309
+
310
+ ```sh
311
+ npm install tangerine undici ioredis
312
+ ```
313
+
314
+ ```js
315
+ // app.js
316
+
317
+ const Redis = require('ioredis');
318
+ const Tangerine = require('tangerine');
319
+
320
+ // <https://github.com/luin/ioredis/issues/1179>
321
+ Redis.Command.setArgumentTransformer('set', (args) => {
322
+ if (typeof args[1] === 'object') args[1] = JSON.stringify(args[1]);
323
+ return args;
324
+ });
325
+
326
+ Redis.Command.setReplyTransformer('get', (value) => {
327
+ if (value && typeof value === 'string') {
328
+ try {
329
+ value = JSON.parse(value);
330
+ } catch {}
331
+ }
332
+
333
+ return value;
334
+ });
335
+
336
+ const cache = new Redis();
337
+ const tangerine = new Tangerine({
338
+ cache,
339
+ setCacheArgs(key, result) {
340
+ return ['PX', Math.round(result.ttl * 1000)];
341
+ }
342
+ });
343
+
344
+ (async () => {
345
+ console.time('without cache');
346
+ await tangerine.resolve('forwardemail.net'); // <-- cached
347
+ console.timeEnd('without cache');
348
+
349
+ console.time('with cache');
350
+ await tangerine.resolve('forwardemail.net'); // <-- uses cached value
351
+ console.timeEnd('with cache');
352
+ })();
353
+ ```
354
+
355
+ ```sh
356
+ ❯ node app
357
+ without cache: 98.25ms
358
+ with cache: 0.091ms
359
+ ```
300
360
 
301
361
 
302
362
  ## Debugging
@@ -333,107 +393,83 @@ BENCHMARK_PROTOCOL="http" BENCHMARK_HOST="127.0.0.1" BENCHMARK_PORT="4000" BENCH
333
393
 
334
394
  ### Tangerine Benchmarks
335
395
 
336
- We have written extensive benchmarks to show that :tangerine: Tangerine has a <u>**faster `resolve()` and `reverse()`**</u> (and fast enough `lookup()`) versus the native Node.js DNS module.
337
-
338
- The initial release v1.0.0 had these benchmark results, which [you can publicly view on GitHub CI actions logs](https://github.com/forwardemail/tangerine/actions?query=event%3Apush):
396
+ We have written extensive benchmarks to show that :tangerine: Tangerine is as fast as the native Node.js DNS module (with the exception of the `lookup` command). Note that performance is opinionated – since rate limiting plays a factor dependent on the DNS servers you are using and since caching is most likely going to takeover.
339
397
 
340
- > [Node 16 on ubuntu-latest](https://github.com/forwardemail/tangerine/actions/runs/4265467648/jobs/7424828382#step:6:1)
398
+ The latest benchmark results are viewable on GitHub under this repository's [GitHub CI actions logs](https://github.com/forwardemail/tangerine/actions?query=event%3Apush):
341
399
 
342
- ```sh
343
- > node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse && node benchmarks/http
400
+ > [Node 16 on ubuntu-latest](https://github.com/forwardemail/tangerine/actions/runs/4297805550/jobs/7491228635#step:6:1)
344
401
 
345
- tangerine.lookup POST with caching using Cloudflare x 521 ops/sec ±186.87% (79 runs sampled)
346
- tangerine.lookup POST without caching using Cloudflare x 252 ops/sec ±1.52% (79 runs sampled)
347
- tangerine.lookup GET with caching using Cloudflare x 11,217 ops/sec ±1.75% (78 runs sampled)
348
- tangerine.lookup GET without caching using Cloudflare x 259 ops/sec ±1.28% (84 runs sampled) <--------
349
- dns.promises.lookup with caching using Cloudflare x 206,286 ops/sec ±0.92% (82 runs sampled)
350
- dns.promises.lookup without caching using Cloudflare x 2,330 ops/sec ±1.86% (80 runs sampled)
402
+ ```diff
403
+ > node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse
404
+
405
+ Started: lookup
406
+ tangerine.lookup POST with caching using Cloudflare x 735 ops/sec ±195.35% (88 runs sampled)
407
+ tangerine.lookup POST without caching using Cloudflare x 142 ops/sec ±0.58% (84 runs sampled)
408
+ tangerine.lookup GET with caching using Cloudflare x 222,397 ops/sec ±0.52% (88 runs sampled)
409
+ +tangerine.lookup GET without caching using Cloudflare x 142 ops/sec ±0.46% (83 runs sampled)
410
+ dns.promises.lookup with caching using Cloudflare x 6,169,417 ops/sec ±1.67% (84 runs sampled)
411
+ -dns.promises.lookup without caching using Cloudflare x 4,186 ops/sec ±0.58% (89 runs sampled)
351
412
  Fastest without caching is: dns.promises.lookup without caching using Cloudflare
352
- tangerine.resolve POST with caching using Cloudflare x 734 ops/sec ±190.07% (84 runs sampled)
353
- tangerine.resolve POST without caching using Cloudflare x 234 ops/sec ±3.75% (82 runs sampled)
354
- tangerine.resolve GET with caching using Cloudflare x 24,040 ops/sec ±1.93% (83 runs sampled)
355
- tangerine.resolve GET without caching using Cloudflare x 215 ops/sec ±16.62% (75 runs sampled)
356
- tangerine.resolve POST with caching using Google x 23,937 ops/sec ±2.04% (81 runs sampled)
357
- tangerine.resolve POST without caching using Google x 213 ops/sec ±9.51% (71 runs sampled)
358
- tangerine.resolve GET with caching using Google x 24,272 ops/sec ±1.74% (83 runs sampled)
359
- tangerine.resolve GET without caching using Google x 257 ops/sec ±4.02% (80 runs sampled)
360
- resolver.resolve with caching using Cloudflare x 158,842 ops/sec ±2.57% (84 runs sampled)
361
- resolver.resolve without caching using Cloudflare x 8.02 ops/sec ±191.78% (41 runs sampled)
362
- Fastest without caching is: tangerine.resolve GET without caching using Google <--------
363
- tangerine.reverse GET with caching x 694 ops/sec ±189.48% (76 runs sampled)
364
- tangerine.reverse GET without caching x 123 ops/sec ±90.74% (81 runs sampled)
365
- resolver.reverse x 0.24 ops/sec ±86.12% (10 runs sampled)
366
- dns.promises.reverse x 0.70 ops/sec ±164.50% (42 runs sampled)
367
- Fastest without caching is: tangerine.reverse GET without caching <--------
368
- http.request POST request x 384 ops/sec ±1.08% (84 runs sampled)
369
- http.request GET request x 398 ops/sec ±0.83% (83 runs sampled)
370
- undici GET request x 206 ops/sec ±5.59% (58 runs sampled)
371
- undici POST request x 211 ops/sec ±4.44% (74 runs sampled)
372
- axios GET request x 343 ops/sec ±1.97% (82 runs sampled)
373
- axios POST request x 350 ops/sec ±3.35% (82 runs sampled)
374
- got GET request x 325 ops/sec ±1.61% (81 runs sampled)
375
- got POST request x 341 ops/sec ±2.86% (84 runs sampled)
376
- fetch GET request x 657 ops/sec ±1.42% (82 runs sampled)
377
- fetch POST request x 680 ops/sec ±1.21% (84 runs sampled)
378
- request GET request x 370 ops/sec ±1.08% (85 runs sampled)
379
- request POST request x 370 ops/sec ±0.88% (84 runs sampled)
380
- superagent GET request x 380 ops/sec ±1.14% (83 runs sampled)
381
- superagent POST request x 386 ops/sec ±1.04% (83 runs sampled)
382
- phin GET request x 396 ops/sec ±0.86% (84 runs sampled)
383
- phin POST request x 398 ops/sec ±0.83% (85 runs sampled)
384
- Fastest is fetch POST request
385
- ```
386
413
 
387
- > **NOTE**: HTTP library benchmark tests above are not based on real-world usage; instead they are using mock libraries such as `nock` and undici's `MockAgent`. An actual HTTP server could be implemented in these benchmarks (pull request is welcome). See [HTTP Library Benchmarks](#http-library-benchmarks) below for more insight into results with real-world servers.
414
+ Started: resolve
415
+ tangerine.resolve POST with caching using Cloudflare x 951 ops/sec ±195.84% (87 runs sampled)
416
+ tangerine.resolve POST without caching using Cloudflare x 135 ops/sec ±1.27% (79 runs sampled)
417
+ tangerine.resolve GET with caching using Cloudflare x 1,134,724 ops/sec ±0.27% (87 runs sampled)
418
+ +tangerine.resolve GET without caching using Cloudflare x 135 ops/sec ±1.34% (81 runs sampled)
419
+ tangerine.resolve POST with caching using Google x 1,103,189 ops/sec ±0.44% (86 runs sampled)
420
+ tangerine.resolve POST without caching using Google x 55.76 ops/sec ±3.57% (80 runs sampled)
421
+ tangerine.resolve GET with caching using Google x 1,140,499 ops/sec ±0.32% (87 runs sampled)
422
+ tangerine.resolve GET without caching using Google x 70.51 ops/sec ±0.93% (84 runs sampled)
423
+ resolver.resolve with caching using Cloudflare x 4,790,171 ops/sec ±0.43% (87 runs sampled)
424
+ -resolver.resolve without caching using Cloudflare x 158 ops/sec ±1.26% (83 runs sampled)
425
+ Fastest without caching is: resolver.resolve without caching using Cloudflare
426
+
427
+ Started: reverse
428
+ tangerine.reverse GET with caching x 771 ops/sec ±195.37% (85 runs sampled)
429
+ +tangerine.reverse GET without caching x 135 ops/sec ±0.74% (81 runs sampled)
430
+ resolver.reverse with caching x 5,353,130 ops/sec ±0.36% (89 runs sampled)
431
+ -resolver.reverse without caching x 1.90 ops/sec ±210.52% (16 runs sampled)
432
+ dns.promises.reverse with caching x 5,123,900 ops/sec ±0.96% (85 runs sampled)
433
+ -dns.promises.reverse without caching x 0.29 ops/sec ±171.85% (18 runs sampled)
434
+ +Fastest without caching is: tangerine.reverse GET without caching
435
+ ```
388
436
 
389
- > [Node 18 on ubuntu latest](https://github.com/forwardemail/tangerine/actions/runs/4265467648/jobs/7424828575#step:6:1)
437
+ > [Node 18 on ubuntu latest](https://github.com/forwardemail/tangerine/actions/runs/4297805550/jobs/7491228742#step:6:1)
390
438
 
391
- ```sh
439
+ ```diff
392
440
  > node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse && node benchmarks/http
393
441
 
394
- tangerine.lookup POST with caching using Cloudflare x 576 ops/sec ±188.97% (84 runs sampled)
395
- tangerine.lookup POST without caching using Cloudflare x 62.80 ops/sec ±0.34% (75 runs sampled)
396
- tangerine.lookup GET with caching using Cloudflare x 16,710 ops/sec ±0.27% (83 runs sampled)
397
- tangerine.lookup GET without caching using Cloudflare x 60.59 ops/sec ±6.15% (75 runs sampled) <--------
398
- dns.promises.lookup with caching using Cloudflare x 251,300 ops/sec ±0.66% (89 runs sampled)
399
- dns.promises.lookup without caching using Cloudflare x 4,189 ops/sec ±0.65% (89 runs sampled)
400
- Fastest without caching is: dns.promises.lookup without caching using Cloudflare <--------
401
- tangerine.resolve POST with caching using Cloudflare x 627 ops/sec ±192.36% (90 runs sampled)
402
- tangerine.resolve POST without caching using Cloudflare x 59.66 ops/sec ±5.69% (74 runs sampled)
403
- tangerine.resolve GET with caching using Cloudflare x 33,813 ops/sec ±0.33% (90 runs sampled)
404
- tangerine.resolve GET without caching using Cloudflare x 60.16 ops/sec ±4.03% (73 runs sampled)
405
- tangerine.resolve POST with caching using Google x 1,184 ops/sec ±189.17% (90 runs sampled)
406
- tangerine.resolve POST without caching using Google x 41.23 ops/sec ±7.30% (70 runs sampled)
407
- tangerine.resolve GET with caching using Google x 33,811 ops/sec ±0.56% (91 runs sampled)
408
- tangerine.resolve GET without caching using Google x 54.34 ops/sec ±5.71% (69 runs sampled)
409
- resolver.resolve with caching using Cloudflare x 202,804 ops/sec ±0.39% (88 runs sampled)
410
- resolver.resolve without caching using Cloudflare x 61.93 ops/sec ±5.76% (76 runs sampled)
411
- Fastest without caching is: resolver.resolve without caching using Cloudflare <--------
412
- tangerine.reverse GET with caching x 594 ops/sec ±192.60% (86 runs sampled)
413
- tangerine.reverse GET without caching x 60.73 ops/sec ±3.06% (74 runs sampled)
414
- resolver.reverse x 66.00 ops/sec ±0.91% (78 runs sampled)
415
- dns.promises.reverse x 1.84 ops/sec ±190.54% (71 runs sampled)
416
- Fastest without caching is: tangerine.reverse GET without caching <--------
417
- http.request POST request x 438 ops/sec ±0.61% (86 runs sampled)
418
- http.request GET request x 442 ops/sec ±0.64% (87 runs sampled)
419
- undici GET request x 203 ops/sec ±3.67% (42 runs sampled)
420
- undici POST request x 194 ops/sec ±3.77% (62 runs sampled)
421
- axios GET request x 403 ops/sec ±1.67% (86 runs sampled)
422
- axios POST request x 414 ops/sec ±0.65% (88 runs sampled)
423
- got GET request x 391 ops/sec ±1.63% (85 runs sampled)
424
- got POST request x 403 ops/sec ±0.90% (85 runs sampled)
425
- fetch GET request x 794 ops/sec ±2.32% (84 runs sampled)
426
- fetch POST request x 821 ops/sec ±0.89% (86 runs sampled)
427
- request GET request x 423 ops/sec ±0.75% (86 runs sampled)
428
- request POST request x 426 ops/sec ±0.78% (86 runs sampled)
429
- superagent GET request x 435 ops/sec ±0.79% (87 runs sampled)
430
- superagent POST request x 437 ops/sec ±0.82% (88 runs sampled)
431
- phin GET request x 443 ops/sec ±0.64% (86 runs sampled)
432
- phin POST request x 445 ops/sec ±0.60% (86 runs sampled)
433
- Fastest is fetch POST request
434
- ```
442
+ Started: lookup
443
+ tangerine.lookup POST with caching using Cloudflare x 666 ops/sec ±195.48% (87 runs sampled)
444
+ tangerine.lookup POST without caching using Cloudflare x 90.81 ops/sec ±8.06% (89 runs sampled)
445
+ tangerine.lookup GET with caching using Cloudflare x 256,141 ops/sec ±1.72% (87 runs sampled)
446
+ +tangerine.lookup GET without caching using Cloudflare x 96.39 ops/sec ±0.31% (89 runs sampled)
447
+ dns.promises.lookup with caching using Cloudflare x 1,473 ops/sec ±195.95% (87 runs sampled)
448
+ -dns.promises.lookup without caching using Cloudflare x 4,191 ops/sec ±0.54% (85 runs sampled)
449
+ Fastest without caching is: dns.promises.lookup without caching using Cloudflare
435
450
 
436
- > **NOTE**: HTTP library benchmark tests above are not based on real-world usage; instead they are using mock libraries such as `nock` and undici's `MockAgent`. An actual HTTP server could be implemented in these benchmarks (pull request is welcome). See [HTTP Library Benchmarks](#http-library-benchmarks) below for more insight into results with real-world servers.
451
+ Started: resolve
452
+ tangerine.resolve POST with caching using Cloudflare x 683 ops/sec ±195.88% (87 runs sampled)
453
+ tangerine.resolve POST without caching using Cloudflare x 93.37 ops/sec ±0.48% (87 runs sampled)
454
+ tangerine.resolve GET with caching using Cloudflare x 1,146,727 ops/sec ±0.58% (88 runs sampled)
455
+ +tangerine.resolve GET without caching using Cloudflare x 93.33 ops/sec ±0.51% (87 runs sampled)
456
+ tangerine.resolve POST with caching using Google x 1,133,683 ops/sec ±2.74% (89 runs sampled)
457
+ tangerine.resolve POST without caching using Google x 83.91 ops/sec ±6.32% (76 runs sampled)
458
+ tangerine.resolve GET with caching using Google x 1,147,212 ops/sec ±0.32% (90 runs sampled)
459
+ tangerine.resolve GET without caching using Google x 79.73 ops/sec ±4.02% (77 runs sampled)
460
+ resolver.resolve with caching using Cloudflare x 5,318,406 ops/sec ±0.67% (86 runs sampled)
461
+ -resolver.resolve without caching using Cloudflare x 100 ops/sec ±1.55% (79 runs sampled)
462
+ Fastest without caching is: resolver.resolve without caching using Cloudflare
463
+
464
+ Started: reverse
465
+ tangerine.reverse GET with caching x 722 ops/sec ±195.42% (88 runs sampled)
466
+ +tangerine.reverse GET without caching x 93.19 ops/sec ±0.74% (87 runs sampled)
467
+ resolver.reverse with caching x 5,520,569 ops/sec ±0.59% (85 runs sampled)
468
+ -resolver.reverse without caching x 17.42 ops/sec ±162.63% (70 runs sampled)
469
+ dns.promises.reverse with caching x 5,164,258 ops/sec ±0.96% (86 runs sampled)
470
+ -dns.promises.reverse without caching x 0.20 ops/sec ±184.87% (25 runs sampled)
471
+ +Fastest without caching is: tangerine.reverse GET without caching
472
+ ```
437
473
 
438
474
  ---
439
475
 
@@ -443,84 +479,84 @@ You can also [run the benchmarks yourself](#benchmarks).
443
479
 
444
480
  Provided below are additional benchmark tests we have run:
445
481
 
446
- > Node v16.18.1 on MacBook Air M1 16GB (without VPN):
482
+ > Node v18.14.2 on MacBook Air M1 16GB (without VPN):
447
483
 
448
- ```sh
449
- node --version
450
- v16.18.1
451
-
452
- ❯ node benchmarks/resolve
453
- tangerine POST with caching using Cloudflare x 1,044 ops/sec ±193.21% (90 runs sampled)
454
- tangerine POST without caching using Cloudflare x 40.93 ops/sec ±53.83% (50 runs sampled)
455
- tangerine GET with caching using Cloudflare x 73,896 ops/sec ±0.27% (90 runs sampled)
456
- tangerine GET without caching using Cloudflare x 38.66 ops/sec ±21.98% (55 runs sampled)
457
- tangerine POST with caching using Google x 992 ops/sec ±193.33% (87 runs sampled)
458
- tangerine POST without caching using Google x 31.98 ops/sec ±21.35% (58 runs sampled)
459
- tangerine GET with caching using Google x 74,410 ops/sec ±0.22% (91 runs sampled)
460
- tangerine GET without caching using Google x 41.52 ops/sec ±18.91% (56 runs sampled)
461
- dns.promises.resolve without caching using Cloudflare x 25.46 ops/sec ±100.19% (50 runs sampled)
462
- dns.promises.resolve with caching using Cloudflare x 505,956 ops/sec ±2.34% (89 runs sampled)
463
- Fastest without caching is: tangerine GET without caching using Google, tangerine GET without caching using Cloudflare
464
- ```
484
+ ```diff
485
+ > node --version
486
+ v18.14.2
465
487
 
466
- > Node v16.18.1 on MacBook Air M1 16GB (with DNS blackholed VPN) – <mark>this highlights the DNS blackhole problem</mark>:
488
+ > node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse
467
489
 
468
- ```sh
469
- node --version
470
- v16.18.1
471
-
472
- node benchmarks/resolve
473
- tangerine POST with caching using Cloudflare x 185 ops/sec ±195.50% (88 runs sampled)
474
- tangerine POST without caching using Cloudflare x 6.48 ops/sec ±35.98% (35 runs sampled)
475
- tangerine GET with caching using Cloudflare x 824 ops/sec ±193.77% (90 runs sampled)
476
- tangerine GET without caching using Cloudflare x 8.66 ops/sec ±8.22% (46 runs sampled)
477
- tangerine POST with caching using Google x 205 ops/sec ±195.45% (88 runs sampled)
478
- tangerine POST without caching using Google x 7.20 ops/sec ±12.28% (40 runs sampled)
479
- tangerine GET with caching using Google x 690 ops/sec ±194.12% (90 runs sampled)
480
- tangerine GET without caching using Google x 7.85 ops/sec ±9.53% (42 runs sampled)
481
- dns.promises.resolve without caching using Cloudflare x 0.09 ops/sec ±5.10% (5 runs sampled) <--------
482
- dns.promises.resolve with caching using Cloudflare x 0.09 ops/sec ±5.13% (5 runs sampled) <--------
483
- Fastest without caching is: tangerine GET without caching using Cloudflare
490
+ Started: lookup
491
+ tangerine.lookup POST with caching using Cloudflare x 1,035 ops/sec ±195.73% (91 runs sampled)
492
+ tangerine.lookup POST without caching using Cloudflare x 52.76 ops/sec ±51.29% (53 runs sampled)
493
+ tangerine.lookup GET with caching using Cloudflare x 694,910 ops/sec ±1.54% (87 runs sampled)
494
+ +tangerine.lookup GET without caching using Cloudflare x 40.18 ops/sec ±60.19% (49 runs sampled)
495
+ dns.promises.lookup with caching using Cloudflare x 12,645,103 ops/sec ±0.26% (90 runs sampled)
496
+ -dns.promises.lookup without caching using Cloudflare x 2,664 ops/sec ±0.54% (88 runs sampled)
497
+ Fastest without caching is: dns.promises.lookup without caching using Cloudflare
498
+
499
+ Started: resolve
500
+ tangerine.resolve POST with caching using Cloudflare x 1,005 ops/sec ±195.93% (91 runs sampled)
501
+ tangerine.resolve POST without caching using Cloudflare x 55.52 ops/sec ±46.26% (57 runs sampled)
502
+ tangerine.resolve GET with caching using Cloudflare x 2,879,865 ops/sec ±0.35% (86 runs sampled)
503
+ +tangerine.resolve GET without caching using Cloudflare x 71.11 ops/sec ±2.94% (74 runs sampled)
504
+ tangerine.resolve POST with caching using Google x 1,292 ops/sec ±195.91% (88 runs sampled)
505
+ tangerine.resolve POST without caching using Google x 36.88 ops/sec ±41.76% (53 runs sampled)
506
+ tangerine.resolve GET with caching using Google x 2,885,428 ops/sec ±0.22% (88 runs sampled)
507
+ tangerine.resolve GET without caching using Google x 70.38 ops/sec ±3.72% (68 runs sampled)
508
+ resolver.resolve with caching using Cloudflare x 10,645,813 ops/sec ±0.23% (91 runs sampled)
509
+ -resolver.resolve without caching using Cloudflare x 71.80 ops/sec ±2.84% (67 runs sampled)
510
+ +Fastest without caching is: resolver.resolve without caching using Cloudflare, tangerine.resolve GET without caching using Cloudflare, tangerine.resolve GET without caching using Google, tangerine.resolve POST without caching using Cloudflare
511
+
512
+ Started: reverse
513
+ tangerine.reverse GET with caching x 917 ops/sec ±195.78% (88 runs sampled)
514
+ +tangerine.reverse GET without caching x 51.15 ops/sec ±51.92% (61 runs sampled)
515
+ resolver.reverse with caching x 11,058,579 ops/sec ±0.37% (88 runs sampled)
516
+ -resolver.reverse without caching x 62.30 ops/sec ±24.83% (64 runs sampled)
517
+ dns.promises.reverse with caching x 11,276,123 ops/sec ±0.17% (90 runs sampled)
518
+ -dns.promises.reverse without caching x 73.46 ops/sec ±1.99% (69 runs sampled)
519
+ Fastest without caching is: dns.promises.reverse without caching, resolver.reverse without caching
484
520
  ```
485
521
 
486
- > Node v18.4.2 on MacBook Air M1 16GB (without VPN):
522
+ > Node v18.14.2 on MacBook Air M1 16GB (with DNS blackholed VPN) – **this highlights the DNS blackhole problem**:
487
523
 
488
- ```sh
489
- node --version
490
- v18.4.2
491
-
492
- ❯ node benchmarks/resolve
493
- tangerine POST with caching using Cloudflare x 817 ops/sec ±193.86% (89 runs sampled)
494
- tangerine POST without caching using Cloudflare x 42.57 ops/sec ±38.18% (62 runs sampled)
495
- tangerine GET with caching using Cloudflare x 853 ops/sec ±193.79% (91 runs sampled)
496
- tangerine GET without caching using Cloudflare x 41.13 ops/sec ±57.37% (48 runs sampled)
497
- tangerine POST with caching using Google x 1,488 ops/sec ±192.10% (90 runs sampled)
498
- tangerine POST without caching using Google x 38.46 ops/sec ±12.08% (59 runs sampled)
499
- tangerine GET with caching using Google x 74,240 ops/sec ±0.31% (90 runs sampled)
500
- tangerine GET without caching using Google x 39.20 ops/sec ±23.52% (58 runs sampled)
501
- dns.promises.resolve without caching using Cloudflare x 59.11 ops/sec ±13.96% (63 runs sampled)
502
- dns.promises.resolve with caching using Cloudflare x 529,961 ops/sec ±0.33% (91 runs sampled)
503
- Fastest without caching is: dns.promises.resolve without caching using Cloudflare, tangerine GET without caching using Cloudflare
504
- ```
524
+ ```diff
525
+ > node --version
526
+ v18.14.2
505
527
 
506
- > Node v18.4.2 on MacBook Air M1 16GB (with DNS blackholed VPN) – <mark>this highlights the DNS blackhole problem</mark>:
528
+ > node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse
507
529
 
508
- ```sh
509
- node --version
510
- v18.4.2
511
-
512
- node benchmarks/resolve
513
- tangerine POST with caching using Cloudflare x 193 ops/sec ±195.49% (91 runs sampled)
514
- tangerine POST without caching using Cloudflare x 8.44 ops/sec ±9.34% (45 runs sampled)
515
- tangerine GET with caching using Cloudflare x 829 ops/sec ±193.83% (88 runs sampled)
516
- tangerine GET without caching using Cloudflare x 7.44 ops/sec ±24.67% (45 runs sampled)
517
- tangerine POST with caching using Google x 255 ops/sec ±195.33% (91 runs sampled)
518
- tangerine POST without caching using Google x 4.59 ops/sec ±77.88% (26 runs sampled)
519
- tangerine GET with caching using Google x 794 ops/sec ±193.95% (92 runs sampled)
520
- tangerine GET without caching using Google x 7.69 ops/sec ±11.23% (42 runs sampled)
521
- dns.promises.resolve without caching using Cloudflare x 0.09 ops/sec ±6.41% (5 runs sampled) <--------
522
- dns.promises.resolve with caching using Cloudflare x 0.09 ops/sec ±6.41% (5 runs sampled) <--------
523
- Fastest without caching is: tangerine POST without caching using Cloudflare, tangerine GET without caching using Cloudflare
530
+ Started: lookup
531
+ tangerine.lookup POST with caching using Cloudflare x 1,327 ops/sec ±195.65% (89 runs sampled)
532
+ tangerine.lookup POST without caching using Cloudflare x 71.11 ops/sec ±8.24% (71 runs sampled)
533
+ tangerine.lookup GET with caching using Cloudflare x 759,816 ops/sec ±0.46% (90 runs sampled)
534
+ +tangerine.lookup GET without caching using Cloudflare x 73.98 ops/sec ±1.78% (69 runs sampled)
535
+ dns.promises.lookup with caching using Cloudflare x 1,744 ops/sec ±195.97% (88 runs sampled)
536
+ -dns.promises.lookup without caching using Cloudflare x 2,717 ops/sec ±0.82% (87 runs sampled)
537
+ Fastest without caching is: dns.promises.lookup without caching using Cloudflare
538
+
539
+ Started: resolve
540
+ tangerine.resolve POST with caching using Cloudflare x 947 ops/sec ±195.93% (91 runs sampled)
541
+ tangerine.resolve POST without caching using Cloudflare x 44.33 ops/sec ±73.30% (75 runs sampled)
542
+ tangerine.resolve GET with caching using Cloudflare x 2,814,737 ops/sec ±0.17% (91 runs sampled)
543
+ +tangerine.resolve GET without caching using Cloudflare x 57.25 ops/sec ±51.61% (73 runs sampled)
544
+ tangerine.resolve POST with caching using Google x 1,087 ops/sec ±195.92% (91 runs sampled)
545
+ tangerine.resolve POST without caching using Google x 36.84 ops/sec ±7.04% (62 runs sampled)
546
+ tangerine.resolve GET with caching using Google x 2,784,199 ops/sec ±0.15% (92 runs sampled)
547
+ tangerine.resolve GET without caching using Google x 47.55 ops/sec ±5.66% (76 runs sampled)
548
+ resolver.resolve with caching using Cloudflare x 0.09 ops/sec ±6.41% (5 runs sampled)
549
+ -resolver.resolve without caching using Cloudflare x 0.10 ops/sec ±6.52% (5 runs sampled)
550
+ +Fastest without caching is: tangerine.resolve GET without caching using Google
551
+
552
+ Started: reverse
553
+ tangerine.reverse GET with caching x 1,345 ops/sec ±195.66% (92 runs sampled)
554
+ +tangerine.reverse GET without caching x 71.73 ops/sec ±3.03% (73 runs sampled)
555
+ resolver.reverse with caching x 0.10 ops/sec ±6.54% (5 runs sampled)
556
+ -resolver.reverse without caching x 0.10 ops/sec ±0.01% (5 runs sampled)
557
+ dns.promises.reverse with caching x 0.10 ops/sec ±6.54% (5 runs sampled)
558
+ -dns.promises.reverse without caching x 0.10 ops/sec ±0.01% (5 runs sampled)
559
+ +Fastest without caching is: tangerine.reverse GET without caching
524
560
  ```
525
561
 
526
562
  Also see this [write-up](https://samknows.com/blog/dns-over-https-performance) on UDP-based DNS versus DNS over HTTPS ("DoH") benchmarks.
@@ -531,39 +567,13 @@ Also see this [write-up](https://samknows.com/blog/dns-over-https-performance) o
531
567
 
532
568
  Originally we wrote this library using [got](https://github.com/sindresorhus/got) – however after running benchmarks and learning of [how performant](https://github.com/sindresorhus/got/issues/1419) undici is, we weren't happy – and we rewrote it with [undici](https://github.com/nodejs/undici). Here are test results from the latest versions of all HTTP libraries against our real-world API (both client and server running locally):
533
569
 
534
- > Node v16.18.1 on MacBook Air M1 16GB (using real-world API server):
535
-
536
- ```sh
537
- ❯ node --version
538
- v16.18.1
539
-
540
- ❯ BENCHMARK_HOST="127.0.0.1" BENCHMARK_PORT="4000" BENCHMARK_PATH="/v1/test" node benchmarks/http
541
- http.request POST request x 860 ops/sec ±6.33% (75 runs sampled)
542
- http.request GET request x 978 ops/sec ±5.17% (83 runs sampled)
543
- undici GET request x 2,732 ops/sec ±4.14% (83 runs sampled)
544
- undici POST request x 1,204 ops/sec ±5.01% (81 runs sampled)
545
- axios GET request x 855 ops/sec ±5.45% (81 runs sampled)
546
- axios POST request x 723 ops/sec ±15.28% (71 runs sampled)
547
- got GET request x 1,355 ops/sec ±16.60% (63 runs sampled)
548
- got POST request x 93.65 ops/sec ±181.51% (29 runs sampled)
549
- fetch GET request x 949 ops/sec ±40.26% (45 runs sampled)
550
- fetch POST request x 672 ops/sec ±22.43% (67 runs sampled)
551
- request GET request x 960 ops/sec ±50.90% (48 runs sampled)
552
- request POST request x 612 ops/sec ±45.48% (57 runs sampled)
553
- superagent GET request x 126 ops/sec ±188.34% (29 runs sampled)
554
- superagent POST request x 747 ops/sec ±18.16% (67 runs sampled)
555
- phin GET request x 374 ops/sec ±147.42% (57 runs sampled)
556
- phin POST request x 566 ops/sec ±38.08% (51 runs sampled)
557
- Fastest is undici GET request
558
- ```
559
-
560
570
  > Node v18.14.2 on MacBook Air M1 16GB (using real-world API server):
561
571
 
562
572
  ```sh
563
- node --version
573
+ > node --version
564
574
  v18.14.2
565
575
 
566
- BENCHMARK_HOST="127.0.0.1" BENCHMARK_PORT="4000" BENCHMARK_PATH="/v1/test" node benchmarks/http
576
+ > BENCHMARK_HOST="127.0.0.1" BENCHMARK_PORT="4000" BENCHMARK_PATH="/v1/test" node benchmarks/http
567
577
  http.request POST request x 765 ops/sec ±9.83% (72 runs sampled)
568
578
  http.request GET request x 1,000 ops/sec ±3.88% (85 runs sampled)
569
579
  undici GET request x 2,740 ops/sec ±5.92% (78 runs sampled)
package/index.js CHANGED
@@ -7,7 +7,6 @@ const { debuglog } = require('node:util');
7
7
  const { getEventListeners, setMaxListeners } = require('node:events');
8
8
  const { isIP, isIPv4, isIPv6 } = require('node:net');
9
9
 
10
- const Keyv = require('keyv');
11
10
  const getStream = require('get-stream');
12
11
  const ipaddr = require('ipaddr.js');
13
12
  const mergeOptions = require('merge-options');
@@ -16,6 +15,7 @@ const pTimeout = require('p-timeout');
16
15
  const pWaitFor = require('p-wait-for');
17
16
  const packet = require('dns-packet');
18
17
  const semver = require('semver');
18
+ const structuredClone = require('@ungap/structured-clone').default;
19
19
  const { getService } = require('port-numbers');
20
20
 
21
21
  const pkg = require('./package.json');
@@ -287,8 +287,19 @@ class Tangerine extends dns.promises.Resolver {
287
287
  ipv6: '::0',
288
288
  ipv4Port: undefined,
289
289
  ipv6Port: undefined,
290
- // cache mapping (e.g. txt -> keyv instance) - see below
290
+ // cache mapping (e.g. txt -> Map/keyv/redis instance) - see below
291
291
  cache: new Map(),
292
+ // <https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl/>
293
+ defaultTTLSeconds: 300,
294
+ maxTTLSeconds: 86400,
295
+ // default is to support ioredis
296
+ // setCacheArgs(key, result) {
297
+ setCacheArgs() {
298
+ // also you have access to `result.expires` which is is ms since epoch
299
+ // (can be converted to Date via `new Date(result.expires)`)
300
+ // return ['PX', Math.round(result.ttl * 1000)];
301
+ return [];
302
+ },
292
303
  // whether to do 1:1 HTTP -> DNS error mapping
293
304
  returnHTTPErrors: false,
294
305
  // whether to smart rotate and bump-to-end servers that have issues
@@ -337,19 +348,6 @@ class Tangerine extends dns.promises.Resolver {
337
348
  // so to turn that off, you need to supply `dnsCache: undefined` in `got` object (?)
338
349
  if (this.options.cache === true) this.options.cache = new Map();
339
350
 
340
- if (this.options.cache instanceof Map) {
341
- // each of the types have their own Keyv with prefix
342
- for (const type of this.constructor.TYPES) {
343
- if (!this.options.cache.get(type))
344
- this.options.cache.set(
345
- type,
346
- new Keyv({
347
- namespace: `dns:${type.toLowerCase()}`
348
- })
349
- );
350
- }
351
- }
352
-
353
351
  // convert `false` logger option into noop
354
352
  // <https://github.com/breejs/bree/issues/147>
355
353
  if (this.options.logger === false)
@@ -1086,7 +1084,7 @@ class Tangerine extends dns.promises.Resolver {
1086
1084
  }
1087
1085
 
1088
1086
  //
1089
- // TODO: every address must be ipv4 or ipv6 (use `new URL` to parse and check)
1087
+ // NOTE: every address must be ipv4 or ipv6 (use `new URL` to parse and check)
1090
1088
  // servers [ string ] - array of RFC 5952 formatted addresses
1091
1089
  //
1092
1090
 
@@ -1123,51 +1121,59 @@ class Tangerine extends dns.promises.Resolver {
1123
1121
  delete options.ecsSubnet;
1124
1122
  }
1125
1123
 
1126
- let cache;
1127
- if (this.options.cache instanceof Map)
1128
- cache = this.options.cache.get(rrtype);
1129
-
1130
- const key = ecsSubnet ? `${ecsSubnet}:${name}` : name;
1124
+ const key = (
1125
+ ecsSubnet ? `${rrtype}:${ecsSubnet}:${name}` : `${rrtype}:${name}`
1126
+ ).toLowerCase();
1131
1127
 
1132
1128
  let result;
1133
1129
  let data;
1134
- if (cache) {
1135
- //
1136
- // <https://github.com/jaredwray/keyv/issues/106>
1130
+ if (this.options.cache) {
1137
1131
  //
1138
- // NOTE: we store `result.lowest_answer_ttl` which was the lowest TTL determined
1132
+ // NOTE: we store `result.ttl` which was the lowest TTL determined
1139
1133
  // (this saves us from duplicating the same `...sort().filter(Number.isFinite)` logic)
1140
1134
  //
1141
- data = await cache.get(key, { raw: true });
1142
- if (data?.value) {
1143
- result = data.value;
1135
+ data = await this.options.cache.get(key);
1136
+ // safeguard in case cache pollution
1137
+ if (data && typeof data === 'object') {
1138
+ debug('cache retrieved', data);
1144
1139
  const now = Date.now();
1140
+ // safeguard in case cache pollution
1145
1141
  if (
1146
- // safeguard in case catch gets polluted
1147
- Number.isFinite(result.lowest_answer_ttl) &&
1148
- result.lowest_answer_ttl > 0 &&
1149
- data.expires &&
1150
- now <= data.expires
1142
+ !Number.isFinite(data.expires) ||
1143
+ data.expires < now ||
1144
+ !Number.isFinite(data.ttl) ||
1145
+ data.ttl < 1
1151
1146
  ) {
1147
+ data = undefined;
1148
+ } else if (options?.ttl) {
1149
+ // clone the data so that we don't mutate cache (e.g. if it's in-memory)
1150
+ // <https://nodejs.org/api/globals.html#structuredclonevalue-options>
1151
+ // <https://github.com/ungap/structured-clone>
1152
+ data = structuredClone(data);
1153
+
1152
1154
  // returns ms -> s conversion
1153
1155
  const ttl = Math.round((data.expires - now) / 1000);
1154
- const diff = result.lowest_answer_ttl - ttl;
1156
+ const diff = data.ttl - ttl;
1155
1157
 
1156
- for (let i = 0; i < result.answers.length; i++) {
1158
+ for (let i = 0; i < data.answers.length; i++) {
1157
1159
  // eslint-disable-next-line max-depth
1158
- if (typeof result.answers[i].ttl === 'number') {
1160
+ if (typeof data.answers[i].ttl === 'number') {
1159
1161
  // subtract ttl from answer
1160
- result.answers[i].ttl = Math.round(result.answers[i].ttl - diff);
1162
+ data.answers[i].ttl = Math.round(data.answers[i].ttl - diff);
1161
1163
 
1162
1164
  // eslint-disable-next-line max-depth
1163
- if (result.answers[i].ttl <= 0) {
1164
- result = undefined;
1165
+ if (data.answers[i].ttl <= 0) {
1165
1166
  data = undefined;
1166
1167
  break;
1167
1168
  }
1168
1169
  }
1169
1170
  }
1170
1171
  }
1172
+
1173
+ // will only use cache if it's still set after parsing ttl
1174
+ result = data;
1175
+ } else {
1176
+ data = undefined;
1171
1177
  }
1172
1178
  }
1173
1179
 
@@ -1200,8 +1206,8 @@ class Tangerine extends dns.promises.Resolver {
1200
1206
  // - The DoH service could not contact Google Public DNS resolvers.
1201
1207
  // - In the case of a 502 response, although retrying on an alternate Google Public DNS address might help, a more effective fallback response would be to try another DoH service, or to switch to traditional UDP or TCP DNS at 8.8.8.8.
1202
1208
  //
1203
- if (cache && result) {
1204
- debug(`cached result found for "${cache.opts.namespace}:${key}"`);
1209
+ if (this.options.cache && result) {
1210
+ debug(`cached result found for "${key}"`);
1205
1211
  } else {
1206
1212
  if (!abortController) {
1207
1213
  abortController = new AbortController();
@@ -1241,31 +1247,37 @@ class Tangerine extends dns.promises.Resolver {
1241
1247
  //
1242
1248
  switch (result.rcode) {
1243
1249
  case 'NOERROR': {
1244
- //
1245
- // NOTE: if the answer was truncated then unset results (?)
1246
- // <https://github.com/EduardoRuizM/native-dnssec-dns/blob/fc27face6c64ab53675840bafc81f70bab48a743/lib/client.js#L354>
1247
- // <https://github.com/hildjj/dohdec/issues/40>
1248
- // if (result.flag_tc) throw createError(name, rrtype, dns.BADRESP);
1250
+ // <https://github.com/hildjj/dohdec/issues/40#issuecomment-1445554626>
1249
1251
  if (result.flag_tc) {
1250
- this.options.logger.error(new Error('Truncated DNS response'), {
1251
- name,
1252
- rrtype,
1253
- result
1254
- });
1255
- } else if (cache && !data?.value) {
1252
+ this.options.logger.error(
1253
+ new Error(
1254
+ 'Truncated DNS response; Defer to https://github.com/hildjj/dohdec/issues/40#issuecomment-1445554626 for insight.'
1255
+ ),
1256
+ {
1257
+ name,
1258
+ rrtype,
1259
+ result
1260
+ }
1261
+ );
1262
+ } else if (this.options.cache && !data) {
1256
1263
  // store in cache based off lowest ttl
1257
- const ttl = result.answers
1264
+ let ttl = result.answers
1258
1265
  .map((answer) => answer.ttl)
1259
1266
  .sort()
1260
1267
  .find((ttl) => Number.isFinite(ttl));
1261
- result.lowest_answer_ttl = ttl;
1262
- await (result.lowest_answer_ttl && result.lowest_answer_ttl > 0
1263
- ? cache.set(
1264
- key,
1265
- result,
1266
- Math.round(result.lowest_answer_ttl * 1000)
1267
- )
1268
- : cache.set(key, result));
1268
+ // if TTL is not a number or is < 1 or is > max then set to default
1269
+ if (
1270
+ !Number.isFinite(ttl) ||
1271
+ ttl < 1 ||
1272
+ ttl > this.options.maxTTLSeconds
1273
+ )
1274
+ ttl = this.options.defaultTTLSeconds;
1275
+ result.ttl = ttl;
1276
+ // this supports both redis-based key/value/ttl and simple key/value implementations
1277
+ result.expires = Date.now() + Math.round(result.ttl * 1000);
1278
+ const args = [key, result, ...this.options.setCacheArgs(key, result)];
1279
+ debug('setting cache', [key, result, ...args]);
1280
+ await this.options.cache.set(...args);
1269
1281
  }
1270
1282
 
1271
1283
  break;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tangerine",
3
- "description": "Tangerine is the best Node.js drop-in replacement for dns.promises.Resolver using DNS over HTTPS (\"DoH\") via undici with built-in retries, timeouts, smart server rotation, AbortControllers, and caching support for multiple backends via Keyv.",
4
- "version": "1.2.2",
3
+ "description": "Tangerine is the best Node.js drop-in replacement for dns.promises.Resolver using DNS over HTTPS (\"DoH\") via undici with built-in retries, timeouts, smart server rotation, AbortControllers, and caching support for multiple backends (with TTL support).",
4
+ "version": "1.3.0",
5
5
  "author": "Forward Email (https://forwardemail.net)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/forwardemail/tangerine/issues"
@@ -10,11 +10,11 @@
10
10
  "Forward Email (https://forwardemail.net)"
11
11
  ],
12
12
  "dependencies": {
13
+ "@ungap/structured-clone": "^1.0.2",
13
14
  "dns-packet": "^5.4.0",
14
15
  "dohdec": "^5.0.3",
15
16
  "get-stream": "6",
16
17
  "ipaddr.js": "^2.0.1",
17
- "keyv": "^4.5.2",
18
18
  "merge-options": "3.0.4",
19
19
  "p-map": "4",
20
20
  "p-timeout": "4",
@@ -35,6 +35,8 @@
35
35
  "fixpack": "^4.0.0",
36
36
  "got": "11",
37
37
  "husky": "^8.0.3",
38
+ "ioredis": "^5.3.1",
39
+ "ioredis-mock": "^8.2.6",
38
40
  "lint-staged": "^13.1.2",
39
41
  "lodash": "^4.17.21",
40
42
  "nock": "^13.3.0",
@@ -150,7 +152,7 @@
150
152
  },
151
153
  "scripts": {
152
154
  "ava": "cross-env NODE_ENV=test ava",
153
- "benchmarks": "node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse && node benchmarks/http",
155
+ "benchmarks": "node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse",
154
156
  "lint": "xo --fix && remark . -qfo && fixpack",
155
157
  "nyc": "cross-env NODE_ENV=test nyc ava",
156
158
  "prepare": "husky install",