@nxtedition/cache 2.1.2 → 2.1.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
@@ -100,7 +100,6 @@ export class Cache extends EventEmi
100
100
  #location
101
101
  #database = null
102
102
  #getQuery = null
103
- #setQuery = null
104
103
  #delQuery = null
105
104
  #purgeStaleQuery = null
106
105
  #evictQuery = null
@@ -110,6 +109,8 @@ export class Cache extends EventEmi
110
109
  #lockPurgeQuery = null
111
110
  #pageCountQuery = null
112
111
  #pageSizeQuery = null
112
+ #setQuery = null
113
+ #setBatch = []
113
114
 
114
115
  #emitError(err ) {
115
116
  if (this.listenerCount('error') > 0) {
@@ -202,7 +203,8 @@ export class Cache extends EventEmi
202
203
 
203
204
  this.#database.exec(`
204
205
  PRAGMA journal_mode = WAL;
205
- PRAGMA synchronous = NORMAL;
206
+ PRAGMA synchronous = OFF;
207
+ PRAGMA wal_autocheckpoint = 10000;
206
208
  PRAGMA cache_size = -${Math.ceil(maxSize / 1024 / 8)};
207
209
  PRAGMA mmap_size = ${maxSize};
208
210
  PRAGMA max_page_count = ${Math.ceil(maxSize / 4096)};
@@ -375,6 +377,7 @@ export class Cache extends EventEmi
375
377
  this.#memory?.purgeStale(Date.now())
376
378
  this.#purgeStaleQuery?.run(Date.now())
377
379
  this.#lockPurgeQuery?.run(Date.now() - 3_600_000)
380
+ this.#database?.exec('PRAGMA wal_checkpoint(TRUNCATE)')
378
381
  this.#database?.exec('PRAGMA optimize')
379
382
  } catch (err) {
380
383
  this.#emitError(err )
@@ -413,12 +416,6 @@ export class Cache extends EventEmi
413
416
  if (now >= cached.stale) {
414
417
  // stale-while-revalidate has expired, purge cached value.
415
418
  this.#memory?.delete(key)
416
- try {
417
- this.#delQuery?.run(key)
418
- } catch {
419
- // Do nothing...
420
- }
421
-
422
419
  cached = undefined
423
420
  }
424
421
  }
@@ -428,6 +425,13 @@ export class Cache extends EventEmi
428
425
  return { value: cached?.value, async: false }
429
426
  }
430
427
 
428
+ {
429
+ const pending = this.#dedupe.get(key)
430
+ if (pending !== undefined) {
431
+ return { async: true, value: pending }
432
+ }
433
+ }
434
+
431
435
  let result
432
436
  try {
433
437
  result = this.#refresh(args, key)
@@ -451,11 +455,6 @@ export class Cache extends EventEmi
451
455
  throw new TypeError('keySelector must return a non-empty string')
452
456
  }
453
457
 
