@nxtedition/nxt-undici 7.3.6 → 7.3.8

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.
@@ -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,27 +59,31 @@ 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
 
72
+ const maxSize = opts?.maxSize ?? 256 * 1024 * 1024
76
73
  this.#db.exec(`
77
74
  PRAGMA journal_mode = WAL;
78
- PRAGMA synchronous = NORMAL;
79
- PRAGMA temp_store = memory;
75
+ PRAGMA synchronous = OFF;
76
+ PRAGMA wal_autocheckpoint = 10000;
77
+ PRAGMA cache_size = -${Math.ceil(maxSize / 1024 / 8)};
78
+ PRAGMA mmap_size = ${maxSize};
79
+ PRAGMA max_page_count = ${Math.ceil(maxSize / 4096)};
80
80
  PRAGMA optimize;
81
81
 
82
82
  CREATE TABLE IF NOT EXISTS cacheInterceptorV${VERSION} (
83
83
  id INTEGER PRIMARY KEY AUTOINCREMENT,
84
84
  url TEXT NOT NULL,
85
85
  method TEXT NOT NULL,
86
- body BUF NULL,
86
+ body BLOB NULL,
87
87
  start INTEGER NOT NULL,
88
88
  end INTEGER NOT NULL,
89
89
  deleteAt INTEGER NOT NULL,
@@ -101,12 +101,6 @@ export class SqliteCacheStore {
101
101
  CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_deleteExpiredValuesQuery ON cacheInterceptorV${VERSION}(deleteAt);
102
102
  `)
103
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
-
110
104
  this.#getValuesQuery = this.#db.prepare(`
111
105
  SELECT
112
106
  id,
@@ -127,6 +121,7 @@ export class SqliteCacheStore {
127
121
  url = ?
128
122
  AND method = ?
129
123
  AND start <= ?
124
+ AND deleteAt > ?
130
125
  ORDER BY
131
126
  deleteAt ASC
132
127
  `)
@@ -154,11 +149,23 @@ export class SqliteCacheStore {
154
149
  `DELETE FROM cacheInterceptorV${VERSION} WHERE deleteAt <= ?`,
155
150
  )
156
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
+
157
158
  stores.add(this)
158
159
  }
159
160
 
160
161
  purgeStale() {
161
- this.#prune()
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.6",
3
+ "version": "7.3.8",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "main": "lib/index.js",