@nxtedition/nxt-undici 7.2.10 → 7.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.
- package/lib/index.d.ts +18 -0
- package/lib/index.js +20 -3
- package/lib/interceptor/cache.js +1 -1
- package/lib/interceptor/log.js +8 -2
- package/lib/interceptor/redirect.js +1 -1
- package/lib/interceptor/response-retry.js +7 -6
- package/lib/interceptor/response-verify.js +2 -2
- package/lib/sqlite-cache-store.js +52 -7
- package/lib/utils.js +5 -5
- package/package.json +12 -9
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Dispatcher } from 'undici-types'
|
|
2
|
+
import type { Logger } from 'pino'
|
|
3
|
+
|
|
4
|
+
export type Headers =
|
|
5
|
+
| Record<string, string | string[] | null | undefined>
|
|
6
|
+
| (Buffer | string | (Buffer | string)[])[]
|
|
7
|
+
|
|
8
|
+
export interface NxtUndiciRequestInit extends RequestInit {
|
|
9
|
+
headers?: Headers
|
|
10
|
+
throwOnError?: boolean
|
|
11
|
+
logger?: Logger
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function request(options: NxtUndiciRequestInit): Promise<Dispatcher.ResponseData>
|
|
15
|
+
export function request(
|
|
16
|
+
url: string | URL,
|
|
17
|
+
options?: NxtUndiciRequestInit,
|
|
18
|
+
): Promise<Dispatcher.ResponseData>
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import undici from '@nxtedition/undici'
|
|
2
|
+
import { Scheduler } from '@nxtedition/scheduler'
|
|
2
3
|
import { parseHeaders } from './utils.js'
|
|
3
4
|
import { request as _request } from './request.js'
|
|
4
5
|
import { SqliteCacheStore } from './sqlite-cache-store.js'
|
|
@@ -79,6 +80,22 @@ export function compose(...interceptors) {
|
|
|
79
80
|
return dispatch
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
const PRIORITY_TOS_MAP = {}
|
|
84
|
+
PRIORITY_TOS_MAP[Scheduler.HIGHEST] = 0xb8 // EF
|
|
85
|
+
PRIORITY_TOS_MAP['highest'] = 0xb8 // EF
|
|
86
|
+
PRIORITY_TOS_MAP[Scheduler.HIGHER] = 0x88 // AF41
|
|
87
|
+
PRIORITY_TOS_MAP['higher'] = 0x88 // AF41
|
|
88
|
+
PRIORITY_TOS_MAP[Scheduler.HIGH] = 0x68 // AF31
|
|
89
|
+
PRIORITY_TOS_MAP['high'] = 0x68 // AF31
|
|
90
|
+
PRIORITY_TOS_MAP[Scheduler.NORMAL] = 0x00 // BE
|
|
91
|
+
PRIORITY_TOS_MAP['normal'] = 0x00 // BE
|
|
92
|
+
PRIORITY_TOS_MAP[Scheduler.LOW] = 0x04 // LE
|
|
93
|
+
PRIORITY_TOS_MAP['low'] = 0x04 // LE
|
|
94
|
+
PRIORITY_TOS_MAP[Scheduler.LOWER] = 0x04 // LE
|
|
95
|
+
PRIORITY_TOS_MAP['lower'] = 0x04 // LE
|
|
96
|
+
PRIORITY_TOS_MAP[Scheduler.LOWEST] = 0x04 // LE
|
|
97
|
+
PRIORITY_TOS_MAP['lowest'] = 0x04 // LE
|
|
98
|
+
|
|
82
99
|
function wrapDispatch(dispatcher) {
|
|
83
100
|
let wrappedDispatcher = dispatcherCache.get(dispatcher)
|
|
84
101
|
if (wrappedDispatcher == null) {
|
|
@@ -107,8 +124,7 @@ function wrapDispatch(dispatcher) {
|
|
|
107
124
|
const userAgent =
|
|
108
125
|
opts.userAgent ?? globalThis.userAgent ?? globalThis.__nxt_undici_user_agent
|
|
109
126
|
if (userAgent != null) {
|
|
110
|
-
headers['user-agent'] ??=
|
|
111
|
-
opts.userAgent ?? globalThis.userAgent ?? globalThis.__nxt_undici_user_agent
|
|
127
|
+
headers['user-agent'] ??= userAgent
|
|
112
128
|
}
|
|
113
129
|
|
|
114
130
|
if (opts.priority != null) {
|
|
@@ -135,7 +151,8 @@ function wrapDispatch(dispatcher) {
|
|
|
135
151
|
opts.timeout?.headers ?? opts.headersTimeout ?? opts.headerTimeout ?? opts.timeout,
|
|
136
152
|
bodyTimeout: opts.timeout?.body ?? opts.bodyTimeout ?? opts.timeout,
|
|
137
153
|
idempotent: opts.idempotent,
|
|
138
|
-
typeOfService:
|
|
154
|
+
typeOfService:
|
|
155
|
+
opts.typeOfService ?? (opts.priority ? (PRIORITY_TOS_MAP[opts.priority] ?? 0) : 0),
|
|
139
156
|
retry: opts.retry ?? 8,
|
|
140
157
|
proxy: opts.proxy ?? false,
|
|
141
158
|
cache: opts.cache ?? false,
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -121,7 +121,7 @@ class CacheHandler extends DecoratorHandler {
|
|
|
121
121
|
const start = contentRange ? contentRange.start : 0
|
|
122
122
|
const end = contentRange ? contentRange.end : contentLength
|
|
123
123
|
|
|
124
|
-
if (end == null || end - start
|
|
124
|
+
if (end == null || end - start <= this.#maxEntrySize) {
|
|
125
125
|
const cachedAt = Date.now()
|
|
126
126
|
this.#value = {
|
|
127
127
|
body: [],
|
package/lib/interceptor/log.js
CHANGED
|
@@ -94,7 +94,10 @@ class Handler extends DecoratorHandler {
|
|
|
94
94
|
headers: this.#headers,
|
|
95
95
|
timing: this.#timing,
|
|
96
96
|
bytesRead: this.#pos,
|
|
97
|
-
bytesReadPerSecond:
|
|
97
|
+
bytesReadPerSecond:
|
|
98
|
+
this.#timing.data >= 0 && this.#timing.end > this.#timing.data
|
|
99
|
+
? (this.#pos * 1e3) / (this.#timing.end - this.#timing.data)
|
|
100
|
+
: 0,
|
|
98
101
|
},
|
|
99
102
|
elapsedTime: this.#timing.end,
|
|
100
103
|
}
|
|
@@ -119,7 +122,10 @@ class Handler extends DecoratorHandler {
|
|
|
119
122
|
headers: this.#headers,
|
|
120
123
|
timing: this.#timing,
|
|
121
124
|
bytesRead: this.#pos,
|
|
122
|
-
bytesReadPerSecond:
|
|
125
|
+
bytesReadPerSecond:
|
|
126
|
+
this.#timing.data >= 0 && this.#timing.end > this.#timing.data
|
|
127
|
+
? (this.#pos * 1e3) / (this.#timing.end - this.#timing.data)
|
|
128
|
+
: 0,
|
|
123
129
|
},
|
|
124
130
|
elapsedTime: this.#timing.end,
|
|
125
131
|
err,
|
|
@@ -59,7 +59,7 @@ class Handler extends DecoratorHandler {
|
|
|
59
59
|
this.#location = typeof headers.location === 'string' ? headers.location : ''
|
|
60
60
|
|
|
61
61
|
if (!this.#location) {
|
|
62
|
-
throw new Error(`Missing redirection location
|
|
62
|
+
throw new Error(`Missing redirection location.`)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
this.#history.push(this.#location)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
import tp from 'node:timers/promises'
|
|
3
|
-
import { DecoratorHandler, isDisturbed, decorateError,
|
|
3
|
+
import { DecoratorHandler, isDisturbed, decorateError, parseContentRange } from '../utils.js'
|
|
4
4
|
|
|
5
5
|
function noop() {}
|
|
6
6
|
|
|
@@ -103,7 +103,7 @@ class Handler extends DecoratorHandler {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
if (statusCode === 206) {
|
|
106
|
-
const { start, size, end = size } =
|
|
106
|
+
const { start, size, end = size } = parseContentRange(headers['content-range']) ?? {}
|
|
107
107
|
if (start == null || end == null || contentLength !== end - start) {
|
|
108
108
|
this.#headersSent = true
|
|
109
109
|
return super.onHeaders(statusCode, headers, resume)
|
|
@@ -147,7 +147,7 @@ class Handler extends DecoratorHandler {
|
|
|
147
147
|
return false
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
const contentRange =
|
|
150
|
+
const contentRange = parseContentRange(headers['content-range'])
|
|
151
151
|
if (!contentRange) {
|
|
152
152
|
this.#maybeError(null)
|
|
153
153
|
return false
|
|
@@ -175,9 +175,8 @@ class Handler extends DecoratorHandler {
|
|
|
175
175
|
this.#pos += chunk.byteLength
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
this.#retryCount = 0
|
|
179
|
-
|
|
180
178
|
if (this.#statusCode < 400) {
|
|
179
|
+
this.#retryCount = 0
|
|
181
180
|
return super.onData(chunk)
|
|
182
181
|
}
|
|
183
182
|
|
|
@@ -195,6 +194,7 @@ class Handler extends DecoratorHandler {
|
|
|
195
194
|
this.#trailers = trailers
|
|
196
195
|
|
|
197
196
|
if (this.#statusCode < 400) {
|
|
197
|
+
this.#retryCount = 0
|
|
198
198
|
return super.onComplete(trailers)
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -285,6 +285,7 @@ class Handler extends DecoratorHandler {
|
|
|
285
285
|
assert(Number.isFinite(this.#pos))
|
|
286
286
|
assert(this.#end == null || (Number.isFinite(this.#end) && this.#end > 0))
|
|
287
287
|
|
|
288
|
+
this.#opts = { ...this.#opts, headers: { ...this.#opts.headers } }
|
|
288
289
|
this.#opts.headers['if-match'] = this.#etag
|
|
289
290
|
this.#opts.headers.range = `bytes=${this.#pos}-${this.#end ? this.#end - 1 : ''}`
|
|
290
291
|
this.#opts.logger?.debug({ err, retryCount: this.#retryCount }, 'retry response body')
|
|
@@ -313,7 +314,7 @@ class Handler extends DecoratorHandler {
|
|
|
313
314
|
|
|
314
315
|
const retryMax = retryOpts?.count ?? 8
|
|
315
316
|
|
|
316
|
-
if (retryCount
|
|
317
|
+
if (retryCount >= retryMax) {
|
|
317
318
|
return false
|
|
318
319
|
}
|
|
319
320
|
|
|
@@ -46,14 +46,14 @@ class Handler extends DecoratorHandler {
|
|
|
46
46
|
|
|
47
47
|
if (this.#contentLength != null && this.#pos !== Number(this.#contentLength)) {
|
|
48
48
|
super.onError(
|
|
49
|
-
Object.assign(new Error('
|
|
49
|
+
Object.assign(new Error('Response Content-Length mismatch'), {
|
|
50
50
|
expected: Number(this.#contentLength),
|
|
51
51
|
actual: this.#pos,
|
|
52
52
|
}),
|
|
53
53
|
)
|
|
54
54
|
} else if (this.#contentMD5 != null && contentMD5 !== this.#contentMD5) {
|
|
55
55
|
super.onError(
|
|
56
|
-
Object.assign(new Error('
|
|
56
|
+
Object.assign(new Error('Response Content-MD5 mismatch'), {
|
|
57
57
|
expected: this.#contentMD5,
|
|
58
58
|
actual: contentMD5,
|
|
59
59
|
}),
|
|
@@ -4,6 +4,23 @@ import { parseRangeHeader, getFastNow } from './utils.js'
|
|
|
4
4
|
|
|
5
5
|
const VERSION = 7
|
|
6
6
|
|
|
7
|
+
/** @typedef {{ purgeStale: () => void } } */
|
|
8
|
+
const stores = new Set()
|
|
9
|
+
|
|
10
|
+
{
|
|
11
|
+
const offPeakBC = new BroadcastChannel('nxt:offPeak')
|
|
12
|
+
offPeakBC.unref()
|
|
13
|
+
offPeakBC.onmessage = () => {
|
|
14
|
+
for (const store of stores) {
|
|
15
|
+
try {
|
|
16
|
+
store.purgeStale()
|
|
17
|
+
} catch (err) {
|
|
18
|
+
process.emitWarning(err)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
7
24
|
/**
|
|
8
25
|
* @typedef {import('undici-types/cache-interceptor.d.ts').default.CacheStore} CacheStore
|
|
9
26
|
* @implements {CacheStore}
|
|
@@ -51,10 +68,10 @@ export class SqliteCacheStore {
|
|
|
51
68
|
#deleteExpiredValuesTime = getFastNow()
|
|
52
69
|
|
|
53
70
|
/**
|
|
54
|
-
* @param {import('undici-types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts & {
|
|
71
|
+
* @param {import('undici-types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts & { maxSize?: number } | undefined} opts
|
|
55
72
|
*/
|
|
56
73
|
constructor(opts) {
|
|
57
|
-
this.#db = new DatabaseSync(opts?.location ?? ':memory:', { timeout:
|
|
74
|
+
this.#db = new DatabaseSync(opts?.location ?? ':memory:', { timeout: 40, ...opts?.db })
|
|
58
75
|
|
|
59
76
|
this.#db.exec(`
|
|
60
77
|
PRAGMA journal_mode = WAL;
|
|
@@ -84,10 +101,18 @@ export class SqliteCacheStore {
|
|
|
84
101
|
CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_deleteExpiredValuesQuery ON cacheInterceptorV${VERSION}(deleteAt);
|
|
85
102
|
`)
|
|
86
103
|
|
|
104
|
+
const maxSize = opts?.maxSize ?? 256 * 1024 * 1024
|
|
105
|
+
{
|
|
106
|
+
const { page_size: pageSize } = this.#db.prepare('PRAGMA page_size').get()
|
|
107
|
+
this.#db.exec(`PRAGMA max_page_count = ${Math.ceil(maxSize / pageSize)}`)
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
this.#getValuesQuery = this.#db.prepare(`
|
|
88
111
|
SELECT
|
|
89
112
|
id,
|
|
90
113
|
body,
|
|
114
|
+
start,
|
|
115
|
+
end,
|
|
91
116
|
deleteAt,
|
|
92
117
|
statusCode,
|
|
93
118
|
statusMessage,
|
|
@@ -128,9 +153,16 @@ export class SqliteCacheStore {
|
|
|
128
153
|
this.#deleteExpiredValuesQuery = this.#db.prepare(
|
|
129
154
|
`DELETE FROM cacheInterceptorV${VERSION} WHERE deleteAt <= ?`,
|
|
130
155
|
)
|
|
156
|
+
|
|
157
|
+
stores.add(this)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
purgeStale() {
|
|
161
|
+
this.#prune()
|
|
131
162
|
}
|
|
132
163
|
|
|
133
164
|
close() {
|
|
165
|
+
stores.delete(this)
|
|
134
166
|
this.#db.close()
|
|
135
167
|
}
|
|
136
168
|
|
|
@@ -141,8 +173,6 @@ export class SqliteCacheStore {
|
|
|
141
173
|
get(key) {
|
|
142
174
|
assertCacheKey(key)
|
|
143
175
|
|
|
144
|
-
this.#prune()
|
|
145
|
-
|
|
146
176
|
const value = this.#findValue(key)
|
|
147
177
|
return value ? makeResult(value) : undefined
|
|
148
178
|
}
|
|
@@ -161,8 +191,19 @@ export class SqliteCacheStore {
|
|
|
161
191
|
assert(Number.isFinite(value.end))
|
|
162
192
|
assert(!body || body?.byteLength === value.end - value.start)
|
|
163
193
|
|
|
164
|
-
|
|
194
|
+
try {
|
|
195
|
+
this.#insert(key, value, body)
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (err?.errcode === 13 /* SQLITE_FULL */) {
|
|
198
|
+
this.#prune()
|
|
199
|
+
this.#insert(key, value, body)
|
|
200
|
+
} else {
|
|
201
|
+
throw err
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
165
205
|
|
|
206
|
+
#insert(key, value, body) {
|
|
166
207
|
this.#insertValueQuery.run(
|
|
167
208
|
makeValueUrl(key),
|
|
168
209
|
key.method,
|
|
@@ -184,8 +225,8 @@ export class SqliteCacheStore {
|
|
|
184
225
|
#prune() {
|
|
185
226
|
const now = getFastNow()
|
|
186
227
|
if (now > this.#deleteExpiredValuesTime) {
|
|
187
|
-
this.#deleteExpiredValuesQuery.run(
|
|
188
|
-
this.#deleteExpiredValuesTime
|
|
228
|
+
this.#deleteExpiredValuesQuery.run(now)
|
|
229
|
+
this.#deleteExpiredValuesTime = now + 60e3
|
|
189
230
|
}
|
|
190
231
|
}
|
|
191
232
|
|
|
@@ -203,6 +244,10 @@ export class SqliteCacheStore {
|
|
|
203
244
|
|
|
204
245
|
const range = parseRangeHeader(headers?.range)
|
|
205
246
|
|
|
247
|
+
if (range === null) {
|
|
248
|
+
return undefined
|
|
249
|
+
}
|
|
250
|
+
|
|
206
251
|
/**
|
|
207
252
|
* @type {SqliteStoreValue[]}
|
|
208
253
|
*/
|
package/lib/utils.js
CHANGED
|
@@ -66,7 +66,7 @@ export function parseContentRange(range) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// Parsed accordingly to RFC 9110
|
|
69
|
-
// https://www.rfc-editor.org/rfc/rfc9110#field.
|
|
69
|
+
// https://www.rfc-editor.org/rfc/rfc9110#field.range
|
|
70
70
|
/**
|
|
71
71
|
* @param {string} [range]
|
|
72
72
|
* @returns {RangeHeader|null|undefined}
|
|
@@ -80,12 +80,12 @@ export function parseRangeHeader(range) {
|
|
|
80
80
|
return null
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const m = range.match(/^bytes
|
|
83
|
+
const m = range.match(/^bytes=(\d+)-(\d+)?$/)
|
|
84
84
|
return m
|
|
85
85
|
? {
|
|
86
86
|
start: parseInt(m[1]),
|
|
87
87
|
end: m[2] ? parseInt(m[2]) + 1 : null,
|
|
88
|
-
size:
|
|
88
|
+
size: null,
|
|
89
89
|
}
|
|
90
90
|
: null
|
|
91
91
|
}
|
|
@@ -154,7 +154,7 @@ export function parseURL(url) {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
export function parseOrigin(url) {
|
|
157
|
-
url =
|
|
157
|
+
url = parseURL(url)
|
|
158
158
|
|
|
159
159
|
if (url.pathname !== '/' || url.search || url.hash) {
|
|
160
160
|
throw new Error('invalid url')
|
|
@@ -390,7 +390,7 @@ export function decorateError(err, opts, { statusCode, headers, trailers, body }
|
|
|
390
390
|
|
|
391
391
|
if (
|
|
392
392
|
typeof body === 'string' &&
|
|
393
|
-
(!headers['content-type'] || headers['content-type'].startsWith('application/json'))
|
|
393
|
+
(!headers?.['content-type'] || headers['content-type'].startsWith('application/json'))
|
|
394
394
|
) {
|
|
395
395
|
try {
|
|
396
396
|
body = JSON.parse(body)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxtedition/nxt-undici",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Robert Nagy <robert.nagy@boffins.se>",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -9,30 +9,33 @@
|
|
|
9
9
|
"lib/*"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@nxtedition/scheduler": "^3.0.
|
|
12
|
+
"@nxtedition/scheduler": "^3.0.8",
|
|
13
13
|
"@nxtedition/undici": "^11.1.3",
|
|
14
|
-
"cache-control-parser": "^2.0
|
|
14
|
+
"cache-control-parser": "^2.2.0",
|
|
15
15
|
"fast-querystring": "^1.1.2",
|
|
16
16
|
"http-errors": "^2.0.1",
|
|
17
17
|
"xxhash-wasm": "^1.1.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@
|
|
21
|
-
"
|
|
22
|
-
"eslint
|
|
20
|
+
"@eslint/js": "^10.0.1",
|
|
21
|
+
"@types/node": "^25.2.3",
|
|
22
|
+
"eslint": "^10.0.0",
|
|
23
|
+
"eslint-plugin-n": "^17.24.0",
|
|
23
24
|
"husky": "^9.1.7",
|
|
24
25
|
"lint-staged": "^16.2.7",
|
|
26
|
+
"pino": "^9.6.0",
|
|
25
27
|
"pinst": "^3.0.0",
|
|
26
28
|
"prettier": "^3.8.1",
|
|
27
29
|
"send": "^1.2.1",
|
|
28
|
-
"tap": "^21.5.
|
|
29
|
-
"undici-types": "^7.
|
|
30
|
+
"tap": "^21.5.1",
|
|
31
|
+
"undici-types": "^7.22.0"
|
|
30
32
|
},
|
|
31
33
|
"scripts": {
|
|
32
34
|
"prepare": "husky",
|
|
33
35
|
"prepublishOnly": "pinst --disable",
|
|
34
36
|
"postpublish": "pinst --enable",
|
|
35
|
-
"test": "tap test"
|
|
37
|
+
"test": "tap test",
|
|
38
|
+
"test:coverage": "tap test --coverage-report=text --coverage-report=html"
|
|
36
39
|
},
|
|
37
40
|
"lint-staged": {
|
|
38
41
|
"*.{js,jsx,md,ts}": [
|