@nxtedition/nxt-undici 7.1.9 → 7.2.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/lib/errors.js CHANGED
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
1
  export class UndiciError extends Error {
4
2
  constructor(message, options) {
5
3
  super(message, options)
package/lib/index.js CHANGED
@@ -18,6 +18,7 @@ export const interceptors = {
18
18
  requestId: (await import('./interceptor/request-id.js')).default,
19
19
  dns: (await import('./interceptor/dns.js')).default,
20
20
  lookup: (await import('./interceptor/lookup.js')).default,
21
+ priority: (await import('./interceptor/priority.js')).default,
21
22
  }
22
23
 
23
24
  export const cache = {
@@ -83,6 +84,7 @@ function wrapDispatch(dispatcher) {
83
84
  if (wrappedDispatcher == null) {
84
85
  wrappedDispatcher = compose(
85
86
  dispatcher,
87
+ interceptors.priority(),
86
88
  interceptors.dns(),
87
89
  interceptors.lookup(),
88
90
  interceptors.requestBodyFactory(),
@@ -106,6 +108,22 @@ function wrapDispatch(dispatcher) {
106
108
  headers['user-agent'] ??=
107
109
  opts.userAgent ?? globalThis.userAgent ?? globalThis.__nxt_undici_user_agent
108
110
 
111
+ let priority
112
+ if (opts.priority == null) {
113
+ // Do nothing
114
+ } else if (opts.priority === 'low' || opts.priority === 0 || opts.priority === '0') {
115
+ headers['nxt-priority'] = 'low'
116
+ priority = 0
117
+ } else if (opts.priority === 'normal' || opts.priority === 1 || opts.priority === '1') {
118
+ headers['nxt-priority'] = 'normal'
119
+ priority = 1
120
+ } else if (opts.priority === 'high' || opts.priority === 2 || opts.priority === '2') {
121
+ headers['nxt-priority'] = 'high'
122
+ priority = 2
123
+ } else {
124
+ throw new TypeError('invalid opts.priority')
125
+ }
126
+
109
127
  if (globalThis.__nxt_undici_global_headers) {
110
128
  Object.assign(headers, globalThis.__nxt_undici_global_headers)
111
129
  }
@@ -136,6 +154,7 @@ function wrapDispatch(dispatcher) {
136
154
  logger: opts.logger ?? null,
137
155
  dns: opts.dns ?? true,
138
156
  connect: opts.connect,
157
+ priority,
139
158
  lookup: opts.lookup ?? defaultLookup,
140
159
  },
141
160
  handler,
@@ -161,7 +180,8 @@ export function dispatch(dispatcher, opts, handler) {
161
180
  * protocol: string?,
162
181
  * pathname: string?,
163
182
  * search: string?,
164
- * }} URLLike
183
+ * }} URLObject
184
+ * @typedef {string|URL|URLObject} URLLike
165
185
  */
166
186
 
167
187
  /**
@@ -174,7 +194,7 @@ export function dispatch(dispatcher, opts, handler) {
174
194
  * id: string | null | undefined,
175
195
  * dispatch: function,
176
196
  * dispatcher: import('@nxtedition/undici').Dispatcher | null | undefined,
177
- * url: URL | URLLike | string | null | undefined,
197
+ * url: URLLike | null | undefined,
178
198
  * origin: string?,
179
199
  * path: string?,
180
200
  * method: string | null | undefined,
@@ -198,13 +218,14 @@ export function dispatch(dispatcher, opts, handler) {
198
218
  * logger: LoggerLike | null | undefined,
199
219
  * dns: object | boolean | null | undefined,
200
220
  * connect: object | null | undefined,
221
+ * priority: 0 | 1 | 2 | "low" | "normal" | "high" | null | undefined,
201
222
  * lookup: ((origin: string | URLLike | Array<string | URLLike> , opts: object, callback: (err: Error | null, address: string | null) => void) => void) | null | undefined,
202
223
  * }} RequestOptions
203
224
  */
204
225
 
205
226
  /**
206
227
  *
207
- * @param {URL|URLLike|string|RequestOptions} urlOrOpts
228
+ * @param {URLLike|RequestOptions} urlOrOpts
208
229
  * @param {RequestOptions|null} [opts]
209
230
  * @returns {Promise<{
210
231
  * body: import('stream').Readable,
@@ -0,0 +1,58 @@
1
+ import { Scheduler } from '../utils/scheduler.js'
2
+ import { DecoratorHandler } from '../utils.js'
3
+
4
+ class Handler extends DecoratorHandler {
5
+ #next
6
+
7
+ constructor(handler, next) {
8
+ super(handler)
9
+ this.#next = next
10
+ }
11
+
12
+ onConnect(abort) {
13
+ this.#release()
14
+ super.onConnect(abort)
15
+ }
16
+
17
+ onComplete(trailers) {
18
+ this.#release()
19
+ super.onComplete(trailers)
20
+ }
21
+
22
+ onError(err) {
23
+ this.#release()
24
+ super.onError(err)
25
+ }
26
+
27
+ #release() {
28
+ if (this.#next) {
29
+ this.#next()
30
+ this.#next = null
31
+ }
32
+ }
33
+ }
34
+
35
+ export default () => (dispatch) => {
36
+ const schedulers = new Map()
37
+
38
+ return (opts, handler) => {
39
+ if (opts.priority == null || !opts.origin) {
40
+ return dispatch(opts, handler)
41
+ }
42
+
43
+ let scheduler = schedulers.get(opts.origin)
44
+ if (!scheduler) {
45
+ scheduler = new Scheduler({ concurrency: 1 })
46
+ schedulers.set(opts.origin, scheduler)
47
+ }
48
+
49
+ scheduler.schedule((next) => {
50
+ try {
51
+ dispatch(opts, new Handler(handler, next))
52
+ } catch (err) {
53
+ next()
54
+ handler.onError(err)
55
+ }
56
+ }, opts.priority)
57
+ }
58
+ }
@@ -18,6 +18,7 @@ export default () => (dispatch) => (opts, handler) => {
18
18
  {
19
19
  ...opts,
20
20
  id: nextId,
21
+ logger: opts.logger?.child({ ureq: { id: nextId } }),
21
22
  headers: {
22
23
  ...opts.headers,
23
24
  'request-id': nextId,
@@ -272,7 +272,7 @@ class Handler extends DecoratorHandler {
272
272
  .then((shouldRetry) => {
273
273
  if (this.#aborted) {
274
274
  this.#maybeError(this.#reason)
275
- } else if (shouldRetry === false || isDisturbed(this.#opts.body)) {
275
+ } else if (!shouldRetry || isDisturbed(this.#opts.body)) {
276
276
  this.#maybeError(err)
277
277
  } else if (!this.#headersSent) {
278
278
  this.#opts.logger?.debug({ err, retryCount: this.#retryCount }, 'retry response headers')
@@ -327,6 +327,7 @@ class Handler extends DecoratorHandler {
327
327
  retryAfter != null && Number.isFinite(retryAfter)
328
328
  ? retryAfter
329
329
  : Math.min(10e3, retryCount * 1e3)
330
+ this.#opts.logger?.debug({ statusCode, retryAfter, delay, retryCount }, 'retry delay')
330
331
  return tp.setTimeout(delay, true, { signal: opts?.signal ?? undefined })
331
332
  }
332
333
 
@@ -347,12 +348,14 @@ class Handler extends DecoratorHandler {
347
348
  'UND_ERR_SOCKET',
348
349
  ].includes(err.code)
349
350
  ) {
351
+ this.#opts.logger?.debug({ err, retryCount }, 'retry delay')
350
352
  return tp.setTimeout(Math.min(10e3, retryCount * 1e3), true, {
351
353
  signal: opts?.signal ?? undefined,
352
354
  })
353
355
  }
354
356
 
355
357
  if (err?.message && ['other side closed'].includes(err.message)) {
358
+ this.#opts.logger?.debug({ err, retryCount }, 'retry delay')
356
359
  return tp.setTimeout(Math.min(10e3, retryCount * 1e3), true, {
357
360
  signal: opts?.signal ?? undefined,
358
361
  })
@@ -0,0 +1,123 @@
1
+ // Extracted from node/lib/internal/fixed_queue.js
2
+
3
+ // Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two.
4
+ const kSize = 2048
5
+ const kMask = kSize - 1
6
+
7
+ // The FixedQueue is implemented as a singly-linked list of fixed-size
8
+ // circular buffers. It looks something like this:
9
+ //
10
+ // head tail
11
+ // | |
12
+ // v v
13
+ // +-----------+ <-----\ +-----------+ <------\ +-----------+
14
+ // | [null] | \----- | next | \------- | next |
15
+ // +-----------+ +-----------+ +-----------+
16
+ // | item | <-- bottom | item | <-- bottom | [empty] |
17
+ // | item | | item | | [empty] |
18
+ // | item | | item | | [empty] |
19
+ // | item | | item | | [empty] |
20
+ // | item | | item | bottom --> | item |
21
+ // | item | | item | | item |
22
+ // | ... | | ... | | ... |
23
+ // | item | | item | | item |
24
+ // | item | | item | | item |
25
+ // | [empty] | <-- top | item | | item |
26
+ // | [empty] | | item | | item |
27
+ // | [empty] | | [empty] | <-- top top --> | [empty] |
28
+ // +-----------+ +-----------+ +-----------+
29
+ //
30
+ // Or, if there is only one circular buffer, it looks something
31
+ // like either of these:
32
+ //
33
+ // head tail head tail
34
+ // | | | |
35
+ // v v v v
36
+ // +-----------+ +-----------+
37
+ // | [null] | | [null] |
38
+ // +-----------+ +-----------+
39
+ // | [empty] | | item |
40
+ // | [empty] | | item |
41
+ // | item | <-- bottom top --> | [empty] |
42
+ // | item | | [empty] |
43
+ // | [empty] | <-- top bottom --> | item |
44
+ // | [empty] | | item |
45
+ // +-----------+ +-----------+
46
+ //
47
+ // Adding a value means moving `top` forward by one, removing means
48
+ // moving `bottom` forward by one. After reaching the end, the queue
49
+ // wraps around.
50
+ //
51
+ // When `top === bottom` the current queue is empty and when
52
+ // `top + 1 === bottom` it's full. This wastes a single space of storage
53
+ // but allows much quicker checks.
54
+
55
+ class FixedCircularBuffer {
56
+ constructor() {
57
+ this.bottom = 0
58
+ this.top = 0
59
+ this.list = new Array(kSize).fill(undefined)
60
+ this.next = null
61
+ }
62
+
63
+ isEmpty() {
64
+ return this.top === this.bottom
65
+ }
66
+
67
+ isFull() {
68
+ return ((this.top + 1) & kMask) === this.bottom
69
+ }
70
+
71
+ push(data) {
72
+ this.list[this.top] = data
73
+ this.top = (this.top + 1) & kMask
74
+ }
75
+
76
+ shift() {
77
+ const nextItem = this.list[this.bottom]
78
+ if (nextItem === undefined) {
79
+ return null
80
+ }
81
+ this.list[this.bottom] = undefined
82
+ this.bottom = (this.bottom + 1) & kMask
83
+ return nextItem
84
+ }
85
+ }
86
+
87
+ const POOL = []
88
+
89
+ export class FixedQueue {
90
+ constructor() {
91
+ this.head = this.tail = POOL.pop() ?? new FixedCircularBuffer()
92
+ this.size = 0
93
+ }
94
+
95
+ isEmpty() {
96
+ return this.head.isEmpty()
97
+ }
98
+
99
+ push(data) {
100
+ if (this.head.isFull()) {
101
+ // Head is full: Creates a new queue, sets the old queue's `.next` to it,
102
+ // and sets it as the new main queue.
103
+ this.head = this.head.next = POOL.pop() ?? new FixedCircularBuffer()
104
+ }
105
+ this.head.push(data)
106
+ }
107
+
108
+ shift() {
109
+ const tail = this.tail
110
+ const next = tail.shift()
111
+ if (tail.isEmpty() && tail.next !== null) {
112
+ // If there is another queue, it forms the new tail.
113
+ this.tail = tail.next
114
+ tail.next = null
115
+ tail.bottom = 0
116
+ tail.top = 0
117
+ if (POOL.length < 64) {
118
+ POOL.push(tail)
119
+ }
120
+ }
121
+ return next
122
+ }
123
+ }
@@ -0,0 +1,74 @@
1
+ import { FixedQueue } from './fixed-queue.js'
2
+
3
+ export class Scheduler {
4
+ /** @type {0} */
5
+ static LOW = 0
6
+ /** @type {1} */
7
+ static NORMAL = 1
8
+ /** @type {2} */
9
+ static HIGH = 2
10
+
11
+ #concurrency
12
+ #running = 0
13
+ #counter = 0
14
+
15
+ #lowQueue = new FixedQueue()
16
+ #normalQueue = new FixedQueue()
17
+ #highQueue = new FixedQueue()
18
+
19
+ /**
20
+ * @param {{ concurrency?: number }} [options]
21
+ */
22
+ constructor({ concurrency = Infinity } = {}) {
23
+ this.#concurrency = concurrency
24
+ }
25
+
26
+ /**
27
+ * @param {(Function) => any} fn
28
+ * @param {0|1|2} priority
29
+ * @returns
30
+ */
31
+ schedule(fn, priority = Scheduler.NORMAL) {
32
+ if (typeof fn !== 'function') {
33
+ throw new TypeError('First argument must be a function')
34
+ }
35
+
36
+ if (priority == null) {
37
+ priority = Scheduler.NORMAL
38
+ }
39
+
40
+ if (!Number.isInteger(priority)) {
41
+ throw new Error('Invalid priority')
42
+ }
43
+
44
+ if (this.#running < this.#concurrency) {
45
+ this.#running++
46
+ return fn(this.#next)
47
+ }
48
+
49
+ if (priority > Scheduler.NORMAL) {
50
+ this.#highQueue.push(fn)
51
+ } else if (priority < Scheduler.NORMAL) {
52
+ this.#lowQueue.push(fn)
53
+ } else {
54
+ this.#normalQueue.push(fn)
55
+ }
56
+ }
57
+
58
+ #next = () => {
59
+ this.#counter++
60
+ this.#running--
61
+
62
+ const fn =
63
+ ((this.#counter & 63) === 0 && this.#lowQueue.shift()) ??
64
+ ((this.#counter & 15) === 0 && this.#normalQueue.shift()) ??
65
+ this.#highQueue.shift() ??
66
+ this.#normalQueue.shift() ??
67
+ this.#lowQueue.shift()
68
+
69
+ if (fn) {
70
+ this.#running++
71
+ fn(this.#next)
72
+ }
73
+ }
74
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/nxt-undici",
3
- "version": "7.1.9",
3
+ "version": "7.2.0",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "main": "lib/index.js",
@@ -12,19 +12,19 @@
12
12
  "@nxtedition/undici": "^11.1.1",
13
13
  "cache-control-parser": "^2.0.6",
14
14
  "fast-querystring": "^1.1.2",
15
- "http-errors": "^2.0.0",
15
+ "http-errors": "^2.0.1",
16
16
  "xxhash-wasm": "^1.1.0"
17
17
  },
18
18
  "devDependencies": {
19
- "@types/node": "^24.9.1",
20
- "eslint": "^9.38.0",
19
+ "@types/node": "^25.0.0",
20
+ "eslint": "^9.39.1",
21
21
  "eslint-plugin-n": "^17.21.3",
22
22
  "husky": "^9.1.7",
23
- "lint-staged": "^16.2.6",
23
+ "lint-staged": "^16.2.7",
24
24
  "pinst": "^3.0.0",
25
- "prettier": "^3.6.2",
25
+ "prettier": "^3.7.4",
26
26
  "send": "^1.2.0",
27
- "tap": "^21.1.0",
27
+ "tap": "^21.5.0",
28
28
  "undici-types": "^7.15.0"
29
29
  },
30
30
  "scripts": {