@lightninglabs/pi-402 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +270 -0
- package/LICENSE +21 -0
- package/README.md +192 -2
- package/package.json +47 -5
- package/src/CLAUDE.md +57 -0
- package/src/commands.ts +104 -0
- package/src/index.ts +138 -0
- package/src/payer.ts +34 -0
- package/src/policy.ts +209 -0
- package/src/provider.ts +568 -0
- package/src/store.ts +387 -0
- package/src/wallet.ts +1179 -0
- package/wasm/package.json +1 -0
- package/wasm/sqlite-bridge.js +240 -0
- package/wasm/sqlite-node-vfs.js +894 -0
- package/wasm/sqlite-worker.js +850 -0
- package/wasm/sqlite3.js +14318 -0
- package/wasm/sqlite3.wasm +0 -0
- package/wasm/wasm_exec.js +575 -0
- package/wasm/wavewalletdk.wasm +4 -0
|
@@ -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
|
+
}
|