@nxtedition/nxt-undici 6.2.9 → 6.2.10
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.js +2 -1
- package/lib/interceptor/cache.js +43 -94
- package/lib/sqlite-cache-store.js +40 -52
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import undici from '@nxtedition/undici'
|
|
2
2
|
import { parseHeaders } from './utils.js'
|
|
3
3
|
import { request as _request } from './request.js'
|
|
4
|
+
import { SqliteCacheStore } from './sqlite-cache-store.js'
|
|
4
5
|
|
|
5
6
|
const dispatcherCache = new WeakMap()
|
|
6
7
|
|
|
@@ -19,7 +20,7 @@ export const interceptors = {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export const cache = {
|
|
22
|
-
SqliteCacheStore
|
|
23
|
+
SqliteCacheStore,
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export { parseHeaders } from './utils.js'
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import undici from '@nxtedition/undici'
|
|
2
2
|
import { DecoratorHandler, parseCacheControl, parseContentRange } from '../utils.js'
|
|
3
|
+
import { SqliteCacheStore, assertCacheKey } from '../sqlite-cache-store.js'
|
|
3
4
|
|
|
4
|
-
const DEFAULT_STORE = new
|
|
5
|
+
const DEFAULT_STORE = new SqliteCacheStore({ location: ':memory:' })
|
|
5
6
|
const DEFAULT_MAX_ENTRY_SIZE = 128 * 1024
|
|
6
7
|
const NOOP = () => {}
|
|
7
8
|
|
|
@@ -12,7 +13,7 @@ class CacheHandler extends DecoratorHandler {
|
|
|
12
13
|
#maxEntrySize
|
|
13
14
|
|
|
14
15
|
constructor(key, { store, handler, maxEntrySize }) {
|
|
15
|
-
|
|
16
|
+
assertCacheKey(key)
|
|
16
17
|
|
|
17
18
|
super(handler)
|
|
18
19
|
|
|
@@ -26,7 +27,7 @@ class CacheHandler extends DecoratorHandler {
|
|
|
26
27
|
this.#value = null
|
|
27
28
|
|
|
28
29
|
super.onConnect((reason) => {
|
|
29
|
-
// TODO (fix):
|
|
30
|
+
// TODO (fix): Cache partial results?
|
|
30
31
|
abort(reason)
|
|
31
32
|
})
|
|
32
33
|
}
|
|
@@ -41,19 +42,26 @@ class CacheHandler extends DecoratorHandler {
|
|
|
41
42
|
return super.onHeaders(statusCode, headers, resume)
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
let contentRange
|
|
46
|
+
if (headers['content-range']) {
|
|
47
|
+
contentRange = parseContentRange(headers['content-range'])
|
|
48
|
+
if (contentRange === null) {
|
|
49
|
+
// We don't support caching responses with invalid content-range...
|
|
50
|
+
return super.onHeaders(statusCode, headers, resume)
|
|
51
|
+
}
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
let contentLength
|
|
55
|
+
if (headers['content-length']) {
|
|
56
|
+
contentLength = Number(headers['content-length'])
|
|
57
|
+
if (!Number.isFinite(contentLength) || contentLength <= 0) {
|
|
58
|
+
// We don't support caching responses with invalid content-length...
|
|
59
|
+
return super.onHeaders(statusCode, headers, resume)
|
|
60
|
+
}
|
|
52
61
|
}
|
|
53
62
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// We don't support caching responses with body...
|
|
63
|
+
if (statusCode === 206 && !contentRange) {
|
|
64
|
+
// We don't support caching range responses without content-range...
|
|
57
65
|
return super.onHeaders(statusCode, headers, resume)
|
|
58
66
|
}
|
|
59
67
|
|
|
@@ -107,19 +115,27 @@ class CacheHandler extends DecoratorHandler {
|
|
|
107
115
|
return super.onHeaders(statusCode, headers, resume)
|
|
108
116
|
}
|
|
109
117
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
const start = contentRange ? contentRange.start : 0
|
|
119
|
+
const end = contentRange ? contentRange.end : contentLength
|
|
120
|
+
|
|
121
|
+
if (end == null || end - start < this.#maxEntrySize) {
|
|
122
|
+
const cachedAt = Date.now()
|
|
123
|
+
this.#value = {
|
|
124
|
+
body: [],
|
|
125
|
+
start,
|
|
126
|
+
end,
|
|
127
|
+
deleteAt: cachedAt + ttl * 1e3,
|
|
128
|
+
statusCode,
|
|
129
|
+
statusMessage: '',
|
|
130
|
+
headers,
|
|
131
|
+
cacheControlDirectives,
|
|
132
|
+
etag: isEtagUsable(headers.etag) ? headers.etag : '',
|
|
133
|
+
vary,
|
|
134
|
+
cachedAt,
|
|
135
|
+
staleAt: 0,
|
|
136
|
+
// Handler state.
|
|
137
|
+
size: 0,
|
|
138
|
+
}
|
|
123
139
|
}
|
|
124
140
|
|
|
125
141
|
return super.onHeaders(statusCode, headers, resume)
|
|
@@ -141,6 +157,7 @@ class CacheHandler extends DecoratorHandler {
|
|
|
141
157
|
|
|
142
158
|
onComplete(trailers) {
|
|
143
159
|
if (this.#value && (!trailers || Object.keys(trailers).length === 0)) {
|
|
160
|
+
this.#value.end ??= this.#value.start + this.#value.size
|
|
144
161
|
this.#store.set(this.#key, this.#value)
|
|
145
162
|
this.#value = null
|
|
146
163
|
}
|
|
@@ -178,76 +195,8 @@ export default () => (dispatch) => (opts, handler) => {
|
|
|
178
195
|
const store = opts.cache.store ?? DEFAULT_STORE
|
|
179
196
|
|
|
180
197
|
// TODO (fix): enable range requests
|
|
181
|
-
const entry = store.get(opts)
|
|
182
198
|
|
|
183
|
-
|
|
184
|
-
// if (opts.headers.range) {
|
|
185
|
-
// const range = parseRangeHeader(opts.headers.range)
|
|
186
|
-
// if (!range) {
|
|
187
|
-
// // Invalid range header...
|
|
188
|
-
// return dispatch(opts, handler)
|
|
189
|
-
// }
|
|
190
|
-
|
|
191
|
-
// // TODO (perf): This is not optimal as all range bodies will be loaded...
|
|
192
|
-
// // Make sure it only returns valid ranges by passing/parsing content range...
|
|
193
|
-
// const entries = store.getAll(opts)
|
|
194
|
-
|
|
195
|
-
// for (const x of entries) {
|
|
196
|
-
// const { statusCode, headers, body } = x
|
|
197
|
-
|
|
198
|
-
// if (!body) {
|
|
199
|
-
// continue
|
|
200
|
-
// }
|
|
201
|
-
|
|
202
|
-
// let contentRange
|
|
203
|
-
// if (statusCode === 200) {
|
|
204
|
-
// // TODO (fix): Implement this...
|
|
205
|
-
// // contentRange = { start: 0, end: body.byteLength }
|
|
206
|
-
// // x = {
|
|
207
|
-
// // ...x,
|
|
208
|
-
// // headers: {
|
|
209
|
-
// // ...x,
|
|
210
|
-
// // 'content-md5': undefined
|
|
211
|
-
// // // TODO (fix): What other headers need to be modified? accept-ranges? etag?
|
|
212
|
-
// // }
|
|
213
|
-
// // }
|
|
214
|
-
// } else if (statusCode === 206) {
|
|
215
|
-
// contentRange = parseContentRange(headers?.['content-range'])
|
|
216
|
-
// }
|
|
217
|
-
|
|
218
|
-
// if (!contentRange) {
|
|
219
|
-
// continue
|
|
220
|
-
// }
|
|
221
|
-
|
|
222
|
-
// if (contentRange.start === range.start && contentRange.end === range.end) {
|
|
223
|
-
// entry = x
|
|
224
|
-
// break
|
|
225
|
-
// }
|
|
226
|
-
|
|
227
|
-
// // TODO (fix): Implement this...
|
|
228
|
-
// // const start = 0
|
|
229
|
-
// // const end = contentRange.end - contentRange.start
|
|
230
|
-
// // x = {
|
|
231
|
-
// // ...x,
|
|
232
|
-
// // body: body.subarray(start, end),
|
|
233
|
-
// // headers: {
|
|
234
|
-
// // ...headers,
|
|
235
|
-
// // 'content-range': `bytes ${start}-${end - 1}/${contentRange.size ?? '*'}`
|
|
236
|
-
// // 'content-md5': undefined
|
|
237
|
-
// // // TODO (fix): What other headers need to be modified? etag?
|
|
238
|
-
// // }
|
|
239
|
-
// // }
|
|
240
|
-
// // TODO (fix): Pick best entry... i.e. what ever fullfills most of the range
|
|
241
|
-
// }
|
|
242
|
-
// } else {
|
|
243
|
-
// entry = store.get(opts)
|
|
244
|
-
|
|
245
|
-
// // TODO (fix): store.get is not optimal as it can return partial (206) responses.
|
|
246
|
-
// // Make sure it only returns valid statusCodes.
|
|
247
|
-
// if (entry?.statusCode === 206) {
|
|
248
|
-
// return dispatch(opts, handler)
|
|
249
|
-
// }
|
|
250
|
-
// }
|
|
199
|
+
const entry = store.get(opts)
|
|
251
200
|
|
|
252
201
|
if (!entry && !cacheControlDirectives['only-if-cached']) {
|
|
253
202
|
return dispatch(
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DatabaseSync } from 'node:sqlite'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { parseRangeHeader } from './utils.js'
|
|
2
4
|
|
|
3
|
-
const VERSION =
|
|
5
|
+
const VERSION = 5
|
|
4
6
|
|
|
5
7
|
// 2gb
|
|
6
8
|
const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000
|
|
@@ -12,6 +14,8 @@ const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000
|
|
|
12
14
|
* @typedef {{
|
|
13
15
|
* id: Readonly<number>,
|
|
14
16
|
* body?: Uint8Array
|
|
17
|
+
* start: number
|
|
18
|
+
* end: number
|
|
15
19
|
* statusCode: number
|
|
16
20
|
* statusMessage: string
|
|
17
21
|
* headers?: string
|
|
@@ -112,6 +116,8 @@ export class SqliteCacheStore {
|
|
|
112
116
|
url TEXT NOT NULL,
|
|
113
117
|
method TEXT NOT NULL,
|
|
114
118
|
body BUF NULL,
|
|
119
|
+
start INTEGER NOT NULL,
|
|
120
|
+
end INTEGER NOT NULL,
|
|
115
121
|
deleteAt INTEGER NOT NULL,
|
|
116
122
|
statusCode INTEGER NOT NULL,
|
|
117
123
|
statusMessage TEXT NOT NULL,
|
|
@@ -126,6 +132,8 @@ export class SqliteCacheStore {
|
|
|
126
132
|
CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_url ON cacheInterceptorV${VERSION}(url);
|
|
127
133
|
CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_method ON cacheInterceptorV${VERSION}(method);
|
|
128
134
|
CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_deleteAt ON cacheInterceptorV${VERSION}(deleteAt);
|
|
135
|
+
CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_start ON cacheInterceptorV${VERSION}(start);
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_end ON cacheInterceptorV${VERSION}(end);
|
|
129
137
|
`)
|
|
130
138
|
|
|
131
139
|
this.#getValuesQuery = this.#db.prepare(`
|
|
@@ -145,6 +153,8 @@ export class SqliteCacheStore {
|
|
|
145
153
|
WHERE
|
|
146
154
|
url = ?
|
|
147
155
|
AND method = ?
|
|
156
|
+
AND start <= ?
|
|
157
|
+
AND end >= ?
|
|
148
158
|
ORDER BY
|
|
149
159
|
deleteAt ASC
|
|
150
160
|
`)
|
|
@@ -154,6 +164,8 @@ export class SqliteCacheStore {
|
|
|
154
164
|
url,
|
|
155
165
|
method,
|
|
156
166
|
body,
|
|
167
|
+
start,
|
|
168
|
+
end,
|
|
157
169
|
deleteAt,
|
|
158
170
|
statusCode,
|
|
159
171
|
statusMessage,
|
|
@@ -163,7 +175,7 @@ export class SqliteCacheStore {
|
|
|
163
175
|
vary,
|
|
164
176
|
cachedAt,
|
|
165
177
|
staleAt
|
|
166
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
178
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
167
179
|
`)
|
|
168
180
|
|
|
169
181
|
this.#deleteByUrlQuery = this.#db.prepare(
|
|
@@ -210,17 +222,7 @@ export class SqliteCacheStore {
|
|
|
210
222
|
|
|
211
223
|
/**
|
|
212
224
|
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
|
|
213
|
-
* @
|
|
214
|
-
*/
|
|
215
|
-
getAll(key) {
|
|
216
|
-
assertCacheKey(key)
|
|
217
|
-
|
|
218
|
-
return this.#findValues(key).map((value) => makeResult(value))
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
|
|
223
|
-
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheValue & { body: null | Buffer | Array<Buffer>}} value
|
|
225
|
+
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheValue & { body: null | Buffer | Array<Buffer>, start: number, end: number }} value
|
|
224
226
|
*/
|
|
225
227
|
set(key, value) {
|
|
226
228
|
assertCacheKey(key)
|
|
@@ -231,12 +233,18 @@ export class SqliteCacheStore {
|
|
|
231
233
|
return
|
|
232
234
|
}
|
|
233
235
|
|
|
236
|
+
assert(Number.isFinite(value.start))
|
|
237
|
+
assert(Number.isFinite(value.end))
|
|
238
|
+
assert(!body || body?.byteLength === value.end - value.start)
|
|
239
|
+
|
|
234
240
|
this.#prune()
|
|
235
241
|
|
|
236
242
|
this.#insertValueQuery.run(
|
|
237
243
|
makeValueUrl(key),
|
|
238
244
|
key.method,
|
|
239
245
|
body,
|
|
246
|
+
value.start,
|
|
247
|
+
value.end,
|
|
240
248
|
value.deleteAt,
|
|
241
249
|
value.statusCode,
|
|
242
250
|
value.statusMessage,
|
|
@@ -301,10 +309,21 @@ export class SqliteCacheStore {
|
|
|
301
309
|
#findValue(key, canBeExpired = false) {
|
|
302
310
|
const { headers, method } = key
|
|
303
311
|
|
|
312
|
+
if (Array.isArray(headers?.range)) {
|
|
313
|
+
return undefined
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const range = parseRangeHeader(headers?.range)
|
|
317
|
+
|
|
304
318
|
/**
|
|
305
319
|
* @type {SqliteStoreValue[]}
|
|
306
320
|
*/
|
|
307
|
-
const values = this.#getValuesQuery.all(
|
|
321
|
+
const values = this.#getValuesQuery.all(
|
|
322
|
+
makeValueUrl(key),
|
|
323
|
+
method,
|
|
324
|
+
range?.start ?? 0,
|
|
325
|
+
range?.end ?? Number.MAX_SAFE_INTEGER,
|
|
326
|
+
)
|
|
308
327
|
|
|
309
328
|
if (values.length === 0) {
|
|
310
329
|
return undefined
|
|
@@ -318,6 +337,11 @@ export class SqliteCacheStore {
|
|
|
318
337
|
|
|
319
338
|
let matches = true
|
|
320
339
|
|
|
340
|
+
// TODO (fix): Allow full and partial match?
|
|
341
|
+
if (range && (range.start !== value.start || range.end !== value.end)) {
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
|
|
321
345
|
if (value.vary) {
|
|
322
346
|
const vary = JSON.parse(value.vary)
|
|
323
347
|
|
|
@@ -336,42 +360,6 @@ export class SqliteCacheStore {
|
|
|
336
360
|
|
|
337
361
|
return undefined
|
|
338
362
|
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
|
|
342
|
-
* @param {boolean} [canBeExpired=false]
|
|
343
|
-
* @returns {SqliteStoreValue[]}
|
|
344
|
-
*/
|
|
345
|
-
#findValues(key, canBeExpired = false) {
|
|
346
|
-
const { headers, method } = key
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* @type {SqliteStoreValue[]}
|
|
350
|
-
*/
|
|
351
|
-
const values = this.#getValuesQuery.all(makeValueUrl(key), method)
|
|
352
|
-
const matches = []
|
|
353
|
-
const now = Date.now()
|
|
354
|
-
for (const value of values) {
|
|
355
|
-
if (now >= value.deleteAt && !canBeExpired) {
|
|
356
|
-
continue
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (value.vary) {
|
|
360
|
-
const vary = JSON.parse(value.vary)
|
|
361
|
-
|
|
362
|
-
for (const header in vary) {
|
|
363
|
-
if (!headerValueEquals(headers?.[header], vary[header])) {
|
|
364
|
-
break
|
|
365
|
-
}
|
|
366
|
-
matches.push(value)
|
|
367
|
-
}
|
|
368
|
-
} else {
|
|
369
|
-
matches.push(value)
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return matches
|
|
374
|
-
}
|
|
375
363
|
}
|
|
376
364
|
|
|
377
365
|
/**
|
|
@@ -429,7 +417,7 @@ function makeResult(value) {
|
|
|
429
417
|
/**
|
|
430
418
|
* @param {any} key
|
|
431
419
|
*/
|
|
432
|
-
function assertCacheKey(key) {
|
|
420
|
+
export function assertCacheKey(key) {
|
|
433
421
|
if (typeof key !== 'object') {
|
|
434
422
|
throw new TypeError(`expected key to be object, got ${typeof key}`)
|
|
435
423
|
}
|
|
@@ -448,7 +436,7 @@ function assertCacheKey(key) {
|
|
|
448
436
|
/**
|
|
449
437
|
* @param {any} value
|
|
450
438
|
*/
|
|
451
|
-
function assertCacheValue(value) {
|
|
439
|
+
export function assertCacheValue(value) {
|
|
452
440
|
if (typeof value !== 'object') {
|
|
453
441
|
throw new TypeError(`expected value to be object, got ${typeof value}`)
|
|
454
442
|
}
|