@nxtedition/cache 2.1.13 → 2.1.15
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/README.md +125 -37
- package/lib/index.d.ts +3 -10
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +583 -899
- package/lib/index.js.map +1 -0
- package/lib/memory.d.ts +1 -0
- package/lib/memory.d.ts.map +1 -0
- package/lib/memory.js +93 -117
- package/lib/memory.js.map +1 -0
- package/package.json +10 -12
package/lib/index.js
CHANGED
|
@@ -1,235 +1,51 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import { MemoryCache, } from "./memory.js"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function noop() {}
|
|
15
|
-
|
|
16
|
-
function maybeToBuffer(value ) {
|
|
17
|
-
return ArrayBuffer.isView(value)
|
|
18
|
-
? Buffer.from(value.buffer, value.byteOffset, value.byteLength)
|
|
19
|
-
: value
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import xxhash from 'xxhash-wasm';
|
|
4
|
+
import { getOrCreate } from '@nxtedition/shared';
|
|
5
|
+
import { MemoryCache } from "./memory.js";
|
|
6
|
+
function maybeToBuffer(value) {
|
|
7
|
+
return ArrayBuffer.isView(value)
|
|
8
|
+
? Buffer.from(value.buffer, value.byteOffset, value.byteLength)
|
|
9
|
+
: value;
|
|
20
10
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return value != null && typeof (value ).then === 'function'
|
|
11
|
+
function isThenable(value) {
|
|
12
|
+
return value != null && typeof value.then === 'function';
|
|
24
13
|
}
|
|
25
|
-
|
|
26
14
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
-
const dbs = new Set
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
15
|
+
const dbs = new Set();
|
|
56
16
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
-
const defaultSerializer
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
export class Cache extends EventEmitter {
|
|
90
|
-
#memory
|
|
91
|
-
#dedupe = new Map ()
|
|
92
|
-
#closed = false
|
|
93
|
-
|
|
94
|
-
#valueSelector
|
|
95
|
-
#keySelector
|
|
96
|
-
|
|
97
|
-
#ttl
|
|
98
|
-
#stale
|
|
99
|
-
#serializer
|
|
100
|
-
|
|
101
|
-
#lockId = null
|
|
102
|
-
#lockArray = null
|
|
103
|
-
#lockSet
|
|
104
|
-
#lockVar = 0
|
|
105
|
-
#lockMean = 5
|
|
106
|
-
#lockTimeout = 10
|
|
107
|
-
#lockMinTimeout = 1
|
|
108
|
-
#lockMaxTimeout = 1_000
|
|
109
|
-
|
|
110
|
-
#flushHandle = null
|
|
111
|
-
#location
|
|
112
|
-
#databaseTimeout = 20
|
|
113
|
-
#database = null
|
|
114
|
-
#getQuery = null
|
|
115
|
-
#delQuery = null
|
|
116
|
-
#purgeStaleQuery = null
|
|
117
|
-
#evictQuery = null
|
|
118
|
-
#lockAcquireQuery = null
|
|
119
|
-
#lockStealQuery = null
|
|
120
|
-
#lockGetQuery = null
|
|
121
|
-
#lockPurgeQuery = null
|
|
122
|
-
#pageCountQuery = null
|
|
123
|
-
#pageSizeQuery = null
|
|
124
|
-
#setQuery = null
|
|
125
|
-
#setBatch =
|
|
126
|
-
[]
|
|
127
|
-
|
|
128
|
-
#emitError = (err ) => {
|
|
129
|
-
if (this.listenerCount('error') > 0) {
|
|
130
|
-
this.emit('error', err)
|
|
131
|
-
} else {
|
|
132
|
-
process.emitWarning(err)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
constructor(
|
|
137
|
-
location ,
|
|
138
|
-
valueSelector ,
|
|
139
|
-
keySelector ,
|
|
140
|
-
opts ,
|
|
141
|
-
) {
|
|
142
|
-
super()
|
|
143
|
-
if (typeof location !== 'string') {
|
|
144
|
-
throw new TypeError('location must be a string')
|
|
145
|
-
}
|
|
146
|
-
this.#location = location
|
|
147
|
-
|
|
148
|
-
if (valueSelector !== undefined && typeof valueSelector !== 'function') {
|
|
149
|
-
throw new TypeError('valueSelector must be a function')
|
|
150
|
-
}
|
|
151
|
-
this.#valueSelector = valueSelector ?? (() => undefined )
|
|
152
|
-
|
|
153
|
-
if (keySelector !== undefined && typeof keySelector !== 'function') {
|
|
154
|
-
throw new TypeError('keySelector must be a function')
|
|
155
|
-
}
|
|
156
|
-
this.#keySelector = keySelector ?? ((...args ) => JSON.stringify(args))
|
|
157
|
-
|
|
158
|
-
if (typeof opts?.ttl === 'number' || opts?.ttl === undefined) {
|
|
159
|
-
const ttl = opts?.ttl ?? Number.MAX_SAFE_INTEGER
|
|
160
|
-
this.#ttl = (_val , _key ) => ttl
|
|
161
|
-
} else if (typeof opts?.ttl === 'function') {
|
|
162
|
-
this.#ttl = opts.ttl
|
|
163
|
-
} else {
|
|
164
|
-
throw new TypeError('ttl must be a undefined, number or a function')
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (typeof opts?.stale === 'number' || opts?.stale === undefined) {
|
|
168
|
-
const stale = opts?.stale ?? Number.MAX_SAFE_INTEGER
|
|
169
|
-
this.#stale = (_val , _key ) => stale
|
|
170
|
-
} else if (typeof opts?.stale === 'function') {
|
|
171
|
-
this.#stale = opts.stale
|
|
172
|
-
} else {
|
|
173
|
-
throw new TypeError('stale must be a undefined, number or a function')
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (opts?.lock === false || opts?.lock === null) {
|
|
177
|
-
this.#lockId = null
|
|
178
|
-
this.#lockArray = null
|
|
179
|
-
this.#lockSet = null
|
|
180
|
-
this.#lockMinTimeout = -1
|
|
181
|
-
this.#lockTimeout = -1
|
|
182
|
-
this.#lockMean = -1
|
|
183
|
-
} else {
|
|
184
|
-
if (opts?.lock !== undefined) {
|
|
185
|
-
if (typeof opts.lock !== 'object' || opts.lock === null) {
|
|
186
|
-
throw new TypeError('lock must be an object')
|
|
187
|
-
}
|
|
188
|
-
if (opts.lock.minTimeout !== undefined) {
|
|
189
|
-
if (typeof opts.lock.minTimeout !== 'number') {
|
|
190
|
-
throw new TypeError('lock.minTimeout must be a number')
|
|
191
|
-
}
|
|
192
|
-
this.#lockMinTimeout = Math.max(0, opts.lock.minTimeout)
|
|
193
|
-
this.#lockTimeout = this.#lockMinTimeout
|
|
194
|
-
this.#lockMean = this.#lockTimeout / 1.2
|
|
195
|
-
}
|
|
196
|
-
if (opts.lock.maxTimeout !== undefined) {
|
|
197
|
-
if (typeof opts.lock.maxTimeout !== 'number') {
|
|
198
|
-
throw new TypeError('lock.maxTimeout must be a number')
|
|
199
|
-
}
|
|
200
|
-
this.#lockMaxTimeout = Math.max(this.#lockMinTimeout, opts.lock.maxTimeout)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
this.#lockId = randomUUID()
|
|
205
|
-
this.#lockArray = new Int32Array(getOrCreate(`__@nxtedition/cache/${location}`, 8 + 1024))
|
|
206
|
-
this.#lockSet = new Set()
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (opts?.serializer !== undefined) {
|
|
210
|
-
if (typeof opts.serializer !== 'object' || opts.serializer === null) {
|
|
211
|
-
throw new TypeError('serializer must be an object')
|
|
212
|
-
}
|
|
213
|
-
if (typeof opts.serializer.serialize !== 'function') {
|
|
214
|
-
throw new TypeError('serializer.serialize must be a function')
|
|
215
|
-
}
|
|
216
|
-
if (typeof opts.serializer.deserialize !== 'function') {
|
|
217
|
-
throw new TypeError('serializer.deserialize must be a function')
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
this.#serializer = opts?.serializer ?? defaultSerializer
|
|
221
|
-
|
|
222
|
-
this.#memory =
|
|
223
|
-
opts?.memory === false || opts?.memory === null ? null : new MemoryCache(opts?.memory)
|
|
224
|
-
|
|
225
|
-
for (let n = 0; opts?.database !== null && opts?.database !== false; n++) {
|
|
226
|
-
try {
|
|
227
|
-
const maxSize = opts?.database?.maxSize ?? 128 * 1024 * 1024
|
|
228
|
-
this.#databaseTimeout = opts?.database?.timeout ?? 20
|
|
229
|
-
|
|
230
|
-
this.#database ??= new DatabaseSync(location, { timeout: this.#databaseTimeout })
|
|
231
|
-
|
|
232
|
-
this.#database.exec(`
|
|
17
|
+
const defaultSerializer = {
|
|
18
|
+
serialize(value) {
|
|
19
|
+
return ArrayBuffer.isView(value) ? value : JSON.stringify(value);
|
|
20
|
+
},
|
|
21
|
+
deserialize(data) {
|
|
22
|
+
// eslint-disable-next-line typescript-eslint/no-unsafe-argument
|
|
23
|
+
return ArrayBuffer.isView(data) ? data : JSON.parse(data);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const VERSION = 5;
|
|
27
|
+
const MAX_DURATION = 365000000e3;
|
|
28
|
+
const HASHER = await xxhash();
|
|
29
|
+
class DatabaseShard {
|
|
30
|
+
location;
|
|
31
|
+
databaseTimeout = 20;
|
|
32
|
+
database = null;
|
|
33
|
+
getQuery = null;
|
|
34
|
+
delQuery = null;
|
|
35
|
+
purgeStaleQuery = null;
|
|
36
|
+
evictQuery = null;
|
|
37
|
+
pageCountQuery = null;
|
|
38
|
+
pageSizeQuery = null;
|
|
39
|
+
setQuery = null;
|
|
40
|
+
setBatch = [];
|
|
41
|
+
constructor(location, opts) {
|
|
42
|
+
this.location = location;
|
|
43
|
+
for (let n = 0; true; n++) {
|
|
44
|
+
try {
|
|
45
|
+
const maxSize = opts?.maxSize ?? 128 * 1024 * 1024;
|
|
46
|
+
this.databaseTimeout = opts?.timeout ?? 20;
|
|
47
|
+
this.database ??= new DatabaseSync(location, { timeout: this.databaseTimeout });
|
|
48
|
+
this.database.exec(`
|
|
233
49
|
PRAGMA journal_mode = WAL;
|
|
234
50
|
PRAGMA synchronous = OFF;
|
|
235
51
|
PRAGMA wal_autocheckpoint = 10000;
|
|
@@ -246,697 +62,565 @@ export class Cache extends EventEmi
|
|
|
246
62
|
) WITHOUT ROWID;
|
|
247
63
|
|
|
248
64
|
CREATE INDEX IF NOT EXISTS cache_v${VERSION}_stale_idx ON cache_v${VERSION}(stale);
|
|
249
|
-
`)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.#database.exec(`
|
|
267
|
-
CREATE TABLE IF NOT EXISTS cache_lock_v${VERSION} (
|
|
268
|
-
key TEXT PRIMARY KEY NOT NULL,
|
|
269
|
-
lock_acquired INTEGER NOT NULL,
|
|
270
|
-
lock_owner TEXT NOT NULL
|
|
271
|
-
);
|
|
272
|
-
`)
|
|
273
|
-
|
|
274
|
-
// ON CONFLICT refreshes lock_acquired when we are the owner,
|
|
275
|
-
// preventing other processes from stealing our still-active lock.
|
|
276
|
-
this.#lockAcquireQuery = this.#database.prepare(
|
|
277
|
-
`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`,
|
|
278
|
-
)
|
|
279
|
-
this.#lockStealQuery = this.#database.prepare(
|
|
280
|
-
`UPDATE cache_lock_v${VERSION} SET lock_acquired = ?, lock_owner = ? WHERE key = ? AND lock_acquired = ?`,
|
|
281
|
-
)
|
|
282
|
-
// Read the current lock state after a failed steal to get the winner's timestamp.
|
|
283
|
-
this.#lockGetQuery = this.#database.prepare(
|
|
284
|
-
`SELECT lock_acquired FROM cache_lock_v${VERSION} WHERE key = ?`,
|
|
285
|
-
)
|
|
286
|
-
this.#lockPurgeQuery = this.#database.prepare(
|
|
287
|
-
`DELETE FROM cache_lock_v${VERSION} WHERE lock_acquired <= ?`,
|
|
288
|
-
)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
this.#pageCountQuery = this.#database.prepare('PRAGMA page_count')
|
|
292
|
-
this.#pageSizeQuery = this.#database.prepare('PRAGMA page_size')
|
|
293
|
-
break
|
|
294
|
-
} catch (err) {
|
|
295
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100)
|
|
296
|
-
|
|
297
|
-
if (n >= 16) {
|
|
298
|
-
this.#database?.close()
|
|
299
|
-
this.#database = null
|
|
300
|
-
|
|
301
|
-
this.#getQuery = null
|
|
302
|
-
this.#setQuery = null
|
|
303
|
-
this.#delQuery = null
|
|
304
|
-
this.#purgeStaleQuery = null
|
|
305
|
-
this.#evictQuery = null
|
|
306
|
-
this.#lockAcquireQuery = null
|
|
307
|
-
this.#lockStealQuery = null
|
|
308
|
-
this.#lockGetQuery = null
|
|
309
|
-
this.#lockPurgeQuery = null
|
|
310
|
-
this.#pageCountQuery = null
|
|
311
|
-
this.#pageSizeQuery = null
|
|
312
|
-
|
|
313
|
-
this.#emitError(err )
|
|
314
|
-
break
|
|
65
|
+
`);
|
|
66
|
+
this.getQuery = this.database.prepare(`SELECT val, ttl, stale FROM cache_v${VERSION} WHERE key = ? AND stale > ?`);
|
|
67
|
+
this.setQuery = this.database.prepare(`INSERT OR REPLACE INTO cache_v${VERSION} (key, val, ttl, stale) VALUES (?, ?, ?, ?)`);
|
|
68
|
+
this.delQuery = this.database.prepare(`DELETE FROM cache_v${VERSION} WHERE key = ?`);
|
|
69
|
+
this.purgeStaleQuery = this.database.prepare(`DELETE FROM cache_v${VERSION} WHERE stale <= ?`);
|
|
70
|
+
this.evictQuery = this.database.prepare(`DELETE FROM cache_v${VERSION} WHERE key IN (SELECT key FROM cache_v${VERSION} ORDER BY stale ASC LIMIT ?)`);
|
|
71
|
+
this.pageCountQuery = this.database.prepare('PRAGMA page_count');
|
|
72
|
+
this.pageSizeQuery = this.database.prepare('PRAGMA page_size');
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
77
|
+
if (n >= 16) {
|
|
78
|
+
this[Symbol.dispose]();
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
315
82
|
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
dbs.add(this)
|
|
320
|
-
|
|
321
|
-
globalThis.__nxt_cache ??= []
|
|
322
|
-
globalThis.__nxt_cache.push(new WeakRef(this))
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
get stats() {
|
|
326
|
-
let database
|
|
327
|
-
if (this.#database) {
|
|
328
|
-
let size
|
|
329
|
-
try {
|
|
330
|
-
const { page_count } = this.#pageCountQuery .get()
|
|
331
|
-
const { page_size } = this.#pageSizeQuery .get()
|
|
332
|
-
size = page_count * page_size
|
|
333
|
-
} catch (err) {
|
|
334
|
-
this.#emitError(err )
|
|
335
|
-
}
|
|
336
|
-
database = { location: this.#location, size }
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return {
|
|
340
|
-
lock:
|
|
341
|
-
this.#lockTimeout >= 0
|
|
342
|
-
? {
|
|
343
|
-
timeout: this.#lockTimeout,
|
|
344
|
-
mean: this.#lockMean,
|
|
345
|
-
stddev: Math.sqrt(this.#lockVar),
|
|
346
|
-
}
|
|
347
|
-
: undefined,
|
|
348
|
-
dedupe: { size: this.#dedupe.size },
|
|
349
|
-
memory: this.#memory?.stats,
|
|
350
|
-
database,
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
[Symbol.dispose]() {
|
|
355
|
-
this.close()
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
close() {
|
|
359
|
-
while (this.#flushHandle) {
|
|
360
|
-
clearImmediate(this.#flushHandle)
|
|
361
|
-
this.#flush()
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
this.#closed = true
|
|
365
|
-
this.#dedupe.clear()
|
|
366
|
-
dbs.delete(this)
|
|
367
|
-
|
|
368
|
-
this.#getQuery = null
|
|
369
|
-
this.#setQuery = null
|
|
370
|
-
this.#delQuery = null
|
|
371
|
-
this.#purgeStaleQuery = null
|
|
372
|
-
this.#evictQuery = null
|
|
373
|
-
this.#lockAcquireQuery = null
|
|
374
|
-
this.#lockStealQuery = null
|
|
375
|
-
this.#lockGetQuery = null
|
|
376
|
-
this.#lockPurgeQuery = null
|
|
377
|
-
this.#pageCountQuery = null
|
|
378
|
-
this.#pageSizeQuery = null
|
|
379
|
-
this.#database?.close()
|
|
380
|
-
this.#database = null
|
|
381
|
-
|
|
382
|
-
globalThis.__nxt_cache = globalThis.__nxt_cache?.filter((ref) => ref.deref() != null) ?? []
|
|
383
|
-
const idx = globalThis.__nxt_cache?.findIndex((ref) => ref.deref() === this) ?? -1
|
|
384
|
-
if (idx !== -1) {
|
|
385
|
-
globalThis.__nxt_cache.splice(idx, 1)
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
get(...args ) {
|
|
390
|
-
if (this.#closed) {
|
|
391
|
-
throw new Error('cache is closed')
|
|
392
|
-
}
|
|
393
|
-
return this.#load(args, true)
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
peek(...args ) {
|
|
397
|
-
if (this.#closed) {
|
|
398
|
-
throw new Error('cache is closed')
|
|
399
|
-
}
|
|
400
|
-
return this.#load(args, false)
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
refresh(...args ) {
|
|
404
|
-
if (this.#closed) {
|
|
405
|
-
throw new Error('cache is closed')
|
|
406
|
-
}
|
|
407
|
-
return this.#refresh(args)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
delete(...args ) {
|
|
411
|
-
if (this.#closed) {
|
|
412
|
-
throw new Error('cache is closed')
|
|
413
|
-
}
|
|
414
|
-
this.#set(this.#keySelector(...args), undefined)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
purgeStale() {
|
|
418
|
-
if (this.#closed) {
|
|
419
|
-
throw new Error('cache is closed')
|
|
420
|
-
}
|
|
421
|
-
this.#memory?.purgeStale(Date.now())
|
|
422
|
-
this.#database?.exec('PRAGMA busy_timeout = 5000')
|
|
423
|
-
try {
|
|
424
|
-
try {
|
|
425
|
-
this.#purgeStaleQuery?.run(Date.now())
|
|
426
|
-
} catch (err) {
|
|
427
|
-
this.#emitError(err )
|
|
428
|
-
}
|
|
429
|
-
try {
|
|
430
|
-
this.#lockPurgeQuery?.run(Date.now() - 3_600_000)
|
|
431
|
-
} catch (err) {
|
|
432
|
-
this.#emitError(err )
|
|
433
|
-
}
|
|
434
|
-
try {
|
|
435
|
-
this.#database?.exec('PRAGMA wal_checkpoint(TRUNCATE)')
|
|
436
|
-
} catch (err) {
|
|
437
|
-
this.#emitError(err )
|
|
438
|
-
}
|
|
439
|
-
try {
|
|
440
|
-
this.#database?.exec('PRAGMA optimize')
|
|
441
|
-
} catch (err) {
|
|
442
|
-
this.#emitError(err )
|
|
443
|
-
}
|
|
444
|
-
} finally {
|
|
445
|
-
try {
|
|
446
|
-
this.#database?.exec(`PRAGMA busy_timeout = ${this.#databaseTimeout}`)
|
|
447
|
-
} catch (err) {
|
|
448
|
-
this.#emitError(err )
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
#load(args , refresh ) {
|
|
454
|
-
const key = this.#keySelector(...args)
|
|
455
|
-
|
|
456
|
-
if (typeof key !== 'string' || key.length === 0) {
|
|
457
|
-
throw new TypeError('keySelector must return a non-empty string')
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const now = Date.now()
|
|
461
|
-
|
|
462
|
-
let cached = this.#memory?.get(key)
|
|
463
|
-
|
|
464
|
-
if (cached === undefined) {
|
|
465
|
-
try {
|
|
466
|
-
const row = this.#getQuery?.get(key, now)
|
|
467
|
-
if (row !== undefined) {
|
|
468
|
-
const entry = this.#loadRow(key, row)
|
|
469
|
-
this.#memory?.set(key, entry)
|
|
470
|
-
cached = entry
|
|
471
|
-
}
|
|
472
|
-
} catch (err) {
|
|
473
|
-
this.#emitError(err )
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (cached !== undefined) {
|
|
478
|
-
if (now < cached.ttl) {
|
|
479
|
-
return { value: cached.value, async: false }
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (now >= cached.stale) {
|
|
483
|
-
// stale-while-revalidate has expired, purge cached value.
|
|
484
|
-
this.#memory?.delete(key)
|
|
485
|
-
cached = undefined
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
if (!refresh) {
|
|
490
|
-
// peek: return stale value if available, undefined if expired or missing
|
|
491
|
-
return { value: cached?.value, async: false }
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
{
|
|
495
|
-
const pending = this.#dedupe.get(key)
|
|
496
|
-
if (pending !== undefined) {
|
|
497
|
-
return { async: true, value: pending }
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
let result
|
|
502
|
-
try {
|
|
503
|
-
result = this.#refresh(args, key)
|
|
504
|
-
} catch (err) {
|
|
505
|
-
if (cached !== undefined) {
|
|
506
|
-
this.#emitError(err )
|
|
507
|
-
return { value: cached.value, async: false }
|
|
508
|
-
}
|
|
509
|
-
throw err
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
if (result.async && cached !== undefined) {
|
|
513
|
-
return { value: cached.value, async: false }
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
return result
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
#refresh(args , key = this.#keySelector(...args)) {
|
|
520
|
-
if (typeof key !== 'string' || key.length === 0) {
|
|
521
|
-
throw new TypeError('keySelector must return a non-empty string')
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (this.#lockId != null) {
|
|
525
|
-
// Another process holds the lock — wait for the value instead of calling valueSelector.
|
|
526
|
-
// Use deferred pattern so #waitForValue can check promise identity to detect delete+get races.
|
|
527
|
-
let lockIdx = -1
|
|
528
|
-
let lockVal = 0
|
|
529
|
-
if (this.#lockArray) {
|
|
530
|
-
lockIdx = HASHER.h32(key) % this.#lockArray.length
|
|
531
|
-
lockVal = Atomics.load(this.#lockArray, lockIdx)
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const lockRes = this.#tryAcquireLock(key, lockIdx)
|
|
535
|
-
|
|
536
|
-
if (typeof lockRes === 'number') {
|
|
537
|
-
const { promise, resolve, reject } = Promise.withResolvers ()
|
|
538
|
-
promise.catch(noop)
|
|
539
|
-
this.#dedupe.set(key, promise)
|
|
540
|
-
this.#waitForValue(args, key, lockIdx, lockVal, lockRes, promise).then(resolve, reject)
|
|
541
|
-
return { async: true, value: promise }
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const res = this.#fetch(args)
|
|
546
|
-
|
|
547
|
-
if (res.async) {
|
|
548
|
-
const promise = res.value.then(
|
|
549
|
-
(value) => {
|
|
550
|
-
if (this.#dedupe.get(key) === promise) {
|
|
551
|
-
this.#set(key, value)
|
|
552
|
-
}
|
|
553
|
-
return value
|
|
554
|
-
},
|
|
555
|
-
(err) => {
|
|
556
|
-
if (this.#dedupe.get(key) === promise) {
|
|
557
|
-
this.#dedupe.delete(key)
|
|
558
|
-
}
|
|
559
|
-
throw err
|
|
560
|
-
},
|
|
561
|
-
)
|
|
562
|
-
promise.catch(noop)
|
|
563
|
-
this.#dedupe.set(key, promise)
|
|
564
|
-
return { async: true, value: promise }
|
|
565
83
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const promise = Promise.resolve(value)
|
|
577
|
-
|
|
578
|
-
if (this.#lockId != null) {
|
|
579
|
-
promise
|
|
580
|
-
.then(() => {
|
|
581
|
-
this.#updateLockTimeout(performance.now() - startTime)
|
|
582
|
-
})
|
|
583
|
-
.catch(this.#emitError)
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
return {
|
|
587
|
-
async: true,
|
|
588
|
-
value: promise,
|
|
589
|
-
}
|
|
84
|
+
[Symbol.dispose]() {
|
|
85
|
+
this.getQuery = null;
|
|
86
|
+
this.setQuery = null;
|
|
87
|
+
this.delQuery = null;
|
|
88
|
+
this.purgeStaleQuery = null;
|
|
89
|
+
this.evictQuery = null;
|
|
90
|
+
this.pageCountQuery = null;
|
|
91
|
+
this.pageSizeQuery = null;
|
|
92
|
+
this.database?.close();
|
|
93
|
+
this.database = null;
|
|
590
94
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
95
|
+
}
|
|
96
|
+
export class Cache extends EventEmitter {
|
|
97
|
+
#memory;
|
|
98
|
+
#dedupe = new Map();
|
|
99
|
+
#closed = false;
|
|
100
|
+
#valueSelector;
|
|
101
|
+
#keySelector;
|
|
102
|
+
#ttl;
|
|
103
|
+
#stale;
|
|
104
|
+
#serializer;
|
|
105
|
+
#lockArray;
|
|
106
|
+
#flushHandle = null;
|
|
107
|
+
#location;
|
|
108
|
+
#shards = null;
|
|
109
|
+
#emitError = (err) => {
|
|
110
|
+
if (this.listenerCount('error') > 0) {
|
|
111
|
+
this.emit('error', err);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
process.emitWarning(err);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
constructor(location, valueSelector, keySelector, opts) {
|
|
118
|
+
super();
|
|
119
|
+
if (typeof location !== 'string') {
|
|
120
|
+
throw new TypeError('location must be a string');
|
|
121
|
+
}
|
|
122
|
+
this.#location = location;
|
|
123
|
+
if (valueSelector !== undefined && typeof valueSelector !== 'function') {
|
|
124
|
+
throw new TypeError('valueSelector must be a function');
|
|
125
|
+
}
|
|
126
|
+
this.#valueSelector = valueSelector ?? (() => undefined);
|
|
127
|
+
if (keySelector !== undefined && typeof keySelector !== 'function') {
|
|
128
|
+
throw new TypeError('keySelector must be a function');
|
|
129
|
+
}
|
|
130
|
+
this.#keySelector = keySelector ?? ((...args) => JSON.stringify(args));
|
|
131
|
+
if (typeof opts?.ttl === 'number' || opts?.ttl === undefined) {
|
|
132
|
+
const ttl = opts?.ttl ?? Number.MAX_SAFE_INTEGER;
|
|
133
|
+
this.#ttl = (_val, _key) => ttl;
|
|
134
|
+
}
|
|
135
|
+
else if (typeof opts?.ttl === 'function') {
|
|
136
|
+
this.#ttl = opts.ttl;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
throw new TypeError('ttl must be a undefined, number or a function');
|
|
140
|
+
}
|
|
141
|
+
if (typeof opts?.stale === 'number' || opts?.stale === undefined) {
|
|
142
|
+
const stale = opts?.stale ?? Number.MAX_SAFE_INTEGER;
|
|
143
|
+
this.#stale = (_val, _key) => stale;
|
|
144
|
+
}
|
|
145
|
+
else if (typeof opts?.stale === 'function') {
|
|
146
|
+
this.#stale = opts.stale;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
throw new TypeError('stale must be a undefined, number or a function');
|
|
150
|
+
}
|
|
151
|
+
if (opts?.lock !== undefined && opts.lock !== false && opts.lock !== null) {
|
|
152
|
+
throw new TypeError('lock must be false, null, or undefined');
|
|
153
|
+
}
|
|
154
|
+
if (opts?.lock === false || opts?.lock === null || location === ':memory:') {
|
|
155
|
+
this.#lockArray = null;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
this.#lockArray = new Int32Array(getOrCreate(`__@nxtedition/cache/${location}`, 64 * 1024));
|
|
159
|
+
}
|
|
160
|
+
if (opts?.serializer !== undefined) {
|
|
161
|
+
if (typeof opts.serializer !== 'object' || opts.serializer === null) {
|
|
162
|
+
throw new TypeError('serializer must be an object');
|
|
163
|
+
}
|
|
164
|
+
if (typeof opts.serializer.serialize !== 'function') {
|
|
165
|
+
throw new TypeError('serializer.serialize must be a function');
|
|
166
|
+
}
|
|
167
|
+
if (typeof opts.serializer.deserialize !== 'function') {
|
|
168
|
+
throw new TypeError('serializer.deserialize must be a function');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
this.#serializer = opts?.serializer ?? defaultSerializer;
|
|
172
|
+
this.#memory =
|
|
173
|
+
opts?.memory === false || opts?.memory === null ? null : new MemoryCache(opts?.memory);
|
|
174
|
+
if (opts?.database === false || opts?.database === null) {
|
|
175
|
+
this.#shards = null;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
this.#shards = [];
|
|
179
|
+
try {
|
|
180
|
+
if (location === ':memory:') {
|
|
181
|
+
this.#shards.push(new DatabaseShard(location, opts?.database));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
const count = opts?.database?.shards ?? 4;
|
|
185
|
+
for (let n = 0; n < count; n++) {
|
|
186
|
+
this.#shards.push(new DatabaseShard(count === 1 ? location : location + '.' + n, {
|
|
187
|
+
...opts?.database,
|
|
188
|
+
maxSize: Math.ceil((opts?.database?.maxSize ?? 128 * 1024 * 1024) / count),
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
for (const shard of this.#shards) {
|
|
195
|
+
shard[Symbol.dispose]();
|
|
196
|
+
}
|
|
197
|
+
this.#shards = null;
|
|
198
|
+
this.#emitError(err);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
dbs.add(this);
|
|
202
|
+
globalThis.__nxt_cache ??= [];
|
|
203
|
+
globalThis.__nxt_cache.push(new WeakRef(this));
|
|
204
|
+
}
|
|
205
|
+
get stats() {
|
|
206
|
+
let database;
|
|
207
|
+
if (this.#shards) {
|
|
208
|
+
let size = 0;
|
|
209
|
+
try {
|
|
210
|
+
for (const shard of this.#shards) {
|
|
211
|
+
const { page_count } = shard.pageCountQuery.get();
|
|
212
|
+
const { page_size } = shard.pageSizeQuery.get();
|
|
213
|
+
size += page_count * page_size;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
size = undefined;
|
|
218
|
+
this.#emitError(err);
|
|
219
|
+
}
|
|
220
|
+
database = { location: this.#location, size };
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
dedupe: { size: this.#dedupe.size },
|
|
224
|
+
memory: this.#memory?.stats,
|
|
225
|
+
database,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
[Symbol.dispose]() {
|
|
229
|
+
this.close();
|
|
230
|
+
}
|
|
231
|
+
close() {
|
|
232
|
+
while (this.#flushHandle) {
|
|
233
|
+
clearImmediate(this.#flushHandle);
|
|
234
|
+
this.#memory?.cork();
|
|
235
|
+
this.#flush();
|
|
236
|
+
}
|
|
237
|
+
this.#closed = true;
|
|
238
|
+
this.#dedupe.clear();
|
|
239
|
+
dbs.delete(this);
|
|
240
|
+
for (const shard of this.#shards ?? []) {
|
|
241
|
+
shard[Symbol.dispose]();
|
|
242
|
+
}
|
|
243
|
+
globalThis.__nxt_cache = globalThis.__nxt_cache?.filter((ref) => ref.deref() != null) ?? [];
|
|
244
|
+
const idx = globalThis.__nxt_cache?.findIndex((ref) => ref.deref() === this) ?? -1;
|
|
245
|
+
if (idx !== -1) {
|
|
246
|
+
globalThis.__nxt_cache.splice(idx, 1);
|
|
247
|
+
}
|
|
594
248
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if (typeof key !== 'string' || key.length === 0) {
|
|
601
|
-
throw new TypeError('key must be a non-empty string')
|
|
249
|
+
get(...args) {
|
|
250
|
+
if (this.#closed) {
|
|
251
|
+
throw new Error('cache is closed');
|
|
252
|
+
}
|
|
253
|
+
return this.#load(args, true);
|
|
602
254
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
this.#memory?.delete(key)
|
|
609
|
-
this.#setBatch = this.#setBatch.filter((item) => item.key !== key)
|
|
610
|
-
try {
|
|
611
|
-
this.#delQuery?.run(key)
|
|
612
|
-
} catch (err) {
|
|
613
|
-
this.#emitError(err )
|
|
614
|
-
}
|
|
615
|
-
return
|
|
255
|
+
peek(...args) {
|
|
256
|
+
if (this.#closed) {
|
|
257
|
+
throw new Error('cache is closed');
|
|
258
|
+
}
|
|
259
|
+
return this.#load(args, false);
|
|
616
260
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
261
|
+
refresh(...args) {
|
|
262
|
+
if (this.#closed) {
|
|
263
|
+
throw new Error('cache is closed');
|
|
264
|
+
}
|
|
265
|
+
const key = this.#keySelector(...args);
|
|
266
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
267
|
+
throw new TypeError('keySelector must return a non-empty string');
|
|
268
|
+
}
|
|
269
|
+
const hash = HASHER.h32(key);
|
|
270
|
+
if (this.#lockArray) {
|
|
271
|
+
const idx = hash % this.#lockArray.length;
|
|
272
|
+
if (Atomics.add(this.#lockArray, idx, 1) >= 0xfffffff) {
|
|
273
|
+
// Roll back the increment before throwing so we don't leak the slot.
|
|
274
|
+
if (Atomics.sub(this.#lockArray, idx, 1) === 1) {
|
|
275
|
+
Atomics.notify(this.#lockArray, idx);
|
|
276
|
+
}
|
|
277
|
+
throw new Error('lock counter overflow');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return this.#refresh(args, key, hash);
|
|
621
281
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
282
|
+
delete(...args) {
|
|
283
|
+
if (this.#closed) {
|
|
284
|
+
throw new Error('cache is closed');
|
|
285
|
+
}
|
|
286
|
+
const key = this.#keySelector(...args);
|
|
287
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
288
|
+
throw new TypeError('key must be a non-empty string');
|
|
289
|
+
}
|
|
290
|
+
const hash = HASHER.h32(key);
|
|
291
|
+
this.#set(key, undefined, hash);
|
|
626
292
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
293
|
+
purgeStale() {
|
|
294
|
+
if (this.#closed) {
|
|
295
|
+
throw new Error('cache is closed');
|
|
296
|
+
}
|
|
297
|
+
this.#memory?.purgeStale(Date.now());
|
|
298
|
+
for (const shard of this.#shards ?? []) {
|
|
299
|
+
shard.database?.exec('PRAGMA busy_timeout = 5000');
|
|
300
|
+
try {
|
|
301
|
+
try {
|
|
302
|
+
shard.purgeStaleQuery?.run(Date.now());
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
this.#emitError(err);
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
shard.database?.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
this.#emitError(err);
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
shard.database?.exec('PRAGMA optimize');
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
this.#emitError(err);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
try {
|
|
322
|
+
shard.database?.exec(`PRAGMA busy_timeout = ${shard.databaseTimeout}`);
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
this.#emitError(err);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
634
329
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
330
|
+
#load(args, refresh) {
|
|
331
|
+
const key = this.#keySelector(...args);
|
|
332
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
333
|
+
throw new TypeError('keySelector must return a non-empty string');
|
|
334
|
+
}
|
|
335
|
+
const now = Date.now();
|
|
336
|
+
const hash = HASHER.h32(key);
|
|
337
|
+
let cached = this.#memory?.get(key);
|
|
338
|
+
if (cached === undefined) {
|
|
339
|
+
try {
|
|
340
|
+
const shard = this.#shards ? this.#shards[hash % this.#shards.length] : null;
|
|
341
|
+
const row = shard?.getQuery?.get(key, now);
|
|
342
|
+
if (row !== undefined) {
|
|
343
|
+
const entry = this.#loadRow(key, row);
|
|
344
|
+
this.#memory?.set(key, entry);
|
|
345
|
+
cached = entry;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
this.#emitError(err);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (cached !== undefined) {
|
|
353
|
+
if (now < cached.ttl) {
|
|
354
|
+
return { value: cached.value, async: false };
|
|
355
|
+
}
|
|
356
|
+
if (now >= cached.stale) {
|
|
357
|
+
// stale-while-revalidate has expired, purge cached value.
|
|
358
|
+
this.#memory?.delete(key);
|
|
359
|
+
cached = undefined;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (!refresh) {
|
|
363
|
+
// peek: return stale value if available, undefined if expired or missing
|
|
364
|
+
return { value: cached?.value, async: false };
|
|
365
|
+
}
|
|
366
|
+
{
|
|
367
|
+
const pending = this.#dedupe.get(key);
|
|
368
|
+
if (pending !== undefined) {
|
|
369
|
+
return { async: true, value: pending };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const idx = this.#lockArray ? hash % this.#lockArray.length : -1;
|
|
373
|
+
if (cached !== undefined) {
|
|
374
|
+
try {
|
|
375
|
+
const val = this.#lockArray ? Atomics.compareExchange(this.#lockArray, idx, 0, 1) : 0;
|
|
376
|
+
if (val === 0) {
|
|
377
|
+
const result = this.#refresh(args, key, hash);
|
|
378
|
+
if (!result.async) {
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
result.value.catch((err) => {
|
|
383
|
+
this.#emitError(err);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
this.#emitError(err);
|
|
390
|
+
}
|
|
391
|
+
return { async: false, value: cached.value };
|
|
392
|
+
}
|
|
393
|
+
while (this.#lockArray) {
|
|
394
|
+
const val = Atomics.compareExchange(this.#lockArray, idx, 0, 1);
|
|
395
|
+
if (val === 0) {
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
const { async, value } = Atomics.waitAsync(this.#lockArray, idx, val, 1e3);
|
|
399
|
+
if (async) {
|
|
400
|
+
return {
|
|
401
|
+
async: true,
|
|
402
|
+
value: value.then(() => this.#load(args, true).value),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return this.#refresh(args, key, hash);
|
|
652
407
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
this.#setBatch.length = 0
|
|
662
|
-
this.#memory?.uncork()
|
|
663
|
-
return
|
|
408
|
+
#release(hash) {
|
|
409
|
+
if (!this.#lockArray) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const idx = hash % this.#lockArray.length;
|
|
413
|
+
if (Atomics.sub(this.#lockArray, idx, 1) === 1) {
|
|
414
|
+
Atomics.notify(this.#lockArray, idx);
|
|
415
|
+
}
|
|
664
416
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const startTime = performance.now()
|
|
668
|
-
for (let retryCount = 0; true; retryCount++) {
|
|
669
|
-
let n = 0
|
|
417
|
+
#refresh(args, key, hash = HASHER.h32(key)) {
|
|
418
|
+
let value;
|
|
670
419
|
try {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
420
|
+
value = this.#valueSelector(...args);
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
this.#release(hash);
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
426
|
+
if (!isThenable(value)) {
|
|
427
|
+
try {
|
|
428
|
+
this.#set(key, value, hash);
|
|
678
429
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
this.#lockSet.add(hash % this.#lockArray.length)
|
|
430
|
+
finally {
|
|
431
|
+
this.#release(hash);
|
|
682
432
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
433
|
+
return { async: false, value };
|
|
434
|
+
}
|
|
435
|
+
// Store the chained promise (with #set + lock-release wired in) in #dedupe
|
|
436
|
+
// so concurrent get() callers see the same Promise instance as the original
|
|
437
|
+
// caller — required for promise-identity-based dedupe semantics.
|
|
438
|
+
const promise = Promise.resolve(value).then((value) => {
|
|
439
|
+
// finally guarantees lock release even if #set throws (user-supplied
|
|
440
|
+
// serializer / ttl / stale functions can throw); otherwise the slot
|
|
441
|
+
// would stay at 1 forever and deadlock the key.
|
|
442
|
+
try {
|
|
443
|
+
// Identity check: if delete() or another refresh replaced our dedupe
|
|
444
|
+
// entry, skip the write so we don't resurrect a deleted key or
|
|
445
|
+
// overwrite a newer value. #set itself clears dedupe on entry.
|
|
446
|
+
if (this.#dedupe.get(key) === promise) {
|
|
447
|
+
this.#set(key, value, hash);
|
|
448
|
+
}
|
|
686
449
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
// TODO (fix): Check that the error is what we expect (something like "no transaction is active")...
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
if (
|
|
712
|
-
(err )?.errcode === 13 /* SQLITE_FULL */ &&
|
|
713
|
-
retryCount < 3 &&
|
|
714
|
-
this.#evictQuery != null
|
|
715
|
-
) {
|
|
716
|
-
this.#evictQuery.run(256)
|
|
717
|
-
} else {
|
|
718
|
-
// Intentional: drop the rolled-back items from the batch and surface
|
|
719
|
-
// the error via #emitError below. The corresponding entries remain in
|
|
720
|
-
// #memory until natural eviction/TTL, which is fine for cache semantics
|
|
721
|
-
// — callers already accept that cache values can disappear at any time.
|
|
722
|
-
// Do NOT flag this as silent data loss (see closed issue #167).
|
|
723
|
-
this.#setBatch.splice(0, n)
|
|
724
|
-
throw err
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
} catch (err) {
|
|
729
|
-
this.#emitError(err )
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
if (this.#setBatch.length > 0) {
|
|
733
|
-
// If we weren't able to flush the entire batch within the time limit, schedule another flush.
|
|
734
|
-
this.#flushHandle = setImmediate(this.#flush)
|
|
735
|
-
} else {
|
|
736
|
-
this.#memory?.uncork()
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
#createEntry(
|
|
741
|
-
key ,
|
|
742
|
-
value ,
|
|
743
|
-
ttl ,
|
|
744
|
-
stale ,
|
|
745
|
-
size ,
|
|
746
|
-
) {
|
|
747
|
-
return {
|
|
748
|
-
ttl,
|
|
749
|
-
stale,
|
|
750
|
-
value,
|
|
751
|
-
key,
|
|
752
|
-
size,
|
|
753
|
-
index: -1,
|
|
754
|
-
counter: -1,
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
#loadRow(key , row ) {
|
|
759
|
-
const value = this.#serializer.deserialize(maybeToBuffer(row.val))
|
|
760
|
-
return this.#createEntry(
|
|
761
|
-
key,
|
|
762
|
-
value,
|
|
763
|
-
row.ttl,
|
|
764
|
-
row.stale,
|
|
765
|
-
ArrayBuffer.isView(row.val) ? row.val.byteLength : row.val.length * 2,
|
|
766
|
-
)
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
// Returns lock_acquired timestamp (number) when contended, or a string status.
|
|
770
|
-
#tryAcquireLock(key , lockIdx ) {
|
|
771
|
-
if (this.#lockAcquireQuery === null || this.#lockId == null) {
|
|
772
|
-
return 'unavailable'
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const now = Date.now()
|
|
776
|
-
|
|
777
|
-
try {
|
|
778
|
-
const row = this.#lockAcquireQuery.get(key, now, this.#lockId)
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
if (row === undefined) {
|
|
783
|
-
return 'unavailable'
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
if (row.lock_owner === this.#lockId) {
|
|
787
|
-
if (this.#lockArray) {
|
|
788
|
-
Atomics.add(this.#lockArray, lockIdx, 1)
|
|
450
|
+
finally {
|
|
451
|
+
this.#release(hash);
|
|
452
|
+
}
|
|
453
|
+
return value;
|
|
454
|
+
}, (err) => {
|
|
455
|
+
try {
|
|
456
|
+
if (this.#dedupe.get(key) === promise) {
|
|
457
|
+
this.#dedupe.delete(key);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
finally {
|
|
461
|
+
this.#release(hash);
|
|
462
|
+
}
|
|
463
|
+
throw err;
|
|
464
|
+
});
|
|
465
|
+
this.#dedupe.set(key, promise);
|
|
466
|
+
return { async: true, value: promise };
|
|
467
|
+
}
|
|
468
|
+
#set(key, value, hash = HASHER.h32(key)) {
|
|
469
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
470
|
+
throw new TypeError('key must be a non-empty string');
|
|
789
471
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
472
|
+
// Drop any in-flight dedupe entry so an async refresh that resolves after
|
|
473
|
+
// this call cannot resurrect a deleted key or overwrite a newer value —
|
|
474
|
+
// the async path's onFulfilled identity-checks before calling #set.
|
|
475
|
+
this.#dedupe.delete(key);
|
|
476
|
+
const shard = this.#shards ? this.#shards[hash % this.#shards.length] : null;
|
|
477
|
+
if (value === undefined) {
|
|
478
|
+
// Slow path... Should not be common...
|
|
479
|
+
this.#memory?.delete(key);
|
|
480
|
+
if (shard) {
|
|
481
|
+
shard.setBatch = shard.setBatch.filter((item) => item.key !== key);
|
|
482
|
+
try {
|
|
483
|
+
shard.delQuery?.run(key);
|
|
484
|
+
}
|
|
485
|
+
catch (err) {
|
|
486
|
+
this.#emitError(err);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
804
490
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
const current = this.#lockGetQuery .get(key)
|
|
809
|
-
if (current !== undefined) {
|
|
810
|
-
return current.lock_acquired
|
|
491
|
+
const ttlValue = Math.min(MAX_DURATION, this.#ttl(value, key) ?? Infinity);
|
|
492
|
+
if (!Number.isFinite(ttlValue) || ttlValue < 0) {
|
|
493
|
+
throw new TypeError('ttl must be undefined, null, or a non-negative finite number');
|
|
811
494
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
495
|
+
const staleValue = Math.min(MAX_DURATION, this.#stale(value, key) ?? Infinity);
|
|
496
|
+
if (!Number.isFinite(staleValue) || staleValue < 0) {
|
|
497
|
+
throw new TypeError('stale must be undefined, null, or a non-negative finite number');
|
|
498
|
+
}
|
|
499
|
+
const now = Date.now();
|
|
500
|
+
const ttl = now + ttlValue;
|
|
501
|
+
const stale = ttl + staleValue;
|
|
502
|
+
if (stale <= now) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const data = this.#serializer.serialize(value);
|
|
506
|
+
const entry = this.#createEntry(key, value, ttl, stale, ArrayBuffer.isView(data) ? data.byteLength : data.length * 2);
|
|
507
|
+
this.#memory?.set(key, entry);
|
|
508
|
+
if (!shard) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (!this.#flushHandle) {
|
|
512
|
+
this.#memory?.cork();
|
|
513
|
+
this.#flushHandle = setImmediate(this.#flush);
|
|
514
|
+
}
|
|
515
|
+
else if (shard.setBatch.length > 512) {
|
|
516
|
+
clearImmediate(this.#flushHandle);
|
|
517
|
+
this.#flush();
|
|
518
|
+
}
|
|
519
|
+
shard.setBatch.push({ key, data, ttl, stale });
|
|
520
|
+
}
|
|
521
|
+
#flush = () => {
|
|
522
|
+
this.#flushHandle = null;
|
|
523
|
+
const startTime = performance.now();
|
|
524
|
+
const len = this.#shards?.length ?? 0;
|
|
525
|
+
const idx = Math.floor(Math.random() * len);
|
|
526
|
+
for (let k = 0; k < len; k++) {
|
|
527
|
+
const shard = this.#shards[(idx + k) % len];
|
|
528
|
+
try {
|
|
529
|
+
for (let retryCount = 0; true; retryCount++) {
|
|
530
|
+
if (!shard.database || shard.setBatch.length === 0) {
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
let n = 0;
|
|
534
|
+
try {
|
|
535
|
+
shard.database.exec('BEGIN');
|
|
536
|
+
while (n < shard.setBatch.length) {
|
|
537
|
+
const { key, data, ttl, stale } = shard.setBatch[n++];
|
|
538
|
+
if (data != null) {
|
|
539
|
+
shard.setQuery?.run(key, data, ttl, stale);
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
shard.delQuery?.run(key);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
shard.database.exec('COMMIT');
|
|
546
|
+
shard.setBatch.splice(0, n);
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
// ROLLBACK is required: a failed statement leaves the connection with
|
|
551
|
+
// an open transaction; without it the next BEGIN would throw.
|
|
552
|
+
// On SQLITE_FULL, SQLite automatically rolls back the transaction, so
|
|
553
|
+
// the explicit ROLLBACK may fail with "no transaction is active" — ignore it.
|
|
554
|
+
try {
|
|
555
|
+
shard.database.exec('ROLLBACK');
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
// already rolled back automatically
|
|
559
|
+
// TODO (fix): Check that the error is what we expect (something like "no transaction is active")...
|
|
560
|
+
}
|
|
561
|
+
if (err?.errcode === 13 /* SQLITE_FULL */ &&
|
|
562
|
+
retryCount < 3 &&
|
|
563
|
+
shard.evictQuery != null) {
|
|
564
|
+
shard.evictQuery.run(256);
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
// Intentional: drop the rolled-back items from the batch and surface
|
|
568
|
+
// the error via #emitError below. The corresponding entries remain in
|
|
569
|
+
// #memory until natural eviction/TTL, which is fine for cache semantics
|
|
570
|
+
// — callers already accept that cache values can disappear at any time.
|
|
571
|
+
// Do NOT flag this as silent data loss (see closed issue #167).
|
|
572
|
+
shard.setBatch.splice(0, n);
|
|
573
|
+
throw err;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
catch (err) {
|
|
579
|
+
shard.setBatch.length = 0;
|
|
580
|
+
this.#emitError(err);
|
|
581
|
+
}
|
|
582
|
+
if (performance.now() - startTime > 10) {
|
|
583
|
+
this.#flushHandle = setImmediate(this.#flush);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
this.#memory?.uncork();
|
|
588
|
+
};
|
|
589
|
+
#createEntry(key, value, ttl, stale, size) {
|
|
590
|
+
return {
|
|
591
|
+
ttl,
|
|
592
|
+
stale,
|
|
593
|
+
value,
|
|
594
|
+
key,
|
|
595
|
+
size,
|
|
596
|
+
index: -1,
|
|
597
|
+
counter: -1,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
#loadRow(key, row) {
|
|
601
|
+
const value = this.#serializer.deserialize(maybeToBuffer(row.val));
|
|
602
|
+
return this.#createEntry(key, value, row.ttl, row.stale, ArrayBuffer.isView(row.val) ? row.val.byteLength : row.val.length * 2);
|
|
917
603
|
}
|
|
918
|
-
}
|
|
919
604
|
}
|
|
920
|
-
|
|
921
605
|
{
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
606
|
+
const offPeakBC = new BroadcastChannel('nxt:offPeak');
|
|
607
|
+
offPeakBC.unref();
|
|
608
|
+
offPeakBC.onmessage = () => {
|
|
609
|
+
for (const db of dbs) {
|
|
610
|
+
try {
|
|
611
|
+
db.purgeStale();
|
|
612
|
+
}
|
|
613
|
+
catch (err) {
|
|
614
|
+
if (db.listenerCount('error') > 0) {
|
|
615
|
+
db.emit('error', err);
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
process.emitWarning(err);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
};
|
|
937
623
|
}
|
|
938
|
-
|
|
939
624
|
/** @deprecated Use `Cache` instead. */
|
|
940
|
-
export const AsyncCache = Cache
|
|
941
|
-
|
|
942
|
-
|
|
625
|
+
export const AsyncCache = Cache;
|
|
626
|
+
//# sourceMappingURL=index.js.map
|