@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 +0 -2
- package/lib/index.js +24 -3
- package/lib/interceptor/priority.js +58 -0
- package/lib/interceptor/request-id.js +1 -0
- package/lib/interceptor/response-retry.js +4 -1
- package/lib/utils/fixed-queue.js +123 -0
- package/lib/utils/scheduler.js +74 -0
- package/package.json +7 -7
package/lib/errors.js
CHANGED
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
|
-
* }}
|
|
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:
|
|
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 {
|
|
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
|
+
}
|
|
@@ -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
|
|
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.
|
|
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.
|
|
15
|
+
"http-errors": "^2.0.1",
|
|
16
16
|
"xxhash-wasm": "^1.1.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@types/node": "^
|
|
20
|
-
"eslint": "^9.
|
|
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.
|
|
23
|
+
"lint-staged": "^16.2.7",
|
|
24
24
|
"pinst": "^3.0.0",
|
|
25
|
-
"prettier": "^3.
|
|
25
|
+
"prettier": "^3.7.4",
|
|
26
26
|
"send": "^1.2.0",
|
|
27
|
-
"tap": "^21.
|
|
27
|
+
"tap": "^21.5.0",
|
|
28
28
|
"undici-types": "^7.15.0"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|