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