@rivetkit/sqlite-wasm 2.2.1-pr.4600.b74ff3b

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.
@@ -0,0 +1,1557 @@
1
+ // src/vfs.ts
2
+ import * as VFS from "@rivetkit/sqlite/src/VFS.js";
3
+ import {
4
+ Factory,
5
+ SQLITE_OPEN_CREATE as SQLITE_OPEN_CREATE2,
6
+ SQLITE_OPEN_READWRITE,
7
+ SQLITE_ROW
8
+ } from "@rivetkit/sqlite";
9
+ import { readFileSync } from "fs";
10
+ import { createRequire } from "module";
11
+ import path from "path";
12
+ import { pathToFileURL } from "url";
13
+
14
+ // src/kv.ts
15
+ var CHUNK_SIZE = 4096;
16
+ var SQLITE_PREFIX = 8;
17
+ var SQLITE_SCHEMA_VERSION = 1;
18
+ var META_PREFIX = 0;
19
+ var CHUNK_PREFIX = 1;
20
+ var FILE_TAG_MAIN = 0;
21
+ var FILE_TAG_JOURNAL = 1;
22
+ var FILE_TAG_WAL = 2;
23
+ var FILE_TAG_SHM = 3;
24
+ function getMetaKey(fileTag) {
25
+ const key = new Uint8Array(4);
26
+ key[0] = SQLITE_PREFIX;
27
+ key[1] = SQLITE_SCHEMA_VERSION;
28
+ key[2] = META_PREFIX;
29
+ key[3] = fileTag;
30
+ return key;
31
+ }
32
+ function getChunkKey(fileTag, chunkIndex) {
33
+ const key = new Uint8Array(8);
34
+ key[0] = SQLITE_PREFIX;
35
+ key[1] = SQLITE_SCHEMA_VERSION;
36
+ key[2] = CHUNK_PREFIX;
37
+ key[3] = fileTag;
38
+ key[4] = chunkIndex >>> 24 & 255;
39
+ key[5] = chunkIndex >>> 16 & 255;
40
+ key[6] = chunkIndex >>> 8 & 255;
41
+ key[7] = chunkIndex & 255;
42
+ return key;
43
+ }
44
+ function getChunkKeyRangeEnd(fileTag) {
45
+ const key = new Uint8Array(4);
46
+ key[0] = SQLITE_PREFIX;
47
+ key[1] = SQLITE_SCHEMA_VERSION;
48
+ key[2] = CHUNK_PREFIX;
49
+ key[3] = fileTag + 1;
50
+ return key;
51
+ }
52
+
53
+ // src/generated/empty-db-page.ts
54
+ 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]);
55
+ var EMPTY_DB_PAGE = (() => {
56
+ const page = new Uint8Array(4096);
57
+ page.set(HEADER_PREFIX);
58
+ return page;
59
+ })();
60
+
61
+ // schemas/file-meta/versioned.ts
62
+ import { createVersionedDataHandler } from "vbare";
63
+
64
+ // dist/schemas/file-meta/v1.ts
65
+ import * as bare from "@rivetkit/bare-ts";
66
+ var config = /* @__PURE__ */ bare.Config({});
67
+ function readFileMeta(bc) {
68
+ return {
69
+ size: bare.readU64(bc)
70
+ };
71
+ }
72
+ function writeFileMeta(bc, x) {
73
+ bare.writeU64(bc, x.size);
74
+ }
75
+ function encodeFileMeta(x) {
76
+ const bc = new bare.ByteCursor(
77
+ new Uint8Array(config.initialBufferLength),
78
+ config
79
+ );
80
+ writeFileMeta(bc, x);
81
+ return new Uint8Array(bc.view.buffer, bc.view.byteOffset, bc.offset);
82
+ }
83
+ function decodeFileMeta(bytes) {
84
+ const bc = new bare.ByteCursor(bytes, config);
85
+ const result = readFileMeta(bc);
86
+ if (bc.offset < bc.view.byteLength) {
87
+ throw new bare.BareError(bc.offset, "remaining bytes");
88
+ }
89
+ return result;
90
+ }
91
+
92
+ // schemas/file-meta/versioned.ts
93
+ var CURRENT_VERSION = 1;
94
+ var FILE_META_VERSIONED = createVersionedDataHandler({
95
+ deserializeVersion: (bytes, version) => {
96
+ switch (version) {
97
+ case 1:
98
+ return decodeFileMeta(bytes);
99
+ default:
100
+ throw new Error(`Unknown version ${version}`);
101
+ }
102
+ },
103
+ serializeVersion: (data, version) => {
104
+ switch (version) {
105
+ case 1:
106
+ return encodeFileMeta(data);
107
+ default:
108
+ throw new Error(`Unknown version ${version}`);
109
+ }
110
+ },
111
+ deserializeConverters: () => [],
112
+ serializeConverters: () => []
113
+ });
114
+
115
+ // src/vfs.ts
116
+ function createNodeRequire() {
117
+ return createRequire(
118
+ path.join(process.cwd(), "__rivetkit_sqlite_require__.cjs")
119
+ );
120
+ }
121
+ var TEXT_ENCODER = new TextEncoder();
122
+ var TEXT_DECODER = new TextDecoder();
123
+ var SQLITE_MAX_PATHNAME_BYTES = 64;
124
+ var UINT32_SIZE = 4294967296;
125
+ var MAX_CHUNK_INDEX = 4294967295;
126
+ var MAX_FILE_SIZE_BYTES = (MAX_CHUNK_INDEX + 1) * CHUNK_SIZE;
127
+ var MAX_FILE_SIZE_HI32 = Math.floor(MAX_FILE_SIZE_BYTES / UINT32_SIZE);
128
+ var MAX_FILE_SIZE_LO32 = MAX_FILE_SIZE_BYTES % UINT32_SIZE;
129
+ var KV_MAX_BATCH_KEYS = 128;
130
+ var SQLITE_IOCAP_BATCH_ATOMIC = 16384;
131
+ var SQLITE_FCNTL_BEGIN_ATOMIC_WRITE = 31;
132
+ var SQLITE_FCNTL_COMMIT_ATOMIC_WRITE = 32;
133
+ var SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE = 33;
134
+ var SQLITE_ASYNC_METHODS = /* @__PURE__ */ new Set([
135
+ "xOpen",
136
+ "xClose",
137
+ "xRead",
138
+ "xWrite",
139
+ "xTruncate",
140
+ "xSync",
141
+ "xFileSize",
142
+ "xDelete",
143
+ "xAccess",
144
+ "xFileControl"
145
+ ]);
146
+ function isSqliteEsmFactory(value) {
147
+ return typeof value === "function";
148
+ }
149
+ function isSQLiteModule(value) {
150
+ if (!value || typeof value !== "object") {
151
+ return false;
152
+ }
153
+ const candidate = value;
154
+ return typeof candidate.UTF8ToString === "function" && candidate.HEAPU8 instanceof Uint8Array;
155
+ }
156
+ async function loadSqliteRuntime(wasmModule) {
157
+ const require2 = createNodeRequire();
158
+ const sqliteModulePath = require2.resolve(
159
+ ["@rivetkit/sqlite", "dist", "wa-sqlite-async.mjs"].join("/")
160
+ );
161
+ const sqliteModule = await nativeDynamicImport(
162
+ pathToFileURL(sqliteModulePath).href
163
+ );
164
+ if (!isSqliteEsmFactory(sqliteModule.default)) {
165
+ throw new Error("Invalid SQLite ESM factory export");
166
+ }
167
+ const sqliteEsmFactory = sqliteModule.default;
168
+ let module;
169
+ if (wasmModule) {
170
+ module = await sqliteEsmFactory({
171
+ instantiateWasm(imports, receiveInstance) {
172
+ WebAssembly.instantiate(wasmModule, imports).then(
173
+ (instance) => {
174
+ receiveInstance(instance);
175
+ }
176
+ );
177
+ return {};
178
+ }
179
+ });
180
+ } else {
181
+ const sqliteDistPath = "@rivetkit/sqlite/dist/";
182
+ const wasmPath = require2.resolve(
183
+ sqliteDistPath + "wa-sqlite-async.wasm"
184
+ );
185
+ const wasmBinary = readFileSync(wasmPath);
186
+ module = await sqliteEsmFactory({ wasmBinary });
187
+ }
188
+ if (!isSQLiteModule(module)) {
189
+ throw new Error("Invalid SQLite runtime module");
190
+ }
191
+ return {
192
+ sqlite3: Factory(module),
193
+ module
194
+ };
195
+ }
196
+ async function nativeDynamicImport(specifier) {
197
+ try {
198
+ return await import(specifier);
199
+ } catch (directError) {
200
+ const importer = new Function(
201
+ "moduleSpecifier",
202
+ "return import(moduleSpecifier);"
203
+ );
204
+ try {
205
+ return await importer(specifier);
206
+ } catch {
207
+ throw directError;
208
+ }
209
+ }
210
+ }
211
+ function encodeFileMeta2(size) {
212
+ const meta = { size: BigInt(size) };
213
+ return FILE_META_VERSIONED.serializeWithEmbeddedVersion(
214
+ meta,
215
+ CURRENT_VERSION
216
+ );
217
+ }
218
+ function decodeFileMeta2(data) {
219
+ const meta = FILE_META_VERSIONED.deserializeWithEmbeddedVersion(data);
220
+ return Number(meta.size);
221
+ }
222
+ function isValidFileSize(size) {
223
+ return Number.isSafeInteger(size) && size >= 0 && size <= MAX_FILE_SIZE_BYTES;
224
+ }
225
+ var AsyncMutex = class {
226
+ #locked = false;
227
+ #waiting = [];
228
+ async acquire() {
229
+ while (this.#locked) {
230
+ await new Promise((resolve) => this.#waiting.push(resolve));
231
+ }
232
+ this.#locked = true;
233
+ }
234
+ release() {
235
+ this.#locked = false;
236
+ const next = this.#waiting.shift();
237
+ if (next) {
238
+ next();
239
+ }
240
+ }
241
+ async run(fn) {
242
+ await this.acquire();
243
+ try {
244
+ return await fn();
245
+ } finally {
246
+ this.release();
247
+ }
248
+ }
249
+ };
250
+ var Database = class {
251
+ #sqlite3;
252
+ #handle;
253
+ #fileName;
254
+ #onClose;
255
+ #sqliteMutex;
256
+ #closed = false;
257
+ constructor(sqlite3, handle, fileName, onClose, sqliteMutex) {
258
+ this.#sqlite3 = sqlite3;
259
+ this.#handle = handle;
260
+ this.#fileName = fileName;
261
+ this.#onClose = onClose;
262
+ this.#sqliteMutex = sqliteMutex;
263
+ }
264
+ /**
265
+ * Execute SQL with optional row callback
266
+ * @param sql - SQL statement to execute
267
+ * @param callback - Called for each result row with (row, columns)
268
+ */
269
+ async exec(sql, callback) {
270
+ await this.#sqliteMutex.run(async () => {
271
+ await this.#sqlite3.exec(this.#handle, sql, callback);
272
+ });
273
+ }
274
+ /**
275
+ * Execute a parameterized SQL statement (no result rows)
276
+ * @param sql - SQL statement with ? placeholders
277
+ * @param params - Parameter values to bind
278
+ */
279
+ async run(sql, params) {
280
+ await this.#sqliteMutex.run(async () => {
281
+ for await (const stmt of this.#sqlite3.statements(
282
+ this.#handle,
283
+ sql
284
+ )) {
285
+ if (params) {
286
+ this.#sqlite3.bind_collection(stmt, params);
287
+ }
288
+ while (await this.#sqlite3.step(stmt) === SQLITE_ROW) {
289
+ }
290
+ }
291
+ });
292
+ }
293
+ /**
294
+ * Execute a parameterized SQL query and return results
295
+ * @param sql - SQL query with ? placeholders
296
+ * @param params - Parameter values to bind
297
+ * @returns Object with rows (array of arrays) and columns (column names)
298
+ */
299
+ async query(sql, params) {
300
+ return this.#sqliteMutex.run(async () => {
301
+ const rows = [];
302
+ let columns = [];
303
+ for await (const stmt of this.#sqlite3.statements(
304
+ this.#handle,
305
+ sql
306
+ )) {
307
+ if (params) {
308
+ this.#sqlite3.bind_collection(stmt, params);
309
+ }
310
+ while (await this.#sqlite3.step(stmt) === SQLITE_ROW) {
311
+ if (columns.length === 0) {
312
+ columns = this.#sqlite3.column_names(stmt);
313
+ }
314
+ rows.push(this.#sqlite3.row(stmt));
315
+ }
316
+ }
317
+ return { rows, columns };
318
+ });
319
+ }
320
+ /**
321
+ * Close the database
322
+ */
323
+ async close() {
324
+ if (this.#closed) {
325
+ return;
326
+ }
327
+ this.#closed = true;
328
+ await this.#sqliteMutex.run(async () => {
329
+ await this.#sqlite3.close(this.#handle);
330
+ });
331
+ await this.#onClose();
332
+ }
333
+ /**
334
+ * Get the database file name
335
+ */
336
+ get fileName() {
337
+ return this.#fileName;
338
+ }
339
+ /**
340
+ * Get the raw @rivetkit/sqlite API (for advanced usage)
341
+ */
342
+ get sqlite3() {
343
+ return this.#sqlite3;
344
+ }
345
+ /**
346
+ * Get the raw database handle (for advanced usage)
347
+ */
348
+ get handle() {
349
+ return this.#handle;
350
+ }
351
+ };
352
+ var SqliteVfs = class {
353
+ #sqlite3 = null;
354
+ #sqliteSystem = null;
355
+ #initPromise = null;
356
+ #openMutex = new AsyncMutex();
357
+ #sqliteMutex = new AsyncMutex();
358
+ #instanceId;
359
+ #destroyed = false;
360
+ #openDatabases = /* @__PURE__ */ new Set();
361
+ #wasmModule;
362
+ constructor(wasmModule) {
363
+ this.#instanceId = crypto.randomUUID().replace(/-/g, "").slice(0, 8);
364
+ this.#wasmModule = wasmModule;
365
+ }
366
+ /**
367
+ * Initialize @rivetkit/sqlite and VFS (called once per instance)
368
+ */
369
+ async #ensureInitialized() {
370
+ if (this.#destroyed) {
371
+ throw new Error("SqliteVfs is closed");
372
+ }
373
+ if (this.#sqlite3 && this.#sqliteSystem) {
374
+ return;
375
+ }
376
+ if (!this.#initPromise) {
377
+ this.#initPromise = (async () => {
378
+ const { sqlite3, module } = await loadSqliteRuntime(
379
+ this.#wasmModule
380
+ );
381
+ if (this.#destroyed) {
382
+ return;
383
+ }
384
+ this.#sqlite3 = sqlite3;
385
+ this.#sqliteSystem = new SqliteSystem(
386
+ sqlite3,
387
+ module,
388
+ `kv-vfs-${this.#instanceId}`
389
+ );
390
+ this.#sqliteSystem.register();
391
+ })();
392
+ }
393
+ try {
394
+ await this.#initPromise;
395
+ } catch (error) {
396
+ this.#initPromise = null;
397
+ throw error;
398
+ }
399
+ }
400
+ /**
401
+ * Open a SQLite database using KV storage backend
402
+ *
403
+ * @param fileName - The database file name (typically the actor ID)
404
+ * @param options - KV storage operations for this database
405
+ * @returns A Database instance
406
+ */
407
+ async open(fileName, options) {
408
+ if (this.#destroyed) {
409
+ throw new Error("SqliteVfs is closed");
410
+ }
411
+ await this.#openMutex.acquire();
412
+ try {
413
+ for (const db2 of this.#openDatabases) {
414
+ if (db2.fileName === fileName) {
415
+ throw new Error(
416
+ `SqliteVfs: fileName "${fileName}" is already open on this instance`
417
+ );
418
+ }
419
+ }
420
+ await this.#ensureInitialized();
421
+ if (!this.#sqlite3 || !this.#sqliteSystem) {
422
+ throw new Error("Failed to initialize SQLite");
423
+ }
424
+ const sqlite3 = this.#sqlite3;
425
+ const sqliteSystem = this.#sqliteSystem;
426
+ sqliteSystem.registerFile(fileName, options);
427
+ const db = await this.#sqliteMutex.run(
428
+ async () => sqlite3.open_v2(
429
+ fileName,
430
+ SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE2,
431
+ sqliteSystem.name
432
+ )
433
+ );
434
+ await this.#sqliteMutex.run(async () => {
435
+ await sqlite3.exec(db, "PRAGMA page_size = 4096");
436
+ await sqlite3.exec(db, "PRAGMA journal_mode = DELETE");
437
+ await sqlite3.exec(db, "PRAGMA synchronous = NORMAL");
438
+ await sqlite3.exec(db, "PRAGMA temp_store = MEMORY");
439
+ await sqlite3.exec(db, "PRAGMA auto_vacuum = NONE");
440
+ await sqlite3.exec(db, "PRAGMA locking_mode = EXCLUSIVE");
441
+ });
442
+ const onClose = async () => {
443
+ this.#openDatabases.delete(database);
444
+ await this.#openMutex.run(async () => {
445
+ sqliteSystem.unregisterFile(fileName);
446
+ });
447
+ };
448
+ const database = new Database(
449
+ sqlite3,
450
+ db,
451
+ fileName,
452
+ onClose,
453
+ this.#sqliteMutex
454
+ );
455
+ this.#openDatabases.add(database);
456
+ return database;
457
+ } finally {
458
+ this.#openMutex.release();
459
+ }
460
+ }
461
+ /**
462
+ * Force-close all Database handles whose fileName exactly matches the
463
+ * given name. Snapshots the set to an array before iterating to avoid
464
+ * mutation during async iteration.
465
+ *
466
+ * Uses exact file name match because short names are numeric strings
467
+ * ('0', '1', ..., '10', '11', ...) and a prefix match like
468
+ * startsWith('1') would incorrectly match '10', '11', etc., causing
469
+ * cross-actor corruption. Sidecar files (-journal, -wal, -shm) are not
470
+ * tracked as separate Database handles, so prefix matching for sidecars
471
+ * is not needed.
472
+ */
473
+ async forceCloseByFileName(fileName) {
474
+ const snapshot = [...this.#openDatabases];
475
+ let allSucceeded = true;
476
+ for (const db of snapshot) {
477
+ if (db.fileName === fileName) {
478
+ try {
479
+ await db.close();
480
+ } catch {
481
+ allSucceeded = false;
482
+ this.#openDatabases.delete(db);
483
+ const sqliteSystem = this.#sqliteSystem;
484
+ if (sqliteSystem) {
485
+ await this.#openMutex.run(async () => {
486
+ sqliteSystem.unregisterFile(db.fileName);
487
+ });
488
+ }
489
+ }
490
+ }
491
+ }
492
+ return { allSucceeded };
493
+ }
494
+ /**
495
+ * Force-close all open Database handles. Best-effort: errors are
496
+ * swallowed so this is safe to call during instance teardown.
497
+ */
498
+ async forceCloseAll() {
499
+ const snapshot = [...this.#openDatabases];
500
+ for (const db of snapshot) {
501
+ try {
502
+ await db.close();
503
+ } catch {
504
+ }
505
+ }
506
+ }
507
+ /**
508
+ * Tears down this VFS instance and releases internal references.
509
+ */
510
+ async destroy() {
511
+ if (this.#destroyed) {
512
+ return;
513
+ }
514
+ this.#destroyed = true;
515
+ const initPromise = this.#initPromise;
516
+ if (initPromise) {
517
+ try {
518
+ await initPromise;
519
+ } catch {
520
+ }
521
+ }
522
+ if (this.#sqliteSystem) {
523
+ await this.#sqliteSystem.close();
524
+ }
525
+ this.#sqliteSystem = null;
526
+ this.#sqlite3 = null;
527
+ this.#initPromise = null;
528
+ }
529
+ /**
530
+ * Alias for destroy to align with DB-style lifecycle naming.
531
+ */
532
+ async close() {
533
+ await this.destroy();
534
+ }
535
+ };
536
+ var SqliteSystem = class {
537
+ name;
538
+ mxPathName = SQLITE_MAX_PATHNAME_BYTES;
539
+ mxPathname = SQLITE_MAX_PATHNAME_BYTES;
540
+ #registeredFiles = /* @__PURE__ */ new Map();
541
+ #openFiles = /* @__PURE__ */ new Map();
542
+ #sqlite3;
543
+ #module;
544
+ #heapDataView;
545
+ #heapDataViewBuffer;
546
+ constructor(sqlite3, module, name) {
547
+ this.name = name;
548
+ this.#sqlite3 = sqlite3;
549
+ this.#module = module;
550
+ this.#heapDataViewBuffer = module.HEAPU8.buffer;
551
+ this.#heapDataView = new DataView(this.#heapDataViewBuffer);
552
+ }
553
+ async close() {
554
+ this.#openFiles.clear();
555
+ this.#registeredFiles.clear();
556
+ }
557
+ isReady() {
558
+ return true;
559
+ }
560
+ hasAsyncMethod(methodName) {
561
+ return SQLITE_ASYNC_METHODS.has(methodName);
562
+ }
563
+ /**
564
+ * Registers the VFS with SQLite
565
+ */
566
+ register() {
567
+ this.#sqlite3.vfs_register(this, false);
568
+ }
569
+ /**
570
+ * Registers a file with its KV options (before opening).
571
+ */
572
+ registerFile(fileName, options) {
573
+ this.#registeredFiles.set(fileName, options);
574
+ }
575
+ /**
576
+ * Unregisters a file's KV options (after closing).
577
+ */
578
+ unregisterFile(fileName) {
579
+ this.#registeredFiles.delete(fileName);
580
+ }
581
+ /**
582
+ * Resolve file path to a registered database file or one of its SQLite
583
+ * sidecars (-journal, -wal, -shm). File tags are reused across files
584
+ * because each file's KvVfsOptions routes to a separate KV namespace.
585
+ */
586
+ #resolveFile(path3) {
587
+ const directOptions = this.#registeredFiles.get(path3);
588
+ if (directOptions) {
589
+ return { options: directOptions, fileTag: FILE_TAG_MAIN };
590
+ }
591
+ if (path3.endsWith("-journal")) {
592
+ const baseName = path3.slice(0, -8);
593
+ const options = this.#registeredFiles.get(baseName);
594
+ if (options) {
595
+ return { options, fileTag: FILE_TAG_JOURNAL };
596
+ }
597
+ } else if (path3.endsWith("-wal")) {
598
+ const baseName = path3.slice(0, -4);
599
+ const options = this.#registeredFiles.get(baseName);
600
+ if (options) {
601
+ return { options, fileTag: FILE_TAG_WAL };
602
+ }
603
+ } else if (path3.endsWith("-shm")) {
604
+ const baseName = path3.slice(0, -4);
605
+ const options = this.#registeredFiles.get(baseName);
606
+ if (options) {
607
+ return { options, fileTag: FILE_TAG_SHM };
608
+ }
609
+ }
610
+ return null;
611
+ }
612
+ #resolveFileOrThrow(path3) {
613
+ const resolved = this.#resolveFile(path3);
614
+ if (resolved) {
615
+ return resolved;
616
+ }
617
+ if (this.#registeredFiles.size === 0) {
618
+ throw new Error(`No KV options registered for file: ${path3}`);
619
+ }
620
+ const registered = Array.from(this.#registeredFiles.keys()).join(", ");
621
+ throw new Error(
622
+ `Unsupported SQLite file path ${path3}. Registered base names: ${registered}.`
623
+ );
624
+ }
625
+ #chunkKey(file, chunkIndex) {
626
+ return getChunkKey(file.fileTag, chunkIndex);
627
+ }
628
+ async xOpen(_pVfs, zName, fileId, flags, pOutFlags) {
629
+ var _a, _b, _c;
630
+ const path3 = this.#decodeFilename(zName, flags);
631
+ if (!path3) {
632
+ return VFS.SQLITE_CANTOPEN;
633
+ }
634
+ const { options, fileTag } = this.#resolveFileOrThrow(path3);
635
+ const metaKey = getMetaKey(fileTag);
636
+ let sizeData;
637
+ try {
638
+ sizeData = await options.get(metaKey);
639
+ } catch (error) {
640
+ (_a = options.onError) == null ? void 0 : _a.call(options, error);
641
+ return VFS.SQLITE_CANTOPEN;
642
+ }
643
+ let size;
644
+ if (sizeData) {
645
+ size = decodeFileMeta2(sizeData);
646
+ if (!isValidFileSize(size)) {
647
+ return VFS.SQLITE_IOERR;
648
+ }
649
+ } else if (flags & VFS.SQLITE_OPEN_CREATE) {
650
+ if (fileTag === FILE_TAG_MAIN) {
651
+ const chunkKey = getChunkKey(fileTag, 0);
652
+ size = EMPTY_DB_PAGE.length;
653
+ try {
654
+ await options.putBatch([
655
+ [chunkKey, EMPTY_DB_PAGE],
656
+ [metaKey, encodeFileMeta2(size)]
657
+ ]);
658
+ } catch (error) {
659
+ (_b = options.onError) == null ? void 0 : _b.call(options, error);
660
+ return VFS.SQLITE_CANTOPEN;
661
+ }
662
+ } else {
663
+ size = 0;
664
+ try {
665
+ await options.put(metaKey, encodeFileMeta2(size));
666
+ } catch (error) {
667
+ (_c = options.onError) == null ? void 0 : _c.call(options, error);
668
+ return VFS.SQLITE_CANTOPEN;
669
+ }
670
+ }
671
+ } else {
672
+ return VFS.SQLITE_CANTOPEN;
673
+ }
674
+ this.#openFiles.set(fileId, {
675
+ path: path3,
676
+ fileTag,
677
+ metaKey,
678
+ size,
679
+ metaDirty: false,
680
+ flags,
681
+ options,
682
+ batchMode: false,
683
+ dirtyBuffer: null,
684
+ savedFileSize: 0
685
+ });
686
+ this.#writeInt32(pOutFlags, flags);
687
+ return VFS.SQLITE_OK;
688
+ }
689
+ async xClose(fileId) {
690
+ var _a, _b;
691
+ const file = this.#openFiles.get(fileId);
692
+ if (!file) {
693
+ return VFS.SQLITE_OK;
694
+ }
695
+ try {
696
+ if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
697
+ await this.#delete(file.path);
698
+ } else if (file.metaDirty) {
699
+ await file.options.put(file.metaKey, encodeFileMeta2(file.size));
700
+ file.metaDirty = false;
701
+ }
702
+ } catch (error) {
703
+ (_b = (_a = file.options).onError) == null ? void 0 : _b.call(_a, error);
704
+ this.#openFiles.delete(fileId);
705
+ return VFS.SQLITE_IOERR;
706
+ }
707
+ this.#openFiles.delete(fileId);
708
+ return VFS.SQLITE_OK;
709
+ }
710
+ async xRead(fileId, pData, iAmt, iOffsetLo, iOffsetHi) {
711
+ var _a;
712
+ if (iAmt === 0) {
713
+ return VFS.SQLITE_OK;
714
+ }
715
+ const file = this.#openFiles.get(fileId);
716
+ if (!file) {
717
+ return VFS.SQLITE_IOERR_READ;
718
+ }
719
+ let data = this.#module.HEAPU8.subarray(pData, pData + iAmt);
720
+ const options = file.options;
721
+ const requestedLength = iAmt;
722
+ const iOffset = delegalize(iOffsetLo, iOffsetHi);
723
+ if (iOffset < 0) {
724
+ return VFS.SQLITE_IOERR_READ;
725
+ }
726
+ const fileSize = file.size;
727
+ if (iOffset >= fileSize) {
728
+ data.fill(0);
729
+ return VFS.SQLITE_IOERR_SHORT_READ;
730
+ }
731
+ const startChunk = Math.floor(iOffset / CHUNK_SIZE);
732
+ const endChunk = Math.floor(
733
+ (iOffset + requestedLength - 1) / CHUNK_SIZE
734
+ );
735
+ const chunkKeys = [];
736
+ const chunkIndexToBuffered = /* @__PURE__ */ new Map();
737
+ for (let i = startChunk; i <= endChunk; i++) {
738
+ if (file.batchMode && file.dirtyBuffer) {
739
+ const buffered = file.dirtyBuffer.get(i);
740
+ if (buffered) {
741
+ chunkIndexToBuffered.set(i, buffered);
742
+ continue;
743
+ }
744
+ }
745
+ chunkKeys.push(this.#chunkKey(file, i));
746
+ }
747
+ let kvChunks;
748
+ try {
749
+ kvChunks = chunkKeys.length > 0 ? await options.getBatch(chunkKeys) : [];
750
+ } catch (error) {
751
+ (_a = options.onError) == null ? void 0 : _a.call(options, error);
752
+ return VFS.SQLITE_IOERR_READ;
753
+ }
754
+ data = this.#module.HEAPU8.subarray(pData, pData + iAmt);
755
+ let kvIdx = 0;
756
+ for (let i = startChunk; i <= endChunk; i++) {
757
+ const chunkData = chunkIndexToBuffered.get(i) ?? kvChunks[kvIdx++];
758
+ const chunkOffset = i * CHUNK_SIZE;
759
+ const readStart = Math.max(0, iOffset - chunkOffset);
760
+ const readEnd = Math.min(
761
+ CHUNK_SIZE,
762
+ iOffset + requestedLength - chunkOffset
763
+ );
764
+ if (chunkData) {
765
+ const sourceStart = readStart;
766
+ const sourceEnd = Math.min(readEnd, chunkData.length);
767
+ const destStart = chunkOffset + readStart - iOffset;
768
+ if (sourceEnd > sourceStart) {
769
+ data.set(
770
+ chunkData.subarray(sourceStart, sourceEnd),
771
+ destStart
772
+ );
773
+ }
774
+ if (sourceEnd < readEnd) {
775
+ const zeroStart = destStart + (sourceEnd - sourceStart);
776
+ const zeroEnd = destStart + (readEnd - readStart);
777
+ data.fill(0, zeroStart, zeroEnd);
778
+ }
779
+ } else {
780
+ const destStart = chunkOffset + readStart - iOffset;
781
+ const destEnd = destStart + (readEnd - readStart);
782
+ data.fill(0, destStart, destEnd);
783
+ }
784
+ }
785
+ const actualBytes = Math.min(requestedLength, fileSize - iOffset);
786
+ if (actualBytes < requestedLength) {
787
+ data.fill(0, actualBytes);
788
+ return VFS.SQLITE_IOERR_SHORT_READ;
789
+ }
790
+ return VFS.SQLITE_OK;
791
+ }
792
+ async xWrite(fileId, pData, iAmt, iOffsetLo, iOffsetHi) {
793
+ var _a, _b;
794
+ if (iAmt === 0) {
795
+ return VFS.SQLITE_OK;
796
+ }
797
+ const file = this.#openFiles.get(fileId);
798
+ if (!file) {
799
+ return VFS.SQLITE_IOERR_WRITE;
800
+ }
801
+ let data = this.#module.HEAPU8.subarray(pData, pData + iAmt);
802
+ const iOffset = delegalize(iOffsetLo, iOffsetHi);
803
+ if (iOffset < 0) {
804
+ return VFS.SQLITE_IOERR_WRITE;
805
+ }
806
+ const options = file.options;
807
+ const writeLength = iAmt;
808
+ const writeEndOffset = iOffset + writeLength;
809
+ if (writeEndOffset > MAX_FILE_SIZE_BYTES) {
810
+ return VFS.SQLITE_IOERR_WRITE;
811
+ }
812
+ const startChunk = Math.floor(iOffset / CHUNK_SIZE);
813
+ const endChunk = Math.floor((iOffset + writeLength - 1) / CHUNK_SIZE);
814
+ if (file.batchMode && file.dirtyBuffer) {
815
+ for (let i = startChunk; i <= endChunk; i++) {
816
+ const chunkOffset = i * CHUNK_SIZE;
817
+ const sourceStart = Math.max(0, chunkOffset - iOffset);
818
+ const sourceEnd = Math.min(
819
+ writeLength,
820
+ chunkOffset + CHUNK_SIZE - iOffset
821
+ );
822
+ file.dirtyBuffer.set(
823
+ i,
824
+ data.subarray(sourceStart, sourceEnd).slice()
825
+ );
826
+ }
827
+ const newSize2 = Math.max(file.size, writeEndOffset);
828
+ if (newSize2 !== file.size) {
829
+ file.size = newSize2;
830
+ file.metaDirty = true;
831
+ }
832
+ return VFS.SQLITE_OK;
833
+ }
834
+ const plans = [];
835
+ const chunkKeysToFetch = [];
836
+ for (let i = startChunk; i <= endChunk; i++) {
837
+ const chunkOffset = i * CHUNK_SIZE;
838
+ const writeStart = Math.max(0, iOffset - chunkOffset);
839
+ const writeEnd = Math.min(
840
+ CHUNK_SIZE,
841
+ iOffset + writeLength - chunkOffset
842
+ );
843
+ const existingBytesInChunk = Math.max(
844
+ 0,
845
+ Math.min(CHUNK_SIZE, file.size - chunkOffset)
846
+ );
847
+ const needsExisting = writeStart > 0 || existingBytesInChunk > writeEnd;
848
+ const chunkKey = this.#chunkKey(file, i);
849
+ let existingChunkIndex = -1;
850
+ if (needsExisting) {
851
+ existingChunkIndex = chunkKeysToFetch.length;
852
+ chunkKeysToFetch.push(chunkKey);
853
+ }
854
+ plans.push({
855
+ chunkKey,
856
+ chunkOffset,
857
+ writeStart,
858
+ writeEnd,
859
+ existingChunkIndex
860
+ });
861
+ }
862
+ let existingChunks;
863
+ try {
864
+ existingChunks = chunkKeysToFetch.length > 0 ? await options.getBatch(chunkKeysToFetch) : [];
865
+ } catch (error) {
866
+ (_a = options.onError) == null ? void 0 : _a.call(options, error);
867
+ return VFS.SQLITE_IOERR_WRITE;
868
+ }
869
+ data = this.#module.HEAPU8.subarray(pData, pData + iAmt);
870
+ const entriesToWrite = [];
871
+ for (const plan of plans) {
872
+ const existingChunk = plan.existingChunkIndex >= 0 ? existingChunks[plan.existingChunkIndex] : null;
873
+ let newChunk;
874
+ if (existingChunk) {
875
+ newChunk = new Uint8Array(
876
+ Math.max(existingChunk.length, plan.writeEnd)
877
+ );
878
+ newChunk.set(existingChunk);
879
+ } else {
880
+ newChunk = new Uint8Array(plan.writeEnd);
881
+ }
882
+ const sourceStart = plan.chunkOffset + plan.writeStart - iOffset;
883
+ const sourceEnd = sourceStart + (plan.writeEnd - plan.writeStart);
884
+ newChunk.set(
885
+ data.subarray(sourceStart, sourceEnd),
886
+ plan.writeStart
887
+ );
888
+ entriesToWrite.push([plan.chunkKey, newChunk]);
889
+ }
890
+ const previousSize = file.size;
891
+ const previousMetaDirty = file.metaDirty;
892
+ const newSize = Math.max(file.size, writeEndOffset);
893
+ if (newSize !== previousSize) {
894
+ file.size = newSize;
895
+ file.metaDirty = true;
896
+ }
897
+ if (file.metaDirty) {
898
+ entriesToWrite.push([file.metaKey, encodeFileMeta2(file.size)]);
899
+ }
900
+ try {
901
+ await options.putBatch(entriesToWrite);
902
+ } catch (error) {
903
+ (_b = options.onError) == null ? void 0 : _b.call(options, error);
904
+ file.size = previousSize;
905
+ file.metaDirty = previousMetaDirty;
906
+ return VFS.SQLITE_IOERR_WRITE;
907
+ }
908
+ if (file.metaDirty) {
909
+ file.metaDirty = false;
910
+ }
911
+ file.metaDirty = false;
912
+ return VFS.SQLITE_OK;
913
+ }
914
+ async xTruncate(fileId, sizeLo, sizeHi) {
915
+ var _a, _b, _c;
916
+ const file = this.#openFiles.get(fileId);
917
+ if (!file) {
918
+ return VFS.SQLITE_IOERR_TRUNCATE;
919
+ }
920
+ const size = delegalize(sizeLo, sizeHi);
921
+ if (size < 0 || size > MAX_FILE_SIZE_BYTES) {
922
+ return VFS.SQLITE_IOERR_TRUNCATE;
923
+ }
924
+ const options = file.options;
925
+ if (size >= file.size) {
926
+ if (size > file.size) {
927
+ const previousSize2 = file.size;
928
+ const previousMetaDirty2 = file.metaDirty;
929
+ file.size = size;
930
+ file.metaDirty = true;
931
+ try {
932
+ await options.put(file.metaKey, encodeFileMeta2(file.size));
933
+ } catch (error) {
934
+ (_a = options.onError) == null ? void 0 : _a.call(options, error);
935
+ file.size = previousSize2;
936
+ file.metaDirty = previousMetaDirty2;
937
+ return VFS.SQLITE_IOERR_TRUNCATE;
938
+ }
939
+ file.metaDirty = false;
940
+ }
941
+ return VFS.SQLITE_OK;
942
+ }
943
+ const lastChunkToKeep = Math.floor((size - 1) / CHUNK_SIZE);
944
+ const lastExistingChunk = Math.floor((file.size - 1) / CHUNK_SIZE);
945
+ const previousSize = file.size;
946
+ const previousMetaDirty = file.metaDirty;
947
+ file.size = size;
948
+ file.metaDirty = true;
949
+ try {
950
+ await options.put(file.metaKey, encodeFileMeta2(file.size));
951
+ } catch (error) {
952
+ (_b = options.onError) == null ? void 0 : _b.call(options, error);
953
+ file.size = previousSize;
954
+ file.metaDirty = previousMetaDirty;
955
+ return VFS.SQLITE_IOERR_TRUNCATE;
956
+ }
957
+ file.metaDirty = false;
958
+ try {
959
+ if (size > 0 && size % CHUNK_SIZE !== 0) {
960
+ const lastChunkKey = this.#chunkKey(file, lastChunkToKeep);
961
+ const lastChunkData = await options.get(lastChunkKey);
962
+ if (lastChunkData && lastChunkData.length > size % CHUNK_SIZE) {
963
+ const truncatedChunk = lastChunkData.subarray(
964
+ 0,
965
+ size % CHUNK_SIZE
966
+ );
967
+ await options.put(lastChunkKey, truncatedChunk);
968
+ }
969
+ }
970
+ const keysToDelete = [];
971
+ for (let i = lastChunkToKeep + 1; i <= lastExistingChunk; i++) {
972
+ keysToDelete.push(this.#chunkKey(file, i));
973
+ }
974
+ for (let b = 0; b < keysToDelete.length; b += KV_MAX_BATCH_KEYS) {
975
+ await options.deleteBatch(
976
+ keysToDelete.slice(b, b + KV_MAX_BATCH_KEYS)
977
+ );
978
+ }
979
+ } catch (error) {
980
+ (_c = options.onError) == null ? void 0 : _c.call(options, error);
981
+ return VFS.SQLITE_IOERR_TRUNCATE;
982
+ }
983
+ return VFS.SQLITE_OK;
984
+ }
985
+ async xSync(fileId, _flags) {
986
+ var _a, _b;
987
+ const file = this.#openFiles.get(fileId);
988
+ if (!file || !file.metaDirty) {
989
+ return VFS.SQLITE_OK;
990
+ }
991
+ try {
992
+ await file.options.put(file.metaKey, encodeFileMeta2(file.size));
993
+ } catch (error) {
994
+ (_b = (_a = file.options).onError) == null ? void 0 : _b.call(_a, error);
995
+ return VFS.SQLITE_IOERR_FSYNC;
996
+ }
997
+ file.metaDirty = false;
998
+ return VFS.SQLITE_OK;
999
+ }
1000
+ async xFileSize(fileId, pSize) {
1001
+ const file = this.#openFiles.get(fileId);
1002
+ if (!file) {
1003
+ return VFS.SQLITE_IOERR_FSTAT;
1004
+ }
1005
+ this.#writeBigInt64(pSize, BigInt(file.size));
1006
+ return VFS.SQLITE_OK;
1007
+ }
1008
+ async xDelete(_pVfs, zName, _syncDir) {
1009
+ try {
1010
+ await this.#delete(this.#module.UTF8ToString(zName));
1011
+ } catch (error) {
1012
+ return VFS.SQLITE_IOERR_DELETE;
1013
+ }
1014
+ return VFS.SQLITE_OK;
1015
+ }
1016
+ /**
1017
+ * Internal delete implementation.
1018
+ * Uses deleteRange for O(1) chunk deletion instead of enumerating
1019
+ * individual chunk keys. The chunk keys for a file tag are
1020
+ * lexicographically contiguous, so range deletion is always safe.
1021
+ */
1022
+ async #delete(path3) {
1023
+ const { options, fileTag } = this.#resolveFileOrThrow(path3);
1024
+ const metaKey = getMetaKey(fileTag);
1025
+ const sizeData = await options.get(metaKey);
1026
+ if (!sizeData) {
1027
+ return;
1028
+ }
1029
+ await options.deleteRange(
1030
+ getChunkKey(fileTag, 0),
1031
+ getChunkKeyRangeEnd(fileTag)
1032
+ );
1033
+ await options.deleteBatch([metaKey]);
1034
+ }
1035
+ async xAccess(_pVfs, zName, _flags, pResOut) {
1036
+ var _a, _b;
1037
+ const path3 = this.#module.UTF8ToString(zName);
1038
+ const resolved = this.#resolveFile(path3);
1039
+ if (!resolved) {
1040
+ this.#writeInt32(pResOut, 0);
1041
+ return VFS.SQLITE_OK;
1042
+ }
1043
+ const compactMetaKey = getMetaKey(resolved.fileTag);
1044
+ let metaData;
1045
+ try {
1046
+ metaData = await resolved.options.get(compactMetaKey);
1047
+ } catch (error) {
1048
+ (_b = (_a = resolved.options).onError) == null ? void 0 : _b.call(_a, error);
1049
+ return VFS.SQLITE_IOERR_ACCESS;
1050
+ }
1051
+ this.#writeInt32(pResOut, metaData ? 1 : 0);
1052
+ return VFS.SQLITE_OK;
1053
+ }
1054
+ xCheckReservedLock(_fileId, pResOut) {
1055
+ this.#writeInt32(pResOut, 0);
1056
+ return VFS.SQLITE_OK;
1057
+ }
1058
+ xLock(_fileId, _flags) {
1059
+ return VFS.SQLITE_OK;
1060
+ }
1061
+ xUnlock(_fileId, _flags) {
1062
+ return VFS.SQLITE_OK;
1063
+ }
1064
+ async xFileControl(fileId, flags, _pArg) {
1065
+ var _a;
1066
+ switch (flags) {
1067
+ case SQLITE_FCNTL_BEGIN_ATOMIC_WRITE: {
1068
+ const file = this.#openFiles.get(fileId);
1069
+ if (!file) return VFS.SQLITE_NOTFOUND;
1070
+ file.savedFileSize = file.size;
1071
+ file.batchMode = true;
1072
+ file.metaDirty = false;
1073
+ file.dirtyBuffer = /* @__PURE__ */ new Map();
1074
+ return VFS.SQLITE_OK;
1075
+ }
1076
+ case SQLITE_FCNTL_COMMIT_ATOMIC_WRITE: {
1077
+ const file = this.#openFiles.get(fileId);
1078
+ if (!file) return VFS.SQLITE_NOTFOUND;
1079
+ const { dirtyBuffer, options } = file;
1080
+ const maxDirtyPages = file.metaDirty ? KV_MAX_BATCH_KEYS - 1 : KV_MAX_BATCH_KEYS;
1081
+ if (dirtyBuffer && dirtyBuffer.size > maxDirtyPages) {
1082
+ dirtyBuffer.clear();
1083
+ file.dirtyBuffer = null;
1084
+ file.size = file.savedFileSize;
1085
+ file.metaDirty = false;
1086
+ file.batchMode = false;
1087
+ return VFS.SQLITE_IOERR;
1088
+ }
1089
+ const entries = [];
1090
+ if (dirtyBuffer) {
1091
+ for (const [chunkIndex, data] of dirtyBuffer) {
1092
+ entries.push([this.#chunkKey(file, chunkIndex), data]);
1093
+ }
1094
+ dirtyBuffer.clear();
1095
+ }
1096
+ if (file.metaDirty) {
1097
+ entries.push([file.metaKey, encodeFileMeta2(file.size)]);
1098
+ }
1099
+ try {
1100
+ await options.putBatch(entries);
1101
+ } catch (error) {
1102
+ (_a = options.onError) == null ? void 0 : _a.call(options, error);
1103
+ file.dirtyBuffer = null;
1104
+ file.size = file.savedFileSize;
1105
+ file.metaDirty = false;
1106
+ file.batchMode = false;
1107
+ return VFS.SQLITE_IOERR;
1108
+ }
1109
+ file.dirtyBuffer = null;
1110
+ file.metaDirty = false;
1111
+ file.batchMode = false;
1112
+ return VFS.SQLITE_OK;
1113
+ }
1114
+ case SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE: {
1115
+ const file = this.#openFiles.get(fileId);
1116
+ if (!file || !file.batchMode) return VFS.SQLITE_OK;
1117
+ if (file.dirtyBuffer) {
1118
+ file.dirtyBuffer.clear();
1119
+ file.dirtyBuffer = null;
1120
+ }
1121
+ file.size = file.savedFileSize;
1122
+ file.metaDirty = false;
1123
+ file.batchMode = false;
1124
+ return VFS.SQLITE_OK;
1125
+ }
1126
+ default:
1127
+ return VFS.SQLITE_NOTFOUND;
1128
+ }
1129
+ }
1130
+ // Return CHUNK_SIZE so SQLite aligns journal I/O to chunk boundaries.
1131
+ // Must match the native VFS (kv_io_sector_size in sqlite-native/src/vfs.rs).
1132
+ xSectorSize(_fileId) {
1133
+ return CHUNK_SIZE;
1134
+ }
1135
+ xDeviceCharacteristics(_fileId) {
1136
+ return SQLITE_IOCAP_BATCH_ATOMIC;
1137
+ }
1138
+ xFullPathname(_pVfs, zName, nOut, zOut) {
1139
+ const path3 = this.#module.UTF8ToString(zName);
1140
+ const bytes = TEXT_ENCODER.encode(path3);
1141
+ const out = this.#module.HEAPU8.subarray(zOut, zOut + nOut);
1142
+ if (bytes.length >= out.length) {
1143
+ return VFS.SQLITE_IOERR;
1144
+ }
1145
+ out.set(bytes, 0);
1146
+ out[bytes.length] = 0;
1147
+ return VFS.SQLITE_OK;
1148
+ }
1149
+ #decodeFilename(zName, flags) {
1150
+ if (!zName) {
1151
+ return null;
1152
+ }
1153
+ if (flags & VFS.SQLITE_OPEN_URI) {
1154
+ let pName = zName;
1155
+ let state = 1;
1156
+ const charCodes = [];
1157
+ while (state) {
1158
+ const charCode = this.#module.HEAPU8[pName++];
1159
+ if (charCode) {
1160
+ charCodes.push(charCode);
1161
+ continue;
1162
+ }
1163
+ if (!this.#module.HEAPU8[pName]) {
1164
+ state = null;
1165
+ }
1166
+ switch (state) {
1167
+ case 1:
1168
+ charCodes.push("?".charCodeAt(0));
1169
+ state = 2;
1170
+ break;
1171
+ case 2:
1172
+ charCodes.push("=".charCodeAt(0));
1173
+ state = 3;
1174
+ break;
1175
+ case 3:
1176
+ charCodes.push("&".charCodeAt(0));
1177
+ state = 2;
1178
+ break;
1179
+ }
1180
+ }
1181
+ return TEXT_DECODER.decode(new Uint8Array(charCodes));
1182
+ }
1183
+ return this.#module.UTF8ToString(zName);
1184
+ }
1185
+ #heapView() {
1186
+ const heapBuffer = this.#module.HEAPU8.buffer;
1187
+ if (heapBuffer !== this.#heapDataViewBuffer) {
1188
+ this.#heapDataViewBuffer = heapBuffer;
1189
+ this.#heapDataView = new DataView(heapBuffer);
1190
+ }
1191
+ return this.#heapDataView;
1192
+ }
1193
+ #writeInt32(pointer, value) {
1194
+ const heapByteOffset = this.#module.HEAPU8.byteOffset + pointer;
1195
+ this.#heapView().setInt32(heapByteOffset, value, true);
1196
+ }
1197
+ #writeBigInt64(pointer, value) {
1198
+ const heapByteOffset = this.#module.HEAPU8.byteOffset + pointer;
1199
+ this.#heapView().setBigInt64(heapByteOffset, value, true);
1200
+ }
1201
+ };
1202
+ function delegalize(lo32, hi32) {
1203
+ const hi = hi32 >>> 0;
1204
+ const lo = lo32 >>> 0;
1205
+ if (hi > MAX_FILE_SIZE_HI32) {
1206
+ return -1;
1207
+ }
1208
+ if (hi === MAX_FILE_SIZE_HI32 && lo > MAX_FILE_SIZE_LO32) {
1209
+ return -1;
1210
+ }
1211
+ return hi * UINT32_SIZE + lo;
1212
+ }
1213
+
1214
+ // src/pool.ts
1215
+ import { readFileSync as readFileSync2 } from "fs";
1216
+ import { createRequire as createRequire2 } from "module";
1217
+ import path2 from "path";
1218
+ function createNodeRequire2() {
1219
+ return createRequire2(
1220
+ path2.join(process.cwd(), "__rivetkit_sqlite_require__.cjs")
1221
+ );
1222
+ }
1223
+ var SqliteVfsPool = class {
1224
+ #config;
1225
+ #modulePromise = null;
1226
+ #instances = /* @__PURE__ */ new Set();
1227
+ #actorToInstance = /* @__PURE__ */ new Map();
1228
+ #actorToHandle = /* @__PURE__ */ new Map();
1229
+ #shuttingDown = false;
1230
+ constructor(config2) {
1231
+ if (!Number.isInteger(config2.actorsPerInstance) || config2.actorsPerInstance < 1) {
1232
+ throw new Error(
1233
+ `actorsPerInstance must be a positive integer, got ${config2.actorsPerInstance}`
1234
+ );
1235
+ }
1236
+ this.#config = config2;
1237
+ }
1238
+ /**
1239
+ * Compile the WASM module once and cache the promise. Subsequent calls
1240
+ * return the same promise, avoiding redundant compilation.
1241
+ */
1242
+ #getModule() {
1243
+ if (!this.#modulePromise) {
1244
+ this.#modulePromise = (async () => {
1245
+ const require2 = createNodeRequire2();
1246
+ const wasmPath = require2.resolve(
1247
+ "@rivetkit/sqlite/dist/wa-sqlite-async.wasm"
1248
+ );
1249
+ const wasmBinary = readFileSync2(wasmPath);
1250
+ return WebAssembly.compile(wasmBinary);
1251
+ })();
1252
+ this.#modulePromise.catch(() => {
1253
+ this.#modulePromise = null;
1254
+ });
1255
+ }
1256
+ return this.#modulePromise;
1257
+ }
1258
+ /** Number of live WASM instances in the pool. */
1259
+ get instanceCount() {
1260
+ return this.#instances.size;
1261
+ }
1262
+ /** Number of actors currently assigned to pool instances. */
1263
+ get actorCount() {
1264
+ return this.#actorToInstance.size;
1265
+ }
1266
+ /**
1267
+ * Acquire a pooled VFS handle for the given actor. Returns a
1268
+ * PooledSqliteHandle with sticky assignment. If the actor is already
1269
+ * assigned, the existing handle is returned.
1270
+ *
1271
+ * Bin-packing: picks the instance with the most actors that still has
1272
+ * capacity. If all instances are full, creates a new one using the
1273
+ * cached WASM module.
1274
+ */
1275
+ async acquire(actorId) {
1276
+ if (this.#shuttingDown) {
1277
+ throw new Error("SqliteVfsPool is shutting down");
1278
+ }
1279
+ const existingHandle = this.#actorToHandle.get(actorId);
1280
+ if (existingHandle) {
1281
+ return existingHandle;
1282
+ }
1283
+ let bestInstance = null;
1284
+ let bestCount = -1;
1285
+ for (const instance of this.#instances) {
1286
+ if (instance.destroying) continue;
1287
+ const count = instance.actors.size;
1288
+ if (count < this.#config.actorsPerInstance && count > bestCount) {
1289
+ bestInstance = instance;
1290
+ bestCount = count;
1291
+ }
1292
+ }
1293
+ if (!bestInstance) {
1294
+ const wasmModule = await this.#getModule();
1295
+ if (this.#shuttingDown) {
1296
+ throw new Error("SqliteVfsPool is shutting down");
1297
+ }
1298
+ const existingHandleAfterAwait = this.#actorToHandle.get(actorId);
1299
+ if (existingHandleAfterAwait) {
1300
+ return existingHandleAfterAwait;
1301
+ }
1302
+ for (const instance of this.#instances) {
1303
+ if (instance.destroying) continue;
1304
+ const count = instance.actors.size;
1305
+ if (count < this.#config.actorsPerInstance && count > bestCount) {
1306
+ bestInstance = instance;
1307
+ bestCount = count;
1308
+ }
1309
+ }
1310
+ if (!bestInstance) {
1311
+ const vfs = new SqliteVfs(wasmModule);
1312
+ bestInstance = {
1313
+ vfs,
1314
+ actors: /* @__PURE__ */ new Set(),
1315
+ shortNameCounter: 0,
1316
+ actorShortNames: /* @__PURE__ */ new Map(),
1317
+ availableShortNames: /* @__PURE__ */ new Set(),
1318
+ poisonedShortNames: /* @__PURE__ */ new Set(),
1319
+ opsInFlight: 0,
1320
+ idleTimer: null,
1321
+ destroying: false
1322
+ };
1323
+ this.#instances.add(bestInstance);
1324
+ }
1325
+ }
1326
+ this.#cancelIdleTimer(bestInstance);
1327
+ let shortName;
1328
+ const recycled = bestInstance.availableShortNames.values().next();
1329
+ if (!recycled.done) {
1330
+ shortName = recycled.value;
1331
+ bestInstance.availableShortNames.delete(shortName);
1332
+ } else {
1333
+ shortName = String(bestInstance.shortNameCounter++);
1334
+ }
1335
+ bestInstance.actors.add(actorId);
1336
+ bestInstance.actorShortNames.set(actorId, shortName);
1337
+ this.#actorToInstance.set(actorId, bestInstance);
1338
+ const handle = new PooledSqliteHandle(shortName, actorId, this);
1339
+ this.#actorToHandle.set(actorId, handle);
1340
+ return handle;
1341
+ }
1342
+ /**
1343
+ * Release an actor's assignment from the pool. Force-closes all database
1344
+ * handles for the actor, recycles or poisons the short name, and
1345
+ * decrements the instance refcount.
1346
+ */
1347
+ async release(actorId) {
1348
+ const instance = this.#actorToInstance.get(actorId);
1349
+ if (!instance) {
1350
+ return;
1351
+ }
1352
+ const shortName = instance.actorShortNames.get(actorId);
1353
+ if (shortName === void 0) {
1354
+ return;
1355
+ }
1356
+ const { allSucceeded } = await instance.vfs.forceCloseByFileName(shortName);
1357
+ if (allSucceeded) {
1358
+ instance.availableShortNames.add(shortName);
1359
+ } else {
1360
+ instance.poisonedShortNames.add(shortName);
1361
+ }
1362
+ instance.actors.delete(actorId);
1363
+ instance.actorShortNames.delete(actorId);
1364
+ this.#actorToInstance.delete(actorId);
1365
+ this.#actorToHandle.delete(actorId);
1366
+ if (instance.actors.size === 0 && instance.opsInFlight === 0 && !this.#shuttingDown) {
1367
+ this.#startIdleTimer(instance);
1368
+ }
1369
+ }
1370
+ /**
1371
+ * Track an in-flight operation on an instance. Increments opsInFlight
1372
+ * before running fn, decrements after using try/finally to prevent
1373
+ * drift from exceptions. If the decrement brings opsInFlight to 0
1374
+ * with refcount also 0, starts the idle timer.
1375
+ */
1376
+ async #trackOp(instance, fn) {
1377
+ instance.opsInFlight++;
1378
+ try {
1379
+ return await fn();
1380
+ } finally {
1381
+ instance.opsInFlight--;
1382
+ if (instance.actors.size === 0 && instance.opsInFlight === 0 && !instance.destroying && !this.#shuttingDown) {
1383
+ this.#startIdleTimer(instance);
1384
+ }
1385
+ }
1386
+ }
1387
+ /**
1388
+ * Open a database on behalf of an actor, tracked as an in-flight
1389
+ * operation. Used by PooledSqliteHandle to avoid exposing PoolInstance.
1390
+ */
1391
+ async openForActor(actorId, shortName, options) {
1392
+ const instance = this.#actorToInstance.get(actorId);
1393
+ if (!instance) {
1394
+ throw new Error(
1395
+ `Actor ${actorId} is not assigned to any pool instance`
1396
+ );
1397
+ }
1398
+ return this.#trackOp(
1399
+ instance,
1400
+ () => instance.vfs.open(shortName, options)
1401
+ );
1402
+ }
1403
+ /**
1404
+ * Track an in-flight database operation for the given actor. Resolves the
1405
+ * actor's pool instance and wraps the operation with opsInFlight tracking.
1406
+ * If the actor has already been released, the operation runs without
1407
+ * tracking since the instance may already be destroyed.
1408
+ */
1409
+ async trackOpForActor(actorId, fn) {
1410
+ const instance = this.#actorToInstance.get(actorId);
1411
+ if (!instance) {
1412
+ return fn();
1413
+ }
1414
+ return this.#trackOp(instance, fn);
1415
+ }
1416
+ #startIdleTimer(instance) {
1417
+ if (instance.idleTimer || instance.destroying) return;
1418
+ const idleDestroyMs = this.#config.idleDestroyMs ?? 3e4;
1419
+ instance.idleTimer = setTimeout(() => {
1420
+ instance.idleTimer = null;
1421
+ if (instance.actors.size === 0 && instance.opsInFlight === 0 && !instance.destroying) {
1422
+ this.#destroyInstance(instance);
1423
+ }
1424
+ }, idleDestroyMs);
1425
+ }
1426
+ #cancelIdleTimer(instance) {
1427
+ if (instance.idleTimer) {
1428
+ clearTimeout(instance.idleTimer);
1429
+ instance.idleTimer = null;
1430
+ }
1431
+ }
1432
+ async #destroyInstance(instance) {
1433
+ instance.destroying = true;
1434
+ this.#cancelIdleTimer(instance);
1435
+ this.#instances.delete(instance);
1436
+ try {
1437
+ await instance.vfs.forceCloseAll();
1438
+ await instance.vfs.destroy();
1439
+ } catch (error) {
1440
+ console.warn("SqliteVfsPool: failed to destroy instance", error);
1441
+ }
1442
+ }
1443
+ /**
1444
+ * Graceful shutdown. Rejects new acquire() calls, cancels idle timers,
1445
+ * force-closes all databases, destroys all VFS instances, and clears pool
1446
+ * state.
1447
+ */
1448
+ async shutdown() {
1449
+ this.#shuttingDown = true;
1450
+ const instances = [...this.#instances];
1451
+ for (const instance of instances) {
1452
+ this.#cancelIdleTimer(instance);
1453
+ this.#instances.delete(instance);
1454
+ if (instance.opsInFlight > 0) {
1455
+ console.warn(
1456
+ `SqliteVfsPool: shutting down instance with ${instance.opsInFlight} in-flight operation(s). Concurrent close is safe due to Database.close() idempotency.`
1457
+ );
1458
+ }
1459
+ try {
1460
+ await instance.vfs.forceCloseAll();
1461
+ await instance.vfs.destroy();
1462
+ } catch (error) {
1463
+ console.warn(
1464
+ "SqliteVfsPool: failed to destroy instance during shutdown",
1465
+ error
1466
+ );
1467
+ }
1468
+ }
1469
+ this.#actorToInstance.clear();
1470
+ this.#actorToHandle.clear();
1471
+ }
1472
+ };
1473
+ var TrackedDatabase = class {
1474
+ #inner;
1475
+ #pool;
1476
+ #actorId;
1477
+ constructor(inner, pool, actorId) {
1478
+ this.#inner = inner;
1479
+ this.#pool = pool;
1480
+ this.#actorId = actorId;
1481
+ }
1482
+ async exec(...args) {
1483
+ return this.#pool.trackOpForActor(
1484
+ this.#actorId,
1485
+ () => this.#inner.exec(...args)
1486
+ );
1487
+ }
1488
+ async run(...args) {
1489
+ return this.#pool.trackOpForActor(
1490
+ this.#actorId,
1491
+ () => this.#inner.run(...args)
1492
+ );
1493
+ }
1494
+ async query(...args) {
1495
+ return this.#pool.trackOpForActor(
1496
+ this.#actorId,
1497
+ () => this.#inner.query(...args)
1498
+ );
1499
+ }
1500
+ async close() {
1501
+ return this.#pool.trackOpForActor(
1502
+ this.#actorId,
1503
+ () => this.#inner.close()
1504
+ );
1505
+ }
1506
+ get fileName() {
1507
+ return this.#inner.fileName;
1508
+ }
1509
+ };
1510
+ var PooledSqliteHandle = class {
1511
+ #shortName;
1512
+ #actorId;
1513
+ #pool;
1514
+ #released = false;
1515
+ constructor(shortName, actorId, pool) {
1516
+ this.#shortName = shortName;
1517
+ this.#actorId = actorId;
1518
+ this.#pool = pool;
1519
+ }
1520
+ /**
1521
+ * Open a database on the shared instance. Uses the pool-assigned short
1522
+ * name as the VFS file path, with the caller's KvVfsOptions for KV
1523
+ * routing. The open call itself is tracked as an in-flight operation,
1524
+ * and the returned Database is wrapped so that exec(), run(), query(),
1525
+ * and close() are also tracked via opsInFlight.
1526
+ */
1527
+ async open(_fileName, options) {
1528
+ if (this.#released) {
1529
+ throw new Error("PooledSqliteHandle has been released");
1530
+ }
1531
+ const db = await this.#pool.openForActor(
1532
+ this.#actorId,
1533
+ this.#shortName,
1534
+ options
1535
+ );
1536
+ return new TrackedDatabase(db, this.#pool, this.#actorId);
1537
+ }
1538
+ /**
1539
+ * Release this actor's assignment back to the pool. Idempotent: calling
1540
+ * destroy() more than once is a no-op, preventing double-release from
1541
+ * decrementing the instance refcount below actual.
1542
+ */
1543
+ async destroy() {
1544
+ if (this.#released) {
1545
+ return;
1546
+ }
1547
+ this.#released = true;
1548
+ await this.#pool.release(this.#actorId);
1549
+ }
1550
+ };
1551
+ export {
1552
+ Database,
1553
+ PooledSqliteHandle,
1554
+ SqliteVfs,
1555
+ SqliteVfsPool
1556
+ };
1557
+ //# sourceMappingURL=index.js.map