@darcas/smart-dns-promises 1.0.0 → 1.1.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.
- package/README.md +71 -6
- package/dist/commonjs/index.d.ts +114 -14
- package/dist/commonjs/index.d.ts.map +1 -1
- package/dist/commonjs/index.js +181 -35
- package/dist/commonjs/index.min.js +1 -6
- package/dist/commonjs/index.min.js.map +4 -4
- package/dist/esm/index.d.ts +114 -14
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +180 -34
- package/dist/esm/index.min.js +1 -6
- package/dist/esm/index.min.js.map +4 -4
- package/package.json +26 -29
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# SmartDns
|
|
2
2
|
|
|
3
|
-

|
|
4
|
-

|
|
5
|
-

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+

|
|
7
8
|
|
|
8
9
|
A simple and efficient DNS resolver with caching and configurable DNS providers for Node.js 20 and 22 or above.
|
|
9
10
|
|
|
@@ -11,9 +12,14 @@ The purpose of this library is to increase the speed and performance of DNS reso
|
|
|
11
12
|
|
|
12
13
|
## Features
|
|
13
14
|
|
|
14
|
-
- DNS resolution with caching for faster lookups.
|
|
15
|
+
- DNS resolution with caching for faster lookups (zero runtime dependencies).
|
|
16
|
+
- Cache entries expire according to the **real DNS record TTL** (configurable min/max clamp).
|
|
17
|
+
- Negative caching of failed lookups to avoid hammering the resolver on down hosts.
|
|
18
|
+
- In-flight deduplication: concurrent lookups for the same hostname trigger a single DNS query.
|
|
19
|
+
- Optional stale-while-revalidate: expired entries are served immediately and refreshed in background.
|
|
20
|
+
- Lookup statistics: hits, misses, errors, revalidations and average resolve time.
|
|
15
21
|
- Supports configurable DNS providers: CloudFlare, Google, and OpenDNS.
|
|
16
|
-
- Allows custom result order for DNS resolutions: IPv4 first, IPv6 first, or verbatim.
|
|
22
|
+
- Allows custom result order for DNS resolutions: IPv4 first, IPv6 first, or verbatim, plus `ipv4`/`ipv6` family selection.
|
|
17
23
|
- Singleton pattern to ensure only one instance of the resolver is used.
|
|
18
24
|
- Manual configuration of DNS server addresses.
|
|
19
25
|
|
|
@@ -34,6 +40,7 @@ yarn add @darcas/smart-dns-promises
|
|
|
34
40
|
## Usage
|
|
35
41
|
|
|
36
42
|
> In environments such as APIs, it is recommended to call `factory` as early as possible in the application lifecycle to benefit from Node.js's DNS system configuration.
|
|
43
|
+
> See [Process-wide behaviour](#process-wide-behaviour) for details.
|
|
37
44
|
|
|
38
45
|
### Creating an instance
|
|
39
46
|
|
|
@@ -47,6 +54,37 @@ const dns = SmartDns.factory();
|
|
|
47
54
|
|
|
48
55
|
// Optionally, configure DNS provider and result order
|
|
49
56
|
const dnsWithConfig = SmartDns.factory('Google', 'ipv4first', 600000);
|
|
57
|
+
|
|
58
|
+
// Advanced options (4th argument)
|
|
59
|
+
const dnsAdvanced = SmartDns.factory('CloudFlare', 'ipv4first', undefined, {
|
|
60
|
+
swr: true, // serve stale entries and refresh in background
|
|
61
|
+
negativeTtl: 30000, // how long failed lookups are negatively cached (ms)
|
|
62
|
+
minTtl: 1000, // clamp for record TTLs coming from DNS (ms)
|
|
63
|
+
maxTtl: 3600000, // clamp for record TTLs coming from DNS (ms)
|
|
64
|
+
family: 'ipv6', // resolve AAAA records instead of A records
|
|
65
|
+
onStats: (stats) => console.log(stats),
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> Configuration passed to `factory` after the first call is ignored, because the singleton has already been created.
|
|
70
|
+
|
|
71
|
+
### Process-wide behaviour
|
|
72
|
+
|
|
73
|
+
`SmartDns` intentionally configures **Node's process-global DNS system** (`node:dns`):
|
|
74
|
+
|
|
75
|
+
- `setProvider()` and `setServers()` replace the resolver servers for the **whole process**, not just for this library.
|
|
76
|
+
- The `resultOrder` argument of `factory`/the constructor calls Node's `dns.setDefaultResultOrder()`, which is global too.
|
|
77
|
+
|
|
78
|
+
This is by design: every HTTP client in the application — axios, fetch, or any other dependency performing DNS lookups — benefits from (and is affected by) the configured provider. For this reason, call `factory()` as **early as possible** in the application lifecycle, ideally once, so your entire Node.js software runs with a consistent and fast DNS configuration.
|
|
79
|
+
|
|
80
|
+
If multiple configurations are applied, the last one wins for the whole process.
|
|
81
|
+
|
|
82
|
+
### Statistics
|
|
83
|
+
|
|
84
|
+
Every lookup updates the instance counters, available via the `stats` getter:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
const { hits, misses, errors, revalidations, avgResolveMs } = dns.stats
|
|
50
88
|
```
|
|
51
89
|
### Setting the DNS provider
|
|
52
90
|
|
|
@@ -118,6 +156,33 @@ axios.interceptors.request.use(async (config: InternalAxiosRequestConfig): Promi
|
|
|
118
156
|
})
|
|
119
157
|
```
|
|
120
158
|
|
|
159
|
+
## Example with fetch
|
|
160
|
+
|
|
161
|
+
The same idea with the global `fetch`: resolve once, request the IP directly and pass the original hostname in the `Host` header.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { SmartDns } from '@darcas/smart-dns-promises'
|
|
165
|
+
|
|
166
|
+
const dns = SmartDns.factory()
|
|
167
|
+
|
|
168
|
+
async function smartFetch(url: string, init: RequestInit = {}): Promise<Response> {
|
|
169
|
+
const { urlReplaced } = await dns.resolver(url)
|
|
170
|
+
|
|
171
|
+
return fetch(urlReplaced, {
|
|
172
|
+
...init,
|
|
173
|
+
headers: {
|
|
174
|
+
...init.headers,
|
|
175
|
+
Host: new URL(url).hostname,
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Two things to keep in mind:
|
|
182
|
+
|
|
183
|
+
1. Replacing the hostname with the IP address means the TLS handshake is performed against the IP: this works out of the box with plain HTTP endpoints or controlled environments, but on public HTTPS endpoints the certificate is issued to the hostname, so the request will fail certificate validation. For HTTPS use cases prefer the axios example above.
|
|
184
|
+
2. Thanks to the cache, subsequent requests to the same host skip the DNS lookup entirely.
|
|
185
|
+
|
|
121
186
|
## Contributing
|
|
122
187
|
|
|
123
188
|
If you'd like to contribute to the project, feel free to fork it and create a pull request. Please ensure that your changes are well-tested and properly documented.
|
package/dist/commonjs/index.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @author Dario Casertano <dario@casertano.name>
|
|
3
|
-
* @copyright Copyright (c) 2024 Casertano Dario – All rights reserved.
|
|
4
|
-
* @license MIT
|
|
5
|
-
*/
|
|
6
|
-
import { LRUCache } from "lru-cache";
|
|
7
1
|
/**
|
|
8
2
|
* Represents a hostname of a server.
|
|
9
3
|
*/
|
|
@@ -16,6 +10,66 @@ type IPAddress = string;
|
|
|
16
10
|
* Specifies the order of DNS resolution results.
|
|
17
11
|
*/
|
|
18
12
|
type ResultOrder = 'ipv4first' | 'ipv6first' | 'verbatim';
|
|
13
|
+
/**
|
|
14
|
+
* Specifies which IP address family to resolve.
|
|
15
|
+
*/
|
|
16
|
+
type AddressFamily = 'ipv4' | 'ipv6';
|
|
17
|
+
/**
|
|
18
|
+
* A cached resolved address with its expiry timestamp.
|
|
19
|
+
*/
|
|
20
|
+
interface CacheEntry {
|
|
21
|
+
address: IPAddress;
|
|
22
|
+
expiresAt: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Advanced options for cache and resolver behaviour.
|
|
26
|
+
*/
|
|
27
|
+
export interface SmartDnsOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Which IP address family to resolve.
|
|
30
|
+
* @default 'ipv4'
|
|
31
|
+
*/
|
|
32
|
+
family?: AddressFamily;
|
|
33
|
+
/**
|
|
34
|
+
* Upper bound for record TTLs coming from the DNS server, in milliseconds.
|
|
35
|
+
* @default 3600000
|
|
36
|
+
*/
|
|
37
|
+
maxTtl?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Lower bound for record TTLs coming from the DNS server, in milliseconds.
|
|
40
|
+
* @default 1000
|
|
41
|
+
*/
|
|
42
|
+
minTtl?: number;
|
|
43
|
+
/**
|
|
44
|
+
* How long failed lookups are negatively cached, in milliseconds.
|
|
45
|
+
* @default 30000
|
|
46
|
+
*/
|
|
47
|
+
negativeTtl?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Called with updated statistics after every lookup.
|
|
50
|
+
*/
|
|
51
|
+
onStats?: (stats: SmartDnsStats) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Serve expired entries immediately and refresh them in the background.
|
|
54
|
+
* @default false
|
|
55
|
+
*/
|
|
56
|
+
swr?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Lookup statistics for the current instance.
|
|
60
|
+
*/
|
|
61
|
+
export interface SmartDnsStats {
|
|
62
|
+
/** Average wall time of actual DNS queries, in milliseconds. */
|
|
63
|
+
avgResolveMs: number;
|
|
64
|
+
/** Failed lookups (including those served from the negative cache). */
|
|
65
|
+
errors: number;
|
|
66
|
+
/** Lookups served from cache. */
|
|
67
|
+
hits: number;
|
|
68
|
+
/** Lookups that required an actual DNS query. */
|
|
69
|
+
misses: number;
|
|
70
|
+
/** Background refreshes triggered by stale-while-revalidate. */
|
|
71
|
+
revalidations: number;
|
|
72
|
+
}
|
|
19
73
|
/**
|
|
20
74
|
* Enum for built-in DNS providers.
|
|
21
75
|
* @readonly
|
|
@@ -38,31 +92,70 @@ export declare class SmartDnsProviderError extends Error {
|
|
|
38
92
|
*/
|
|
39
93
|
export declare class SmartDnsResolverError extends Error {
|
|
40
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Minimal zero-dependency LRU cache with expiry support.
|
|
97
|
+
*/
|
|
98
|
+
declare class LruCache {
|
|
99
|
+
private readonly maxEntries;
|
|
100
|
+
private readonly entries;
|
|
101
|
+
constructor(maxEntries: number);
|
|
102
|
+
get(hostname: Hostname): CacheEntry | undefined;
|
|
103
|
+
set(hostname: Hostname, entry: CacheEntry): void;
|
|
104
|
+
}
|
|
41
105
|
/**
|
|
42
106
|
* SmartDns class for managing DNS resolution with caching and configurable DNS providers.
|
|
107
|
+
*
|
|
108
|
+
* Configuration is applied to Node's process-global DNS system (`node:dns`):
|
|
109
|
+
* `setProvider` and `setServers` replace the resolver servers for the whole
|
|
110
|
+
* process, and `resultOrder` calls `dns.setDefaultResultOrder`. Every HTTP
|
|
111
|
+
* client in the application is affected by (and benefits from) this setup.
|
|
112
|
+
* Instantiate and configure SmartDns as early as possible in the application
|
|
113
|
+
* lifecycle, ideally once.
|
|
43
114
|
*/
|
|
44
115
|
export declare class SmartDns {
|
|
116
|
+
private readonly ttl;
|
|
45
117
|
/**
|
|
46
118
|
* Cache for hostname-IP mappings.
|
|
47
119
|
*/
|
|
48
|
-
protected readonly cache:
|
|
120
|
+
protected readonly cache: LruCache;
|
|
121
|
+
private readonly inflight;
|
|
122
|
+
private readonly negative;
|
|
123
|
+
private readonly swr;
|
|
124
|
+
private readonly negativeTtl;
|
|
125
|
+
private readonly minTtl;
|
|
126
|
+
private readonly maxTtl;
|
|
127
|
+
private readonly family;
|
|
128
|
+
private readonly onStats?;
|
|
129
|
+
private readonly counters;
|
|
49
130
|
/**
|
|
50
131
|
* Creates an instance of SmartDns.
|
|
51
132
|
* @param [dnsProvider] Optional DNS provider to use.
|
|
52
133
|
* @param [resultOrder='ipv4first'] Order of DNS resolution results.
|
|
53
|
-
* @param [ttl=3600000]
|
|
134
|
+
* @param [ttl=3600000] Fallback time-to-live for cached entries in milliseconds.
|
|
135
|
+
* @param [options] Advanced cache and resolver options.
|
|
54
136
|
*/
|
|
55
|
-
protected constructor(dnsProvider?: DnsProvider, resultOrder?: ResultOrder, ttl?: number);
|
|
137
|
+
protected constructor(dnsProvider?: DnsProvider, resultOrder?: ResultOrder, ttl?: number, options?: SmartDnsOptions);
|
|
56
138
|
/**
|
|
57
139
|
* Creates or retrieves a singleton instance of SmartDns.
|
|
140
|
+
*
|
|
141
|
+
* As in v1, configuration passed after the first call is ignored because the
|
|
142
|
+
* singleton has already been created.
|
|
143
|
+
*
|
|
58
144
|
* @param [dnsProvider] Optional DNS provider to use.
|
|
59
145
|
* @param [resultOrder] Order of DNS resolution results.
|
|
60
|
-
* @param [ttl]
|
|
146
|
+
* @param [ttl] Fallback time-to-live for cached entries in milliseconds.
|
|
147
|
+
* @param [options] Advanced cache and resolver options.
|
|
61
148
|
* @returns The singleton SmartDns instance.
|
|
62
149
|
*/
|
|
63
|
-
static factory(dnsProvider?: DnsProvider, resultOrder?: ResultOrder, ttl?: number): SmartDns;
|
|
150
|
+
static factory(dnsProvider?: DnsProvider, resultOrder?: ResultOrder, ttl?: number, options?: SmartDnsOptions): SmartDns;
|
|
151
|
+
/**
|
|
152
|
+
* Current lookup statistics for this instance.
|
|
153
|
+
*/
|
|
154
|
+
get stats(): SmartDnsStats;
|
|
64
155
|
/**
|
|
65
156
|
* Sets the DNS servers by the provider.
|
|
157
|
+
*
|
|
158
|
+
* Note: this mutates the process-global DNS configuration via `node:dns`.
|
|
66
159
|
* @param dnsProvider The DNS provider to use.
|
|
67
160
|
* @throws {SmartDnsProviderError} If the DNS provider is unsupported.
|
|
68
161
|
*/
|
|
@@ -70,21 +163,28 @@ export declare class SmartDns {
|
|
|
70
163
|
/**
|
|
71
164
|
* Resolves a URL and retrieves its IP address, hostname, and updated URL.
|
|
72
165
|
* @async
|
|
73
|
-
* @param url The URL to resolve.
|
|
166
|
+
* @param url The URL to resolve (must start with http/https).
|
|
74
167
|
* @returns An object containing the resolved IP address, hostname, and the URL with the hostname replaced by the IP address.
|
|
75
|
-
* @throws {SmartDnsResolverError} If the URL is invalid.
|
|
168
|
+
* @throws {SmartDnsResolverError} If the URL is invalid or the lookup was recently failing.
|
|
76
169
|
* @throws {Error} If the resolution fails.
|
|
77
170
|
*/
|
|
78
171
|
resolver(url: string): Promise<{
|
|
79
172
|
address: IPAddress;
|
|
80
173
|
hostname: Hostname;
|
|
81
174
|
urlReplaced: string;
|
|
82
|
-
}
|
|
175
|
+
}>;
|
|
83
176
|
/**
|
|
84
177
|
* Manually sets DNS servers.
|
|
178
|
+
*
|
|
179
|
+
* Note: this mutates the process-global DNS configuration via `node:dns`.
|
|
85
180
|
* @param servers Array of DNS server IP addresses.
|
|
86
181
|
*/
|
|
87
182
|
setServers(servers: IPAddress[]): void;
|
|
183
|
+
private resolveAddress;
|
|
184
|
+
private fetchAddress;
|
|
185
|
+
private revalidate;
|
|
186
|
+
private doResolve;
|
|
187
|
+
private emit;
|
|
88
188
|
}
|
|
89
189
|
export {};
|
|
90
190
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/commonjs/index.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
2
|
+
/*
|
|
3
|
+
* Dario Casertano <dario@casertano.name>
|
|
4
|
+
* Copyright (c) 2026 Casertano Dario – All rights reserved.
|
|
5
|
+
* MIT
|
|
6
|
+
*/
|
|
3
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
8
|
exports.SmartDns = exports.SmartDnsResolverError = exports.SmartDnsProviderError = exports.DnsProvider = void 0;
|
|
5
|
-
/**
|
|
6
|
-
* @author Dario Casertano <dario@casertano.name>
|
|
7
|
-
* @copyright Copyright (c) 2024 Casertano Dario – All rights reserved.
|
|
8
|
-
* @license MIT
|
|
9
|
-
*/
|
|
10
|
-
const lru_cache_1 = require("lru-cache");
|
|
11
9
|
const promises_1 = require("node:dns/promises");
|
|
12
10
|
/**
|
|
13
11
|
* Enum for built-in DNS providers.
|
|
@@ -34,26 +32,84 @@ exports.SmartDnsProviderError = SmartDnsProviderError;
|
|
|
34
32
|
class SmartDnsResolverError extends Error {
|
|
35
33
|
}
|
|
36
34
|
exports.SmartDnsResolverError = SmartDnsResolverError;
|
|
35
|
+
/**
|
|
36
|
+
* Minimal zero-dependency LRU cache with expiry support.
|
|
37
|
+
*/
|
|
38
|
+
class LruCache {
|
|
39
|
+
maxEntries;
|
|
40
|
+
entries = new Map();
|
|
41
|
+
constructor(maxEntries) {
|
|
42
|
+
this.maxEntries = maxEntries;
|
|
43
|
+
}
|
|
44
|
+
get(hostname) {
|
|
45
|
+
const entry = this.entries.get(hostname);
|
|
46
|
+
if (!entry) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
this.entries.delete(hostname);
|
|
50
|
+
this.entries.set(hostname, entry);
|
|
51
|
+
return entry;
|
|
52
|
+
}
|
|
53
|
+
set(hostname, entry) {
|
|
54
|
+
this.entries.delete(hostname);
|
|
55
|
+
this.entries.set(hostname, entry);
|
|
56
|
+
if (this.entries.size > this.maxEntries) {
|
|
57
|
+
const oldest = this.entries.keys().next().value;
|
|
58
|
+
if (oldest !== undefined) {
|
|
59
|
+
this.entries.delete(oldest);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
37
64
|
let SmartDnsInstance;
|
|
38
65
|
/**
|
|
39
66
|
* SmartDns class for managing DNS resolution with caching and configurable DNS providers.
|
|
67
|
+
*
|
|
68
|
+
* Configuration is applied to Node's process-global DNS system (`node:dns`):
|
|
69
|
+
* `setProvider` and `setServers` replace the resolver servers for the whole
|
|
70
|
+
* process, and `resultOrder` calls `dns.setDefaultResultOrder`. Every HTTP
|
|
71
|
+
* client in the application is affected by (and benefits from) this setup.
|
|
72
|
+
* Instantiate and configure SmartDns as early as possible in the application
|
|
73
|
+
* lifecycle, ideally once.
|
|
40
74
|
*/
|
|
41
75
|
class SmartDns {
|
|
76
|
+
ttl;
|
|
42
77
|
/**
|
|
43
78
|
* Cache for hostname-IP mappings.
|
|
44
79
|
*/
|
|
45
80
|
cache;
|
|
81
|
+
inflight = new Map();
|
|
82
|
+
negative = new Map();
|
|
83
|
+
swr;
|
|
84
|
+
negativeTtl;
|
|
85
|
+
minTtl;
|
|
86
|
+
maxTtl;
|
|
87
|
+
family;
|
|
88
|
+
onStats;
|
|
89
|
+
counters = {
|
|
90
|
+
errors: 0,
|
|
91
|
+
hits: 0,
|
|
92
|
+
misses: 0,
|
|
93
|
+
resolutions: 0,
|
|
94
|
+
revalidations: 0,
|
|
95
|
+
totalResolveMs: 0,
|
|
96
|
+
};
|
|
46
97
|
/**
|
|
47
98
|
* Creates an instance of SmartDns.
|
|
48
99
|
* @param [dnsProvider] Optional DNS provider to use.
|
|
49
100
|
* @param [resultOrder='ipv4first'] Order of DNS resolution results.
|
|
50
|
-
* @param [ttl=3600000]
|
|
101
|
+
* @param [ttl=3600000] Fallback time-to-live for cached entries in milliseconds.
|
|
102
|
+
* @param [options] Advanced cache and resolver options.
|
|
51
103
|
*/
|
|
52
|
-
constructor(dnsProvider, resultOrder = 'ipv4first', ttl = 3_600_000) {
|
|
53
|
-
this.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
104
|
+
constructor(dnsProvider, resultOrder = 'ipv4first', ttl = 3_600_000, options = {}) {
|
|
105
|
+
this.ttl = ttl;
|
|
106
|
+
this.swr = options.swr ?? false;
|
|
107
|
+
this.negativeTtl = options.negativeTtl ?? 30_000;
|
|
108
|
+
this.minTtl = Math.max(options.minTtl ?? 1_000, 1);
|
|
109
|
+
this.maxTtl = Math.max(options.maxTtl ?? 3_600_000, this.minTtl);
|
|
110
|
+
this.family = options.family ?? 'ipv4';
|
|
111
|
+
this.onStats = options.onStats;
|
|
112
|
+
this.cache = new LruCache(100);
|
|
57
113
|
if (dnsProvider) {
|
|
58
114
|
this.setProvider(dnsProvider);
|
|
59
115
|
}
|
|
@@ -61,19 +117,40 @@ class SmartDns {
|
|
|
61
117
|
}
|
|
62
118
|
/**
|
|
63
119
|
* Creates or retrieves a singleton instance of SmartDns.
|
|
120
|
+
*
|
|
121
|
+
* As in v1, configuration passed after the first call is ignored because the
|
|
122
|
+
* singleton has already been created.
|
|
123
|
+
*
|
|
64
124
|
* @param [dnsProvider] Optional DNS provider to use.
|
|
65
125
|
* @param [resultOrder] Order of DNS resolution results.
|
|
66
|
-
* @param [ttl]
|
|
126
|
+
* @param [ttl] Fallback time-to-live for cached entries in milliseconds.
|
|
127
|
+
* @param [options] Advanced cache and resolver options.
|
|
67
128
|
* @returns The singleton SmartDns instance.
|
|
68
129
|
*/
|
|
69
|
-
static factory(dnsProvider, resultOrder, ttl) {
|
|
130
|
+
static factory(dnsProvider, resultOrder, ttl, options) {
|
|
70
131
|
if (!SmartDnsInstance) {
|
|
71
|
-
SmartDnsInstance = new SmartDns(dnsProvider, resultOrder, ttl);
|
|
132
|
+
SmartDnsInstance = new SmartDns(dnsProvider, resultOrder, ttl, options);
|
|
72
133
|
}
|
|
73
134
|
return SmartDnsInstance;
|
|
74
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Current lookup statistics for this instance.
|
|
138
|
+
*/
|
|
139
|
+
get stats() {
|
|
140
|
+
return {
|
|
141
|
+
avgResolveMs: this.counters.resolutions > 0
|
|
142
|
+
? this.counters.totalResolveMs / this.counters.resolutions
|
|
143
|
+
: 0,
|
|
144
|
+
errors: this.counters.errors,
|
|
145
|
+
hits: this.counters.hits,
|
|
146
|
+
misses: this.counters.misses,
|
|
147
|
+
revalidations: this.counters.revalidations,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
75
150
|
/**
|
|
76
151
|
* Sets the DNS servers by the provider.
|
|
152
|
+
*
|
|
153
|
+
* Note: this mutates the process-global DNS configuration via `node:dns`.
|
|
77
154
|
* @param dnsProvider The DNS provider to use.
|
|
78
155
|
* @throws {SmartDnsProviderError} If the DNS provider is unsupported.
|
|
79
156
|
*/
|
|
@@ -104,42 +181,111 @@ class SmartDns {
|
|
|
104
181
|
/**
|
|
105
182
|
* Resolves a URL and retrieves its IP address, hostname, and updated URL.
|
|
106
183
|
* @async
|
|
107
|
-
* @param url The URL to resolve.
|
|
184
|
+
* @param url The URL to resolve (must start with http/https).
|
|
108
185
|
* @returns An object containing the resolved IP address, hostname, and the URL with the hostname replaced by the IP address.
|
|
109
|
-
* @throws {SmartDnsResolverError} If the URL is invalid.
|
|
186
|
+
* @throws {SmartDnsResolverError} If the URL is invalid or the lookup was recently failing.
|
|
110
187
|
* @throws {Error} If the resolution fails.
|
|
111
188
|
*/
|
|
112
189
|
async resolver(url) {
|
|
113
190
|
if (!/^https?:\/\//.test(url)) {
|
|
114
191
|
throw new SmartDnsResolverError(`The URL must start with http/https.`);
|
|
115
192
|
}
|
|
193
|
+
let parsedUrl;
|
|
116
194
|
try {
|
|
117
|
-
|
|
118
|
-
if (!this.cache.has(hostname)) {
|
|
119
|
-
const resolver = new promises_1.Resolver();
|
|
120
|
-
const [address] = await resolver.resolve4(hostname);
|
|
121
|
-
this.cache.set(hostname, address);
|
|
122
|
-
}
|
|
123
|
-
if (this.cache.has(hostname)) {
|
|
124
|
-
const address = this.cache.get(hostname);
|
|
125
|
-
return {
|
|
126
|
-
address,
|
|
127
|
-
hostname,
|
|
128
|
-
urlReplaced: url.replace(hostname, address),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
return undefined;
|
|
195
|
+
parsedUrl = new URL(url);
|
|
132
196
|
}
|
|
133
|
-
catch
|
|
134
|
-
throw
|
|
197
|
+
catch {
|
|
198
|
+
throw new SmartDnsResolverError(`The URL "${url}" is not a valid URL.`);
|
|
135
199
|
}
|
|
200
|
+
const hostname = parsedUrl.hostname;
|
|
201
|
+
const address = await this.resolveAddress(hostname);
|
|
202
|
+
return {
|
|
203
|
+
address,
|
|
204
|
+
hostname,
|
|
205
|
+
urlReplaced: url.replace(hostname, address),
|
|
206
|
+
};
|
|
136
207
|
}
|
|
137
208
|
/**
|
|
138
209
|
* Manually sets DNS servers.
|
|
210
|
+
*
|
|
211
|
+
* Note: this mutates the process-global DNS configuration via `node:dns`.
|
|
139
212
|
* @param servers Array of DNS server IP addresses.
|
|
140
213
|
*/
|
|
141
214
|
setServers(servers) {
|
|
142
215
|
(0, promises_1.setServers)(servers);
|
|
143
216
|
}
|
|
217
|
+
async resolveAddress(hostname) {
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
const entry = this.cache.get(hostname);
|
|
220
|
+
if (entry && entry.expiresAt > now) {
|
|
221
|
+
this.counters.hits++;
|
|
222
|
+
this.emit();
|
|
223
|
+
return entry.address;
|
|
224
|
+
}
|
|
225
|
+
if (entry && this.swr) {
|
|
226
|
+
this.counters.hits++;
|
|
227
|
+
this.counters.revalidations++;
|
|
228
|
+
this.revalidate(hostname);
|
|
229
|
+
this.emit();
|
|
230
|
+
return entry.address;
|
|
231
|
+
}
|
|
232
|
+
const negativeUntil = this.negative.get(hostname);
|
|
233
|
+
if (negativeUntil !== undefined && negativeUntil > now) {
|
|
234
|
+
this.counters.errors++;
|
|
235
|
+
this.emit();
|
|
236
|
+
throw new SmartDnsResolverError(`A recent lookup for "${hostname}" failed; retrying is rate limited by the negative cache.`);
|
|
237
|
+
}
|
|
238
|
+
this.counters.misses++;
|
|
239
|
+
return this.fetchAddress(hostname).finally(() => {
|
|
240
|
+
this.emit();
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
fetchAddress(hostname) {
|
|
244
|
+
let pending = this.inflight.get(hostname);
|
|
245
|
+
if (!pending) {
|
|
246
|
+
pending = this.doResolve(hostname);
|
|
247
|
+
this.inflight.set(hostname, pending);
|
|
248
|
+
pending.then(() => undefined, () => undefined).finally(() => {
|
|
249
|
+
this.inflight.delete(hostname);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return pending;
|
|
253
|
+
}
|
|
254
|
+
revalidate(hostname) {
|
|
255
|
+
void this.fetchAddress(hostname);
|
|
256
|
+
}
|
|
257
|
+
async doResolve(hostname) {
|
|
258
|
+
const startedAt = Date.now();
|
|
259
|
+
try {
|
|
260
|
+
const resolver = new promises_1.Resolver();
|
|
261
|
+
const records = this.family === 'ipv6'
|
|
262
|
+
? await resolver.resolve6(hostname, { ttl: true })
|
|
263
|
+
: await resolver.resolve4(hostname, { ttl: true });
|
|
264
|
+
const [record] = records;
|
|
265
|
+
if (!record) {
|
|
266
|
+
throw new Error(`No ${this.family} records found for "${hostname}".`);
|
|
267
|
+
}
|
|
268
|
+
const rawTtl = record.ttl > 0 ? record.ttl * 1000 : this.ttl;
|
|
269
|
+
const recordTtl = Math.min(Math.max(rawTtl, this.minTtl), this.maxTtl);
|
|
270
|
+
this.cache.set(hostname, {
|
|
271
|
+
address: record.address,
|
|
272
|
+
expiresAt: Date.now() + recordTtl,
|
|
273
|
+
});
|
|
274
|
+
this.negative.delete(hostname);
|
|
275
|
+
return record.address;
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
this.counters.errors++;
|
|
279
|
+
this.negative.set(hostname, Date.now() + this.negativeTtl);
|
|
280
|
+
throw e;
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
this.counters.resolutions++;
|
|
284
|
+
this.counters.totalResolveMs += Date.now() - startedAt;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
emit() {
|
|
288
|
+
this.onStats?.(this.stats);
|
|
289
|
+
}
|
|
144
290
|
}
|
|
145
291
|
exports.SmartDns = SmartDns;
|
|
@@ -1,7 +1,2 @@
|
|
|
1
|
-
"use strict";var B=(o,t)=>()=>(t||o((t={exports:{}}).exports,t),t.exports);var q=B(D=>{"use strict";Object.defineProperty(D,"__esModule",{value:!0});D.LRUCache=void 0;var F=typeof performance=="object"&&performance&&typeof performance.now=="function"?performance:Date,k=new Set,U=typeof process=="object"&&process?process:{},H=(o,t,e,i)=>{typeof U.emitWarning=="function"?U.emitWarning(o,t,e,i):console.error(`[${e}] ${t}: ${o}`)},R=globalThis.AbortController,P=globalThis.AbortSignal;if(typeof R>"u"){P=class{onabort;_onabort=[];reason;aborted=!1;addEventListener(i,s){this._onabort.push(s)}},R=class{constructor(){t()}signal=new P;abort(i){if(!this.signal.aborted){this.signal.reason=i,this.signal.aborted=!0;for(let s of this.signal._onabort)s(i);this.signal.onabort?.(i)}}};let o=U.env?.LRU_CACHE_IGNORE_AC_WARNING!=="1",t=()=>{o&&(o=!1,H("AbortController is not defined. If using lru-cache in node 14, load an AbortController polyfill from the `node-abort-controller` package. A minimal polyfill is provided for use by LRUCache.fetch(), but it should not be relied upon in other contexts (eg, passing it to other APIs that use AbortController/AbortSignal might have undesirable effects). You may disable this with LRU_CACHE_IGNORE_AC_WARNING=1 in the env.","NO_ABORT_CONTROLLER","ENOTSUP",t))}}var $=o=>!k.has(o),Q=Symbol("type"),A=o=>o&&o===Math.floor(o)&&o>0&&isFinite(o),V=o=>A(o)?o<=Math.pow(2,8)?Uint8Array:o<=Math.pow(2,16)?Uint16Array:o<=Math.pow(2,32)?Uint32Array:o<=Number.MAX_SAFE_INTEGER?O:null:null,O=class extends Array{constructor(t){super(t),this.fill(0)}},G=class o{heap;length;static#l=!1;static create(t){let e=V(t);if(!e)return[];o.#l=!0;let i=new o(t,e);return o.#l=!1,i}constructor(t,e){if(!o.#l)throw new TypeError("instantiate Stack using Stack.create(n)");this.heap=new e(t),this.length=0}push(t){this.heap[this.length++]=t}pop(){return this.heap[--this.length]}},M=class o{#l;#c;#p;#w;#R;#D;ttl;ttlResolution;ttlAutopurge;updateAgeOnGet;updateAgeOnHas;allowStale;noDisposeOnSet;noUpdateTTL;maxEntrySize;sizeCalculation;noDeleteOnFetchRejection;noDeleteOnStaleGet;allowStaleOnFetchAbort;allowStaleOnFetchRejection;ignoreFetchAbort;#n;#S;#s;#i;#t;#a;#u;#o;#h;#m;#r;#_;#b;#d;#y;#E;#f;static unsafeExposeInternals(t){return{starts:t.#b,ttls:t.#d,sizes:t.#_,keyMap:t.#s,keyList:t.#i,valList:t.#t,next:t.#a,prev:t.#u,get head(){return t.#o},get tail(){return t.#h},free:t.#m,isBackgroundFetch:e=>t.#e(e),backgroundFetch:(e,i,s,n)=>t.#x(e,i,s,n),moveToTail:e=>t.#C(e),indexes:e=>t.#A(e),rindexes:e=>t.#v(e),isStale:e=>t.#g(e)}}get max(){return this.#l}get maxSize(){return this.#c}get calculatedSize(){return this.#S}get size(){return this.#n}get fetchMethod(){return this.#R}get memoMethod(){return this.#D}get dispose(){return this.#p}get disposeAfter(){return this.#w}constructor(t){let{max:e=0,ttl:i,ttlResolution:s=1,ttlAutopurge:n,updateAgeOnGet:h,updateAgeOnHas:l,allowStale:r,dispose:g,disposeAfter:_,noDisposeOnSet:f,noUpdateTTL:u,maxSize:c=0,maxEntrySize:v=0,sizeCalculation:d,fetchMethod:S,memoMethod:a,noDeleteOnFetchRejection:w,noDeleteOnStaleGet:b,allowStaleOnFetchRejection:p,allowStaleOnFetchAbort:m,ignoreFetchAbort:T}=t;if(e!==0&&!A(e))throw new TypeError("max option must be a nonnegative integer");let y=e?V(e):Array;if(!y)throw new Error("invalid max value: "+e);if(this.#l=e,this.#c=c,this.maxEntrySize=v||this.#c,this.sizeCalculation=d,this.sizeCalculation){if(!this.#c&&!this.maxEntrySize)throw new TypeError("cannot set sizeCalculation without setting maxSize or maxEntrySize");if(typeof this.sizeCalculation!="function")throw new TypeError("sizeCalculation set to non-function")}if(a!==void 0&&typeof a!="function")throw new TypeError("memoMethod must be a function if defined");if(this.#D=a,S!==void 0&&typeof S!="function")throw new TypeError("fetchMethod must be a function if specified");if(this.#R=S,this.#E=!!S,this.#s=new Map,this.#i=new Array(e).fill(void 0),this.#t=new Array(e).fill(void 0),this.#a=new y(e),this.#u=new y(e),this.#o=0,this.#h=0,this.#m=G.create(e),this.#n=0,this.#S=0,typeof g=="function"&&(this.#p=g),typeof _=="function"?(this.#w=_,this.#r=[]):(this.#w=void 0,this.#r=void 0),this.#y=!!this.#p,this.#f=!!this.#w,this.noDisposeOnSet=!!f,this.noUpdateTTL=!!u,this.noDeleteOnFetchRejection=!!w,this.allowStaleOnFetchRejection=!!p,this.allowStaleOnFetchAbort=!!m,this.ignoreFetchAbort=!!T,this.maxEntrySize!==0){if(this.#c!==0&&!A(this.#c))throw new TypeError("maxSize must be a positive integer if specified");if(!A(this.maxEntrySize))throw new TypeError("maxEntrySize must be a positive integer if specified");this.#P()}if(this.allowStale=!!r,this.noDeleteOnStaleGet=!!b,this.updateAgeOnGet=!!h,this.updateAgeOnHas=!!l,this.ttlResolution=A(s)||s===0?s:1,this.ttlAutopurge=!!n,this.ttl=i||0,this.ttl){if(!A(this.ttl))throw new TypeError("ttl must be a positive integer if specified");this.#U()}if(this.#l===0&&this.ttl===0&&this.#c===0)throw new TypeError("At least one of max, maxSize, or ttl is required");if(!this.ttlAutopurge&&!this.#l&&!this.#c){let C="LRU_CACHE_UNBOUNDED";$(C)&&(k.add(C),H("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.","UnboundedCacheWarning",C,o))}}getRemainingTTL(t){return this.#s.has(t)?1/0:0}#U(){let t=new O(this.#l),e=new O(this.#l);this.#d=t,this.#b=e,this.#G=(n,h,l=F.now())=>{if(e[n]=h!==0?l:0,t[n]=h,h!==0&&this.ttlAutopurge){let r=setTimeout(()=>{this.#g(n)&&this.#F(this.#i[n],"expire")},h+1);r.unref&&r.unref()}},this.#T=n=>{e[n]=t[n]!==0?F.now():0},this.#O=(n,h)=>{if(t[h]){let l=t[h],r=e[h];if(!l||!r)return;n.ttl=l,n.start=r,n.now=i||s();let g=n.now-r;n.remainingTTL=l-g}};let i=0,s=()=>{let n=F.now();if(this.ttlResolution>0){i=n;let h=setTimeout(()=>i=0,this.ttlResolution);h.unref&&h.unref()}return n};this.getRemainingTTL=n=>{let h=this.#s.get(n);if(h===void 0)return 0;let l=t[h],r=e[h];if(!l||!r)return 1/0;let g=(i||s())-r;return l-g},this.#g=n=>{let h=e[n],l=t[n];return!!l&&!!h&&(i||s())-h>l}}#T=()=>{};#O=()=>{};#G=()=>{};#g=()=>!1;#P(){let t=new O(this.#l);this.#S=0,this.#_=t,this.#z=e=>{this.#S-=t[e],t[e]=0},this.#M=(e,i,s,n)=>{if(this.#e(i))return 0;if(!A(s))if(n){if(typeof n!="function")throw new TypeError("sizeCalculation must be a function");if(s=n(i,e),!A(s))throw new TypeError("sizeCalculation return invalid (expect positive integer)")}else throw new TypeError("invalid size value (must be positive integer). When maxSize or maxEntrySize is used, sizeCalculation or size must be set.");return s},this.#W=(e,i,s)=>{if(t[e]=i,this.#c){let n=this.#c-t[e];for(;this.#S>n;)this.#L(!0)}this.#S+=t[e],s&&(s.entrySize=i,s.totalCalculatedSize=this.#S)}}#z=t=>{};#W=(t,e,i)=>{};#M=(t,e,i,s)=>{if(i||s)throw new TypeError("cannot set size without setting maxSize or maxEntrySize on cache");return 0};*#A({allowStale:t=this.allowStale}={}){if(this.#n)for(let e=this.#h;!(!this.#N(e)||((t||!this.#g(e))&&(yield e),e===this.#o));)e=this.#u[e]}*#v({allowStale:t=this.allowStale}={}){if(this.#n)for(let e=this.#o;!(!this.#N(e)||((t||!this.#g(e))&&(yield e),e===this.#h));)e=this.#a[e]}#N(t){return t!==void 0&&this.#s.get(this.#i[t])===t}*entries(){for(let t of this.#A())this.#t[t]!==void 0&&this.#i[t]!==void 0&&!this.#e(this.#t[t])&&(yield[this.#i[t],this.#t[t]])}*rentries(){for(let t of this.#v())this.#t[t]!==void 0&&this.#i[t]!==void 0&&!this.#e(this.#t[t])&&(yield[this.#i[t],this.#t[t]])}*keys(){for(let t of this.#A()){let e=this.#i[t];e!==void 0&&!this.#e(this.#t[t])&&(yield e)}}*rkeys(){for(let t of this.#v()){let e=this.#i[t];e!==void 0&&!this.#e(this.#t[t])&&(yield e)}}*values(){for(let t of this.#A())this.#t[t]!==void 0&&!this.#e(this.#t[t])&&(yield this.#t[t])}*rvalues(){for(let t of this.#v())this.#t[t]!==void 0&&!this.#e(this.#t[t])&&(yield this.#t[t])}[Symbol.iterator](){return this.entries()}[Symbol.toStringTag]="LRUCache";find(t,e={}){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;if(n!==void 0&&t(n,this.#i[i],this))return this.get(this.#i[i],e)}}forEach(t,e=this){for(let i of this.#A()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&t.call(e,n,this.#i[i],this)}}rforEach(t,e=this){for(let i of this.#v()){let s=this.#t[i],n=this.#e(s)?s.__staleWhileFetching:s;n!==void 0&&t.call(e,n,this.#i[i],this)}}purgeStale(){let t=!1;for(let e of this.#v({allowStale:!0}))this.#g(e)&&(this.#F(this.#i[e],"expire"),t=!0);return t}info(t){let e=this.#s.get(t);if(e===void 0)return;let i=this.#t[e],s=this.#e(i)?i.__staleWhileFetching:i;if(s===void 0)return;let n={value:s};if(this.#d&&this.#b){let h=this.#d[e],l=this.#b[e];if(h&&l){let r=h-(F.now()-l);n.ttl=r,n.start=Date.now()}}return this.#_&&(n.size=this.#_[e]),n}dump(){let t=[];for(let e of this.#A({allowStale:!0})){let i=this.#i[e],s=this.#t[e],n=this.#e(s)?s.__staleWhileFetching:s;if(n===void 0||i===void 0)continue;let h={value:n};if(this.#d&&this.#b){h.ttl=this.#d[e];let l=F.now()-this.#b[e];h.start=Math.floor(Date.now()-l)}this.#_&&(h.size=this.#_[e]),t.unshift([i,h])}return t}load(t){this.clear();for(let[e,i]of t){if(i.start){let s=Date.now()-i.start;i.start=F.now()-s}this.set(e,i.value,i)}}set(t,e,i={}){if(e===void 0)return this.delete(t),this;let{ttl:s=this.ttl,start:n,noDisposeOnSet:h=this.noDisposeOnSet,sizeCalculation:l=this.sizeCalculation,status:r}=i,{noUpdateTTL:g=this.noUpdateTTL}=i,_=this.#M(t,e,i.size||0,l);if(this.maxEntrySize&&_>this.maxEntrySize)return r&&(r.set="miss",r.maxEntrySizeExceeded=!0),this.#F(t,"set"),this;let f=this.#n===0?void 0:this.#s.get(t);if(f===void 0)f=this.#n===0?this.#h:this.#m.length!==0?this.#m.pop():this.#n===this.#l?this.#L(!1):this.#n,this.#i[f]=t,this.#t[f]=e,this.#s.set(t,f),this.#a[this.#h]=f,this.#u[f]=this.#h,this.#h=f,this.#n++,this.#W(f,_,r),r&&(r.set="add"),g=!1;else{this.#C(f);let u=this.#t[f];if(e!==u){if(this.#E&&this.#e(u)){u.__abortController.abort(new Error("replaced"));let{__staleWhileFetching:c}=u;c!==void 0&&!h&&(this.#y&&this.#p?.(c,t,"set"),this.#f&&this.#r?.push([c,t,"set"]))}else h||(this.#y&&this.#p?.(u,t,"set"),this.#f&&this.#r?.push([u,t,"set"]));if(this.#z(f),this.#W(f,_,r),this.#t[f]=e,r){r.set="replace";let c=u&&this.#e(u)?u.__staleWhileFetching:u;c!==void 0&&(r.oldValue=c)}}else r&&(r.set="update")}if(s!==0&&!this.#d&&this.#U(),this.#d&&(g||this.#G(f,s,n),r&&this.#O(r,f)),!h&&this.#f&&this.#r){let u=this.#r,c;for(;c=u?.shift();)this.#w?.(...c)}return this}pop(){try{for(;this.#n;){let t=this.#t[this.#o];if(this.#L(!0),this.#e(t)){if(t.__staleWhileFetching)return t.__staleWhileFetching}else if(t!==void 0)return t}}finally{if(this.#f&&this.#r){let t=this.#r,e;for(;e=t?.shift();)this.#w?.(...e)}}}#L(t){let e=this.#o,i=this.#i[e],s=this.#t[e];return this.#E&&this.#e(s)?s.__abortController.abort(new Error("evicted")):(this.#y||this.#f)&&(this.#y&&this.#p?.(s,i,"evict"),this.#f&&this.#r?.push([s,i,"evict"])),this.#z(e),t&&(this.#i[e]=void 0,this.#t[e]=void 0,this.#m.push(e)),this.#n===1?(this.#o=this.#h=0,this.#m.length=0):this.#o=this.#a[e],this.#s.delete(i),this.#n--,e}has(t,e={}){let{updateAgeOnHas:i=this.updateAgeOnHas,status:s}=e,n=this.#s.get(t);if(n!==void 0){let h=this.#t[n];if(this.#e(h)&&h.__staleWhileFetching===void 0)return!1;if(this.#g(n))s&&(s.has="stale",this.#O(s,n));else return i&&this.#T(n),s&&(s.has="hit",this.#O(s,n)),!0}else s&&(s.has="miss");return!1}peek(t,e={}){let{allowStale:i=this.allowStale}=e,s=this.#s.get(t);if(s===void 0||!i&&this.#g(s))return;let n=this.#t[s];return this.#e(n)?n.__staleWhileFetching:n}#x(t,e,i,s){let n=e===void 0?void 0:this.#t[e];if(this.#e(n))return n;let h=new R,{signal:l}=i;l?.addEventListener("abort",()=>h.abort(l.reason),{signal:h.signal});let r={signal:h.signal,options:i,context:s},g=(d,S=!1)=>{let{aborted:a}=h.signal,w=i.ignoreFetchAbort&&d!==void 0;if(i.status&&(a&&!S?(i.status.fetchAborted=!0,i.status.fetchError=h.signal.reason,w&&(i.status.fetchAbortIgnored=!0)):i.status.fetchResolved=!0),a&&!w&&!S)return f(h.signal.reason);let b=c;return this.#t[e]===c&&(d===void 0?b.__staleWhileFetching?this.#t[e]=b.__staleWhileFetching:this.#F(t,"fetch"):(i.status&&(i.status.fetchUpdated=!0),this.set(t,d,r.options))),d},_=d=>(i.status&&(i.status.fetchRejected=!0,i.status.fetchError=d),f(d)),f=d=>{let{aborted:S}=h.signal,a=S&&i.allowStaleOnFetchAbort,w=a||i.allowStaleOnFetchRejection,b=w||i.noDeleteOnFetchRejection,p=c;if(this.#t[e]===c&&(!b||p.__staleWhileFetching===void 0?this.#F(t,"fetch"):a||(this.#t[e]=p.__staleWhileFetching)),w)return i.status&&p.__staleWhileFetching!==void 0&&(i.status.returnedStale=!0),p.__staleWhileFetching;if(p.__returned===p)throw d},u=(d,S)=>{let a=this.#R?.(t,n,r);a&&a instanceof Promise&&a.then(w=>d(w===void 0?void 0:w),S),h.signal.addEventListener("abort",()=>{(!i.ignoreFetchAbort||i.allowStaleOnFetchAbort)&&(d(void 0),i.allowStaleOnFetchAbort&&(d=w=>g(w,!0)))})};i.status&&(i.status.fetchDispatched=!0);let c=new Promise(u).then(g,_),v=Object.assign(c,{__abortController:h,__staleWhileFetching:n,__returned:void 0});return e===void 0?(this.set(t,v,{...r.options,status:void 0}),e=this.#s.get(t)):this.#t[e]=v,v}#e(t){if(!this.#E)return!1;let e=t;return!!e&&e instanceof Promise&&e.hasOwnProperty("__staleWhileFetching")&&e.__abortController instanceof R}async fetch(t,e={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,ttl:h=this.ttl,noDisposeOnSet:l=this.noDisposeOnSet,size:r=0,sizeCalculation:g=this.sizeCalculation,noUpdateTTL:_=this.noUpdateTTL,noDeleteOnFetchRejection:f=this.noDeleteOnFetchRejection,allowStaleOnFetchRejection:u=this.allowStaleOnFetchRejection,ignoreFetchAbort:c=this.ignoreFetchAbort,allowStaleOnFetchAbort:v=this.allowStaleOnFetchAbort,context:d,forceRefresh:S=!1,status:a,signal:w}=e;if(!this.#E)return a&&(a.fetch="get"),this.get(t,{allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,status:a});let b={allowStale:i,updateAgeOnGet:s,noDeleteOnStaleGet:n,ttl:h,noDisposeOnSet:l,size:r,sizeCalculation:g,noUpdateTTL:_,noDeleteOnFetchRejection:f,allowStaleOnFetchRejection:u,allowStaleOnFetchAbort:v,ignoreFetchAbort:c,status:a,signal:w},p=this.#s.get(t);if(p===void 0){a&&(a.fetch="miss");let m=this.#x(t,p,b,d);return m.__returned=m}else{let m=this.#t[p];if(this.#e(m)){let j=i&&m.__staleWhileFetching!==void 0;return a&&(a.fetch="inflight",j&&(a.returnedStale=!0)),j?m.__staleWhileFetching:m.__returned=m}let T=this.#g(p);if(!S&&!T)return a&&(a.fetch="hit"),this.#C(p),s&&this.#T(p),a&&this.#O(a,p),m;let y=this.#x(t,p,b,d),x=y.__staleWhileFetching!==void 0&&i;return a&&(a.fetch=T?"stale":"refresh",x&&T&&(a.returnedStale=!0)),x?y.__staleWhileFetching:y.__returned=y}}async forceFetch(t,e={}){let i=await this.fetch(t,e);if(i===void 0)throw new Error("fetch() returned undefined");return i}memo(t,e={}){let i=this.#D;if(!i)throw new Error("no memoMethod provided to constructor");let{context:s,forceRefresh:n,...h}=e,l=this.get(t,h);if(!n&&l!==void 0)return l;let r=i(t,l,{options:h,context:s});return this.set(t,r,h),r}get(t,e={}){let{allowStale:i=this.allowStale,updateAgeOnGet:s=this.updateAgeOnGet,noDeleteOnStaleGet:n=this.noDeleteOnStaleGet,status:h}=e,l=this.#s.get(t);if(l!==void 0){let r=this.#t[l],g=this.#e(r);return h&&this.#O(h,l),this.#g(l)?(h&&(h.get="stale"),g?(h&&i&&r.__staleWhileFetching!==void 0&&(h.returnedStale=!0),i?r.__staleWhileFetching:void 0):(n||this.#F(t,"expire"),h&&i&&(h.returnedStale=!0),i?r:void 0)):(h&&(h.get="hit"),g?r.__staleWhileFetching:(this.#C(l),s&&this.#T(l),r))}else h&&(h.get="miss")}#I(t,e){this.#u[e]=t,this.#a[t]=e}#C(t){t!==this.#h&&(t===this.#o?this.#o=this.#a[t]:this.#I(this.#u[t],this.#a[t]),this.#I(this.#h,t),this.#h=t)}delete(t){return this.#F(t,"delete")}#F(t,e){let i=!1;if(this.#n!==0){let s=this.#s.get(t);if(s!==void 0)if(i=!0,this.#n===1)this.#j(e);else{this.#z(s);let n=this.#t[s];if(this.#e(n)?n.__abortController.abort(new Error("deleted")):(this.#y||this.#f)&&(this.#y&&this.#p?.(n,t,e),this.#f&&this.#r?.push([n,t,e])),this.#s.delete(t),this.#i[s]=void 0,this.#t[s]=void 0,s===this.#h)this.#h=this.#u[s];else if(s===this.#o)this.#o=this.#a[s];else{let h=this.#u[s];this.#a[h]=this.#a[s];let l=this.#a[s];this.#u[l]=this.#u[s]}this.#n--,this.#m.push(s)}}if(this.#f&&this.#r?.length){let s=this.#r,n;for(;n=s?.shift();)this.#w?.(...n)}return i}clear(){return this.#j("delete")}#j(t){for(let e of this.#v({allowStale:!0})){let i=this.#t[e];if(this.#e(i))i.__abortController.abort(new Error("deleted"));else{let s=this.#i[e];this.#y&&this.#p?.(i,s,t),this.#f&&this.#r?.push([i,s,t])}}if(this.#s.clear(),this.#t.fill(void 0),this.#i.fill(void 0),this.#d&&this.#b&&(this.#d.fill(0),this.#b.fill(0)),this.#_&&this.#_.fill(0),this.#o=0,this.#h=0,this.#m.length=0,this.#S=0,this.#n=0,this.#f&&this.#r){let e=this.#r,i;for(;i=e?.shift();)this.#w?.(...i)}}};D.LRUCache=M});Object.defineProperty(exports,"__esModule",{value:!0});exports.SmartDns=exports.SmartDnsResolverError=exports.SmartDnsProviderError=exports.DnsProvider=void 0;var Y=q(),E=require("node:dns/promises"),z;(function(o){o[o.CloudFlare=0]="CloudFlare",o[o.Google=1]="Google",o[o.OpenDNS=2]="OpenDNS"})(z||(exports.DnsProvider=z={}));var W=class extends Error{};exports.SmartDnsProviderError=W;var L=class extends Error{};exports.SmartDnsResolverError=L;var N,I=class o{cache;constructor(t,e="ipv4first",i=36e5){this.cache=new Y.LRUCache({max:100,ttl:i}),t&&this.setProvider(t),(0,E.setDefaultResultOrder)(e)}static factory(t,e,i){return N||(N=new o(t,e,i)),N}setProvider(t){switch(t){case z.CloudFlare:(0,E.setServers)(["1.1.1.1","1.0.0.1"]);break;case z.Google:(0,E.setServers)(["8.8.8.8","8.8.4.4"]);break;case z.OpenDNS:(0,E.setServers)(["208.67.222.222","208.67.220.220"]);break;default:throw new W(`Unsupported DNS provider: ${t}. You can use the "setServers" method to manually set the DNS server IP addresses.`)}}async resolver(t){if(!/^https?:\/\//.test(t))throw new L("The URL must start with http/https.");try{let e=new URL(t).hostname;if(!this.cache.has(e)){let i=new E.Resolver,[s]=await i.resolve4(e);this.cache.set(e,s)}if(this.cache.has(e)){let i=this.cache.get(e);return{address:i,hostname:e,urlReplaced:t.replace(e,i)}}return}catch(e){throw e}}setServers(t){(0,E.setServers)(t)}};exports.SmartDns=I;
|
|
2
|
-
/**
|
|
3
|
-
* @author Dario Casertano <dario@casertano.name>
|
|
4
|
-
* @copyright Copyright (c) 2024 Casertano Dario – All rights reserved.
|
|
5
|
-
* @license MIT
|
|
6
|
-
*/
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});exports.SmartDns=exports.SmartDnsResolverError=exports.SmartDnsProviderError=exports.DnsProvider=void 0;var n=require("node:dns/promises"),o;(function(i){i[i.CloudFlare=0]="CloudFlare",i[i.Google=1]="Google",i[i.OpenDNS=2]="OpenDNS"})(o||(exports.DnsProvider=o={}));var h=class extends Error{};exports.SmartDnsProviderError=h;var a=class extends Error{};exports.SmartDnsResolverError=a;var d=class{maxEntries;entries=new Map;constructor(t){this.maxEntries=t}get(t){let e=this.entries.get(t);if(e)return this.entries.delete(t),this.entries.set(t,e),e}set(t,e){if(this.entries.delete(t),this.entries.set(t,e),this.entries.size>this.maxEntries){let s=this.entries.keys().next().value;s!==void 0&&this.entries.delete(s)}}},c,u=class i{ttl;cache;inflight=new Map;negative=new Map;swr;negativeTtl;minTtl;maxTtl;family;onStats;counters={errors:0,hits:0,misses:0,resolutions:0,revalidations:0,totalResolveMs:0};constructor(t,e="ipv4first",s=36e5,r={}){this.ttl=s,this.swr=r.swr??!1,this.negativeTtl=r.negativeTtl??3e4,this.minTtl=Math.max(r.minTtl??1e3,1),this.maxTtl=Math.max(r.maxTtl??36e5,this.minTtl),this.family=r.family??"ipv4",this.onStats=r.onStats,this.cache=new d(100),t&&this.setProvider(t),(0,n.setDefaultResultOrder)(e)}static factory(t,e,s,r){return c||(c=new i(t,e,s,r)),c}get stats(){return{avgResolveMs:this.counters.resolutions>0?this.counters.totalResolveMs/this.counters.resolutions:0,errors:this.counters.errors,hits:this.counters.hits,misses:this.counters.misses,revalidations:this.counters.revalidations}}setProvider(t){switch(t){case o.CloudFlare:(0,n.setServers)(["1.1.1.1","1.0.0.1"]);break;case o.Google:(0,n.setServers)(["8.8.8.8","8.8.4.4"]);break;case o.OpenDNS:(0,n.setServers)(["208.67.222.222","208.67.220.220"]);break;default:throw new h(`Unsupported DNS provider: ${t}. You can use the "setServers" method to manually set the DNS server IP addresses.`)}}async resolver(t){if(!/^https?:\/\//.test(t))throw new a("The URL must start with http/https.");let e;try{e=new URL(t)}catch{throw new a(`The URL "${t}" is not a valid URL.`)}let s=e.hostname,r=await this.resolveAddress(s);return{address:r,hostname:s,urlReplaced:t.replace(s,r)}}setServers(t){(0,n.setServers)(t)}async resolveAddress(t){let e=Date.now(),s=this.cache.get(t);if(s&&s.expiresAt>e)return this.counters.hits++,this.emit(),s.address;if(s&&this.swr)return this.counters.hits++,this.counters.revalidations++,this.revalidate(t),this.emit(),s.address;let r=this.negative.get(t);if(r!==void 0&&r>e)throw this.counters.errors++,this.emit(),new a(`A recent lookup for "${t}" failed; retrying is rate limited by the negative cache.`);return this.counters.misses++,this.fetchAddress(t).finally(()=>{this.emit()})}fetchAddress(t){let e=this.inflight.get(t);return e||(e=this.doResolve(t),this.inflight.set(t,e),e.then(()=>{},()=>{}).finally(()=>{this.inflight.delete(t)})),e}revalidate(t){this.fetchAddress(t)}async doResolve(t){let e=Date.now();try{let s=new n.Resolver,r=this.family==="ipv6"?await s.resolve6(t,{ttl:!0}):await s.resolve4(t,{ttl:!0}),[l]=r;if(!l)throw new Error(`No ${this.family} records found for "${t}".`);let v=l.ttl>0?l.ttl*1e3:this.ttl,f=Math.min(Math.max(v,this.minTtl),this.maxTtl);return this.cache.set(t,{address:l.address,expiresAt:Date.now()+f}),this.negative.delete(t),l.address}catch(s){throw this.counters.errors++,this.negative.set(t,Date.now()+this.negativeTtl),s}finally{this.counters.resolutions++,this.counters.totalResolveMs+=Date.now()-e}}emit(){this.onStats?.(this.stats)}};exports.SmartDns=u;
|
|
7
2
|
//# sourceMappingURL=index.min.js.map
|