@rivetkit/sqlite-vfs 2.1.5 → 2.1.6-rc.1
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/dist/tsup/index.cjs +739 -101
- package/dist/tsup/index.cjs.map +1 -1
- package/dist/tsup/index.d.cts +136 -6
- package/dist/tsup/index.d.ts +136 -6
- package/dist/tsup/index.js +737 -99
- package/dist/tsup/index.js.map +1 -1
- package/package.json +4 -3
- package/src/generated/empty-db-page.ts +23 -0
- package/src/index.ts +3 -0
- package/src/kv.ts +18 -3
- package/src/pool.ts +495 -0
- package/src/vfs.ts +604 -131
- package/src/wasm.d.ts +60 -0
package/dist/tsup/index.js
CHANGED
|
@@ -40,6 +40,14 @@ function getChunkKey(fileTag, chunkIndex) {
|
|
|
40
40
|
return key;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// src/generated/empty-db-page.ts
|
|
44
|
+
var HEADER_PREFIX = new Uint8Array([83, 81, 76, 105, 116, 101, 32, 102, 111, 114, 109, 97, 116, 32, 51, 0, 16, 0, 1, 1, 0, 64, 32, 32, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 46, 138, 17, 13, 0, 0, 0, 0, 16, 0, 0]);
|
|
45
|
+
var EMPTY_DB_PAGE = (() => {
|
|
46
|
+
const page = new Uint8Array(4096);
|
|
47
|
+
page.set(HEADER_PREFIX);
|
|
48
|
+
return page;
|
|
49
|
+
})();
|
|
50
|
+
|
|
43
51
|
// schemas/file-meta/versioned.ts
|
|
44
52
|
import { createVersionedDataHandler } from "vbare";
|
|
45
53
|
|
|
@@ -103,6 +111,11 @@ var MAX_CHUNK_INDEX = 4294967295;
|
|
|
103
111
|
var MAX_FILE_SIZE_BYTES = (MAX_CHUNK_INDEX + 1) * CHUNK_SIZE;
|
|
104
112
|
var MAX_FILE_SIZE_HI32 = Math.floor(MAX_FILE_SIZE_BYTES / UINT32_SIZE);
|
|
105
113
|
var MAX_FILE_SIZE_LO32 = MAX_FILE_SIZE_BYTES % UINT32_SIZE;
|
|
114
|
+
var KV_MAX_BATCH_KEYS = 128;
|
|
115
|
+
var SQLITE_IOCAP_BATCH_ATOMIC = 16384;
|
|
116
|
+
var SQLITE_FCNTL_BEGIN_ATOMIC_WRITE = 31;
|
|
117
|
+
var SQLITE_FCNTL_COMMIT_ATOMIC_WRITE = 32;
|
|
118
|
+
var SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE = 33;
|
|
106
119
|
var SQLITE_ASYNC_METHODS = /* @__PURE__ */ new Set([
|
|
107
120
|
"xOpen",
|
|
108
121
|
"xClose",
|
|
@@ -112,7 +125,8 @@ var SQLITE_ASYNC_METHODS = /* @__PURE__ */ new Set([
|
|
|
112
125
|
"xSync",
|
|
113
126
|
"xFileSize",
|
|
114
127
|
"xDelete",
|
|
115
|
-
"xAccess"
|
|
128
|
+
"xAccess",
|
|
129
|
+
"xFileControl"
|
|
116
130
|
]);
|
|
117
131
|
function isSqliteEsmFactory(value) {
|
|
118
132
|
return typeof value === "function";
|
|
@@ -124,18 +138,34 @@ function isSQLiteModule(value) {
|
|
|
124
138
|
const candidate = value;
|
|
125
139
|
return typeof candidate.UTF8ToString === "function" && candidate.HEAPU8 instanceof Uint8Array;
|
|
126
140
|
}
|
|
127
|
-
async function loadSqliteRuntime() {
|
|
128
|
-
const specifier = ["@rivetkit/sqlite", "dist", "wa-sqlite-async.mjs"].join(
|
|
141
|
+
async function loadSqliteRuntime(wasmModule) {
|
|
142
|
+
const specifier = ["@rivetkit/sqlite", "dist", "wa-sqlite-async.mjs"].join(
|
|
143
|
+
"/"
|
|
144
|
+
);
|
|
129
145
|
const sqliteModule = await import(specifier);
|
|
130
146
|
if (!isSqliteEsmFactory(sqliteModule.default)) {
|
|
131
147
|
throw new Error("Invalid SQLite ESM factory export");
|
|
132
148
|
}
|
|
133
149
|
const sqliteEsmFactory = sqliteModule.default;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
150
|
+
let module;
|
|
151
|
+
if (wasmModule) {
|
|
152
|
+
module = await sqliteEsmFactory({
|
|
153
|
+
instantiateWasm(imports, receiveInstance) {
|
|
154
|
+
WebAssembly.instantiate(wasmModule, imports).then((instance) => {
|
|
155
|
+
receiveInstance(instance);
|
|
156
|
+
});
|
|
157
|
+
return {};
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
const require2 = createRequire(import.meta.url);
|
|
162
|
+
const sqliteDistPath = "@rivetkit/sqlite/dist/";
|
|
163
|
+
const wasmPath = require2.resolve(
|
|
164
|
+
sqliteDistPath + "wa-sqlite-async.wasm"
|
|
165
|
+
);
|
|
166
|
+
const wasmBinary = readFileSync(wasmPath);
|
|
167
|
+
module = await sqliteEsmFactory({ wasmBinary });
|
|
168
|
+
}
|
|
139
169
|
if (!isSQLiteModule(module)) {
|
|
140
170
|
throw new Error("Invalid SQLite runtime module");
|
|
141
171
|
}
|
|
@@ -189,6 +219,7 @@ var Database = class {
|
|
|
189
219
|
#fileName;
|
|
190
220
|
#onClose;
|
|
191
221
|
#sqliteMutex;
|
|
222
|
+
#closed = false;
|
|
192
223
|
constructor(sqlite3, handle, fileName, onClose, sqliteMutex) {
|
|
193
224
|
this.#sqlite3 = sqlite3;
|
|
194
225
|
this.#handle = handle;
|
|
@@ -213,7 +244,10 @@ var Database = class {
|
|
|
213
244
|
*/
|
|
214
245
|
async run(sql, params) {
|
|
215
246
|
await this.#sqliteMutex.run(async () => {
|
|
216
|
-
for await (const stmt of this.#sqlite3.statements(
|
|
247
|
+
for await (const stmt of this.#sqlite3.statements(
|
|
248
|
+
this.#handle,
|
|
249
|
+
sql
|
|
250
|
+
)) {
|
|
217
251
|
if (params) {
|
|
218
252
|
this.#sqlite3.bind_collection(stmt, params);
|
|
219
253
|
}
|
|
@@ -232,7 +266,10 @@ var Database = class {
|
|
|
232
266
|
return this.#sqliteMutex.run(async () => {
|
|
233
267
|
const rows = [];
|
|
234
268
|
let columns = [];
|
|
235
|
-
for await (const stmt of this.#sqlite3.statements(
|
|
269
|
+
for await (const stmt of this.#sqlite3.statements(
|
|
270
|
+
this.#handle,
|
|
271
|
+
sql
|
|
272
|
+
)) {
|
|
236
273
|
if (params) {
|
|
237
274
|
this.#sqlite3.bind_collection(stmt, params);
|
|
238
275
|
}
|
|
@@ -250,10 +287,20 @@ var Database = class {
|
|
|
250
287
|
* Close the database
|
|
251
288
|
*/
|
|
252
289
|
async close() {
|
|
290
|
+
if (this.#closed) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.#closed = true;
|
|
253
294
|
await this.#sqliteMutex.run(async () => {
|
|
254
295
|
await this.#sqlite3.close(this.#handle);
|
|
255
296
|
});
|
|
256
|
-
this.#onClose();
|
|
297
|
+
await this.#onClose();
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Get the database file name
|
|
301
|
+
*/
|
|
302
|
+
get fileName() {
|
|
303
|
+
return this.#fileName;
|
|
257
304
|
}
|
|
258
305
|
/**
|
|
259
306
|
* Get the raw @rivetkit/sqlite API (for advanced usage)
|
|
@@ -276,8 +323,11 @@ var SqliteVfs = class {
|
|
|
276
323
|
#sqliteMutex = new AsyncMutex();
|
|
277
324
|
#instanceId;
|
|
278
325
|
#destroyed = false;
|
|
279
|
-
|
|
326
|
+
#openDatabases = /* @__PURE__ */ new Set();
|
|
327
|
+
#wasmModule;
|
|
328
|
+
constructor(wasmModule) {
|
|
280
329
|
this.#instanceId = crypto.randomUUID().replace(/-/g, "").slice(0, 8);
|
|
330
|
+
this.#wasmModule = wasmModule;
|
|
281
331
|
}
|
|
282
332
|
/**
|
|
283
333
|
* Initialize @rivetkit/sqlite and VFS (called once per instance)
|
|
@@ -291,7 +341,9 @@ var SqliteVfs = class {
|
|
|
291
341
|
}
|
|
292
342
|
if (!this.#initPromise) {
|
|
293
343
|
this.#initPromise = (async () => {
|
|
294
|
-
const { sqlite3, module } = await loadSqliteRuntime(
|
|
344
|
+
const { sqlite3, module } = await loadSqliteRuntime(
|
|
345
|
+
this.#wasmModule
|
|
346
|
+
);
|
|
295
347
|
if (this.#destroyed) {
|
|
296
348
|
return;
|
|
297
349
|
}
|
|
@@ -324,6 +376,13 @@ var SqliteVfs = class {
|
|
|
324
376
|
}
|
|
325
377
|
await this.#openMutex.acquire();
|
|
326
378
|
try {
|
|
379
|
+
for (const db2 of this.#openDatabases) {
|
|
380
|
+
if (db2.fileName === fileName) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`SqliteVfs: fileName "${fileName}" is already open on this instance`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
327
386
|
await this.#ensureInitialized();
|
|
328
387
|
if (!this.#sqlite3 || !this.#sqliteSystem) {
|
|
329
388
|
throw new Error("Failed to initialize SQLite");
|
|
@@ -338,20 +397,79 @@ var SqliteVfs = class {
|
|
|
338
397
|
sqliteSystem.name
|
|
339
398
|
)
|
|
340
399
|
);
|
|
341
|
-
|
|
342
|
-
|
|
400
|
+
await this.#sqliteMutex.run(async () => {
|
|
401
|
+
await sqlite3.exec(db, "PRAGMA page_size = 4096");
|
|
402
|
+
await sqlite3.exec(db, "PRAGMA journal_mode = DELETE");
|
|
403
|
+
await sqlite3.exec(db, "PRAGMA synchronous = NORMAL");
|
|
404
|
+
await sqlite3.exec(db, "PRAGMA temp_store = MEMORY");
|
|
405
|
+
await sqlite3.exec(db, "PRAGMA auto_vacuum = NONE");
|
|
406
|
+
await sqlite3.exec(db, "PRAGMA locking_mode = EXCLUSIVE");
|
|
407
|
+
});
|
|
408
|
+
const onClose = async () => {
|
|
409
|
+
this.#openDatabases.delete(database);
|
|
410
|
+
await this.#openMutex.run(async () => {
|
|
411
|
+
sqliteSystem.unregisterFile(fileName);
|
|
412
|
+
});
|
|
343
413
|
};
|
|
344
|
-
|
|
414
|
+
const database = new Database(
|
|
345
415
|
sqlite3,
|
|
346
416
|
db,
|
|
347
417
|
fileName,
|
|
348
418
|
onClose,
|
|
349
419
|
this.#sqliteMutex
|
|
350
420
|
);
|
|
421
|
+
this.#openDatabases.add(database);
|
|
422
|
+
return database;
|
|
351
423
|
} finally {
|
|
352
424
|
this.#openMutex.release();
|
|
353
425
|
}
|
|
354
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Force-close all Database handles whose fileName exactly matches the
|
|
429
|
+
* given name. Snapshots the set to an array before iterating to avoid
|
|
430
|
+
* mutation during async iteration.
|
|
431
|
+
*
|
|
432
|
+
* Uses exact file name match because short names are numeric strings
|
|
433
|
+
* ('0', '1', ..., '10', '11', ...) and a prefix match like
|
|
434
|
+
* startsWith('1') would incorrectly match '10', '11', etc., causing
|
|
435
|
+
* cross-actor corruption. Sidecar files (-journal, -wal, -shm) are not
|
|
436
|
+
* tracked as separate Database handles, so prefix matching for sidecars
|
|
437
|
+
* is not needed.
|
|
438
|
+
*/
|
|
439
|
+
async forceCloseByFileName(fileName) {
|
|
440
|
+
const snapshot = [...this.#openDatabases];
|
|
441
|
+
let allSucceeded = true;
|
|
442
|
+
for (const db of snapshot) {
|
|
443
|
+
if (db.fileName === fileName) {
|
|
444
|
+
try {
|
|
445
|
+
await db.close();
|
|
446
|
+
} catch {
|
|
447
|
+
allSucceeded = false;
|
|
448
|
+
this.#openDatabases.delete(db);
|
|
449
|
+
const sqliteSystem = this.#sqliteSystem;
|
|
450
|
+
if (sqliteSystem) {
|
|
451
|
+
await this.#openMutex.run(async () => {
|
|
452
|
+
sqliteSystem.unregisterFile(db.fileName);
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return { allSucceeded };
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Force-close all open Database handles. Best-effort: errors are
|
|
462
|
+
* swallowed so this is safe to call during instance teardown.
|
|
463
|
+
*/
|
|
464
|
+
async forceCloseAll() {
|
|
465
|
+
const snapshot = [...this.#openDatabases];
|
|
466
|
+
for (const db of snapshot) {
|
|
467
|
+
try {
|
|
468
|
+
await db.close();
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
355
473
|
/**
|
|
356
474
|
* Tears down this VFS instance and releases internal references.
|
|
357
475
|
*/
|
|
@@ -385,8 +503,7 @@ var SqliteSystem = class {
|
|
|
385
503
|
name;
|
|
386
504
|
mxPathName = SQLITE_MAX_PATHNAME_BYTES;
|
|
387
505
|
mxPathname = SQLITE_MAX_PATHNAME_BYTES;
|
|
388
|
-
#
|
|
389
|
-
#mainFileOptions = null;
|
|
506
|
+
#registeredFiles = /* @__PURE__ */ new Map();
|
|
390
507
|
#openFiles = /* @__PURE__ */ new Map();
|
|
391
508
|
#sqlite3;
|
|
392
509
|
#module;
|
|
@@ -401,8 +518,7 @@ var SqliteSystem = class {
|
|
|
401
518
|
}
|
|
402
519
|
async close() {
|
|
403
520
|
this.#openFiles.clear();
|
|
404
|
-
this.#
|
|
405
|
-
this.#mainFileOptions = null;
|
|
521
|
+
this.#registeredFiles.clear();
|
|
406
522
|
}
|
|
407
523
|
isReady() {
|
|
408
524
|
return true;
|
|
@@ -417,48 +533,45 @@ var SqliteSystem = class {
|
|
|
417
533
|
this.#sqlite3.vfs_register(this, false);
|
|
418
534
|
}
|
|
419
535
|
/**
|
|
420
|
-
* Registers a file with its KV options (before opening)
|
|
536
|
+
* Registers a file with its KV options (before opening).
|
|
421
537
|
*/
|
|
422
538
|
registerFile(fileName, options) {
|
|
423
|
-
|
|
424
|
-
this.#mainFileName = fileName;
|
|
425
|
-
this.#mainFileOptions = options;
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
if (this.#mainFileName !== fileName) {
|
|
429
|
-
throw new Error(
|
|
430
|
-
`SqliteSystem is actor-scoped and expects one main file. Got ${fileName}, expected ${this.#mainFileName}.`
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
this.#mainFileOptions = options;
|
|
539
|
+
this.#registeredFiles.set(fileName, options);
|
|
434
540
|
}
|
|
435
541
|
/**
|
|
436
|
-
* Unregisters a file's KV options (after closing)
|
|
542
|
+
* Unregisters a file's KV options (after closing).
|
|
437
543
|
*/
|
|
438
544
|
unregisterFile(fileName) {
|
|
439
|
-
|
|
440
|
-
this.#mainFileName = null;
|
|
441
|
-
this.#mainFileOptions = null;
|
|
442
|
-
}
|
|
545
|
+
this.#registeredFiles.delete(fileName);
|
|
443
546
|
}
|
|
444
547
|
/**
|
|
445
|
-
* Resolve file path to
|
|
548
|
+
* Resolve file path to a registered database file or one of its SQLite
|
|
549
|
+
* sidecars (-journal, -wal, -shm). File tags are reused across files
|
|
550
|
+
* because each file's KvVfsOptions routes to a separate KV namespace.
|
|
446
551
|
*/
|
|
447
552
|
#resolveFile(path) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (path === this.#mainFileName) {
|
|
452
|
-
return { options: this.#mainFileOptions, fileTag: FILE_TAG_MAIN };
|
|
453
|
-
}
|
|
454
|
-
if (path === `${this.#mainFileName}-journal`) {
|
|
455
|
-
return { options: this.#mainFileOptions, fileTag: FILE_TAG_JOURNAL };
|
|
553
|
+
const directOptions = this.#registeredFiles.get(path);
|
|
554
|
+
if (directOptions) {
|
|
555
|
+
return { options: directOptions, fileTag: FILE_TAG_MAIN };
|
|
456
556
|
}
|
|
457
|
-
if (path
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
557
|
+
if (path.endsWith("-journal")) {
|
|
558
|
+
const baseName = path.slice(0, -8);
|
|
559
|
+
const options = this.#registeredFiles.get(baseName);
|
|
560
|
+
if (options) {
|
|
561
|
+
return { options, fileTag: FILE_TAG_JOURNAL };
|
|
562
|
+
}
|
|
563
|
+
} else if (path.endsWith("-wal")) {
|
|
564
|
+
const baseName = path.slice(0, -4);
|
|
565
|
+
const options = this.#registeredFiles.get(baseName);
|
|
566
|
+
if (options) {
|
|
567
|
+
return { options, fileTag: FILE_TAG_WAL };
|
|
568
|
+
}
|
|
569
|
+
} else if (path.endsWith("-shm")) {
|
|
570
|
+
const baseName = path.slice(0, -4);
|
|
571
|
+
const options = this.#registeredFiles.get(baseName);
|
|
572
|
+
if (options) {
|
|
573
|
+
return { options, fileTag: FILE_TAG_SHM };
|
|
574
|
+
}
|
|
462
575
|
}
|
|
463
576
|
return null;
|
|
464
577
|
}
|
|
@@ -467,11 +580,12 @@ var SqliteSystem = class {
|
|
|
467
580
|
if (resolved) {
|
|
468
581
|
return resolved;
|
|
469
582
|
}
|
|
470
|
-
if (
|
|
583
|
+
if (this.#registeredFiles.size === 0) {
|
|
471
584
|
throw new Error(`No KV options registered for file: ${path}`);
|
|
472
585
|
}
|
|
586
|
+
const registered = Array.from(this.#registeredFiles.keys()).join(", ");
|
|
473
587
|
throw new Error(
|
|
474
|
-
`Unsupported SQLite file path ${path}.
|
|
588
|
+
`Unsupported SQLite file path ${path}. Registered base names: ${registered}.`
|
|
475
589
|
);
|
|
476
590
|
}
|
|
477
591
|
#chunkKey(file, chunkIndex) {
|
|
@@ -484,7 +598,12 @@ var SqliteSystem = class {
|
|
|
484
598
|
}
|
|
485
599
|
const { options, fileTag } = this.#resolveFileOrThrow(path);
|
|
486
600
|
const metaKey = getMetaKey(fileTag);
|
|
487
|
-
|
|
601
|
+
let sizeData;
|
|
602
|
+
try {
|
|
603
|
+
sizeData = await options.get(metaKey);
|
|
604
|
+
} catch {
|
|
605
|
+
return VFS.SQLITE_CANTOPEN;
|
|
606
|
+
}
|
|
488
607
|
let size;
|
|
489
608
|
if (sizeData) {
|
|
490
609
|
size = decodeFileMeta2(sizeData);
|
|
@@ -492,8 +611,25 @@ var SqliteSystem = class {
|
|
|
492
611
|
return VFS.SQLITE_IOERR;
|
|
493
612
|
}
|
|
494
613
|
} else if (flags & VFS.SQLITE_OPEN_CREATE) {
|
|
495
|
-
|
|
496
|
-
|
|
614
|
+
if (fileTag === FILE_TAG_MAIN) {
|
|
615
|
+
const chunkKey = getChunkKey(fileTag, 0);
|
|
616
|
+
size = EMPTY_DB_PAGE.length;
|
|
617
|
+
try {
|
|
618
|
+
await options.putBatch([
|
|
619
|
+
[chunkKey, EMPTY_DB_PAGE],
|
|
620
|
+
[metaKey, encodeFileMeta2(size)]
|
|
621
|
+
]);
|
|
622
|
+
} catch {
|
|
623
|
+
return VFS.SQLITE_CANTOPEN;
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
size = 0;
|
|
627
|
+
try {
|
|
628
|
+
await options.put(metaKey, encodeFileMeta2(size));
|
|
629
|
+
} catch {
|
|
630
|
+
return VFS.SQLITE_CANTOPEN;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
497
633
|
} else {
|
|
498
634
|
return VFS.SQLITE_CANTOPEN;
|
|
499
635
|
}
|
|
@@ -504,7 +640,10 @@ var SqliteSystem = class {
|
|
|
504
640
|
size,
|
|
505
641
|
metaDirty: false,
|
|
506
642
|
flags,
|
|
507
|
-
options
|
|
643
|
+
options,
|
|
644
|
+
batchMode: false,
|
|
645
|
+
dirtyBuffer: null,
|
|
646
|
+
savedFileSize: 0
|
|
508
647
|
});
|
|
509
648
|
this.#writeInt32(pOutFlags, flags);
|
|
510
649
|
return VFS.SQLITE_OK;
|
|
@@ -514,14 +653,19 @@ var SqliteSystem = class {
|
|
|
514
653
|
if (!file) {
|
|
515
654
|
return VFS.SQLITE_OK;
|
|
516
655
|
}
|
|
517
|
-
|
|
518
|
-
|
|
656
|
+
try {
|
|
657
|
+
if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
|
|
658
|
+
await this.#delete(file.path);
|
|
659
|
+
} else if (file.metaDirty) {
|
|
660
|
+
await file.options.put(
|
|
661
|
+
file.metaKey,
|
|
662
|
+
encodeFileMeta2(file.size)
|
|
663
|
+
);
|
|
664
|
+
file.metaDirty = false;
|
|
665
|
+
}
|
|
666
|
+
} catch {
|
|
519
667
|
this.#openFiles.delete(fileId);
|
|
520
|
-
return VFS.
|
|
521
|
-
}
|
|
522
|
-
if (file.metaDirty) {
|
|
523
|
-
await file.options.put(file.metaKey, encodeFileMeta2(file.size));
|
|
524
|
-
file.metaDirty = false;
|
|
668
|
+
return VFS.SQLITE_IOERR;
|
|
525
669
|
}
|
|
526
670
|
this.#openFiles.delete(fileId);
|
|
527
671
|
return VFS.SQLITE_OK;
|
|
@@ -534,7 +678,7 @@ var SqliteSystem = class {
|
|
|
534
678
|
if (!file) {
|
|
535
679
|
return VFS.SQLITE_IOERR_READ;
|
|
536
680
|
}
|
|
537
|
-
|
|
681
|
+
let data = this.#module.HEAPU8.subarray(pData, pData + iAmt);
|
|
538
682
|
const options = file.options;
|
|
539
683
|
const requestedLength = iAmt;
|
|
540
684
|
const iOffset = delegalize(iOffsetLo, iOffsetHi);
|
|
@@ -547,14 +691,31 @@ var SqliteSystem = class {
|
|
|
547
691
|
return VFS.SQLITE_IOERR_SHORT_READ;
|
|
548
692
|
}
|
|
549
693
|
const startChunk = Math.floor(iOffset / CHUNK_SIZE);
|
|
550
|
-
const endChunk = Math.floor(
|
|
694
|
+
const endChunk = Math.floor(
|
|
695
|
+
(iOffset + requestedLength - 1) / CHUNK_SIZE
|
|
696
|
+
);
|
|
551
697
|
const chunkKeys = [];
|
|
698
|
+
const chunkIndexToBuffered = /* @__PURE__ */ new Map();
|
|
552
699
|
for (let i = startChunk; i <= endChunk; i++) {
|
|
700
|
+
if (file.batchMode && file.dirtyBuffer) {
|
|
701
|
+
const buffered = file.dirtyBuffer.get(i);
|
|
702
|
+
if (buffered) {
|
|
703
|
+
chunkIndexToBuffered.set(i, buffered);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
553
707
|
chunkKeys.push(this.#chunkKey(file, i));
|
|
554
708
|
}
|
|
555
|
-
|
|
709
|
+
let kvChunks;
|
|
710
|
+
try {
|
|
711
|
+
kvChunks = chunkKeys.length > 0 ? await options.getBatch(chunkKeys) : [];
|
|
712
|
+
} catch {
|
|
713
|
+
return VFS.SQLITE_IOERR_READ;
|
|
714
|
+
}
|
|
715
|
+
data = this.#module.HEAPU8.subarray(pData, pData + iAmt);
|
|
716
|
+
let kvIdx = 0;
|
|
556
717
|
for (let i = startChunk; i <= endChunk; i++) {
|
|
557
|
-
const chunkData =
|
|
718
|
+
const chunkData = chunkIndexToBuffered.get(i) ?? kvChunks[kvIdx++];
|
|
558
719
|
const chunkOffset = i * CHUNK_SIZE;
|
|
559
720
|
const readStart = Math.max(0, iOffset - chunkOffset);
|
|
560
721
|
const readEnd = Math.min(
|
|
@@ -566,7 +727,10 @@ var SqliteSystem = class {
|
|
|
566
727
|
const sourceEnd = Math.min(readEnd, chunkData.length);
|
|
567
728
|
const destStart = chunkOffset + readStart - iOffset;
|
|
568
729
|
if (sourceEnd > sourceStart) {
|
|
569
|
-
data.set(
|
|
730
|
+
data.set(
|
|
731
|
+
chunkData.subarray(sourceStart, sourceEnd),
|
|
732
|
+
destStart
|
|
733
|
+
);
|
|
570
734
|
}
|
|
571
735
|
if (sourceEnd < readEnd) {
|
|
572
736
|
const zeroStart = destStart + (sourceEnd - sourceStart);
|
|
@@ -594,7 +758,7 @@ var SqliteSystem = class {
|
|
|
594
758
|
if (!file) {
|
|
595
759
|
return VFS.SQLITE_IOERR_WRITE;
|
|
596
760
|
}
|
|
597
|
-
|
|
761
|
+
let data = this.#module.HEAPU8.subarray(pData, pData + iAmt);
|
|
598
762
|
const iOffset = delegalize(iOffsetLo, iOffsetHi);
|
|
599
763
|
if (iOffset < 0) {
|
|
600
764
|
return VFS.SQLITE_IOERR_WRITE;
|
|
@@ -607,6 +771,26 @@ var SqliteSystem = class {
|
|
|
607
771
|
}
|
|
608
772
|
const startChunk = Math.floor(iOffset / CHUNK_SIZE);
|
|
609
773
|
const endChunk = Math.floor((iOffset + writeLength - 1) / CHUNK_SIZE);
|
|
774
|
+
if (file.batchMode && file.dirtyBuffer) {
|
|
775
|
+
for (let i = startChunk; i <= endChunk; i++) {
|
|
776
|
+
const chunkOffset = i * CHUNK_SIZE;
|
|
777
|
+
const sourceStart = Math.max(0, chunkOffset - iOffset);
|
|
778
|
+
const sourceEnd = Math.min(
|
|
779
|
+
writeLength,
|
|
780
|
+
chunkOffset + CHUNK_SIZE - iOffset
|
|
781
|
+
);
|
|
782
|
+
file.dirtyBuffer.set(
|
|
783
|
+
i,
|
|
784
|
+
data.subarray(sourceStart, sourceEnd).slice()
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
const newSize2 = Math.max(file.size, writeEndOffset);
|
|
788
|
+
if (newSize2 !== file.size) {
|
|
789
|
+
file.size = newSize2;
|
|
790
|
+
file.metaDirty = true;
|
|
791
|
+
}
|
|
792
|
+
return VFS.SQLITE_OK;
|
|
793
|
+
}
|
|
610
794
|
const plans = [];
|
|
611
795
|
const chunkKeysToFetch = [];
|
|
612
796
|
for (let i = startChunk; i <= endChunk; i++) {
|
|
@@ -635,23 +819,35 @@ var SqliteSystem = class {
|
|
|
635
819
|
existingChunkIndex
|
|
636
820
|
});
|
|
637
821
|
}
|
|
638
|
-
|
|
822
|
+
let existingChunks;
|
|
823
|
+
try {
|
|
824
|
+
existingChunks = chunkKeysToFetch.length > 0 ? await options.getBatch(chunkKeysToFetch) : [];
|
|
825
|
+
} catch {
|
|
826
|
+
return VFS.SQLITE_IOERR_WRITE;
|
|
827
|
+
}
|
|
828
|
+
data = this.#module.HEAPU8.subarray(pData, pData + iAmt);
|
|
639
829
|
const entriesToWrite = [];
|
|
640
830
|
for (const plan of plans) {
|
|
641
831
|
const existingChunk = plan.existingChunkIndex >= 0 ? existingChunks[plan.existingChunkIndex] : null;
|
|
642
832
|
let newChunk;
|
|
643
833
|
if (existingChunk) {
|
|
644
|
-
newChunk = new Uint8Array(
|
|
834
|
+
newChunk = new Uint8Array(
|
|
835
|
+
Math.max(existingChunk.length, plan.writeEnd)
|
|
836
|
+
);
|
|
645
837
|
newChunk.set(existingChunk);
|
|
646
838
|
} else {
|
|
647
839
|
newChunk = new Uint8Array(plan.writeEnd);
|
|
648
840
|
}
|
|
649
841
|
const sourceStart = plan.chunkOffset + plan.writeStart - iOffset;
|
|
650
842
|
const sourceEnd = sourceStart + (plan.writeEnd - plan.writeStart);
|
|
651
|
-
newChunk.set(
|
|
843
|
+
newChunk.set(
|
|
844
|
+
data.subarray(sourceStart, sourceEnd),
|
|
845
|
+
plan.writeStart
|
|
846
|
+
);
|
|
652
847
|
entriesToWrite.push([plan.chunkKey, newChunk]);
|
|
653
848
|
}
|
|
654
849
|
const previousSize = file.size;
|
|
850
|
+
const previousMetaDirty = file.metaDirty;
|
|
655
851
|
const newSize = Math.max(file.size, writeEndOffset);
|
|
656
852
|
if (newSize !== previousSize) {
|
|
657
853
|
file.size = newSize;
|
|
@@ -660,7 +856,13 @@ var SqliteSystem = class {
|
|
|
660
856
|
if (file.metaDirty) {
|
|
661
857
|
entriesToWrite.push([file.metaKey, encodeFileMeta2(file.size)]);
|
|
662
858
|
}
|
|
663
|
-
|
|
859
|
+
try {
|
|
860
|
+
await options.putBatch(entriesToWrite);
|
|
861
|
+
} catch {
|
|
862
|
+
file.size = previousSize;
|
|
863
|
+
file.metaDirty = previousMetaDirty;
|
|
864
|
+
return VFS.SQLITE_IOERR_WRITE;
|
|
865
|
+
}
|
|
664
866
|
if (file.metaDirty) {
|
|
665
867
|
file.metaDirty = false;
|
|
666
868
|
}
|
|
@@ -678,34 +880,57 @@ var SqliteSystem = class {
|
|
|
678
880
|
const options = file.options;
|
|
679
881
|
if (size >= file.size) {
|
|
680
882
|
if (size > file.size) {
|
|
883
|
+
const previousSize2 = file.size;
|
|
884
|
+
const previousMetaDirty2 = file.metaDirty;
|
|
681
885
|
file.size = size;
|
|
682
886
|
file.metaDirty = true;
|
|
683
|
-
|
|
887
|
+
try {
|
|
888
|
+
await options.put(file.metaKey, encodeFileMeta2(file.size));
|
|
889
|
+
} catch {
|
|
890
|
+
file.size = previousSize2;
|
|
891
|
+
file.metaDirty = previousMetaDirty2;
|
|
892
|
+
return VFS.SQLITE_IOERR_TRUNCATE;
|
|
893
|
+
}
|
|
684
894
|
file.metaDirty = false;
|
|
685
895
|
}
|
|
686
896
|
return VFS.SQLITE_OK;
|
|
687
897
|
}
|
|
688
898
|
const lastChunkToKeep = Math.floor((size - 1) / CHUNK_SIZE);
|
|
689
899
|
const lastExistingChunk = Math.floor((file.size - 1) / CHUNK_SIZE);
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
keysToDelete.push(this.#chunkKey(file, i));
|
|
693
|
-
}
|
|
694
|
-
if (keysToDelete.length > 0) {
|
|
695
|
-
await options.deleteBatch(keysToDelete);
|
|
696
|
-
}
|
|
697
|
-
if (size > 0 && size % CHUNK_SIZE !== 0) {
|
|
698
|
-
const lastChunkKey = this.#chunkKey(file, lastChunkToKeep);
|
|
699
|
-
const lastChunkData = await options.get(lastChunkKey);
|
|
700
|
-
if (lastChunkData && lastChunkData.length > size % CHUNK_SIZE) {
|
|
701
|
-
const truncatedChunk = lastChunkData.subarray(0, size % CHUNK_SIZE);
|
|
702
|
-
await options.put(lastChunkKey, truncatedChunk);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
900
|
+
const previousSize = file.size;
|
|
901
|
+
const previousMetaDirty = file.metaDirty;
|
|
705
902
|
file.size = size;
|
|
706
903
|
file.metaDirty = true;
|
|
707
|
-
|
|
904
|
+
try {
|
|
905
|
+
await options.put(file.metaKey, encodeFileMeta2(file.size));
|
|
906
|
+
} catch {
|
|
907
|
+
file.size = previousSize;
|
|
908
|
+
file.metaDirty = previousMetaDirty;
|
|
909
|
+
return VFS.SQLITE_IOERR_TRUNCATE;
|
|
910
|
+
}
|
|
708
911
|
file.metaDirty = false;
|
|
912
|
+
try {
|
|
913
|
+
if (size > 0 && size % CHUNK_SIZE !== 0) {
|
|
914
|
+
const lastChunkKey = this.#chunkKey(file, lastChunkToKeep);
|
|
915
|
+
const lastChunkData = await options.get(lastChunkKey);
|
|
916
|
+
if (lastChunkData && lastChunkData.length > size % CHUNK_SIZE) {
|
|
917
|
+
const truncatedChunk = lastChunkData.subarray(
|
|
918
|
+
0,
|
|
919
|
+
size % CHUNK_SIZE
|
|
920
|
+
);
|
|
921
|
+
await options.put(lastChunkKey, truncatedChunk);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const keysToDelete = [];
|
|
925
|
+
for (let i = lastChunkToKeep + 1; i <= lastExistingChunk; i++) {
|
|
926
|
+
keysToDelete.push(this.#chunkKey(file, i));
|
|
927
|
+
}
|
|
928
|
+
for (let b = 0; b < keysToDelete.length; b += KV_MAX_BATCH_KEYS) {
|
|
929
|
+
await options.deleteBatch(keysToDelete.slice(b, b + KV_MAX_BATCH_KEYS));
|
|
930
|
+
}
|
|
931
|
+
} catch {
|
|
932
|
+
return VFS.SQLITE_IOERR_TRUNCATE;
|
|
933
|
+
}
|
|
709
934
|
return VFS.SQLITE_OK;
|
|
710
935
|
}
|
|
711
936
|
async xSync(fileId, _flags) {
|
|
@@ -713,7 +938,11 @@ var SqliteSystem = class {
|
|
|
713
938
|
if (!file || !file.metaDirty) {
|
|
714
939
|
return VFS.SQLITE_OK;
|
|
715
940
|
}
|
|
716
|
-
|
|
941
|
+
try {
|
|
942
|
+
await file.options.put(file.metaKey, encodeFileMeta2(file.size));
|
|
943
|
+
} catch {
|
|
944
|
+
return VFS.SQLITE_IOERR_FSYNC;
|
|
945
|
+
}
|
|
717
946
|
file.metaDirty = false;
|
|
718
947
|
return VFS.SQLITE_OK;
|
|
719
948
|
}
|
|
@@ -726,7 +955,11 @@ var SqliteSystem = class {
|
|
|
726
955
|
return VFS.SQLITE_OK;
|
|
727
956
|
}
|
|
728
957
|
async xDelete(_pVfs, zName, _syncDir) {
|
|
729
|
-
|
|
958
|
+
try {
|
|
959
|
+
await this.#delete(this.#module.UTF8ToString(zName));
|
|
960
|
+
} catch {
|
|
961
|
+
return VFS.SQLITE_IOERR_DELETE;
|
|
962
|
+
}
|
|
730
963
|
return VFS.SQLITE_OK;
|
|
731
964
|
}
|
|
732
965
|
/**
|
|
@@ -745,7 +978,9 @@ var SqliteSystem = class {
|
|
|
745
978
|
for (let i = 0; i < numChunks; i++) {
|
|
746
979
|
keysToDelete.push(getChunkKey(fileTag, i));
|
|
747
980
|
}
|
|
748
|
-
|
|
981
|
+
for (let b = 0; b < keysToDelete.length; b += KV_MAX_BATCH_KEYS) {
|
|
982
|
+
await options.deleteBatch(keysToDelete.slice(b, b + KV_MAX_BATCH_KEYS));
|
|
983
|
+
}
|
|
749
984
|
}
|
|
750
985
|
async xAccess(_pVfs, zName, _flags, pResOut) {
|
|
751
986
|
const path = this.#module.UTF8ToString(zName);
|
|
@@ -755,7 +990,12 @@ var SqliteSystem = class {
|
|
|
755
990
|
return VFS.SQLITE_OK;
|
|
756
991
|
}
|
|
757
992
|
const compactMetaKey = getMetaKey(resolved.fileTag);
|
|
758
|
-
|
|
993
|
+
let metaData;
|
|
994
|
+
try {
|
|
995
|
+
metaData = await resolved.options.get(compactMetaKey);
|
|
996
|
+
} catch {
|
|
997
|
+
return VFS.SQLITE_IOERR_ACCESS;
|
|
998
|
+
}
|
|
759
999
|
this.#writeInt32(pResOut, metaData ? 1 : 0);
|
|
760
1000
|
return VFS.SQLITE_OK;
|
|
761
1001
|
}
|
|
@@ -769,11 +1009,72 @@ var SqliteSystem = class {
|
|
|
769
1009
|
xUnlock(_fileId, _flags) {
|
|
770
1010
|
return VFS.SQLITE_OK;
|
|
771
1011
|
}
|
|
772
|
-
xFileControl(
|
|
773
|
-
|
|
1012
|
+
async xFileControl(fileId, flags, _pArg) {
|
|
1013
|
+
switch (flags) {
|
|
1014
|
+
case SQLITE_FCNTL_BEGIN_ATOMIC_WRITE: {
|
|
1015
|
+
const file = this.#openFiles.get(fileId);
|
|
1016
|
+
if (!file) return VFS.SQLITE_NOTFOUND;
|
|
1017
|
+
file.savedFileSize = file.size;
|
|
1018
|
+
file.batchMode = true;
|
|
1019
|
+
file.metaDirty = false;
|
|
1020
|
+
file.dirtyBuffer = /* @__PURE__ */ new Map();
|
|
1021
|
+
return VFS.SQLITE_OK;
|
|
1022
|
+
}
|
|
1023
|
+
case SQLITE_FCNTL_COMMIT_ATOMIC_WRITE: {
|
|
1024
|
+
const file = this.#openFiles.get(fileId);
|
|
1025
|
+
if (!file) return VFS.SQLITE_NOTFOUND;
|
|
1026
|
+
const { dirtyBuffer, options } = file;
|
|
1027
|
+
const maxDirtyPages = file.metaDirty ? KV_MAX_BATCH_KEYS - 1 : KV_MAX_BATCH_KEYS;
|
|
1028
|
+
if (dirtyBuffer && dirtyBuffer.size > maxDirtyPages) {
|
|
1029
|
+
dirtyBuffer.clear();
|
|
1030
|
+
file.dirtyBuffer = null;
|
|
1031
|
+
file.size = file.savedFileSize;
|
|
1032
|
+
file.metaDirty = false;
|
|
1033
|
+
file.batchMode = false;
|
|
1034
|
+
return VFS.SQLITE_IOERR;
|
|
1035
|
+
}
|
|
1036
|
+
const entries = [];
|
|
1037
|
+
if (dirtyBuffer) {
|
|
1038
|
+
for (const [chunkIndex, data] of dirtyBuffer) {
|
|
1039
|
+
entries.push([this.#chunkKey(file, chunkIndex), data]);
|
|
1040
|
+
}
|
|
1041
|
+
dirtyBuffer.clear();
|
|
1042
|
+
}
|
|
1043
|
+
if (file.metaDirty) {
|
|
1044
|
+
entries.push([file.metaKey, encodeFileMeta2(file.size)]);
|
|
1045
|
+
}
|
|
1046
|
+
try {
|
|
1047
|
+
await options.putBatch(entries);
|
|
1048
|
+
} catch {
|
|
1049
|
+
file.dirtyBuffer = null;
|
|
1050
|
+
file.size = file.savedFileSize;
|
|
1051
|
+
file.metaDirty = false;
|
|
1052
|
+
file.batchMode = false;
|
|
1053
|
+
return VFS.SQLITE_IOERR;
|
|
1054
|
+
}
|
|
1055
|
+
file.dirtyBuffer = null;
|
|
1056
|
+
file.metaDirty = false;
|
|
1057
|
+
file.batchMode = false;
|
|
1058
|
+
return VFS.SQLITE_OK;
|
|
1059
|
+
}
|
|
1060
|
+
case SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE: {
|
|
1061
|
+
const file = this.#openFiles.get(fileId);
|
|
1062
|
+
if (!file || !file.batchMode) return VFS.SQLITE_OK;
|
|
1063
|
+
if (file.dirtyBuffer) {
|
|
1064
|
+
file.dirtyBuffer.clear();
|
|
1065
|
+
file.dirtyBuffer = null;
|
|
1066
|
+
}
|
|
1067
|
+
file.size = file.savedFileSize;
|
|
1068
|
+
file.metaDirty = false;
|
|
1069
|
+
file.batchMode = false;
|
|
1070
|
+
return VFS.SQLITE_OK;
|
|
1071
|
+
}
|
|
1072
|
+
default:
|
|
1073
|
+
return VFS.SQLITE_NOTFOUND;
|
|
1074
|
+
}
|
|
774
1075
|
}
|
|
775
1076
|
xDeviceCharacteristics(_fileId) {
|
|
776
|
-
return
|
|
1077
|
+
return SQLITE_IOCAP_BATCH_ATOMIC;
|
|
777
1078
|
}
|
|
778
1079
|
xFullPathname(_pVfs, zName, nOut, zOut) {
|
|
779
1080
|
const path = this.#module.UTF8ToString(zName);
|
|
@@ -850,8 +1151,345 @@ function delegalize(lo32, hi32) {
|
|
|
850
1151
|
}
|
|
851
1152
|
return hi * UINT32_SIZE + lo;
|
|
852
1153
|
}
|
|
1154
|
+
|
|
1155
|
+
// src/pool.ts
|
|
1156
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1157
|
+
import { createRequire as createRequire2 } from "module";
|
|
1158
|
+
var SqliteVfsPool = class {
|
|
1159
|
+
#config;
|
|
1160
|
+
#modulePromise = null;
|
|
1161
|
+
#instances = /* @__PURE__ */ new Set();
|
|
1162
|
+
#actorToInstance = /* @__PURE__ */ new Map();
|
|
1163
|
+
#actorToHandle = /* @__PURE__ */ new Map();
|
|
1164
|
+
#shuttingDown = false;
|
|
1165
|
+
constructor(config2) {
|
|
1166
|
+
if (!Number.isInteger(config2.actorsPerInstance) || config2.actorsPerInstance < 1) {
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
`actorsPerInstance must be a positive integer, got ${config2.actorsPerInstance}`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
this.#config = config2;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Compile the WASM module once and cache the promise. Subsequent calls
|
|
1175
|
+
* return the same promise, avoiding redundant compilation.
|
|
1176
|
+
*/
|
|
1177
|
+
#getModule() {
|
|
1178
|
+
if (!this.#modulePromise) {
|
|
1179
|
+
this.#modulePromise = (async () => {
|
|
1180
|
+
const require2 = createRequire2(import.meta.url);
|
|
1181
|
+
const wasmPath = require2.resolve(
|
|
1182
|
+
"@rivetkit/sqlite/dist/wa-sqlite-async.wasm"
|
|
1183
|
+
);
|
|
1184
|
+
const wasmBinary = readFileSync2(wasmPath);
|
|
1185
|
+
return WebAssembly.compile(wasmBinary);
|
|
1186
|
+
})();
|
|
1187
|
+
this.#modulePromise.catch(() => {
|
|
1188
|
+
this.#modulePromise = null;
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
return this.#modulePromise;
|
|
1192
|
+
}
|
|
1193
|
+
/** Number of live WASM instances in the pool. */
|
|
1194
|
+
get instanceCount() {
|
|
1195
|
+
return this.#instances.size;
|
|
1196
|
+
}
|
|
1197
|
+
/** Number of actors currently assigned to pool instances. */
|
|
1198
|
+
get actorCount() {
|
|
1199
|
+
return this.#actorToInstance.size;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Acquire a pooled VFS handle for the given actor. Returns a
|
|
1203
|
+
* PooledSqliteHandle with sticky assignment. If the actor is already
|
|
1204
|
+
* assigned, the existing handle is returned.
|
|
1205
|
+
*
|
|
1206
|
+
* Bin-packing: picks the instance with the most actors that still has
|
|
1207
|
+
* capacity. If all instances are full, creates a new one using the
|
|
1208
|
+
* cached WASM module.
|
|
1209
|
+
*/
|
|
1210
|
+
async acquire(actorId) {
|
|
1211
|
+
if (this.#shuttingDown) {
|
|
1212
|
+
throw new Error("SqliteVfsPool is shutting down");
|
|
1213
|
+
}
|
|
1214
|
+
const existingHandle = this.#actorToHandle.get(actorId);
|
|
1215
|
+
if (existingHandle) {
|
|
1216
|
+
return existingHandle;
|
|
1217
|
+
}
|
|
1218
|
+
let bestInstance = null;
|
|
1219
|
+
let bestCount = -1;
|
|
1220
|
+
for (const instance of this.#instances) {
|
|
1221
|
+
if (instance.destroying) continue;
|
|
1222
|
+
const count = instance.actors.size;
|
|
1223
|
+
if (count < this.#config.actorsPerInstance && count > bestCount) {
|
|
1224
|
+
bestInstance = instance;
|
|
1225
|
+
bestCount = count;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (!bestInstance) {
|
|
1229
|
+
const wasmModule = await this.#getModule();
|
|
1230
|
+
if (this.#shuttingDown) {
|
|
1231
|
+
throw new Error("SqliteVfsPool is shutting down");
|
|
1232
|
+
}
|
|
1233
|
+
const existingHandleAfterAwait = this.#actorToHandle.get(actorId);
|
|
1234
|
+
if (existingHandleAfterAwait) {
|
|
1235
|
+
return existingHandleAfterAwait;
|
|
1236
|
+
}
|
|
1237
|
+
for (const instance of this.#instances) {
|
|
1238
|
+
if (instance.destroying) continue;
|
|
1239
|
+
const count = instance.actors.size;
|
|
1240
|
+
if (count < this.#config.actorsPerInstance && count > bestCount) {
|
|
1241
|
+
bestInstance = instance;
|
|
1242
|
+
bestCount = count;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
if (!bestInstance) {
|
|
1246
|
+
const vfs = new SqliteVfs(wasmModule);
|
|
1247
|
+
bestInstance = {
|
|
1248
|
+
vfs,
|
|
1249
|
+
actors: /* @__PURE__ */ new Set(),
|
|
1250
|
+
shortNameCounter: 0,
|
|
1251
|
+
actorShortNames: /* @__PURE__ */ new Map(),
|
|
1252
|
+
availableShortNames: /* @__PURE__ */ new Set(),
|
|
1253
|
+
poisonedShortNames: /* @__PURE__ */ new Set(),
|
|
1254
|
+
opsInFlight: 0,
|
|
1255
|
+
idleTimer: null,
|
|
1256
|
+
destroying: false
|
|
1257
|
+
};
|
|
1258
|
+
this.#instances.add(bestInstance);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
this.#cancelIdleTimer(bestInstance);
|
|
1262
|
+
let shortName;
|
|
1263
|
+
const recycled = bestInstance.availableShortNames.values().next();
|
|
1264
|
+
if (!recycled.done) {
|
|
1265
|
+
shortName = recycled.value;
|
|
1266
|
+
bestInstance.availableShortNames.delete(shortName);
|
|
1267
|
+
} else {
|
|
1268
|
+
shortName = String(bestInstance.shortNameCounter++);
|
|
1269
|
+
}
|
|
1270
|
+
bestInstance.actors.add(actorId);
|
|
1271
|
+
bestInstance.actorShortNames.set(actorId, shortName);
|
|
1272
|
+
this.#actorToInstance.set(actorId, bestInstance);
|
|
1273
|
+
const handle = new PooledSqliteHandle(
|
|
1274
|
+
shortName,
|
|
1275
|
+
actorId,
|
|
1276
|
+
this
|
|
1277
|
+
);
|
|
1278
|
+
this.#actorToHandle.set(actorId, handle);
|
|
1279
|
+
return handle;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Release an actor's assignment from the pool. Force-closes all database
|
|
1283
|
+
* handles for the actor, recycles or poisons the short name, and
|
|
1284
|
+
* decrements the instance refcount.
|
|
1285
|
+
*/
|
|
1286
|
+
async release(actorId) {
|
|
1287
|
+
const instance = this.#actorToInstance.get(actorId);
|
|
1288
|
+
if (!instance) {
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const shortName = instance.actorShortNames.get(actorId);
|
|
1292
|
+
if (shortName === void 0) {
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
const { allSucceeded } = await instance.vfs.forceCloseByFileName(shortName);
|
|
1296
|
+
if (allSucceeded) {
|
|
1297
|
+
instance.availableShortNames.add(shortName);
|
|
1298
|
+
} else {
|
|
1299
|
+
instance.poisonedShortNames.add(shortName);
|
|
1300
|
+
}
|
|
1301
|
+
instance.actors.delete(actorId);
|
|
1302
|
+
instance.actorShortNames.delete(actorId);
|
|
1303
|
+
this.#actorToInstance.delete(actorId);
|
|
1304
|
+
this.#actorToHandle.delete(actorId);
|
|
1305
|
+
if (instance.actors.size === 0 && instance.opsInFlight === 0 && !this.#shuttingDown) {
|
|
1306
|
+
this.#startIdleTimer(instance);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Track an in-flight operation on an instance. Increments opsInFlight
|
|
1311
|
+
* before running fn, decrements after using try/finally to prevent
|
|
1312
|
+
* drift from exceptions. If the decrement brings opsInFlight to 0
|
|
1313
|
+
* with refcount also 0, starts the idle timer.
|
|
1314
|
+
*/
|
|
1315
|
+
async #trackOp(instance, fn) {
|
|
1316
|
+
instance.opsInFlight++;
|
|
1317
|
+
try {
|
|
1318
|
+
return await fn();
|
|
1319
|
+
} finally {
|
|
1320
|
+
instance.opsInFlight--;
|
|
1321
|
+
if (instance.actors.size === 0 && instance.opsInFlight === 0 && !instance.destroying && !this.#shuttingDown) {
|
|
1322
|
+
this.#startIdleTimer(instance);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Open a database on behalf of an actor, tracked as an in-flight
|
|
1328
|
+
* operation. Used by PooledSqliteHandle to avoid exposing PoolInstance.
|
|
1329
|
+
*/
|
|
1330
|
+
async openForActor(actorId, shortName, options) {
|
|
1331
|
+
const instance = this.#actorToInstance.get(actorId);
|
|
1332
|
+
if (!instance) {
|
|
1333
|
+
throw new Error(`Actor ${actorId} is not assigned to any pool instance`);
|
|
1334
|
+
}
|
|
1335
|
+
return this.#trackOp(
|
|
1336
|
+
instance,
|
|
1337
|
+
() => instance.vfs.open(shortName, options)
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Track an in-flight database operation for the given actor. Resolves the
|
|
1342
|
+
* actor's pool instance and wraps the operation with opsInFlight tracking.
|
|
1343
|
+
* If the actor has already been released, the operation runs without
|
|
1344
|
+
* tracking since the instance may already be destroyed.
|
|
1345
|
+
*/
|
|
1346
|
+
async trackOpForActor(actorId, fn) {
|
|
1347
|
+
const instance = this.#actorToInstance.get(actorId);
|
|
1348
|
+
if (!instance) {
|
|
1349
|
+
return fn();
|
|
1350
|
+
}
|
|
1351
|
+
return this.#trackOp(instance, fn);
|
|
1352
|
+
}
|
|
1353
|
+
#startIdleTimer(instance) {
|
|
1354
|
+
if (instance.idleTimer || instance.destroying) return;
|
|
1355
|
+
const idleDestroyMs = this.#config.idleDestroyMs ?? 3e4;
|
|
1356
|
+
instance.idleTimer = setTimeout(() => {
|
|
1357
|
+
instance.idleTimer = null;
|
|
1358
|
+
if (instance.actors.size === 0 && instance.opsInFlight === 0 && !instance.destroying) {
|
|
1359
|
+
this.#destroyInstance(instance);
|
|
1360
|
+
}
|
|
1361
|
+
}, idleDestroyMs);
|
|
1362
|
+
}
|
|
1363
|
+
#cancelIdleTimer(instance) {
|
|
1364
|
+
if (instance.idleTimer) {
|
|
1365
|
+
clearTimeout(instance.idleTimer);
|
|
1366
|
+
instance.idleTimer = null;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
async #destroyInstance(instance) {
|
|
1370
|
+
instance.destroying = true;
|
|
1371
|
+
this.#cancelIdleTimer(instance);
|
|
1372
|
+
this.#instances.delete(instance);
|
|
1373
|
+
try {
|
|
1374
|
+
await instance.vfs.forceCloseAll();
|
|
1375
|
+
await instance.vfs.destroy();
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
console.warn("SqliteVfsPool: failed to destroy instance", error);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Graceful shutdown. Rejects new acquire() calls, cancels idle timers,
|
|
1382
|
+
* force-closes all databases, destroys all VFS instances, and clears pool
|
|
1383
|
+
* state.
|
|
1384
|
+
*/
|
|
1385
|
+
async shutdown() {
|
|
1386
|
+
this.#shuttingDown = true;
|
|
1387
|
+
const instances = [...this.#instances];
|
|
1388
|
+
for (const instance of instances) {
|
|
1389
|
+
this.#cancelIdleTimer(instance);
|
|
1390
|
+
this.#instances.delete(instance);
|
|
1391
|
+
if (instance.opsInFlight > 0) {
|
|
1392
|
+
console.warn(
|
|
1393
|
+
`SqliteVfsPool: shutting down instance with ${instance.opsInFlight} in-flight operation(s). Concurrent close is safe due to Database.close() idempotency.`
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
try {
|
|
1397
|
+
await instance.vfs.forceCloseAll();
|
|
1398
|
+
await instance.vfs.destroy();
|
|
1399
|
+
} catch (error) {
|
|
1400
|
+
console.warn("SqliteVfsPool: failed to destroy instance during shutdown", error);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
this.#actorToInstance.clear();
|
|
1404
|
+
this.#actorToHandle.clear();
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
var TrackedDatabase = class {
|
|
1408
|
+
#inner;
|
|
1409
|
+
#pool;
|
|
1410
|
+
#actorId;
|
|
1411
|
+
constructor(inner, pool, actorId) {
|
|
1412
|
+
this.#inner = inner;
|
|
1413
|
+
this.#pool = pool;
|
|
1414
|
+
this.#actorId = actorId;
|
|
1415
|
+
}
|
|
1416
|
+
async exec(...args) {
|
|
1417
|
+
return this.#pool.trackOpForActor(
|
|
1418
|
+
this.#actorId,
|
|
1419
|
+
() => this.#inner.exec(...args)
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
async run(...args) {
|
|
1423
|
+
return this.#pool.trackOpForActor(
|
|
1424
|
+
this.#actorId,
|
|
1425
|
+
() => this.#inner.run(...args)
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
async query(...args) {
|
|
1429
|
+
return this.#pool.trackOpForActor(
|
|
1430
|
+
this.#actorId,
|
|
1431
|
+
() => this.#inner.query(...args)
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
async close() {
|
|
1435
|
+
return this.#pool.trackOpForActor(
|
|
1436
|
+
this.#actorId,
|
|
1437
|
+
() => this.#inner.close()
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
get fileName() {
|
|
1441
|
+
return this.#inner.fileName;
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
var PooledSqliteHandle = class {
|
|
1445
|
+
#shortName;
|
|
1446
|
+
#actorId;
|
|
1447
|
+
#pool;
|
|
1448
|
+
#released = false;
|
|
1449
|
+
constructor(shortName, actorId, pool) {
|
|
1450
|
+
this.#shortName = shortName;
|
|
1451
|
+
this.#actorId = actorId;
|
|
1452
|
+
this.#pool = pool;
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Open a database on the shared instance. Uses the pool-assigned short
|
|
1456
|
+
* name as the VFS file path, with the caller's KvVfsOptions for KV
|
|
1457
|
+
* routing. The open call itself is tracked as an in-flight operation,
|
|
1458
|
+
* and the returned Database is wrapped so that exec(), run(), query(),
|
|
1459
|
+
* and close() are also tracked via opsInFlight.
|
|
1460
|
+
*/
|
|
1461
|
+
async open(_fileName, options) {
|
|
1462
|
+
if (this.#released) {
|
|
1463
|
+
throw new Error("PooledSqliteHandle has been released");
|
|
1464
|
+
}
|
|
1465
|
+
const db = await this.#pool.openForActor(
|
|
1466
|
+
this.#actorId,
|
|
1467
|
+
this.#shortName,
|
|
1468
|
+
options
|
|
1469
|
+
);
|
|
1470
|
+
return new TrackedDatabase(
|
|
1471
|
+
db,
|
|
1472
|
+
this.#pool,
|
|
1473
|
+
this.#actorId
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Release this actor's assignment back to the pool. Idempotent: calling
|
|
1478
|
+
* destroy() more than once is a no-op, preventing double-release from
|
|
1479
|
+
* decrementing the instance refcount below actual.
|
|
1480
|
+
*/
|
|
1481
|
+
async destroy() {
|
|
1482
|
+
if (this.#released) {
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
this.#released = true;
|
|
1486
|
+
await this.#pool.release(this.#actorId);
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
853
1489
|
export {
|
|
854
1490
|
Database,
|
|
855
|
-
|
|
1491
|
+
PooledSqliteHandle,
|
|
1492
|
+
SqliteVfs,
|
|
1493
|
+
SqliteVfsPool
|
|
856
1494
|
};
|
|
857
1495
|
//# sourceMappingURL=index.js.map
|