@nxtedition/nxt-undici 7.3.7 → 7.3.9

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.
@@ -4,7 +4,7 @@ import { SqliteCacheStore } from '../sqlite-cache-store.js'
4
4
 
5
5
  const DEFAULT_STORE = new SqliteCacheStore({ location: ':memory:' })
6
6
  const DEFAULT_MAX_ENTRY_SIZE = 128 * 1024
7
- const DEFAULT_MAX_ENTRY_TTL = 24 * 3600
7
+ const DEFAULT_MAX_ENTRY_TTL = 30 * 24 * 3600
8
8
  const NOOP = () => {}
9
9
 
10
10
  class CacheHandler extends DecoratorHandler {
@@ -1,8 +1,8 @@
1
1
  import { DatabaseSync } from 'node:sqlite'
2
- import assert from 'node:assert'
3
2
  import { parseRangeHeader, getFastNow } from './utils.js'
4
3
 
5
- const VERSION = 7
4
+ // Bump version when the URL key format or schema changes to invalidate old caches.
5
+ const VERSION = 9
6
6
 
7
7
  /** @typedef {{ purgeStale: () => void } } */
8
8
  const stores = new Set()
@@ -12,11 +12,7 @@ const stores = new Set()
12
12
  offPeakBC.unref()
13
13
  offPeakBC.onmessage = () => {
14
14
  for (const store of stores) {
15
- try {
16
- store.purgeStale()
17
- } catch (err) {
18
- process.emitWarning(err)
19
- }
15
+ store.purgeStale()
20
16
  }
21
17
  }
22
18
  }
