@nxtedition/nxt-undici 6.0.3 → 6.0.4

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 CHANGED
@@ -1,7 +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 './cache/sqlite-cache-store.js'
4
+ import { cacheStores } from 'undici'
5
5
 
6
6
  const dispatcherCache = new WeakMap()
7
7
 
@@ -20,7 +20,7 @@ export const interceptors = {
20
20
  }
21
21
 
22
22
  export const cache = {
23
- SqliteCacheStore,
23
+ SqliteCacheStore: cacheStores.SqliteCacheStore,
24
24
  }
25
25
 
26
26
  export { parseHeaders } from './utils.js'
@@ -1,7 +1,7 @@
1
- import { SqliteCacheStore } from '../cache/sqlite-cache-store.js'
1
+ import { cacheStores } from 'undici'
2
2
  import { DecoratorHandler, parseCacheControl } from '../utils.js'
3
3
 
4
- const DEFAULT_STORE = new SqliteCacheStore({ location: ':memory:' })
4
+ const DEFAULT_STORE = new cacheStores.SqliteCacheStore({ location: ':memory:' })
5
5
  const MAX_ENTRY_SIZE = 128 * 1024
6
6
 
7
7
  class CacheHandler extends DecoratorHandler {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/nxt-undici",
3
- "version": "6.0.3",
3
+ "version": "6.0.4",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "main": "lib/index.js",
@@ -11,7 +11,8 @@
11
11
  "dependencies": {
12
12
  "@nxtedition/undici": "^10.0.2",
13
13
  "cache-control-parser": "^2.0.6",
14
- "http-errors": "^2.0.0"
14
+ "http-errors": "^2.0.0",
15
+ "undici": "^7.3.0"
15
16
  },
16
17
  "devDependencies": {
17
18
  "@types/node": "^22.10.7",
@@ -1,445 +0,0 @@
1
- 'use strict'
2
-
3
- import { assertCacheKey, assertCacheValue } from './util.js'
4
-
5
- let DatabaseSync
6
- try {
7
- DatabaseSync = await import('node:sqlite').then((x) => x.DatabaseSync)
8
- } catch {
9
- // Do nothing...
10
- }
11
-
12
- const VERSION = 3
13
-
14
- // 2gb
15
- const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000
16
-
17
- /**
18
- * @typedef {import('undici-types/cache-interceptor.d.ts').default.CacheStore} CacheStore
19
- * @implements {CacheStore}
20
- *
21
- * @typedef {{
22
- * id: Readonly<number>
23
- * body?: Buffer
24
- * statusCode: number
25
- * statusMessage: string
26
- * headers?: string
27
- * vary?: string
28
- * etag?: string
29
- * cacheControlDirectives?: string
30
- * cachedAt: number
31
- * staleAt: number
32
- * deleteAt: number
33
- * }} SqliteStoreValue
34
- */
35
-
36
- export class SqliteCacheStore {
37
- #maxEntrySize = MAX_ENTRY_SIZE
38
- #maxCount = Infinity
39
-
40
- /**
41
- * @type {import('node:sqlite').DatabaseSync}
42
- */
43
- #db
44
-
45
- /**
46
- * @type {import('node:sqlite').StatementSync}
47
- */
48
- #getValuesQuery
49
-
50
- /**
51
- * @type {import('node:sqlite').StatementSync}
52
- */
53
- #updateValueQuery
54
-
55
- /**
56
- * @type {import('node:sqlite').StatementSync}
57
- */
58
- #insertValueQuery
59
-
60
- /**
61
- * @type {import('node:sqlite').StatementSync}
62
- */
63
- #deleteExpiredValuesQuery
64
-
65
- /**
66
- * @type {import('node:sqlite').StatementSync}
67
- */
68
- #deleteByUrlQuery
69
-
70
- /**
71
- * @type {import('node:sqlite').StatementSync}
72
- */
73
- #countEntriesQuery
74
-
75
- /**
76
- * @type {import('node:sqlite').StatementSync}
77
- */
78
- #deleteOldValuesQuery
79
-
80
- /**
81
- * @param {import('undici-types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts | undefined} opts
82
- */
83
- constructor(opts) {
84
- if (opts) {
85
- if (typeof opts !== 'object') {
86
- throw new TypeError('SqliteCacheStore options must be an object')
87
- }
88
-
89
- if (opts.maxEntrySize !== undefined) {
90
- if (
91
- typeof opts.maxEntrySize !== 'number' ||
92
- !Number.isInteger(opts.maxEntrySize) ||
93
- opts.maxEntrySize < 0
94
- ) {
95
- throw new TypeError(
96
- 'SqliteCacheStore options.maxEntrySize must be a non-negative integer',
97
- )
98
- }
99
-
100
- if (opts.maxEntrySize > MAX_ENTRY_SIZE) {
101
- throw new TypeError('SqliteCacheStore options.maxEntrySize must be less than 2gb')
102
- }
103
-
104
- this.#maxEntrySize = opts.maxEntrySize
105
- }
106
-
107
- if (opts.maxCount !== undefined) {
108
- if (
109
- typeof opts.maxCount !== 'number' ||
110
- !Number.isInteger(opts.maxCount) ||
111
- opts.maxCount < 0
112
- ) {
113
- throw new TypeError('SqliteCacheStore options.maxCount must be a non-negative integer')
114
- }
115
- this.#maxCount = opts.maxCount
116
- }
117
- }
118
-
119
- this.#db = new DatabaseSync(opts?.location ?? ':memory:')
120
-
121
- this.#db.exec(`
122
- CREATE TABLE IF NOT EXISTS cacheInterceptorV${VERSION} (
123
- -- Data specific to us
124
- id INTEGER PRIMARY KEY AUTOINCREMENT,
125
- url TEXT NOT NULL,
126
- method TEXT NOT NULL,
127
-
128
- -- Data returned to the interceptor
129
- body BUF NULL,
130
- deleteAt INTEGER NOT NULL,
131
- statusCode INTEGER NOT NULL,
132
- statusMessage TEXT NOT NULL,
133
- headers TEXT NULL,
134
- cacheControlDirectives TEXT NULL,
135
- etag TEXT NULL,
136
- vary TEXT NULL,
137
- cachedAt INTEGER NOT NULL,
138
- staleAt INTEGER NOT NULL
139
- );
140
-
141
- CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_url ON cacheInterceptorV${VERSION}(url);
142
- CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_method ON cacheInterceptorV${VERSION}(method);
143
- CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_deleteAt ON cacheInterceptorV${VERSION}(deleteAt);
144
- `)
145
-
146
- this.#getValuesQuery = this.#db.prepare(`
147
- SELECT
148
- id,
149
- body,
150
- deleteAt,
151
- statusCode,
152
- statusMessage,
153
- headers,
154
- etag,
155
- cacheControlDirectives,
156
- vary,
157
- cachedAt,
158
- staleAt
159
- FROM cacheInterceptorV${VERSION}
160
- WHERE
161
- url = ?
162
- AND method = ?
163
- ORDER BY
164
- deleteAt ASC
165
- `)
166
-
167
- this.#updateValueQuery = this.#db.prepare(`
168
- UPDATE cacheInterceptorV${VERSION} SET
169
- body = ?,
170
- deleteAt = ?,
171
- statusCode = ?,
172
- statusMessage = ?,
173
- headers = ?,
174
- etag = ?,
175
- cacheControlDirectives = ?,
176
- cachedAt = ?,
177
- staleAt = ?,
178
- deleteAt = ?
179
- WHERE
180
- id = ?
181
- `)
182
-
183
- this.#insertValueQuery = this.#db.prepare(`
184
- INSERT INTO cacheInterceptorV${VERSION} (
185
- url,
186
- method,
187
- body,
188
- deleteAt,
189
- statusCode,
190
- statusMessage,
191
- headers,
192
- etag,
193
- cacheControlDirectives,
194
- vary,
195
- cachedAt,
196
- staleAt,
197
- deleteAt
198
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
199
- `)
200
-
201
- this.#deleteByUrlQuery = this.#db.prepare(
202
- `DELETE FROM cacheInterceptorV${VERSION} WHERE url = ?`,
203
- )
204
-
205
- this.#countEntriesQuery = this.#db.prepare(
206
- `SELECT COUNT(*) AS total FROM cacheInterceptorV${VERSION}`,
207
- )
208
-
209
- this.#deleteExpiredValuesQuery = this.#db.prepare(
210
- `DELETE FROM cacheInterceptorV${VERSION} WHERE deleteAt <= ?`,
211
- )
212
-
213
- this.#deleteOldValuesQuery =
214
- this.#maxCount === Infinity
215
- ? null
216
- : this.#db.prepare(`
217
- DELETE FROM cacheInterceptorV${VERSION}
218
- WHERE id IN (
219
- SELECT
220
- id
221
- FROM cacheInterceptorV${VERSION}
222
- ORDER BY cachedAt DESC
223
- LIMIT ?
224
- )
225
- `)
226
- }
227
-
228
- close() {
229
- this.#db.close()
230
- }
231
-
232
- /**
233
- * @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
234
- * @returns {import('undici-types/cache-interceptor.d.ts').default.GetResult | undefined}
235
- */
236
- get(key) {
237
- assertCacheKey(key)
238
-
239
- const value = this.#findValue(key)
240
-
241
- if (!value) {
242
- return undefined
243
- }
244
-
245
- /**
246
- * @type {import('undici-types/cache-interceptor.d.ts').default.GetResult}
247
- */
248
- const result = {
249
- body: value.body ? Buffer.from(value.body) : null,
250
- statusCode: value.statusCode,
251
- statusMessage: value.statusMessage,
252
- headers: value.headers ? JSON.parse(value.headers) : undefined,
253
- etag: value.etag ? value.etag : undefined,
254
- vary: value.vary ? JSON.parse(value.vary) : undefined,
255
- cacheControlDirectives: value.cacheControlDirectives
256
- ? JSON.parse(value.cacheControlDirectives)
257
- : undefined,
258
- cachedAt: value.cachedAt,
259
- staleAt: value.staleAt,
260
- deleteAt: value.deleteAt,
261
- }
262
-
263
- return result
264
- }
265
-
266
- /**
267
- * @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
268
- * @param {import('undici-types/cache-interceptor.d.ts').default.CacheValue & { body: Buffer | Array<Buffer> | null }} value
269
- */
270
- set(key, value) {
271
- assertCacheKey(key)
272
- assertCacheValue(value)
273
-
274
- const body = Array.isArray(value.body) ? Buffer.concat(value.body) : value.body
275
- const size = body ? body.byteLength : 0
276
-
277
- if (size > this.#maxEntrySize) {
278
- return false
279
- }
280
-
281
- const url = this.#makeValueUrl(key)
282
-
283
- const existingValue = this.#findValue(key, true)
284
- if (existingValue) {
285
- // Updating an existing response, let's overwrite it
286
- this.#updateValueQuery.run(
287
- body,
288
- value.deleteAt,
289
- value.statusCode,
290
- value.statusMessage,
291
- value.headers ? JSON.stringify(value.headers) : null,
292
- value.etag ? value.etag : null,
293
- value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
294
- value.cachedAt,
295
- value.staleAt,
296
- value.deleteAt,
297
- existingValue.id,
298
- )
299
- } else {
300
- this.#prune()
301
- // New response, let's insert it
302
- this.#insertValueQuery.run(
303
- url,
304
- key.method,
305
- body,
306
- value.deleteAt,
307
- value.statusCode,
308
- value.statusMessage,
309
- value.headers ? JSON.stringify(value.headers) : null,
310
- value.etag ? value.etag : null,
311
- value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
312
- value.vary ? JSON.stringify(value.vary) : null,
313
- value.cachedAt,
314
- value.staleAt,
315
- value.deleteAt,
316
- )
317
- }
318
-
319
- return true
320
- }
321
-
322
- /**
323
- * @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
324
- */
325
- delete(key) {
326
- if (typeof key !== 'object') {
327
- throw new TypeError(`expected key to be object, got ${typeof key}`)
328
- }
329
-
330
- this.#deleteByUrlQuery.run(this.#makeValueUrl(key))
331
- }
332
-
333
- #prune() {
334
- if (this.size <= this.#maxCount) {
335
- return 0
336
- }
337
-
338
- {
339
- const removed = this.#deleteExpiredValuesQuery.run(Date.now()).changes
340
- if (removed > 0) {
341
- return removed
342
- }
343
- }
344
-
345
- {
346
- const removed = this.#deleteOldValuesQuery.run(
347
- Math.max(Math.floor(this.#maxCount * 0.1), 1),
348
- ).changes
349
- if (removed > 0) {
350
- return removed
351
- }
352
- }
353
-
354
- return 0
355
- }
356
-
357
- /**
358
- * Counts the number of rows in the cache
359
- * @returns {Number}
360
- */
361
- get size() {
362
- const { total } = this.#countEntriesQuery.get()
363
- return total
364
- }
365
-
366
- /**
367
- * @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
368
- * @returns {string}
369
- */
370
- #makeValueUrl(key) {
371
- return `${key.origin}/${key.path}`
372
- }
373
-
374
- /**
375
- * @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
376
- * @param {boolean} [canBeExpired=false]
377
- * @returns {SqliteStoreValue | undefined}
378
- */
379
- #findValue(key, canBeExpired = false) {
380
- const url = this.#makeValueUrl(key)
381
- const { headers, method } = key
382
-
383
- /**
384
- * @type {SqliteStoreValue[]}
385
- */
386
- const values = this.#getValuesQuery.all(url, method)
387
-
388
- if (values.length === 0) {
389
- return undefined
390
- }
391
-
392
- const now = Date.now()
393
- for (const value of values) {
394
- if (now >= value.deleteAt && !canBeExpired) {
395
- return undefined
396
- }
397
-
398
- let matches = true
399
-
400
- if (value.vary) {
401
- if (!headers) {
402
- return undefined
403
- }
404
-
405
- const vary = JSON.parse(value.vary)
406
-
407
- for (const header in vary) {
408
- if (!headerValueEquals(headers[header], vary[header])) {
409
- matches = false
410
- break
411
- }
412
- }
413
- }
414
-
415
- if (matches) {
416
- return value
417
- }
418
- }
419
-
420
- return undefined
421
- }
422
- }
423
-
424
- /**
425
- * @param {string|string[]|null|undefined} lhs
426
- * @param {string|string[]|null|undefined} rhs
427
- * @returns {boolean}
428
- */
429
- function headerValueEquals(lhs, rhs) {
430
- if (Array.isArray(lhs) && Array.isArray(rhs)) {
431
- if (lhs.length !== rhs.length) {
432
- return false
433
- }
434
-
435
- for (let i = 0; i < lhs.length; i++) {
436
- if (rhs.includes(lhs[i])) {
437
- return false
438
- }
439
- }
440
-
441
- return true
442
- }
443
-
444
- return lhs === rhs
445
- }
package/lib/cache/util.js DELETED
@@ -1,51 +0,0 @@
1
- /**
2
- * @param {any} key
3
- */
4
- export function assertCacheKey(key) {
5
- if (typeof key !== 'object') {
6
- throw new TypeError(`expected key to be object, got ${typeof key}`)
7
- }
8
-
9
- for (const property of ['origin', 'method', 'path']) {
10
- if (typeof key[property] !== 'string') {
11
- throw new TypeError(`expected key.${property} to be string, got ${typeof key[property]}`)
12
- }
13
- }
14
-
15
- if (key.headers !== undefined && typeof key.headers !== 'object') {
16
- throw new TypeError(`expected headers to be object, got ${typeof key}`)
17
- }
18
- }
19
-
20
- /**
21
- * @param {any} value
22
- */
23
- export function assertCacheValue(value) {
24
- if (typeof value !== 'object') {
25
- throw new TypeError(`expected value to be object, got ${typeof value}`)
26
- }
27
-
28
- for (const property of ['statusCode', 'cachedAt', 'staleAt', 'deleteAt']) {
29
- if (typeof value[property] !== 'number') {
30
- throw new TypeError(`expected value.${property} to be number, got ${typeof value[property]}`)
31
- }
32
- }
33
-
34
- if (typeof value.statusMessage !== 'string') {
35
- throw new TypeError(
36
- `expected value.statusMessage to be string, got ${typeof value.statusMessage}`,
37
- )
38
- }
39
-
40
- if (value.headers != null && typeof value.headers !== 'object') {
41
- throw new TypeError(`expected value.rawHeaders to be object, got ${typeof value.headers}`)
42
- }
43
-
44
- if (value.vary !== undefined && typeof value.vary !== 'object') {
45
- throw new TypeError(`expected value.vary to be object, got ${typeof value.vary}`)
46
- }
47
-
48
- if (value.etag !== undefined && typeof value.etag !== 'string') {
49
- throw new TypeError(`expected value.etag to be string, got ${typeof value.etag}`)
50
- }
51
- }