@nxtedition/cache 2.0.3 → 2.0.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.d.ts CHANGED
@@ -1,14 +1,20 @@
1
+ import { EventEmitter } from 'node:events';
1
2
  import { type MemoryOptions } from './memory.ts';
2
3
  export type { MemoryOptions } from './memory.ts';
3
4
  export interface DatabaseOptions {
4
5
  timeout?: number;
5
6
  maxSize?: number;
6
7
  }
8
+ export interface Serializer<V> {
9
+ serialize: (value: V) => Buffer | Uint8Array | string;
10
+ deserialize: (data: Buffer | string) => V;
11
+ }
7
12
  export interface CacheOptions<V> {
8
13
  ttl?: number | ((value: V, key: string) => number);
9
14
  stale?: number | ((value: V, key: string) => number);
10
15
  memory?: MemoryOptions | false | null;
11
16
  database?: DatabaseOptions | false | null;
17
+ serializer?: Serializer<V>;
12
18
  }
13
19
  export type CacheResult<V> = {
14
20
  value: V | undefined;
@@ -22,7 +28,7 @@ declare global {
22
28
  stats: Cache['stats'];
23
29
  }[];
24
30
  }
25
- export declare class Cache<V = unknown, A extends unknown[] = unknown[]> {
31
+ export declare class Cache<V = unknown, A extends unknown[] = unknown[]> extends EventEmitter {
26
32
  #private;
27
33
  constructor(location: string, valueSelector: (...args: A) => V | PromiseLike<V>, keySelector: (...args: A) => string, opts?: CacheOptions<V>);
28
34
  get stats(): {
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto'
2
+ import { EventEmitter } from 'node:events'
2
3
  import { DatabaseSync, } from 'node:sqlite'
3
4
  import { setTimeout as delay } from 'node:timers/promises'
4
5
  import { MemoryCache, } from "./memory.js"
@@ -7,25 +8,18 @@ import { MemoryCache, } from "./memory
7
8
 
8
9
  function noop() {}
9
10
 
11
+ function maybeToBuffer(value ) {
12
+ return ArrayBuffer.isView(value)
13
+ ? Buffer.from(value.buffer, value.byteOffset, value.byteLength)
14
+ : value
15
+ }
16
+
10
17
  function isThenable(value ) {
11
18
  return value != null && typeof (value ).then === 'function'
12
19
  }
13
20
 
14
- const dbs = new Set ()
15
-
16
- {
17
- const offPeakBC = new BroadcastChannel('nxt:offPeak')
18
- offPeakBC.unref()
19
- offPeakBC.onmessage = () => {
20
- for (const db of dbs) {
21
- try {
22
- db.purgeStale()
23
- } catch (err) {
24
- process.emitWarning(err )
25
- }
26
- }
27
- }
28
- }
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ const dbs = new Set ()
29
23
 
30
24
 
31
25
 
@@ -44,11 +38,27 @@ const dbs = new Set ()
44
38
 
45
39
 
46
40
 
41
+
42
+
43
+
44
+
45
+
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ const defaultSerializer = {
48
+ serialize(value) {
49
+ return ArrayBuffer.isView(value) ? (value ) : JSON.stringify(value)
50
+ },
51
+ deserialize(data) {
52
+ return ArrayBuffer.isView(data) ? data : JSON.parse(data)
53
+ },
54
+ }
55
+
47
56
 
48
57
 
49
58
 
50
59
 
51
60
 
61
+
52
62
 
53
63
 
54
64
 
@@ -60,12 +70,10 @@ const dbs = new Set ()
60
70
 
61
71
 
62
72
 
63
- const VERSION = 4
73
+ const VERSION = 5
64
74
  const MAX_DURATION = 365000000e3
65
75
 
66
- globalThis.__nxt_cache ??= []
67
-
68
- export class Cache {
76
+ export class Cache extends EventEmitter {
69
77
  #memory
70
78
  #dedupe = new Map ()
71
79
  #closed = false
@@ -75,6 +83,7 @@ export class Cache {
75
83
 
76
84
  #ttl
77
85
  #stale
86
+ #serializer
78
87
 
79
88
  #lockMean = 5
80
89
  #lockVar = 0
@@ -95,12 +104,21 @@ export class Cache {
95
104
  #pageCountQuery = null
96
105
  #pageSizeQuery = null
97
106
 
107
+ #emitError(err ) {
108
+ if (this.listenerCount('error') > 0) {
109
+ this.emit('error', err)
110
+ } else {
111
+ process.emitWarning(err)
112
+ }
113
+ }
114
+
98
115
  constructor(
99
116
  location ,
100
117
  valueSelector ,
101
118
  keySelector ,
102
119
  opts ,
103
120
  ) {
121
+ super()
104
122
  if (typeof location !== 'string') {
105
123
  throw new TypeError('location must be a string')
106
124
  }
@@ -134,6 +152,19 @@ export class Cache {
134
152
  throw new TypeError('stale must be a undefined, number or a function')
135
153
  }
136
154
 
155
+ if (opts?.serializer !== undefined) {
156
+ if (typeof opts.serializer !== 'object' || opts.serializer === null) {
157
+ throw new TypeError('serializer must be an object')
158
+ }
159
+ if (typeof opts.serializer.serialize !== 'function') {
160
+ throw new TypeError('serializer.serialize must be a function')
161
+ }
162
+ if (typeof opts.serializer.deserialize !== 'function') {
163
+ throw new TypeError('serializer.deserialize must be a function')
164
+ }
165
+ }
166
+ this.#serializer = opts?.serializer ?? defaultSerializer
167
+
137
168
  this.#memory =
138
169
  opts?.memory === false || opts?.memory === null ? null : new MemoryCache(opts?.memory)
139
170
 
@@ -148,6 +179,9 @@ export class Cache {
148
179
  PRAGMA journal_mode = WAL;
149
180
  PRAGMA synchronous = NORMAL;
150
181
  PRAGMA temp_store = memory;
182
+ PRAGMA cache_size = -${Math.ceil(maxSize / 1024 / 8)};
183
+ PRAGMA mmap_size = ${maxSize};
184
+ PRAGMA max_page_count = ${Math.ceil(maxSize / 4096)};
151
185
  PRAGMA optimize;
152
186
 
153
187
  CREATE TABLE IF NOT EXISTS cache_v${VERSION} (
@@ -155,7 +189,9 @@ export class Cache {
155
189
  val BLOB NOT NULL,
156
190
  ttl INTEGER NOT NULL,
157
191
  stale INTEGER NOT NULL
158
- );
192
+ ) WITHOUT ROWID;
193
+
194
+ CREATE INDEX IF NOT EXISTS cache_v${VERSION}_stale_idx ON cache_v${VERSION}(stale);
159
195
 
160
196
  CREATE TABLE IF NOT EXISTS cache_lock_v${VERSION} (
161
197
  key TEXT PRIMARY KEY NOT NULL,
@@ -164,13 +200,6 @@ export class Cache {
164
200
  );
165
201
  `)
166
202
 
167
- {
168
- const { page_size } = this.#database.prepare('PRAGMA page_size').get()
169
-
170
-
171
- this.#database.exec(`PRAGMA max_page_count = ${Math.ceil(maxSize / page_size)}`)
172
- }
173
-
174
203
  this.#getQuery = this.#database.prepare(
175
204
  `SELECT val, ttl, stale FROM cache_v${VERSION} WHERE key = ? AND stale > ?`,
176
205
  )
@@ -185,7 +214,7 @@ export class Cache {
185
214
  `DELETE FROM cache_v${VERSION} WHERE key IN (SELECT key FROM cache_v${VERSION} ORDER BY stale ASC LIMIT ?)`,
186
215
  )
187
216
 
188
- // Bug A fix: ON CONFLICT refreshes lock_acquired when we are the owner,
217
+ // ON CONFLICT refreshes lock_acquired when we are the owner,
189
218
  // preventing other processes from stealing our still-active lock.
190
219
  this.#lockAcquireQuery = this.#database.prepare(
191
220
  `INSERT INTO cache_lock_v${VERSION} (key, lock_acquired, lock_owner) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET lock_acquired = CASE WHEN lock_owner = excluded.lock_owner THEN excluded.lock_acquired ELSE lock_acquired END RETURNING lock_acquired, lock_owner`,
@@ -193,7 +222,7 @@ export class Cache {
193
222
  this.#lockStealQuery = this.#database.prepare(
194
223
  `UPDATE cache_lock_v${VERSION} SET lock_acquired = ?, lock_owner = ? WHERE key = ? AND lock_acquired = ?`,
195
224
  )
196
- // Bug B fix: read the current lock state after a failed steal to get the winner's timestamp.
225
+ // Read the current lock state after a failed steal to get the winner's timestamp.
197
226
  this.#lockGetQuery = this.#database.prepare(
198
227
  `SELECT lock_acquired FROM cache_lock_v${VERSION} WHERE key = ?`,
199
228
  )
@@ -220,7 +249,7 @@ export class Cache {
220
249
  this.#pageCountQuery = null
221
250
  this.#pageSizeQuery = null
222
251
 
223
- process.emitWarning(err )
252
+ this.#emitError(err )
224
253
  break
225
254
  }
226
255
  }
@@ -228,6 +257,7 @@ export class Cache {
228
257
 
229
258
  dbs.add(this)
230
259
 
260
+ globalThis.__nxt_cache ??= []
231
261
  globalThis.__nxt_cache.push(this)
232
262
  }
233
263
 
@@ -240,7 +270,7 @@ export class Cache {
240
270
  const { page_size } = this.#pageSizeQuery .get()
241
271
  size = page_count * page_size
242
272
  } catch (err) {
243
- process.emitWarning(err )
273
+ this.#emitError(err )
244
274
  }
245
275
  database = { location: this.#location, size }
246
276
  }
@@ -276,7 +306,7 @@ export class Cache {
276
306
  this.#database?.close()
277
307
  this.#database = null
278
308
 
279
- const idx = globalThis.__nxt_cache.indexOf(this)
309
+ const idx = globalThis.__nxt_cache?.indexOf(this) ?? -1
280
310
  if (idx !== -1) {
281
311
  globalThis.__nxt_cache.splice(idx, 1)
282
312
  }
@@ -319,7 +349,7 @@ export class Cache {
319
349
  this.#purgeStaleQuery?.run(Date.now())
320
350
  this.#lockPurgeQuery?.run(Date.now() - 3_600_000)
321
351
  } catch (err) {
322
- process.emitWarning(err )
352
+ this.#emitError(err )
323
353
  }
324
354
  }
325
355
 
@@ -343,7 +373,7 @@ export class Cache {
343
373
  cached = entry
344
374
  }
345
375
  } catch (err) {
346
- process.emitWarning(err )
376
+ this.#emitError(err )
347
377
  }
348
378
  }
349
379
 
@@ -365,18 +395,20 @@ export class Cache {
365
395
  }
366
396
  }
367
397
 
398
+ if (!refresh) {
399
+ // peek: return stale value if available, undefined if expired or missing
400
+ return { value: cached?.value, async: false }
401
+ }
402
+
368
403
  let result
369
- if (refresh) {
370
- try {
371
- result = this.#refresh(args, key)
372
- } catch (err) {
373
- if (cached !== undefined) {
374
- return { value: cached.value, async: false }
375
- }
376
- throw err
404
+ try {
405
+ result = this.#refresh(args, key)
406
+ } catch (err) {
407
+ if (cached !== undefined) {
408
+ this.#emitError(err )
409
+ return { value: cached.value, async: false }
377
410
  }
378
- } else {
379
- result = { value: undefined, async: false }
411
+ throw err
380
412
  }
381
413
 
382
414
  if (result.async && cached !== undefined) {
@@ -463,7 +495,7 @@ export class Cache {
463
495
  try {
464
496
  this.#delQuery?.run(key)
465
497
  } catch (err) {
466
- process.emitWarning(err )
498
+ this.#emitError(err )
467
499
  }
468
500
  return
469
501
  }
@@ -486,7 +518,7 @@ export class Cache {
486
518
  return
487
519
  }
488
520
 
489
- const data = ArrayBuffer.isView(value) ? value : JSON.stringify(value )
521
+ const data = this.#serializer.serialize(value)
490
522
 
491
523
  const entry = this.#createEntry(
492
524
  key,
@@ -504,11 +536,11 @@ export class Cache {
504
536
  try {
505
537
  this.#evictQuery?.run(256)
506
538
  this.#setQuery?.run(key, data , ttl, stale)
507
- } catch {
508
- process.emitWarning(err )
539
+ } catch (evictErr) {
540
+ this.#emitError(evictErr )
509
541
  }
510
542
  } else {
511
- process.emitWarning(err )
543
+ this.#emitError(err )
512
544
  }
513
545
  }
514
546
  }
@@ -523,9 +555,7 @@ export class Cache {
523
555
  return {
524
556
  ttl,
525
557
  stale,
526
- value: ArrayBuffer.isView(value)
527
- ? (Buffer.from(value.buffer, value.byteOffset, value.byteLength) )
528
- : value,
558
+ value,
529
559
  key,
530
560
  size,
531
561
  index: -1,
@@ -534,7 +564,7 @@ export class Cache {
534
564
  }
535
565
 
536
566
  #loadRow(key , row ) {
537
- const value = (ArrayBuffer.isView(row.val) ? row.val : JSON.parse(row.val))
567
+ const value = this.#serializer.deserialize(maybeToBuffer(row.val))
538
568
  return this.#createEntry(
539
569
  key,
540
570
  value,
@@ -585,7 +615,7 @@ export class Cache {
585
615
 
586
616
  return lockedAt
587
617
  } catch (err) {
588
- process.emitWarning(err )
618
+ this.#emitError(err )
589
619
  return 'unavailable'
590
620
  }
591
621
  }
@@ -632,7 +662,7 @@ export class Cache {
632
662
  return entry.value
633
663
  }
634
664
  } catch (err) {
635
- process.emitWarning(err )
665
+ this.#emitError(err )
636
666
  }
637
667
 
638
668
  // Try to acquire lock for takeover
@@ -669,6 +699,24 @@ export class Cache {
669
699
  }
670
700
  }
671
701
 
702
+ {
703
+ const offPeakBC = new BroadcastChannel('nxt:offPeak')
704
+ offPeakBC.unref()
705
+ offPeakBC.onmessage = () => {
706
+ for (const db of dbs) {
707
+ try {
708
+ db.purgeStale()
709
+ } catch (err) {
710
+ if (db.listenerCount('error') > 0) {
711
+ db.emit('error', err)
712
+ } else {
713
+ process.emitWarning(err )
714
+ }
715
+ }
716
+ }
717
+ }
718
+ }
719
+
672
720
  /** @deprecated Use `Cache` instead. */
673
721
  export const AsyncCache = Cache
674
722
  /** @deprecated Use `CacheOptions` instead. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/cache",
3
- "version": "2.0.3",
3
+ "version": "2.0.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": "cfc529e921c93a1fb0292018e2026f2b326ec998"
33
+ "gitHead": "9b8156711c1909480df222a003871e2d9cded24c"
34
34
  }