@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 +7 -1
- package/lib/index.js +103 -55
- package/package.json +2 -2
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
|
-
|
|
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 =
|
|
73
|
+
const VERSION = 5
|
|
64
74
|
const MAX_DURATION = 365000000e3
|
|
65
75
|
|
|
66
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
539
|
+
} catch (evictErr) {
|
|
540
|
+
this.#emitError(evictErr )
|
|
509
541
|
}
|
|
510
542
|
} else {
|
|
511
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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": "
|
|
33
|
+
"gitHead": "9b8156711c1909480df222a003871e2d9cded24c"
|
|
34
34
|
}
|