454
- const existing = this.#dedupe.get(key)
455
- if (existing !== undefined) {
456
- return { async: true, value: existing }
457
- }
458
-
459
458
  if (this.#lockTimeout >= 0) {
460
459
  // Another process holds the lock — wait for the value instead of calling valueSelector.
461
460
  // Use deferred pattern so #waitForValue can check promise identity to detect delete+get races.
@@ -521,7 +520,9 @@ export class Cache extends EventEmi
521
520
  this.#dedupe.delete(key)
522
521
 
523
522
  if (value === undefined) {
523
+ // Slow path... Should not be common...
524
524
  this.#memory?.delete(key)
525
+ this.#setBatch = this.#setBatch.filter((item) => item.key !== key)
525
526
  try {
526
527
  this.#delQuery?.run(key)
527
528
  } catch (err) {
@@ -549,7 +550,6 @@ export class Cache extends EventEmi
549
550
  }
550
551
 
551
552
  const data = this.#serializer.serialize(value)
552
-
553
553
  const entry = this.#createEntry(
554
554
  key,
555
555
  value,
@@ -559,19 +559,74 @@ export class Cache extends EventEmi
559
559
  )
560
560
  this.#memory?.set(key, entry)
561
561
 
562
+ if (this.#setBatch.length === 0) {
563
+ this.#memory?.cork()
564
+ setImmediate(this.#flush)
565
+ }
566
+
567
+ this.#setBatch.push({ key, data, ttl, stale })
568
+ }
569
+
570
+ #flush = () => {
571
+ if (this.#setBatch.length === 0 || this.#closed || this.#database == null) {
572
+ this.#setBatch.length = 0
573
+ this.#memory?.uncork()
574
+ return
575
+ }
576
+
562
577
  try {
563
- this.#setQuery?.run(key, data , ttl, stale)
564
- } catch (err) {
565
- if ((err )?.errcode === 13 /* SQLITE_FULL */) {
578
+ const startTime = performance.now()
579
+ for (let retryCount = 0; true; retryCount++) {
580
+ let n = 0
566
581
  try {
567
- this.#evictQuery?.run(256)
568
- this.#setQuery?.run(key, data , ttl, stale)
569
- } catch (evictErr) {
570
- this.#emitError(evictErr )
582
+ this.#database.exec('BEGIN')
583
+ while (n < this.#setBatch.length) {
584
+ const { key, data, ttl, stale } = this.#setBatch[n++]
585
+ if (data != null) {
586
+ this.#setQuery?.run(key, data, ttl, stale)
587
+ } else {
588
+ this.#delQuery?.run(key)
589
+ }
590
+ if ((n & 0xf) === 0 && performance.now() - startTime > 10) {
591
+ break
592
+ }
593
+ }
594
+ this.#database.exec('COMMIT')
595
+ this.#setBatch.splice(0, n)
596
+ break
597
+ } catch (err) {
598
+ // ROLLBACK is required: a failed statement leaves the connection with
599
+ // an open transaction; without it the next BEGIN would throw.
600
+ // On SQLITE_FULL, SQLite automatically rolls back the transaction, so
601
+ // the explicit ROLLBACK may fail with "no transaction is active" — ignore it.
602
+ try {
603
+ this.#database.exec('ROLLBACK')
604
+ } catch {
605
+ // already rolled back automatically
606
+ // TODO (fix): Check that the error is what we expect (something like "no transaction is active")...
607
+ }
608
+
609
+ if (
610
+ (err )?.errcode === 13 /* SQLITE_FULL */ &&
611
+ retryCount < 3 &&
612
+ this.#evictQuery != null
613
+ ) {
614
+ this.#evictQuery.run(256)
615
+ } else {
616
+ this.#setBatch.splice(0, n)
617
+ throw err
618
+ }
571
619
  }
572
- } else {
573
- this.#emitError(err )
574
620
  }
621
+ } catch (err) {
622
+ this.#emitError(err )
623
+ }
624
+
625
+ if (this.#setBatch.length > 0) {
626
+ // If we weren't able to flush the entire batch within the time limit, schedule another flush.
627
+ setImmediate(this.#flush)
628
+ } else {
629
+ this.#memory?.uncork()
575
630
  }
576
631
  }
577
632
 
package/lib/memory.d.ts CHANGED
@@ -15,6 +15,8 @@ export declare class MemoryCache<V> {
15
15
  #private;
16
16
  constructor(opts?: MemoryOptions);
17
17
  set(key: string, entry: MemoryCacheEntry<V>): void;
18
+ cork(): void;
19
+ uncork(): void;
18
20
  get(key: string): MemoryCacheEntry<V> | undefined;
19
21
  delete(key: string): void;
20
22
  purgeStale(now: number): void;
package/lib/memory.js CHANGED
@@ -26,6 +26,7 @@ export class MemoryCache {
26
26
  #count = 0
27
27
 
28
28
  #counter = 0
29
+ #cork = 0
29
30
 
30
31
  constructor(opts ) {
31
32
  if (opts?.maxSize != null && (!Number.isInteger(opts.maxSize) || opts.maxSize < 1)) {
@@ -59,6 +60,19 @@ export class MemoryCache {
59
60
  this.#prune()
60
61
  }
61
62
 
63
+ cork() {
64
+ this.#cork += 1
65
+ }
66
+
67
+ uncork() {
68
+ if (this.#cork > 0) {
69
+ this.#cork -= 1
70
+ if (this.#cork === 0) {
71
+ this.#prune()
72
+ }
73
+ }
74
+ }
75
+
62
76
  get(key ) {
63
77
  const entry = this.#map.get(key)
64
78
  if (entry != null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/cache",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -30,5 +30,5 @@
30
30
  "tsd": "^0.33.0",
31
31
  "typescript": "^5.9.3"
32
32
  },
33
- "gitHead": "f0aff304c4a0a7246ea0cb34f8d8b3839dc11e81"
33
+ "gitHead": "f7c04358e008a7b0f28e2ca76a81ebdc07fbd734"
34
34
  }