@nxtedition/nxt-undici 6.2.9 → 6.2.11
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 +33 -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 = 6
|
|
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,7 @@ 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);
|
|
129
136
|
`)
|
|
130
137
|
|
|
131
138
|
this.#getValuesQuery = this.#db.prepare(`
|
|
@@ -145,6 +152,7 @@ export class SqliteCacheStore {
|
|
|
145
152
|
WHERE
|
|
146
153
|
url = ?
|
|
147
154
|
AND method = ?
|
|
155
|
+
AND start <= ?
|
|
148
156
|
ORDER BY
|
|
149
157
|
deleteAt ASC
|
|
150
158
|
`)
|
|
@@ -154,6 +162,8 @@ export class SqliteCacheStore {
|
|
|
154
162
|
url,
|
|
155
163
|
method,
|
|
156
164
|
body,
|
|
165
|
+
start,
|
|
166
|
+
end,
|
|
157
167
|
deleteAt,
|
|
158
168
|
statusCode,
|
|
159
169
|
statusMessage,
|
|
@@ -163,7 +173,7 @@ export class SqliteCacheStore {
|
|
|
163
173
|
vary,
|
|
164
174
|
cachedAt,
|
|
165
175
|
staleAt
|
|
166
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
176
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
167
177
|
`)
|
|
168
178
|
|
|
169
179
|
this.#deleteByUrlQuery = this.#db.prepare(
|
|
@@ -210,17 +220,7 @@ export class SqliteCacheStore {
|
|
|
210
220
|
|
|
211
221
|
/**
|
|
212
222
|
* @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
|
|
223
|
+
* @param {import('undici-types/cache-interceptor.d.ts').default.CacheValue & { body: null | Buffer | Array<Buffer>, start: number, end: number }} value
|
|
224
224
|
*/
|
|
225
225
|
set(key, value) {
|
|
226
226
|
assertCacheKey(key)
|
|
@@ -231,12 +231,18 @@ export class SqliteCacheStore {
|
|
|
231
231
|
return
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
assert(Number.isFinite(value.start))
|
|
235
|
+
assert(Number.isFinite(value.end))
|
|
236
|
+
assert(!body || body?.byteLength === value.end - value.start)
|
|
237
|
+
|
|
234
238
|
this.#prune()
|
|
235
239
|
|
|
236
240
|
this.#insertValueQuery.run(
|
|
237
241
|
makeValueUrl(key),
|
|
238
242
|
key.method,
|
|
239
243
|
body,
|
|
244
|
+
value.start,
|
|
245
|
+
value.end,
|
|
240
246
|
value.deleteAt,
|
|
241
247
|
value.statusCode,
|
|
242
248
|
value.statusMessage,
|
|
@@ -301,10 +307,16 @@ export class SqliteCacheStore {
|
|
|
301
307
|
#findValue(key, canBeExpired = false) {
|
|
302
308
|
const { headers, method } = key
|
|
303
309
|
|
|
310
|
+
if (Array.isArray(headers?.range)) {
|
|
311
|
+
return undefined
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const range = parseRangeHeader(headers?.range)
|
|
315
|
+
|
|
304
316
|
/**
|
|
305
317
|
* @type {SqliteStoreValue[]}
|
|
306
318
|
*/
|
|
307
|
-
const values = this.#getValuesQuery.all(makeValueUrl(key), method)
|
|
319
|
+
const values = this.#getValuesQuery.all(makeValueUrl(key), method, range?.start ?? 0)
|
|
308
320
|
|
|
309
321
|
if (values.length === 0) {
|
|
310
322
|
return undefined
|
|
@@ -318,6 +330,11 @@ export class SqliteCacheStore {
|
|
|
318
330
|
|
|
319
331
|
let matches = true
|
|
320
332
|
|
|
333
|
+
// TODO (fix): Allow full and partial match?
|
|
334
|
+
if (range && (range.start !== value.start || range.end !== value.end)) {
|
|
335
|
+
continue
|
|
336
|
+
}
|
|
337
|
+
|
|
321
338
|
if (value.vary) {
|
|
322
339
|
const vary = JSON.parse(value.vary)
|
|
323
340
|
|
|
@@ -336,42 +353,6 @@ export class SqliteCacheStore {
|
|
|
336
353
|
|
|
337
354
|
return undefined
|
|
338
355
|
}
|
|
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
356
|
}
|
|
376
357
|
|
|
377
358
|
/**
|
|
@@ -429,7 +410,7 @@ function makeResult(value) {
|
|
|
429
410
|
/**
|
|
430
411
|
* @param {any} key
|
|
431
412
|
*/
|
|
432
|
-
function assertCacheKey(key) {
|
|
413
|
+
export function assertCacheKey(key) {
|
|
433
414
|
if (typeof key !== 'object') {
|
|
434
415
|
throw new TypeError(`expected key to be object, got ${typeof key}`)
|
|
435
416
|
}
|
|
@@ -448,7 +429,7 @@ function assertCacheKey(key) {
|
|
|
448
429
|
/**
|
|
449
430
|
* @param {any} value
|
|
450
431
|
*/
|
|
451
|
-
function assertCacheValue(value) {
|
|
432
|
+
export function assertCacheValue(value) {
|
|
452
433
|
if (typeof value !== 'object') {
|
|
453
434
|
throw new TypeError(`expected value to be object, got ${typeof value}`)
|
|
454
435
|
}
|