@nxtedition/nxt-undici 7.3.10 → 7.3.12

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.
@@ -63,6 +63,9 @@ export class SqliteCacheStore {
63
63
  */
64
64
  #evictQuery
65
65
 
66
+ #insertBatch = []
67
+ #closed = false
68
+
66
69
  /**
67
70
  * @param {import('undici-types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts & { maxSize?: number } | undefined} opts
68
71
  */
@@ -170,6 +173,10 @@ export class SqliteCacheStore {
170
173
 
171
174
  close() {
172
175
  stores.delete(this)
176
+ if (this.#insertBatch.length > 0) {
177
+ this.#flush()
178
+ }
179
+ this.#closed = true
173
180
  this.#db.close()
174
181
  }
175
182
 
@@ -180,6 +187,10 @@ export class SqliteCacheStore {
180
187
  get(key) {
181
188
  assertCacheKey(key)
182
189
 
190
+ if (this.#closed) {
191
+ return undefined
192
+ }
193
+
183
194
  const value = this.#findValue(key)
184
195
  return value ? makeResult(value) : undefined
185
196
  }
@@ -220,39 +231,117 @@ export class SqliteCacheStore {
220
231
  )
221
232
  }
222
233
 
234
+ if (this.#closed) {
235
+ return
236
+ }
237
+
238
+ if (this.#insertBatch.length === 0) {
239
+ setImmediate(this.#flush)
240
+ }
241
+
242
+ this.#insertBatch.push({
243
+ url: makeValueUrl(key),
244
+ method: key.method,
245
+ body,
246
+ start: value.start,
247
+ end: value.end,
248
+ deleteAt: value.deleteAt,
249
+ statusCode: value.statusCode,
250
+ statusMessage: value.statusMessage,
251
+ headers: value.headers ? JSON.stringify(value.headers) : null,
252
+ etag: value.etag != null ? value.etag : null,
253
+ cacheControlDirectives: value.cacheControlDirectives
254
+ ? JSON.stringify(value.cacheControlDirectives)
255
+ : null,
256
+ vary: value.vary ? JSON.stringify(value.vary) : null,
257
+ cachedAt: value.cachedAt,
258
+ staleAt: value.staleAt,
259
+ })
260
+ }
261
+
262
+ #flush = () => {
263
+ if (this.#insertBatch.length === 0) return
264
+ if (this.#closed) {
265
+ this.#insertBatch.length = 0
266
+ return
267
+ }
223
268
  try {
224
- this.#insert(key, value, body)
225
- } catch (err) {
226
- if (err?.errcode === 13 /* SQLITE_FULL */) {
269
+ const startTime = performance.now()
270
+ for (let retryCount = 0; true; retryCount++) {
271
+ let n = 0
227
272
  try {
228
- this.#evictQuery.run(256)
229
- this.#insert(key, value, body)
230
- } catch (evictErr) {
231
- process.emitWarning(evictErr)
273
+ this.#db.exec('BEGIN')
274
+ while (n < this.#insertBatch.length) {
275
+ const {
276
+ url,
277
+ method,
278
+ body,
279
+ start,
280
+ end,
281
+ deleteAt,
282
+ statusCode,
283
+ statusMessage,
284
+ headers,
285
+ etag,
286
+ cacheControlDirectives,
287
+ vary,
288
+ cachedAt,
289
+ staleAt,
290
+ } = this.#insertBatch[n++]
291
+ this.#insertValueQuery.run(
292
+ url,
293
+ method,
294
+ body,
295
+ start,
296
+ end,
297
+ deleteAt,
298
+ statusCode,
299
+ statusMessage,
300
+ headers,
301
+ etag,
302
+ cacheControlDirectives,
303
+ vary,
304
+ cachedAt,
305
+ staleAt,
306
+ )
307
+ if ((n & 0xf) === 0 && performance.now() - startTime > 10) {
308
+ break
309
+ }
310
+ }
311
+ this.#db.exec('COMMIT')
312
+ this.#insertBatch.splice(0, n)
313
+ break
314
+ } catch (err) {
315
+ // ROLLBACK is required: a failed statement leaves the connection with
316
+ // an open transaction; without it the next BEGIN would throw.
317
+ // On SQLITE_FULL, SQLite automatically rolls back the transaction, so
318
+ // the explicit ROLLBACK may fail with "no transaction is active" — ignore it.
319
+ try {
320
+ this.#db.exec('ROLLBACK')
321
+ } catch {
322
+ // already rolled back automatically
323
+ // TODO (fix): Check that the error is what we expect (something like "no transaction is active")...
324
+ }
325
+
326
+ if (err?.errcode === 13 /* SQLITE_FULL */ && retryCount < 3) {
327
+ this.#evictQuery.run(256)
328
+ } else {
329
+ // If BEGIN failed (n=0) clear the whole batch to avoid an infinite
330
+ // retry loop. If an INSERT/COMMIT failed, only clear the entries
331
+ // that were part of the attempted transaction.
332
+ this.#insertBatch.splice(0, n || this.#insertBatch.length)
333
+ throw err
334
+ }
232
335
  }
233
- } else {
234
- process.emitWarning(err)
235
336
  }
337
+ } catch (err) {
338
+ process.emitWarning(err)
236
339
  }
237
- }
238
340
 
239
- #insert(key, value, body) {
240
- this.#insertValueQuery.run(
241
- makeValueUrl(key),
242
- key.method,
243
- body,
244
- value.start,
245
- value.end,
246
- value.deleteAt,
247
- value.statusCode,
248
- value.statusMessage,
249
- value.headers ? JSON.stringify(value.headers) : null,
250
- value.etag != null ? value.etag : null,
251
- value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
252
- value.vary ? JSON.stringify(value.vary) : null,
253
- value.cachedAt,
254
- value.staleAt,
255
- )
341
+ if (this.#insertBatch.length > 0) {
342
+ // If we weren't able to flush the entire batch within the time limit, schedule another flush.
343
+ setImmediate(this.#flush)
344
+ }
256
345
  }
257
346
 
258
347
  /**
@@ -272,20 +361,32 @@ export class SqliteCacheStore {
272
361
  return undefined
273
362
  }
274
363
 
364
+ const url = makeValueUrl(key)
365
+ const now = getFastNow()
366
+ const requestedStart = range?.start ?? 0
367
+
275
368
  /**
276
369
  * @type {SqliteStoreValue[]}
277
370
  */
278
- const values = this.#getValuesQuery.all(
279
- makeValueUrl(key),
280
- method,
281
- range?.start ?? 0,
282
- getFastNow(),
283
- )
371
+ const values = this.#getValuesQuery.all(url, method, requestedStart, now)
372
+
373
+ for (const entry of this.#insertBatch) {
374
+ if (
375
+ entry.url === url &&
376
+ entry.method === method &&
377
+ entry.start <= requestedStart &&
378
+ entry.deleteAt > now
379
+ ) {
380
+ values.push(entry)
381
+ }
382
+ }
284
383
 
285
384
  if (values.length === 0) {
286
385
  return undefined
287
386
  }
288
387
 
388
+ values.sort((a, b) => b.cachedAt - a.cachedAt)
389
+
289
390
  for (const value of values) {
290
391
  // TODO (fix): Allow full and partial match?
291
392
  if (range && (range.start !== value.start || range.end !== value.end)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/nxt-undici",
3
- "version": "7.3.10",
3
+ "version": "7.3.12",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "main": "lib/index.js",