@nxtedition/cache 2.1.12 → 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/README.md +33 -27
- package/lib/index.d.ts +2 -10
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +541 -897
- 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 +83 -118
- package/lib/memory.js.map +1 -0
- package/package.json +10 -12
package/lib/index.js
CHANGED
|
@@ -1,235 +1,124 @@
|
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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,693 +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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
672
|
-
|
|
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
|
-
|
|
681
|
-
this.#lockSet.add(hash % this.#lockArray.length)
|
|
269
|
+
catch (err) {
|
|
270
|
+
this.#emitError(err);
|
|
682
271
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
break
|
|
272
|
+
try {
|
|
273
|
+
this.#database?.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
686
274
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
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
|
-
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
|
-
|
|
894
|
-
{
|
|
895
|
-
const promise = this.#dedupe.get(key)
|
|
896
|
-
if (promise !== undefined && promise !== selfPromise) {
|
|
897
|
-
return promise
|
|
898
|
-
}
|
|
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);
|
|
899
369
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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);
|
|
913
559
|
}
|
|
914
|
-
}
|
|
915
560
|
}
|
|
916
|
-
|
|
917
561
|
{
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
+
};
|
|
933
579
|
}
|
|
934
|
-
|
|
935
580
|
/** @deprecated Use `Cache` instead. */
|
|
936
|
-
export const AsyncCache = Cache
|
|
937
|
-
|
|
938
|
-
|
|
581
|
+
export const AsyncCache = Cache;
|
|
582
|
+
//# sourceMappingURL=index.js.map
|