@nxtedition/nxt-undici 7.3.7 → 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.
- package/lib/sqlite-cache-store.js +73 -48
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
62
|
+
* @type {import('node:sqlite').StatementSync}
|
|
67
63
|
*/
|
|
68
|
-
#
|
|
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:
|
|
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
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
285
|
-
|
|
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}
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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']) {
|