@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.
@@ -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
- const require2 = createRequire(import.meta.url);
135
- const sqliteDistPath = "@rivetkit/sqlite/dist/";
136
- const wasmPath = require2.resolve(sqliteDistPath + "wa-sqlite-async.wasm");
137
- const wasmBinary = readFileSync(wasmPath);
138
- const module = await sqliteEsmFactory({ wasmBinary });
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(this.#handle, sql)) {
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(this.#handle, sql)) {
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
- constructor() {
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
- const onClose = () => {
342
- sqliteSystem.unregisterFile(fileName);
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
- return new Database(
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
- #mainFileName = null;
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.#mainFileName = null;
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
- if (!this.#mainFileName) {
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
- if (this.#mainFileName === fileName) {
440
- this.#mainFileName = null;
441
- this.#mainFileOptions = null;
442
- }
545
+ this.#registeredFiles.delete(fileName);
443
546
  }
444
547
  /**
445
- * Resolve file path to the actor's main DB file or known SQLite sidecars.
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
- if (!this.#mainFileName || !this.#mainFileOptions) {
449
- return null;
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 === `${this.#mainFileName}-wal`) {
458
- return { options: this.#mainFileOptions, fileTag: FILE_TAG_WAL };
459
- }
460
- if (path === `${this.#mainFileName}-shm`) {
461
- return { options: this.#mainFileOptions, fileTag: FILE_TAG_SHM };
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 (!this.#mainFileName) {
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}. Expected one of ${this.#mainFileName}, ${this.#mainFileName}-journal, ${this.#mainFileName}-wal, ${this.#mainFileName}-shm.`
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
- const sizeData = await options.get(metaKey);
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
- size = 0;
496
- await options.put(metaKey, encodeFileMeta2(size));
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
- if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
518
- await this.#delete(file.path);
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.SQLITE_OK;
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
- const data = this.#module.HEAPU8.subarray(pData, pData + iAmt);
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((iOffset + requestedLength - 1) / CHUNK_SIZE);
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
- const chunks = await options.getBatch(chunkKeys);
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 = chunks[i - startChunk];
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(chunkData.subarray(sourceStart, sourceEnd), destStart);
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
- const data = this.#module.HEAPU8.subarray(pData, pData + iAmt);
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
- const existingChunks = chunkKeysToFetch.length > 0 ? await options.getBatch(chunkKeysToFetch) : [];
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(Math.max(existingChunk.length, plan.writeEnd));
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(data.subarray(sourceStart, sourceEnd), plan.writeStart);
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
- await options.putBatch(entriesToWrite);
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
- await options.put(file.metaKey, encodeFileMeta2(file.size));
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 keysToDelete = [];
691
- for (let i = lastChunkToKeep + 1; i <= lastExistingChunk; i++) {
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
- await options.put(file.metaKey, encodeFileMeta2(file.size));
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
- await file.options.put(file.metaKey, encodeFileMeta2(file.size));
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
- await this.#delete(this.#module.UTF8ToString(zName));
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
- await options.deleteBatch(keysToDelete);
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
- const metaData = await resolved.options.get(compactMetaKey);
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(_fileId, _flags, _pArg) {
773
- return VFS.SQLITE_NOTFOUND;
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 0;
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
- SqliteVfs
1491
+ PooledSqliteHandle,
1492
+ SqliteVfs,
1493
+ SqliteVfsPool
856
1494
  };
857
1495
  //# sourceMappingURL=index.js.map