@nxtedition/cache 2.1.13 → 2.1.14

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