@@ -63,15 +59,15 @@ export class SqliteCacheStore {
63
59
  #deleteExpiredValuesQuery
64
60
 
65
61
  /**
66
- * @type {number}
62
+ * @type {import('node:sqlite').StatementSync}
67
63
  */
68
- #deleteExpiredValuesTime = getFastNow()
64
+ #evictQuery
69
65
 
70
66
  /**
71
67
  * @param {import('undici-types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts & { maxSize?: number } | undefined} opts
72
68
  */
73
69
  constructor(opts) {
74
- this.#db = new DatabaseSync(opts?.location ?? ':memory:', { timeout: 40, ...opts?.db })
70
+ this.#db = new DatabaseSync(opts?.location ?? ':memory:', { timeout: 20, ...opts?.db })
75
71
 
76
72
  const maxSize = opts?.maxSize ?? 256 * 1024 * 1024
77
73
  this.#db.exec(`
@@ -87,7 +83,7 @@ export class SqliteCacheStore {
87
83
  id INTEGER PRIMARY KEY AUTOINCREMENT,
88
84
  url TEXT NOT NULL,
89
85
  method TEXT NOT NULL,
90
- body BUF NULL,
86
+ body BLOB NULL,
91
87
  start INTEGER NOT NULL,
92
88
  end INTEGER NOT NULL,
93
89
  deleteAt INTEGER NOT NULL,
@@ -125,6 +121,7 @@ export class SqliteCacheStore {
125
121
  url = ?
126
122
  AND method = ?
127
123
  AND start <= ?
124
+ AND deleteAt > ?
128
125
  ORDER BY
129
126
  deleteAt ASC
130
127
  `)
@@ -152,13 +149,23 @@ export class SqliteCacheStore {
152
149
  `DELETE FROM cacheInterceptorV${VERSION} WHERE deleteAt <= ?`,
153
150
  )
154
151
 
152
+ // Evict the N entries expiring soonest. Used on SQLITE_FULL to free space
153
+ // without requiring expired entries to already exist.
154
+ this.#evictQuery = this.#db.prepare(
155
+ `DELETE FROM cacheInterceptorV${VERSION} WHERE id IN (SELECT id FROM cacheInterceptorV${VERSION} ORDER BY deleteAt ASC LIMIT ?)`,
156
+ )
157
+
155
158
  stores.add(this)
156
159
  }
157
160
 
158
161
  purgeStale() {
159
- this.#prune()
160
- this.#db.exec('PRAGMA wal_checkpoint(TRUNCATE)')
161
- this.#db.exec('PRAGMA optimize')
162
+ try {
163
+ this.#deleteExpiredValuesQuery.run(getFastNow())
164
+ this.#db.exec('PRAGMA wal_checkpoint(TRUNCATE)')
165
+ this.#db.exec('PRAGMA optimize')
166
+ } catch (err) {
167
+ process.emitWarning(err)
168
+ }
162
169
  }
163
170
 
164
171
  close() {
@@ -187,18 +194,44 @@ export class SqliteCacheStore {
187
194
 
188
195
  const body = Array.isArray(value.body) ? Buffer.concat(value.body) : value.body
189
196
 
190
- assert(Number.isFinite(value.start))
191
- assert(Number.isFinite(value.end))
192
- assert(!body || body?.byteLength === value.end - value.start)
197
+ if (typeof value.start !== 'number') {
198
+ throw new TypeError(
199
+ `expected value.start to be a number, got ${printType(value.start)} [${value.start}]`,
200
+ )
201
+ }
202
+ if (!Number.isFinite(value.start) || value.start < 0) {
203
+ throw new RangeError(
204
+ `expected value.start to be a non-negative finite number, got ${value.start}`,
205
+ )
206
+ }
207
+ if (typeof value.end !== 'number') {
208
+ throw new TypeError(
209
+ `expected value.end to be a number, got ${printType(value.end)} [${value.end}]`,
210
+ )
211
+ }
212
+ if (!Number.isFinite(value.end) || value.end < value.start) {
213
+ throw new RangeError(
214
+ `expected value.end to be a finite number >= start (${value.start}), got ${value.end}`,
215
+ )
216
+ }
217
+ if (body && body.byteLength !== value.end - value.start) {
218
+ throw new RangeError(
219
+ `body length ${body.byteLength} does not match end - start (${value.end} - ${value.start} = ${value.end - value.start})`,
220
+ )
221
+ }
193
222
 
194
223
  try {
195
224
  this.#insert(key, value, body)
196
225
  } catch (err) {
197
226
  if (err?.errcode === 13 /* SQLITE_FULL */) {
198
- this.#prune()
199
- this.#insert(key, value, body)
227
+ try {
228
+ this.#evictQuery.run(256)
229
+ this.#insert(key, value, body)
230
+ } catch (evictErr) {
231
+ process.emitWarning(evictErr)
232
+ }
200
233
  } else {
201
- throw err
234
+ process.emitWarning(err)
202
235
  }
203
236
  }
204
237
  }
@@ -210,11 +243,11 @@ export class SqliteCacheStore {
210
243
  body,
211
244
  value.start,
212
245
  value.end,
213
- value.deleteAt ?? Date.now() + 3600e3,
246
+ value.deleteAt,
214
247
  value.statusCode,
215
248
  value.statusMessage,
216
249
  value.headers ? JSON.stringify(value.headers) : null,
217
- value.etag ? value.etag : null,
250
+ value.etag != null ? value.etag : null,
218
251
  value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
219
252
  value.vary ? JSON.stringify(value.vary) : null,
220
253
  value.cachedAt,
@@ -222,20 +255,11 @@ export class SqliteCacheStore {
222
255
  )
223
256
  }
224
257
 
225
- #prune() {
226
- const now = getFastNow()
227
- if (now > this.#deleteExpiredValuesTime) {
228
- this.#deleteExpiredValuesQuery.run(now)
229
- this.#deleteExpiredValuesTime = now + 60e3
230
- }
231
- }
232
-
233
258
  /**
234
259
  * @param {import('undici-types/cache-interceptor.d.ts').default.CacheKey} key
235
- * @param {boolean} [canBeExpired=false]
236
260
  * @returns {SqliteStoreValue | undefined}
237
261
  */
238
- #findValue(key, canBeExpired = false) {
262
+ #findValue(key) {
239
263
  const { headers, method } = key
240
264
 
241
265
  if (Array.isArray(headers?.range)) {
@@ -251,20 +275,18 @@ export class SqliteCacheStore {
251
275
  /**
252
276
  * @type {SqliteStoreValue[]}
253
277
  */
254
- const values = this.#getValuesQuery.all(makeValueUrl(key), method, range?.start ?? 0)
278
+ const values = this.#getValuesQuery.all(
279
+ makeValueUrl(key),
280
+ method,
281
+ range?.start ?? 0,
282
+ getFastNow(),
283
+ )
255
284
 
256
285
  if (values.length === 0) {
257
286
  return undefined
258
287
  }
259
288
 
260
- const now = getFastNow()
261
289
  for (const value of values) {
262
- if (now >= value.deleteAt && !canBeExpired) {
263
- return undefined
264
- }
265
-
266
- let matches = true
267
-
268
290
  // TODO (fix): Allow full and partial match?
269
291
  if (range && (range.start !== value.start || range.end !== value.end)) {
270
292
  continue
@@ -272,6 +294,7 @@ export class SqliteCacheStore {
272
294
 
273
295
  if (value.vary) {
274
296
  const vary = JSON.parse(value.vary)
297
+ let matches = true
275
298
 
276
299
  for (const header in vary) {
277
300
  if (!headerValueEquals(headers?.[header], vary[header])) {
@@ -279,11 +302,13 @@ export class SqliteCacheStore {
279
302
  break
280
303
  }
281
304
  }
282
- }
283
305
 
284
- if (matches) {
285
- return value
306
+ if (!matches) {
307
+ continue
308
+ }
286
309
  }
310
+
311
+ return value
287
312
  }
288
313
 
289
314
  return undefined
@@ -320,7 +345,7 @@ function headerValueEquals(lhs, rhs) {
320
345
  * @returns {string}
321
346
  */
322
347
  function makeValueUrl(key) {
323
- return `${key.origin}/${key.path}`
348
+ return `${key.origin}${key.path}`
324
349
  }
325
350
 
326
351
  function makeResult(value) {
@@ -331,7 +356,7 @@ function makeResult(value) {
331
356
  statusCode: value.statusCode,
332
357
  statusMessage: value.statusMessage,
333
358
  headers: value.headers ? JSON.parse(value.headers) : undefined,
334
- etag: value.etag ? value.etag : undefined,
359
+ etag: value.etag != null ? value.etag : undefined,
335
360
  vary: value.vary ? JSON.parse(value.vary) : undefined,
336
361
  cacheControlDirectives: value.cacheControlDirectives
337
362
  ? JSON.parse(value.cacheControlDirectives)
@@ -351,7 +376,7 @@ function printType(val) {
351
376
  */
352
377
  function assertCacheKey(key) {
353
378
  if (typeof key !== 'object' || key == null) {
354
- throw new TypeError(`expected key to be object, got ${typeof printType(key)} [${key}]`)
379
+ throw new TypeError(`expected key to be object, got ${printType(key)} [${key}]`)
355
380
  }
356
381
 
357
382
  for (const property of ['origin', 'method', 'path']) {
@@ -364,7 +389,7 @@ function assertCacheKey(key) {
364
389
 
365
390
  if (key.headers !== undefined && typeof key.headers !== 'object') {
366
391
  throw new TypeError(
367
- `expected headers to be object, got ${typeof printType(key)} [${key.headers}]`,
392
+ `expected headers to be object, got ${printType(key.headers)} [${key.headers}]`,
368
393
  )
369
394
  }
370
395
  }
@@ -374,7 +399,7 @@ function assertCacheKey(key) {
374
399
  */
375
400
  function assertCacheValue(value) {
376
401
  if (typeof value !== 'object' || value == null) {
377
- throw new TypeError(`expected value to be object, got ${typeof printType(value)}`)
402
+ throw new TypeError(`expected value to be object, got ${printType(value)}`)
378
403
  }
379
404
 
380
405
  for (const property of ['statusCode', 'cachedAt', 'staleAt', 'deleteAt']) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/nxt-undici",
3
- "version": "7.3.7",
3
+ "version": "7.3.9",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "main": "lib/index.js",