@nxtedition/nxt-undici 7.3.19 → 7.3.21

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/lib/index.d.ts CHANGED
@@ -176,7 +176,8 @@ export interface CacheStore {
176
176
  key: CacheKey,
177
177
  value: CacheValue & { body: null | Buffer | Buffer[]; start: number; end: number },
178
178
  ): void
179
- purgeStale(): void
179
+ gc(): void
180
+ clear(): void
180
181
  close(): void
181
182
  }
182
183
 
@@ -242,7 +243,8 @@ export class SqliteCacheStore implements CacheStore {
242
243
  key: CacheKey,
243
244
  value: CacheValue & { body: null | Buffer | Buffer[]; start: number; end: number },
244
245
  ): void
245
- purgeStale(): void
246
+ gc(): void
247
+ clear(): void
246
248
  close(): void
247
249
  }
248
250
 
@@ -52,7 +52,6 @@ export default () => (dispatch) => {
52
52
  pending: 0,
53
53
  errored: 0,
54
54
  counter: 0,
55
- timeout: 0,
56
55
  }
57
56
  })
58
57
 
@@ -68,35 +67,31 @@ export default () => (dispatch) => {
68
67
  }
69
68
 
70
69
  return async (opts, handler) => {
71
- if (!opts.dns || !opts.origin) {
72
- return dispatch(opts, handler)
73
- }
70
+ try {
71
+ if (!opts.dns || !opts.origin) {
72
+ return dispatch(opts, handler)
73
+ }
74
74
 
75
- const ttl = opts.dns.ttl ?? 2e3
76
- const url = new URL(opts.path ?? '', opts.origin)
77
- const balance = opts.dns.balance
75
+ const ttl = opts.dns.ttl ?? 2e3
76
+ const url = new URL(opts.path ?? '', opts.origin)
77
+ const balance = opts.dns.balance
78
78
 
79
- const { host, hostname, pathname } = url
79
+ const { host, hostname, pathname } = url
80
80
 
81
- if (net.isIP(hostname)) {
82
- return dispatch(opts, handler)
83
- }
81
+ if (net.isIP(hostname)) {
82
+ return dispatch(opts, handler)
83
+ }
84
84
 
85
- try {
86
85
  const now = getFastNow()
87
86
 
88
87
  let records = cache.get(hostname)
89
88
 
90
89
  if (records == null || records.every((x) => x.expires < now)) {
91
90
  const [err, val] = await resolve(hostname, { ttl })
92
-
93
91
  if (err) {
94
92
  throw err
95
93
  }
96
-
97
94
  records = val
98
- } else if (records.some((x) => x.expires < now + 1e3)) {
99
- resolve(hostname, { ttl })
100
95
  }
101
96
 
102
97
  let record
@@ -108,7 +103,7 @@ export default () => (dispatch) => {
108
103
 
109
104
  for (let i = 0; i < records.length; i++) {
110
105
  const idx = (hash + i) % records.length
111
- if (records[idx].expires >= now && records[idx].timeout < now) {
106
+ if (records[idx].expires >= now) {
112
107
  record = records[idx]
113
108
  break
114
109
  }
@@ -116,13 +111,14 @@ export default () => (dispatch) => {
116
111
  }
117
112
 
118
113
  if (record == null) {
119
- records.sort(
114
+ // toSorted — balance:'hash' relies on the cached array's index order.
115
+ const sorted = records.toSorted(
120
116
  (a, b) => a.errored - b.errored || a.pending - b.pending || a.counter - b.counter,
121
117
  )
122
118
 
123
- for (let i = 0; i < records.length; i++) {
124
- if (records[i].expires >= now && records[i].timeout < now) {
125
- record = records[i]
119
+ for (let i = 0; i < sorted.length; i++) {
120
+ if (sorted[i].expires >= now) {
121
+ record = sorted[i]
126
122
  break
127
123
  }
128
124
  }
@@ -135,7 +131,15 @@ export default () => (dispatch) => {
135
131
  })
136
132
  }
137
133
 
138
- url.hostname = record.address
134
+ // Pre-emptive refresh when any record is past half its TTL — the
135
+ // in-flight request still uses the already-selected `record`; the
136
+ // refreshed records land in cache for the next request, smoothing
137
+ // out DNS lookup latency. `resolve()` dedupes via `promises`.
138
+ if (records.some((x) => x.expires < now + ttl / 2)) {
139
+ resolve(hostname, { ttl })
140
+ }
141
+
142
+ url.hostname = net.isIPv6(record.address) ? `[${record.address}]` : record.address
139
143
 
140
144
  record.counter++
141
145
  record.pending++
@@ -150,10 +154,6 @@ export default () => (dispatch) => {
150
154
  } else if (statusCode != null && statusCode >= 500) {
151
155
  record.errored++
152
156
  }
153
-
154
- if (err != null || statusCode >= 500) {
155
- record.timeout = getFastNow() + 10e3
156
- }
157
157
  }),
158
158
  )
159
159
  } catch (err) {
@@ -4,7 +4,7 @@ import { parseRangeHeader, getFastNow } from './utils.js'
4
4
  // Bump version when the URL key format or schema changes to invalidate old caches.
5
5
  const VERSION = 10
6
6
 
7
- /** @typedef {{ purgeStale: () => void } } */
7
+ /** @typedef {{ gc: () => void, clear: () => void } } */
8
8
  const stores = new Set()
9
9
 
10
10
  {
@@ -12,7 +12,17 @@ const stores = new Set()
12
12
  offPeakBC.unref()
13
13
  offPeakBC.onmessage = () => {
14
14
  for (const store of stores) {
15
- store.purgeStale()
15
+ store.gc()
16
+ }
17
+ }
18
+ }
19
+
20
+ {
21
+ const clearCacheBC = new BroadcastChannel('nxt:clearCache')
22
+ clearCacheBC.unref()
23
+ clearCacheBC.onmessage = () => {
24
+ for (const store of stores) {
25
+ store.clear()
16
26
  }
17
27
  }
18
28
  }
@@ -42,6 +52,11 @@ export class SqliteCacheStore {
42
52
  */
43
53
  #db
44
54
 
55
+ /**
56
+ * @type {number}
57
+ */
58
+ #dbTimeout = 20
59
+
45
60
  /**
46
61
  * @type {import('node:sqlite').StatementSync}
47
62
  */
@@ -69,7 +84,11 @@ export class SqliteCacheStore {
69
84
  * @param {import('undici-types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts & { maxSize?: number } | undefined} opts
70
85
  */
71
86
  constructor(opts) {
72
- this.#db = new DatabaseSync(opts?.location ?? ':memory:', { timeout: 20, ...opts?.db })
87
+ this.#dbTimeout = opts?.db?.timeout ?? this.#dbTimeout
88
+ this.#db = new DatabaseSync(opts?.location ?? ':memory:', {
89
+ ...opts?.db,
90
+ timeout: this.#dbTimeout,
91
+ })
73
92
 
74
93
  const maxSize = opts?.maxSize ?? 256 * 1024 * 1024
75
94
  this.#db.exec(`
@@ -157,13 +176,43 @@ export class SqliteCacheStore {
157
176
  stores.add(this)
158
177
  }
159
178
 
160
- purgeStale() {
179
+ gc() {
161
180
  try {
181
+ this.#db.exec('PRAGMA busy_timeout = 1000')
162
182
  this.#deleteExpiredValuesQuery.run(getFastNow())
163
183
  this.#db.exec('PRAGMA wal_checkpoint(TRUNCATE)')
164
184
  this.#db.exec('PRAGMA optimize')
165
185
  } catch (err) {
166
186
  process.emitWarning(err)
187
+ } finally {
188
+ try {
189
+ this.#db.exec(`PRAGMA busy_timeout = ${this.#dbTimeout}`)
190
+ } catch (err) {
191
+ process.emitWarning(err)
192
+ }
193
+ }
194
+ }
195
+
196
+ clear() {
197
+ this.#insertBatch.length = 0
198
+
199
+ if (this.#closed) {
200
+ return
201
+ }
202
+
203
+ try {
204
+ this.#db.exec('PRAGMA busy_timeout = 1000')
205
+ this.#db.exec(`DELETE FROM cacheInterceptorV${VERSION}`)
206
+ this.#db.exec('PRAGMA wal_checkpoint(TRUNCATE)')
207
+ this.#db.exec('PRAGMA optimize')
208
+ } catch (err) {
209
+ process.emitWarning(err)
210
+ } finally {
211
+ try {
212
+ this.#db.exec(`PRAGMA busy_timeout = ${this.#dbTimeout}`)
213
+ } catch (err) {
214
+ process.emitWarning(err)
215
+ }
167
216
  }
168
217
  }
169
218
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/nxt-undici",
3
- "version": "7.3.19",
3
+ "version": "7.3.21",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "main": "lib/index.js",