@nxtedition/cache 2.1.0 → 2.1.2

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
@@ -5,6 +5,9 @@ export interface DatabaseOptions {
5
5
  timeout?: number;
6
6
  maxSize?: number;
7
7
  }
8
+ export interface LockOptions {
9
+ minTimeout?: number;
10
+ }
8
11
  export interface Serializer<V> {
9
12
  serialize: (value: V) => Buffer | Uint8Array | string;
10
13
  deserialize: (data: Buffer | string) => V;
@@ -12,6 +15,7 @@ export interface Serializer<V> {
12
15
  export interface CacheOptions<V> {
13
16
  ttl?: number | ((value: V, key: string) => number);
14
17
  stale?: number | ((value: V, key: string) => number);
18
+ lock?: LockOptions | false | null;
15
19
  memory?: MemoryOptions | false | null;
16
20
  database?: DatabaseOptions | false | null;
17
21
  serializer?: Serializer<V>;
@@ -28,15 +32,15 @@ declare global {
28
32
  stats: Cache['stats'];
29
33
  }[];
30
34
  }
31
- export declare class Cache<V = unknown, A extends unknown[] = unknown[]> extends EventEmitter {
35
+ export declare class Cache<V = unknown, A extends unknown[] = [string]> extends EventEmitter {
32
36
  #private;
33
- constructor(location: string, valueSelector: (...args: A) => V | PromiseLike<V>, keySelector: (...args: A) => string, opts?: CacheOptions<V>);
37
+ constructor(location: string, valueSelector?: (...args: A) => V | PromiseLike<V>, keySelector?: (...args: A) => string, opts?: CacheOptions<V>);
34
38
  get stats(): {
35
39
  lock: {
36
40
  timeout: number;
37
41
  mean: number;
38
42
  stddev: number;
39
- };
43
+ } | undefined;
40
44
  dedupe: {
41
45
  size: number;
42
46
  };
package/lib/index.js CHANGED
@@ -8,7 +8,6 @@ import { MemoryCache, } from "./memory
8
8
 
9
9
  function noop() {}
10
10
 
11
- // eslint-disable-next-line typescript-eslint/no-redundant-type-constituents
12
11
  function maybeToBuffer(value ) {
13
12
  return ArrayBuffer.isView(value)
14
13
  ? Buffer.from(value.buffer, value.byteOffset, value.byteLength)
@@ -37,6 +36,10 @@ const dbs = new Set ()
37
36
 
38
37
 
39
38
 
39
+
40
+
41
+
42
+
40
43
 
41
44
 
42
45
 
@@ -58,6 +61,7 @@ const defaultSerializer = {
58
61
 
59
62
 
60
63
 
64
+
61
65
 
62
66
 
63
67
 
@@ -75,7 +79,7 @@ const defaultSerializer = {
75
79
  const VERSION = 5
76
80
  const MAX_DURATION = 365000000e3
77
81
 
78
- export class Cache extends EventEmitter {
82
+ export class Cache extends EventEmitter {
79
83
  #memory
80
84
  #dedupe = new Map ()
81
85
  #closed = false
@@ -87,10 +91,11 @@ export class Cache extends EventEm
87
91
  #stale
88
92
  #serializer
89
93
 
90
- #lockMean = 5
91
- #lockVar = 0
92
- #lockTimeout = 10
93
94
  #lockId = randomUUID()
95
+ #lockVar = 0
96
+ #lockMean = 5
97
+ #lockTimeout = 10
98
+ #lockMinTimeout = 1
94
99
 
95
100
  #location
96
101
  #database = null
@@ -116,8 +121,8 @@ export class Cache extends EventEm
116
121
 
117
122
  constructor(
118
123
  location ,
119
- valueSelector ,
120
- keySelector ,
124
+ valueSelector ,
125
+ keySelector ,
121
126
  opts ,
122
127
  ) {
123
128
  super()
@@ -126,15 +131,15 @@ export class Cache extends EventEm
126
131
  }
127
132
  this.#location = location
128
133
 
129
- if (typeof valueSelector !== 'function') {
134
+ if (valueSelector !== undefined && typeof valueSelector !== 'function') {
130
135
  throw new TypeError('valueSelector must be a function')
131
136
  }
132
- this.#valueSelector = valueSelector
137
+ this.#valueSelector = valueSelector ?? (() => undefined )
133
138
 
134
- if (typeof keySelector !== 'function') {
139
+ if (keySelector !== undefined && typeof keySelector !== 'function') {
135
140
  throw new TypeError('keySelector must be a function')
136
141
  }
137
- this.#keySelector = keySelector
142
+ this.#keySelector = keySelector ?? ((...args ) => JSON.stringify(args))
138
143
 
139
144
  if (typeof opts?.ttl === 'number' || opts?.ttl === undefined) {
140
145
  const ttl = opts?.ttl ?? Number.MAX_SAFE_INTEGER
@@ -154,6 +159,24 @@ export class Cache extends EventEm
154
159
  throw new TypeError('stale must be a undefined, number or a function')
155
160
  }
156
161
 
162
+ if (opts?.lock === false || opts?.lock === null) {
163
+ this.#lockMinTimeout = -1
164
+ this.#lockTimeout = -1
165
+ this.#lockMean = -1
166
+ } else if (opts?.lock !== undefined) {
167
+ if (typeof opts.lock !== 'object' || opts.lock === null) {
168
+ throw new TypeError('lock must be an object')
169
+ }
170
+ if (opts.lock.minTimeout !== undefined) {
171
+ if (typeof opts.lock.minTimeout !== 'number') {
172
+ throw new TypeError('lock.minTimeout must be a number')
173
+ }
174
+ this.#lockMinTimeout = Math.max(0, opts.lock.minTimeout)
175
+ this.#lockTimeout = this.#lockMinTimeout
176
+ this.#lockMean = this.#lockTimeout / 1.2
177
+ }
178
+ }
179
+
157
180
  if (opts?.serializer !== undefined) {
158
181
  if (typeof opts.serializer !== 'object' || opts.serializer === null) {
159
182
  throw new TypeError('serializer must be an object')
@@ -180,7 +203,6 @@ export class Cache extends EventEm
180
203
  this.#database.exec(`
181
204
  PRAGMA journal_mode = WAL;
182
205
  PRAGMA synchronous = NORMAL;
183
- PRAGMA temp_store = memory;
184
206
  PRAGMA cache_size = -${Math.ceil(maxSize / 1024 / 8)};
185
207
  PRAGMA mmap_size = ${maxSize};
186
208
  PRAGMA max_page_count = ${Math.ceil(maxSize / 4096)};
@@ -278,11 +300,14 @@ export class Cache extends EventEm
278
300
  }
279
301
 
280
302
  return {
281
- lock: {
282
- timeout: this.#lockTimeout,
283
- mean: this.#lockMean,
284
- stddev: Math.sqrt(this.#lockVar),
285
- },
303
+ lock:
304
+ this.#lockTimeout >= 0
305
+ ? {
306
+ timeout: this.#lockTimeout,
307
+ mean: this.#lockMean,
308
+ stddev: Math.sqrt(this.#lockVar),
309
+ }
310
+ : undefined,
286
311
  dedupe: { size: this.#dedupe.size },
287
312
  memory: this.#memory?.stats,
288
313
  database,
@@ -350,6 +375,7 @@ export class Cache extends EventEm
350
375
  this.#memory?.purgeStale(Date.now())
351
376
  this.#purgeStaleQuery?.run(Date.now())
352
377
  this.#lockPurgeQuery?.run(Date.now() - 3_600_000)
378
+ this.#database?.exec('PRAGMA optimize')
353
379
  } catch (err) {
354
380
  this.#emitError(err )
355
381
  }
@@ -430,15 +456,17 @@ export class Cache extends EventEm
430
456
  return { async: true, value: existing }
431
457
  }
432
458
 
433
- // Another process holds the lock — wait for the value instead of calling valueSelector.
434
- // Use deferred pattern so #waitForValue can check promise identity to detect delete+get races.
435
- const lockResult = this.#tryAcquireLock(key)
436
- if (typeof lockResult === 'number') {
437
- const { promise, resolve, reject } = Promise.withResolvers ()
438
- promise.catch(noop)
439
- this.#dedupe.set(key, promise)
440
- this.#waitForValue(args, key, lockResult, promise).then(resolve, reject)
441
- return { async: true, value: promise }
459
+ if (this.#lockTimeout >= 0) {
460
+ // Another process holds the lock wait for the value instead of calling valueSelector.
461
+ // Use deferred pattern so #waitForValue can check promise identity to detect delete+get races.
462
+ const lockResult = this.#tryAcquireLock(key)
463
+ if (typeof lockResult === 'number') {
464
+ const { promise, resolve, reject } = Promise.withResolvers ()
465
+ promise.catch(noop)
466
+ this.#dedupe.set(key, promise)
467
+ this.#waitForValue(args, key, lockResult, promise).then(resolve, reject)
468
+ return { async: true, value: promise }
469
+ }
442
470
  }
443
471
 
444
472
  const res = this.#fetch(args)
@@ -507,7 +535,7 @@ export class Cache extends EventEm
507
535
  throw new TypeError('ttl must be nully or a positive integer')
508
536
  }
509
537
 
510
- const staleValue = Math.min(MAX_DURATION, this.#stale(value, key) ?? Number.MAX_SAFE_INTEGER)
538
+ const staleValue = Math.min(MAX_DURATION, this.#stale(value, key) ?? Infinity)
511
539
  if (!Number.isFinite(staleValue) || staleValue < 0) {
512
540
  throw new TypeError('stale must be nully or a positive integer')
513
541
  }
@@ -631,7 +659,7 @@ export class Cache extends EventEm
631
659
  this.#lockMean += alpha * diff
632
660
  this.#lockVar = (1 - alpha) * (this.#lockVar + alpha * diff * diff)
633
661
  this.#lockTimeout = Math.max(
634
- 10,
662
+ this.#lockMinTimeout,
635
663
  Math.min(1_000, Math.ceil(this.#lockMean * 1.2 + Math.sqrt(this.#lockVar) * 3)),
636
664
  )
637
665
  }
package/lib/memory.js CHANGED
@@ -78,7 +78,7 @@ export class MemoryCache {
78
78
  #delete(entry ) {
79
79
  this.#map.delete(entry.key)
80
80
 
81
- this.#size -= entry.size
81
+ this.#size -= entry.size ?? 0
82
82
  this.#count -= 1
83
83
 
84
84
  const tmp = this.#arr.pop()
@@ -107,7 +107,7 @@ export class MemoryCache {
107
107
  }
108
108
 
109
109
  #prune() {
110
- while (this.#size > this.#maxSize || this.#count > this.#maxCount) {
110
+ while (this.#arr.length > 0 && (this.#size > this.#maxSize || this.#count > this.#maxCount)) {
111
111
  const e1 = this.#arr[(Math.random() * this.#arr.length) | 0]
112
112
  const e2 = this.#arr[(Math.random() * this.#arr.length) | 0]
113
113
  const e = e2.counter > e1.counter ? e1 : e2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/cache",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
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": "80358629a4abe8d9065ac1d1b92ef378493c2dcb"
33
+ "gitHead": "f0aff304c4a0a7246ea0cb34f8d8b3839dc11e81"
34
34
  }