@lightninglabs/pi-402 0.0.0-stage → 0.0.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.
@@ -0,0 +1,850 @@
1
+ // Dedicated SQLite worker using the supported sqlite3.oo1 API directly.
2
+ //
3
+ // This file runs in two very different workers: a browser Web Worker, where
4
+ // SQLite persists through OPFS, and a node:worker_threads worker, where it
5
+ // persists through the node:fs VFS in sqlite-node-vfs.js. The two hosts differ
6
+ // in how scripts load, how the wasm binary is fetched, and how messages cross
7
+ // the thread boundary, so each of those three seams is behind a small
8
+ // abstraction rather than being sprinkled through the request handlers.
9
+
10
+ const workerProtocolVersion = 1;
11
+ let sqlite3;
12
+ let nextDbId = 1;
13
+ const dbs = new Map();
14
+ const openPersistentFiles = new Map();
15
+ let sqliteJSURL = "sqlite3.js";
16
+
17
+ // isNodeWorker reports whether we are running under node:worker_threads. A
18
+ // browser Worker has importScripts and no Node process object, so the two
19
+ // hosts are cleanly distinguishable.
20
+ const isNodeWorker =
21
+ typeof globalThis.process === "object" &&
22
+ !!globalThis.process?.versions?.node &&
23
+ typeof globalThis.importScripts !== "function";
24
+
25
+ // nodeParentPort and nodeAssetDir are the Node-only handles: the message
26
+ // channel back to the bridge, and the directory the bridge told us the runtime
27
+ // assets live in. Both are undefined in a browser.
28
+ let nodeParentPort;
29
+ let nodeAssetDir = "";
30
+
31
+ // nodeVfsUtil is the handle returned when the node:fs VFS is installed. It is
32
+ // how the worker asks the VFS questions SQLite will not answer, such as which
33
+ // process is holding a database it just failed to open.
34
+ let nodeVfsUtil = null;
35
+ if (isNodeWorker) {
36
+ const workerThreads = require("node:worker_threads");
37
+ nodeParentPort = workerThreads.parentPort;
38
+ nodeAssetDir = workerThreads.workerData?.assetDir || process.cwd();
39
+ }
40
+
41
+ // PERSISTENT_VFS_NAMES are the VFS names that promise durable storage. Naming
42
+ // one of these explicitly is a request this worker will never quietly satisfy
43
+ // with memory instead.
44
+ const PERSISTENT_VFS_NAMES = new Set([
45
+ "opfs",
46
+ "opfs-wl",
47
+ "opfs-sahpool",
48
+ "nodefs",
49
+ ]);
50
+
51
+ function normalizeError(error) {
52
+ if (!error) return "unknown sqlite worker error";
53
+ if (error.stack) return error.stack;
54
+ if (error.message) return error.message;
55
+ return String(error);
56
+ }
57
+
58
+ function vfsList() {
59
+ return sqlite3.capi.sqlite3_js_vfs_list();
60
+ }
61
+
62
+ function hasVFS(name) {
63
+ return !!sqlite3.capi.sqlite3_vfs_find(name);
64
+ }
65
+
66
+ // loadSQLiteInBrowser loads sqlite3.js the way a Web Worker does, letting
67
+ // Emscripten fetch sqlite3.wasm over the network relative to that script.
68
+ async function loadSQLiteInBrowser() {
69
+ importScripts(sqliteJSURL);
70
+ return sqlite3InitModule();
71
+ }
72
+
73
+ // loadSQLiteInNode loads sqlite3.js off disk. Emscripten's default loader
74
+ // would fetch() the wasm binary by relative URL, which Node cannot resolve
75
+ // into an absolute one, so the binary is handed over directly instead.
76
+ async function loadSQLiteInNode() {
77
+ const nodeFS = require("node:fs");
78
+ const nodePath = require("node:path");
79
+
80
+ const jsPath = nodePath.isAbsolute(sqliteJSURL)
81
+ ? sqliteJSURL
82
+ : nodePath.join(nodeAssetDir, sqliteJSURL);
83
+ const wasmPath = nodePath.join(nodePath.dirname(jsPath), "sqlite3.wasm");
84
+
85
+ // Every OPFS VFS needs browser APIs that do not exist here, and sqlite3.js
86
+ // reports each failed install as a stack trace on stderr. Turning them off
87
+ // up front keeps a normal Node startup quiet, so a warning that does appear
88
+ // is worth reading.
89
+ globalThis.sqlite3ApiConfig = Object.assign(
90
+ {},
91
+ globalThis.sqlite3ApiConfig,
92
+ {
93
+ disable: {
94
+ vfs: {
95
+ opfs: true,
96
+ "opfs-vfs": true,
97
+ "opfs-sahpool": true,
98
+ "opfs-wl": true,
99
+ },
100
+ },
101
+ }
102
+ );
103
+
104
+ const init = require(jsPath);
105
+ return init({
106
+ instantiateWasm(imports, onSuccess) {
107
+ const bytes = nodeFS.readFileSync(wasmPath);
108
+ WebAssembly.instantiate(bytes, imports).then((result) => {
109
+ onSuccess(result.instance, result.module);
110
+ });
111
+ return {};
112
+ },
113
+ });
114
+ }
115
+
116
+ async function ensureSQLite(options = {}) {
117
+ if (sqlite3) return sqlite3;
118
+
119
+ sqliteJSURL = options.sqliteJSURL || sqliteJSURL;
120
+ sqlite3 = isNodeWorker
121
+ ? await loadSQLiteInNode()
122
+ : await loadSQLiteInBrowser();
123
+
124
+ if (isNodeWorker) {
125
+ // Node has a real filesystem, so the durable VFS is installed eagerly and
126
+ // its absence is fatal rather than something to warn about: there is no
127
+ // second durable option to fall through to here.
128
+ const nodePath = require("node:path");
129
+ const { installNodeFsVfs } = require(
130
+ nodePath.join(nodeAssetDir, "sqlite-node-vfs.js")
131
+ );
132
+ nodeVfsUtil = installNodeFsVfs(sqlite3, {
133
+ directory: options.rootDir || undefined,
134
+ });
135
+
136
+ return sqlite3;
137
+ }
138
+
139
+ if (sqlite3.installOpfsSAHPoolVfs && !hasVFS("opfs-sahpool")) {
140
+ await sqlite3.installOpfsSAHPoolVfs({ name: "opfs-sahpool" }).catch((error) => {
141
+ sqlite3.config.warn("Ignoring inability to install opfs-sahpool:", error.message);
142
+ });
143
+ }
144
+
145
+ return sqlite3;
146
+ }
147
+
148
+ function openFlags(mode) {
149
+ switch ((mode || "rwc").toLowerCase()) {
150
+ case "ro":
151
+ return "r";
152
+ case "rw":
153
+ return "w";
154
+ case "rwc":
155
+ case "memory":
156
+ default:
157
+ return "c";
158
+ }
159
+ }
160
+
161
+ function normalizeFilename(file) {
162
+ if (!file || file === ":memory:") return ":memory:";
163
+ return file;
164
+ }
165
+
166
+ function normalizeRequestedVFS(vfs) {
167
+ switch ((vfs || "auto").toLowerCase()) {
168
+ case "":
169
+ case "auto":
170
+ case "opfs-auto":
171
+ return "auto";
172
+ case "opfs-sah":
173
+ case "sahpool":
174
+ return "opfs-sahpool";
175
+ case "node":
176
+ case "node-fs":
177
+ case "nodefs":
178
+ return "nodefs";
179
+ default:
180
+ return vfs;
181
+ }
182
+ }
183
+
184
+ // autoVFSCandidates lists the durable VFSes to try, best first, when the
185
+ // caller did not name one. The lists do not overlap because no host offers
186
+ // both OPFS and node:fs.
187
+ function autoVFSCandidates() {
188
+ return isNodeWorker ? ["nodefs"] : ["opfs-wl", "opfs-sahpool", "opfs"];
189
+ }
190
+
191
+ // normalizePersistentFilename adapts a caller's filename to what the target
192
+ // VFS expects. OPFS wants an origin-local path rooted at "/", while the
193
+ // node:fs VFS resolves names against a real directory and must be left alone:
194
+ // forcing a leading slash there would rewrite every relative database name
195
+ // into one at the filesystem root.
196
+ function normalizePersistentFilename(vfsName, file) {
197
+ if (vfsName === "nodefs") return file;
198
+ if (file.startsWith("/") || file.startsWith("file:")) return file;
199
+ return `/${file}`;
200
+ }
201
+
202
+ function makeURI(filename, params) {
203
+ const pairs = [];
204
+ for (const [key, value] of Object.entries(params || {})) {
205
+ if (value === undefined || value === null || value === "") continue;
206
+ pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
207
+ }
208
+ if (!pairs.length) return filename;
209
+ const sep = filename.includes("?") ? "&" : "?";
210
+ return `${filename}${sep}${pairs.join("&")}`;
211
+ }
212
+
213
+ function quoteIdentifier(identifier) {
214
+ return `"${String(identifier).replace(/"/g, '""')}"`;
215
+ }
216
+
217
+ function sqlLiteral(value) {
218
+ if (value === null || value === undefined) return "NULL";
219
+ if (value instanceof Uint8Array) {
220
+ let hex = "";
221
+ for (const byte of value) hex += byte.toString(16).padStart(2, "0");
222
+ return `X'${hex}'`;
223
+ }
224
+ if (value instanceof ArrayBuffer) return sqlLiteral(new Uint8Array(value));
225
+ if (typeof value === "number") {
226
+ if (!Number.isFinite(value)) return "NULL";
227
+ return String(value);
228
+ }
229
+ if (typeof value === "bigint") return String(value);
230
+ if (typeof value === "boolean") return value ? "1" : "0";
231
+ return `'${String(value).replace(/'/g, "''")}'`;
232
+ }
233
+
234
+ function normalizeValue(value) {
235
+ if (value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, "__wasmSqliteInt64")) {
236
+ return BigInt(value.__wasmSqliteInt64);
237
+ }
238
+ if (typeof value === "bigint") {
239
+ const asNumber = Number(value);
240
+ if (Number.isSafeInteger(asNumber)) return asNumber;
241
+ return value.toString();
242
+ }
243
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
244
+ return value;
245
+ }
246
+
247
+ function stripParamPrefix(name) {
248
+ return String(name || "").replace(/^[:@$]/, "");
249
+ }
250
+
251
+ function normalizeNamedParams(stmt, params) {
252
+ const normalized = Object.create(null);
253
+ const usedKeys = new Set();
254
+
255
+ for (let i = 1; i <= stmt.parameterCount; i++) {
256
+ const bindName = stmt.getParamName(i);
257
+ if (!bindName) continue;
258
+
259
+ const bareName = stripParamPrefix(bindName);
260
+ let value;
261
+ let matchedKey = "";
262
+ for (const candidate of [bindName, bareName, `:${bareName}`, `$${bareName}`, `@${bareName}`]) {
263
+ if (Object.prototype.hasOwnProperty.call(params, candidate)) {
264
+ value = params[candidate];
265
+ matchedKey = candidate;
266
+ break;
267
+ }
268
+ }
269
+ if (!matchedKey) {
270
+ throw new Error(`missing named SQL parameter: ${bindName}`);
271
+ }
272
+
273
+ normalized[bindName] = normalizeValue(value);
274
+ usedKeys.add(matchedKey);
275
+ }
276
+
277
+ for (const key of Object.keys(params)) {
278
+ if (!usedKeys.has(key)) {
279
+ throw new Error(`unused named SQL parameter: ${key}`);
280
+ }
281
+ }
282
+
283
+ return normalized;
284
+ }
285
+
286
+ function numericParamIndex(name) {
287
+ const match = String(name || "").match(/^[:@$?]([1-9][0-9]*)$/);
288
+ if (!match) return 0;
289
+ return Number(match[1]);
290
+ }
291
+
292
+ function normalizePositionalParams(stmt, params) {
293
+ if (!params.length) return params;
294
+
295
+ const normalized = [];
296
+ const usedParamIndexes = new Set();
297
+
298
+ for (let i = 1; i <= stmt.parameterCount; i++) {
299
+ const bindName = stmt.getParamName(i);
300
+ const numericIndex = numericParamIndex(bindName);
301
+ const paramIndex = numericIndex ? numericIndex - 1 : i - 1;
302
+
303
+ if (paramIndex < 0 || paramIndex >= params.length) {
304
+ throw new Error(`missing SQL parameter for ${bindName || `?${i}`}`);
305
+ }
306
+
307
+ normalized.push(normalizeValue(params[paramIndex]));
308
+ usedParamIndexes.add(paramIndex);
309
+ }
310
+
311
+ for (let i = 0; i < params.length; i++) {
312
+ if (!usedParamIndexes.has(i)) {
313
+ throw new Error(`unused SQL parameter at index ${i + 1}`);
314
+ }
315
+ }
316
+
317
+ return normalized;
318
+ }
319
+
320
+ function bindParams(stmt, params) {
321
+ if (!params) return;
322
+ if (Array.isArray(params)) {
323
+ if (!params.length) return;
324
+ stmt.bind(normalizePositionalParams(stmt, params));
325
+ return;
326
+ }
327
+
328
+ if (typeof params === "object") {
329
+ const keys = Object.keys(params);
330
+ if (!keys.length) return;
331
+ stmt.bind(normalizeNamedParams(stmt, params));
332
+ return;
333
+ }
334
+
335
+ throw new Error("invalid SQL parameter container");
336
+ }
337
+
338
+ function getDB(dbId) {
339
+ const db = dbs.get(dbId);
340
+ if (!db) throw new Error(`database is not open: ${dbId}`);
341
+ return db;
342
+ }
343
+
344
+ function runQuery(db, sql, params) {
345
+ const stmt = db.prepare(sql);
346
+ try {
347
+ bindParams(stmt, params);
348
+ const columnNames = stmt.columnCount ? stmt.getColumnNames([]) : [];
349
+ const resultRows = [];
350
+ while (stmt.step()) {
351
+ const row = stmt.get([]);
352
+ resultRows.push(row.map(normalizeValue));
353
+ }
354
+ return { columnNames, resultRows };
355
+ } finally {
356
+ stmt.finalize();
357
+ }
358
+ }
359
+
360
+ function runExec(db, sql, params) {
361
+ const beforeChanges = db.changes(true);
362
+ // Route any non-empty params (positional array OR named object) through
363
+ // bindParams, exactly like runQuery. bindParams sends positional arrays to
364
+ // normalizePositionalParams, which remaps each $N to its slot BY NUMBER.
365
+ // db.exec({ bind: [...] }) would instead bind the array by SQLite slot order
366
+ // (i.e. order of first appearance), silently mis-binding any statement whose
367
+ // $N are out of textual order -- e.g. UPDATE ... SET col = $3 WHERE k = $1
368
+ // binds k against the value meant for col and matches zero rows. Only the
369
+ // empty-params case falls through to db.exec.
370
+ const hasParams =
371
+ params && (Array.isArray(params) ? params.length : Object.keys(params).length);
372
+ if (hasParams) {
373
+ const stmt = db.prepare(sql);
374
+ try {
375
+ bindParams(stmt, params);
376
+ while (stmt.step()) {}
377
+ } finally {
378
+ stmt.finalize();
379
+ }
380
+ } else {
381
+ db.exec({ sql });
382
+ }
383
+
384
+ return {
385
+ rowsAffected: db.changes(true) - beforeChanges,
386
+ lastInsertId: Number(sqlite3.capi.sqlite3_last_insert_rowid(db.pointer))
387
+ };
388
+ }
389
+
390
+ function runExecBatch(db, sql, paramRows, options = {}) {
391
+ if (!Array.isArray(paramRows)) {
392
+ throw new Error("batch parameter rows must be an array");
393
+ }
394
+
395
+ if (paramRows.length === 0) {
396
+ return {
397
+ rowsAffected: 0,
398
+ lastInsertId: Number(sqlite3.capi.sqlite3_last_insert_rowid(db.pointer)),
399
+ batchCount: 0
400
+ };
401
+ }
402
+
403
+ const beforeChanges = db.changes(true);
404
+ const useTransaction = options.transaction !== false;
405
+ let stmt;
406
+ let committed = false;
407
+
408
+ try {
409
+ if (useTransaction) db.exec("BEGIN IMMEDIATE");
410
+
411
+ stmt = db.prepare(sql);
412
+ for (const params of paramRows) {
413
+ bindParams(stmt, params || []);
414
+ while (stmt.step()) {}
415
+ stmt.reset(true);
416
+ }
417
+
418
+ if (useTransaction) {
419
+ db.exec("COMMIT");
420
+ committed = true;
421
+ }
422
+
423
+ return {
424
+ rowsAffected: db.changes(true) - beforeChanges,
425
+ lastInsertId: Number(sqlite3.capi.sqlite3_last_insert_rowid(db.pointer)),
426
+ batchCount: paramRows.length
427
+ };
428
+ } catch (error) {
429
+ if (useTransaction && !committed) {
430
+ try {
431
+ db.exec("ROLLBACK");
432
+ } catch (_) {}
433
+ }
434
+ throw error;
435
+ } finally {
436
+ if (stmt) stmt.finalize();
437
+ }
438
+ }
439
+
440
+ async function init(args = {}) {
441
+ if (args.expectedBridgeProtocolVersion && args.expectedBridgeProtocolVersion !== workerProtocolVersion) {
442
+ throw new Error(`SQLite bridge protocol mismatch: expected ${workerProtocolVersion}, got ${args.expectedBridgeProtocolVersion}`);
443
+ }
444
+ await ensureSQLite(args);
445
+ return { version: sqlite3.version, vfsList: vfsList(), workerProtocolVersion };
446
+ }
447
+
448
+ // annotateLockedOpen replaces a failed nodefs open with one that names the
449
+ // process holding the database, when that is what went wrong.
450
+ //
451
+ // SQLite reports a lock collision as a bare SQLITE_CANTOPEN, the same code it
452
+ // uses for a missing directory or a permissions failure, and the message the
453
+ // VFS wrote is only reachable through xGetLastError, which the core does not
454
+ // consult. Left alone, a second daemon pointed at one data directory dies with
455
+ // "unable to open database file" and sends whoever is on call looking at file
456
+ // permissions. The lock file knows better, so ask it.
457
+ function annotateLockedOpen(vfsName, filename, error) {
458
+ if (vfsName !== "nodefs" || !nodeVfsUtil) {
459
+ return error;
460
+ }
461
+
462
+ let holder = null;
463
+ try {
464
+ holder = nodeVfsUtil.lockHolder(filename);
465
+ } catch (_) {
466
+ return error;
467
+ }
468
+
469
+ if (!holder) {
470
+ return error;
471
+ }
472
+
473
+ const locked = new Error(
474
+ `database is locked by another process (pid ${holder.pid}): ${filename}`
475
+ );
476
+ locked.sqliteLockHolderPid = holder.pid;
477
+ locked.cause = error;
478
+
479
+ return locked;
480
+ }
481
+
482
+ async function openPersistentDB(vfsName, filename, uriParams, args) {
483
+ if (vfsName === "opfs-sahpool" && sqlite3.installOpfsSAHPoolVfs && !hasVFS("opfs-sahpool")) {
484
+ await sqlite3.installOpfsSAHPoolVfs({ name: "opfs-sahpool" });
485
+ }
486
+ if (!hasVFS(vfsName)) {
487
+ throw new Error(`requested SQLite VFS is unavailable: ${vfsName}`);
488
+ }
489
+
490
+ let resolvedFilename = normalizePersistentFilename(vfsName, filename);
491
+ const resolved = makeURI(resolvedFilename, uriParams);
492
+ const duplicateKey = `${vfsName}:${resolved}`;
493
+ if (openPersistentFiles.has(duplicateKey)) {
494
+ throw new Error(`database already open in this worker: ${resolved}`);
495
+ }
496
+
497
+ let db;
498
+ try {
499
+ if (vfsName === "opfs" && sqlite3.oo1.OpfsDb) {
500
+ db = new sqlite3.oo1.OpfsDb(resolved, openFlags(args.mode));
501
+ } else {
502
+ db = new sqlite3.oo1.DB({ filename: resolved, flags: openFlags(args.mode), vfs: vfsName });
503
+ }
504
+ } catch (error) {
505
+ throw annotateLockedOpen(vfsName, resolvedFilename, error);
506
+ }
507
+
508
+ resolvedFilename = db.dbFilename();
509
+ const vfs = db.dbVfsName() || vfsName;
510
+ openPersistentFiles.set(duplicateKey, true);
511
+ db.__persistentKey = duplicateKey;
512
+
513
+ return { db, filename: resolvedFilename, vfs, persistent: true };
514
+ }
515
+
516
+ // orderPragmas puts any locking_mode pragma at the front and otherwise
517
+ // preserves the caller's order.
518
+ //
519
+ // Callers reasonably think of locking_mode as one setting among several and
520
+ // list it wherever it reads well. On a VFS without shared memory it is not
521
+ // one setting among several: it is the precondition for touching a WAL
522
+ // database at all. Rather than make every caller know that, the driver, which
523
+ // is the part that knows its VFSes have no xShmMap, hoists it.
524
+ function orderPragmas(pragmas) {
525
+ const isLockingMode = (pragma) =>
526
+ /^\s*locking_mode\s*=/i.test(String(pragma));
527
+
528
+ return [
529
+ ...pragmas.filter(isLockingMode),
530
+ ...pragmas.filter((pragma) => !isLockingMode(pragma)),
531
+ ];
532
+ }
533
+
534
+ // extractJournalMode pulls a journal_mode pragma out of the caller's list and
535
+ // returns it alongside the pragmas that remain.
536
+ //
537
+ // The journal mode cannot be applied as an ordinary pragma, because the mode
538
+ // SQLite ends up in and the mode it was asked for are allowed to differ and
539
+ // only applyJournalMode looks. The pragma list is the natural place to write
540
+ // it, being where every other pragma goes, so a caller who does that would get
541
+ // precisely the silent downgrade the readback exists to refuse. Routing it to
542
+ // the checked path is kinder than rejecting it and gives the same answer.
543
+ function extractJournalMode(pragmas, journalMode) {
544
+ const isJournalMode = (pragma) =>
545
+ /^\s*journal_mode\s*=/i.test(String(pragma));
546
+
547
+ const found = pragmas.filter(isJournalMode);
548
+ if (!found.length) {
549
+ return { pragmas, journalMode };
550
+ }
551
+
552
+ // Last one wins, matching what SQLite would have done had these run as
553
+ // ordinary pragmas in order.
554
+ const requested = String(found[found.length - 1]).split("=")[1].trim();
555
+ if (journalMode && journalMode.toLowerCase() !== requested.toLowerCase()) {
556
+ throw new Error(
557
+ `conflicting journal modes: "${journalMode}" was requested as an ` +
558
+ `option and "${requested}" as a pragma`
559
+ );
560
+ }
561
+
562
+ return {
563
+ pragmas: pragmas.filter((pragma) => !isJournalMode(pragma)),
564
+ journalMode: journalMode || requested,
565
+ };
566
+ }
567
+
568
+ // releaseOpened closes a database that was opened but never handed out, and
569
+ // drops the duplicate-open bookkeeping that goes with it.
570
+ function releaseOpened(opened) {
571
+ try {
572
+ if (opened.db.__persistentKey) {
573
+ openPersistentFiles.delete(opened.db.__persistentKey);
574
+ }
575
+ opened.db.close();
576
+ } catch (closeError) {
577
+ sqlite3.config.warn(
578
+ "failed to close a database after a failed open:",
579
+ closeError.message || String(closeError)
580
+ );
581
+ }
582
+ }
583
+
584
+ // applyJournalMode sets the requested journal mode and then proves it took.
585
+ //
586
+ // SQLite answers a journal mode it cannot honor by quietly leaving the
587
+ // database in the mode it was already in, and PRAGMA journal_mode reports the
588
+ // mode in force rather than the mode asked for. A caller that asks for WAL on
589
+ // a VFS or a locking mode that cannot provide it therefore runs on the
590
+ // rollback journal while believing otherwise, and durability settings chosen
591
+ // for WAL do not mean the same thing there: synchronous=NORMAL loses only the
592
+ // recent tail under WAL, but risks a corrupt database under the rollback
593
+ // journal. Reading the mode back turns that class of mistake into a startup
594
+ // error, in the same spirit as refusing to substitute memory for durable
595
+ // storage.
596
+ function applyJournalMode(opened, requested) {
597
+ if (!requested) {
598
+ return;
599
+ }
600
+
601
+ opened.db.exec(`PRAGMA journal_mode=${requested}`);
602
+
603
+ // An in-memory database is always in "memory" journal mode and cannot be
604
+ // moved out of it. There is no silent downgrade to catch, because there was
605
+ // never any durability on offer to lose.
606
+ if (!opened.persistent) {
607
+ return;
608
+ }
609
+
610
+ const effective = String(
611
+ opened.db.selectValue("PRAGMA journal_mode") || ""
612
+ ).toLowerCase();
613
+ if (effective !== requested.toLowerCase()) {
614
+ throw new Error(
615
+ `journal_mode=${requested} was requested but SQLite is in ` +
616
+ `"${effective}" mode on VFS ${opened.vfs}; refusing to run with ` +
617
+ `weaker guarantees than the caller asked for`
618
+ );
619
+ }
620
+ }
621
+
622
+ function openMemoryDB(args) {
623
+ if (args.requirePersistent) {
624
+ throw new Error("persistent storage required but memory VFS was requested");
625
+ }
626
+ return {
627
+ db: new sqlite3.oo1.DB(":memory:", openFlags(args.mode)),
628
+ filename: ":memory:",
629
+ vfs: "memory",
630
+ persistent: false
631
+ };
632
+ }
633
+
634
+ async function open(args) {
635
+ await ensureSQLite();
636
+
637
+ const requestedVFS = normalizeRequestedVFS(args.vfs);
638
+ let filename = normalizeFilename(args.file || args.filename || "/app.db");
639
+ let opened;
640
+
641
+ const uriParams = {};
642
+ if (args.cache) uriParams.cache = args.cache;
643
+
644
+ if (filename === ":memory:" || requestedVFS === "memory") {
645
+ opened = openMemoryDB(args);
646
+ } else {
647
+ const candidates = requestedVFS === "auto"
648
+ ? autoVFSCandidates()
649
+ : [requestedVFS];
650
+ const failures = [];
651
+ for (const candidate of candidates) {
652
+ try {
653
+ opened = await openPersistentDB(candidate, filename, uriParams, args);
654
+ break;
655
+ } catch (error) {
656
+ failures.push(`${candidate}: ${error.message || String(error)}`);
657
+ if (/already open/i.test(error.message || "")) {
658
+ throw error;
659
+ }
660
+ if (requestedVFS !== "auto") {
661
+ throw error;
662
+ }
663
+ }
664
+ }
665
+ if (!opened) {
666
+ // Naming a durable VFS, or asking for persistence outright, is a
667
+ // promise this worker keeps or breaks loudly. Substituting memory here
668
+ // would let a caller run happily against storage that evaporates on
669
+ // exit, which for a wallet database means losing state that is not
670
+ // recoverable from anywhere else.
671
+ if (args.requirePersistent || PERSISTENT_VFS_NAMES.has(requestedVFS)) {
672
+ throw new Error(`persistent storage required but no durable VFS opened; tried ${failures.join("; ")}`);
673
+ }
674
+ sqlite3.config.warn("no durable VFS available, falling back to :memory:", failures.join("; "));
675
+ opened = openMemoryDB(args);
676
+ }
677
+ }
678
+
679
+ if (args.busyTimeout > 0) {
680
+ sqlite3.capi.sqlite3_busy_timeout(opened.db.pointer, args.busyTimeout);
681
+ }
682
+
683
+ // Pragma ordering here is load-bearing rather than cosmetic, and for one
684
+ // reason: none of the VFSes this driver can reach implements xShmMap. That
685
+ // makes SQLite's "WAL without shared memory" mode the only WAL available,
686
+ // and that mode is conditional on the connection already being in
687
+ // locking_mode=EXCLUSIVE. Two things follow.
688
+ //
689
+ // The locking mode goes first, ahead of the caller's other pragmas, because
690
+ // a WAL database cannot even be read by a non-exclusive connection here. A
691
+ // pragma as innocent as auto_vacuum, which reads page 1, fails with
692
+ // SQLITE_CANTOPEN when it runs one statement too early.
693
+ //
694
+ // The journal mode goes last, after the locking mode is in force. Setting
695
+ // it first, as this once did, meant a caller asking for WAL got the
696
+ // rollback journal and no indication that it had.
697
+ try {
698
+ const routed = extractJournalMode(args.pragma || [], args.journalMode);
699
+
700
+ for (const pragma of orderPragmas(routed.pragmas)) {
701
+ if (!pragma) continue;
702
+ try {
703
+ opened.db.exec(`PRAGMA ${pragma}`);
704
+ } catch (error) {
705
+ // Name the pragma. SQLite's own message says only what went wrong,
706
+ // and a DSN carries several of these, so without the name a failure
707
+ // here sends the reader looking through all of them.
708
+ throw new Error(
709
+ `PRAGMA ${pragma} failed on ${opened.filename}: ` +
710
+ (error.message || String(error))
711
+ );
712
+ }
713
+ }
714
+ applyJournalMode(opened, routed.journalMode);
715
+ } catch (error) {
716
+ // Configuration failed after the file was opened, so nothing will ever
717
+ // hand this handle back to the caller. Release it here: a database left
718
+ // open holds the VFS's exclusive claim on the file (an OPFS access handle
719
+ // in a browser, a lock file under nodefs) for the lifetime of the worker,
720
+ // which would turn a retry into "already open" and strand the database.
721
+ releaseOpened(opened);
722
+ throw error;
723
+ }
724
+
725
+ const dbId = `db${nextDbId++}`;
726
+ dbs.set(dbId, opened.db);
727
+
728
+ return { dbId, filename: opened.filename, vfs: opened.vfs, persistent: opened.persistent };
729
+ }
730
+
731
+ async function exec(args) {
732
+ const db = getDB(args.dbId);
733
+ return runExec(db, args.sql, args.params || []);
734
+ }
735
+
736
+ async function execBatch(args) {
737
+ const db = getDB(args.dbId);
738
+ return runExecBatch(db, args.sql, args.paramRows || [], {
739
+ transaction: args.transaction !== false
740
+ });
741
+ }
742
+
743
+ async function query(args) {
744
+ const db = getDB(args.dbId);
745
+ return runQuery(db, args.sql, args.params || []);
746
+ }
747
+
748
+ async function begin(args) {
749
+ getDB(args.dbId).exec("BEGIN IMMEDIATE");
750
+ return {};
751
+ }
752
+
753
+ async function commit(args) {
754
+ getDB(args.dbId).exec("COMMIT");
755
+ return {};
756
+ }
757
+
758
+ async function rollback(args) {
759
+ getDB(args.dbId).exec("ROLLBACK");
760
+ return {};
761
+ }
762
+
763
+ async function close(args) {
764
+ const db = dbs.get(args.dbId);
765
+ if (!db) return {};
766
+
767
+ if (db.__persistentKey) openPersistentFiles.delete(db.__persistentKey);
768
+ db.close();
769
+ dbs.delete(args.dbId);
770
+ return {};
771
+ }
772
+
773
+ async function dump(args) {
774
+ const db = getDB(args.dbId);
775
+ let dumpSQL = "BEGIN TRANSACTION;\n";
776
+
777
+ const schemaRows = runQuery(
778
+ db,
779
+ "SELECT type, name, tbl_name, sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY tbl_name, type DESC, name",
780
+ []
781
+ ).resultRows;
782
+ for (const row of schemaRows) {
783
+ dumpSQL += `${row[3]};\n`;
784
+ }
785
+
786
+ const tableRows = runQuery(
787
+ db,
788
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name",
789
+ []
790
+ ).resultRows;
791
+ for (const [tableName] of tableRows) {
792
+ const columns = runQuery(db, `PRAGMA table_info(${quoteIdentifier(tableName)})`, [])
793
+ .resultRows
794
+ .map((row) => row[1]);
795
+ const rows = runQuery(db, `SELECT * FROM ${quoteIdentifier(tableName)}`, []).resultRows;
796
+ for (const row of rows) {
797
+ dumpSQL += `INSERT INTO ${quoteIdentifier(tableName)} (${columns.map(quoteIdentifier).join(", ")}) VALUES (${row.map(sqlLiteral).join(", ")});\n`;
798
+ }
799
+ }
800
+
801
+ dumpSQL += "COMMIT;\n";
802
+ return { dump: dumpSQL };
803
+ }
804
+
805
+ async function load(args) {
806
+ const db = getDB(args.dbId);
807
+ const tables = runQuery(
808
+ db,
809
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name",
810
+ []
811
+ ).resultRows;
812
+ db.exec("PRAGMA foreign_keys=OFF");
813
+ try {
814
+ for (const [tableName] of tables) {
815
+ db.exec(`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}`);
816
+ }
817
+ db.exec(args.sql || "");
818
+ } catch (error) {
819
+ throw error;
820
+ } finally {
821
+ db.exec("PRAGMA foreign_keys=ON");
822
+ }
823
+ return {};
824
+ }
825
+
826
+ const methods = { init, open, exec, execBatch, query, begin, commit, rollback, close, dump, load };
827
+
828
+ // handleMessage is the host-independent request handler. Both worker flavors
829
+ // deliver the same {id, method, args} envelope and expect the same
830
+ // {id, ok, result|error} reply; only the transport differs.
831
+ async function handleMessage(message = {}) {
832
+ try {
833
+ const method = methods[message.method];
834
+ if (!method) throw new Error(`unknown sqlite worker method: ${message.method}`);
835
+ const result = await method(message.args || {});
836
+ return { id: message.id, ok: true, result };
837
+ } catch (error) {
838
+ return { id: message.id, ok: false, error: normalizeError(error) };
839
+ }
840
+ }
841
+
842
+ if (isNodeWorker) {
843
+ nodeParentPort.on("message", async (message) => {
844
+ nodeParentPort.postMessage(await handleMessage(message || {}));
845
+ });
846
+ } else {
847
+ globalThis.onmessage = async (event) => {
848
+ globalThis.postMessage(await handleMessage(event.data || {}));
849
+ };
850
+ }