@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,894 @@
|
|
|
1
|
+
// A SQLite VFS backed by node:fs, for hosts that run this driver under Node
|
|
2
|
+
// instead of a browser.
|
|
3
|
+
//
|
|
4
|
+
// The browser story is OPFS, which is asynchronous at its core and therefore
|
|
5
|
+
// needs either the async proxy worker or the SAH pool's preallocated
|
|
6
|
+
// synchronous access handles. Node has none of that problem: node:fs exposes
|
|
7
|
+
// genuinely synchronous file operations, which is exactly the shape SQLite's
|
|
8
|
+
// VFS layer wants, so this VFS is a thin translation rather than a pooling
|
|
9
|
+
// scheme. It is modeled on opfs-sahpool for structure only.
|
|
10
|
+
//
|
|
11
|
+
// The one property OPFS gives away for free that a plain filesystem does not
|
|
12
|
+
// is exclusive access: a SyncAccessHandle cannot be held by two tabs at once.
|
|
13
|
+
// A wallet database that two processes write concurrently corrupts silently,
|
|
14
|
+
// so this VFS reconstructs that property with a pid-stamped lock file taken
|
|
15
|
+
// when a main database is opened for writing. Two Node processes pointed at
|
|
16
|
+
// one database fail loudly instead of interleaving writes.
|
|
17
|
+
//
|
|
18
|
+
// That reconstruction is narrower than the thing it replaces, and the limits
|
|
19
|
+
// are worth stating rather than discovering. Node core exposes no flock or
|
|
20
|
+
// fcntl, so the claim rests on a file whose contents name the owning process,
|
|
21
|
+
// and reading it back is only meaningful where process ids mean the same thing
|
|
22
|
+
// to both parties. Concretely:
|
|
23
|
+
//
|
|
24
|
+
// - Writers only. A read-only open takes no lock and is not blocked by one.
|
|
25
|
+
// - One pid namespace. Two containers sharing a bind-mounted data directory
|
|
26
|
+
// have unrelated pid numbering, so each would read the other's lock as
|
|
27
|
+
// belonging to a process that does not exist.
|
|
28
|
+
// - Local filesystems. Over NFS or SMB neither the atomicity this relies on
|
|
29
|
+
// nor the pid check holds.
|
|
30
|
+
//
|
|
31
|
+
// Within a single machine and pid namespace, a recycled pid is handled: the
|
|
32
|
+
// lock records the owner's kernel start time where the platform exposes it, so
|
|
33
|
+
// a reused pid number does not read as the original owner.
|
|
34
|
+
//
|
|
35
|
+
// The lock file is named `<database><LOCK_SUFFIX>`. If a database is stranded
|
|
36
|
+
// behind a lock whose owner is genuinely gone and this VFS declines to reclaim
|
|
37
|
+
// it, deleting that file by hand is the recovery, and is safe exactly when no
|
|
38
|
+
// process is using the database.
|
|
39
|
+
//
|
|
40
|
+
// One method is deliberately absent: xShmMap. Shared memory exists so that
|
|
41
|
+
// several connections can share one WAL index, and this VFS has already ruled
|
|
42
|
+
// that out by holding the database exclusively. WAL is still available, but
|
|
43
|
+
// only by the route SQLite documents for exactly this situation: set
|
|
44
|
+
// locking_mode=EXCLUSIVE before switching journal modes and SQLite keeps the
|
|
45
|
+
// WAL index on the heap instead. Ask for WAL without that and SQLite silently
|
|
46
|
+
// stays on the rollback journal, which is why the worker reads the effective
|
|
47
|
+
// journal mode back and refuses the mismatch.
|
|
48
|
+
|
|
49
|
+
"use strict";
|
|
50
|
+
|
|
51
|
+
const nodeFS = require("node:fs");
|
|
52
|
+
const nodePath = require("node:path");
|
|
53
|
+
const nodeCrypto = require("node:crypto");
|
|
54
|
+
|
|
55
|
+
// DEFAULT_VFS_NAME is the name this VFS registers under, and the name callers
|
|
56
|
+
// pass as `vfs=` in a DSN.
|
|
57
|
+
const DEFAULT_VFS_NAME = "nodefs";
|
|
58
|
+
|
|
59
|
+
// SECTOR_SIZE is what we report to SQLite as the atomic write unit. 4096
|
|
60
|
+
// matches the page size of every filesystem we care about.
|
|
61
|
+
const SECTOR_SIZE = 4096;
|
|
62
|
+
|
|
63
|
+
// LOCK_SUFFIX names the sidecar file carrying the exclusive claim on a main
|
|
64
|
+
// database. It sits next to the database itself, so an operator who needs to
|
|
65
|
+
// clear a lock by hand can find it without knowing this file.
|
|
66
|
+
const LOCK_SUFFIX = "-nodefs-lock";
|
|
67
|
+
|
|
68
|
+
// RECLAIM_STALE_MS bounds how long the reclaim marker is honored. Reclaiming a
|
|
69
|
+
// lock is a handful of syscalls, so a marker older than this was left by a
|
|
70
|
+
// process that died in the middle of one.
|
|
71
|
+
const RECLAIM_STALE_MS = 30_000;
|
|
72
|
+
|
|
73
|
+
// sleepSignal backs xSleep. Atomics.wait needs a shared integer array to wait
|
|
74
|
+
// on; nothing ever notifies this one, so every wait runs to its timeout, which
|
|
75
|
+
// is exactly the blocking sleep SQLite is asking for.
|
|
76
|
+
const sleepSignal = new Int32Array(new SharedArrayBuffer(4));
|
|
77
|
+
|
|
78
|
+
// isNodeHost reports whether we are running under Node rather than a browser
|
|
79
|
+
// or a browser Worker. Callers use it to decide whether installing this VFS is
|
|
80
|
+
// even possible.
|
|
81
|
+
function isNodeHost() {
|
|
82
|
+
return (
|
|
83
|
+
typeof globalThis.process === "object" &&
|
|
84
|
+
!!globalThis.process?.versions?.node &&
|
|
85
|
+
typeof globalThis.importScripts !== "function"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// pidIsAlive reports whether a process id still exists. Signal 0 performs the
|
|
90
|
+
// permission and existence checks without delivering anything.
|
|
91
|
+
function pidIsAlive(pid) {
|
|
92
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
process.kill(pid, 0);
|
|
97
|
+
return true;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// EPERM means the process exists but belongs to another user, which
|
|
100
|
+
// still counts as alive.
|
|
101
|
+
return error.code === "EPERM";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// processStartTicks reads the kernel's record of when a process started, in
|
|
106
|
+
// clock ticks since boot. A pid on its own is not an identity: pids are
|
|
107
|
+
// recycled, and a crashed owner whose number has since been reused would read
|
|
108
|
+
// as alive forever, stranding the database behind a lock nothing will reclaim.
|
|
109
|
+
// The pair (pid, start time) is not reused within a boot, so it is one.
|
|
110
|
+
//
|
|
111
|
+
// Only Linux exposes this for an arbitrary pid. Elsewhere this returns null and
|
|
112
|
+
// the caller falls back to the plain liveness check, which is what this code
|
|
113
|
+
// did before.
|
|
114
|
+
function processStartTicks(pid) {
|
|
115
|
+
try {
|
|
116
|
+
const stat = nodeFS.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
117
|
+
|
|
118
|
+
// The second field is the executable name in parentheses and may
|
|
119
|
+
// itself contain spaces and parentheses, so the fields after it are
|
|
120
|
+
// counted from the last ')' rather than from the start of the line.
|
|
121
|
+
// That puts field 3 at index 0, and starttime is field 22.
|
|
122
|
+
const after = stat.slice(stat.lastIndexOf(")") + 2);
|
|
123
|
+
const ticks = Number(after.split(" ")[19]);
|
|
124
|
+
|
|
125
|
+
return Number.isFinite(ticks) ? ticks : null;
|
|
126
|
+
} catch (_) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ownerIsLive reports whether the process named by a lock file is still
|
|
132
|
+
// running. A pid that exists but started at a different time than the lock
|
|
133
|
+
// records is a recycled number rather than the original owner, so the lock is
|
|
134
|
+
// stale despite the pid being alive.
|
|
135
|
+
function ownerIsLive(owner) {
|
|
136
|
+
if (!owner || !pidIsAlive(owner.pid)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const ticks = processStartTicks(owner.pid);
|
|
141
|
+
if (ticks === null || owner.startTicks === null ||
|
|
142
|
+
owner.startTicks === undefined) {
|
|
143
|
+
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return ticks === owner.startTicks;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// installNodeFsVfs registers the node:fs VFS with the given sqlite3 namespace
|
|
151
|
+
// and returns a small utility handle. It is idempotent: a second call with the
|
|
152
|
+
// same name returns the already-registered handle rather than failing, so a
|
|
153
|
+
// worker can call it unconditionally on every open.
|
|
154
|
+
function installNodeFsVfs(sqlite3, options = {}) {
|
|
155
|
+
if (!isNodeHost()) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
"the nodefs SQLite VFS requires a Node host; use OPFS in " +
|
|
158
|
+
"a browser"
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const vfsName = options.name || DEFAULT_VFS_NAME;
|
|
163
|
+
const capi = sqlite3.capi;
|
|
164
|
+
const wasm = sqlite3.wasm;
|
|
165
|
+
|
|
166
|
+
if (capi.sqlite3_vfs_find(vfsName)) {
|
|
167
|
+
return installNodeFsVfs.installed?.get(vfsName);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// rootDir anchors relative database names. Absolute names are used as
|
|
171
|
+
// given, so a host that already knows its data directory can simply pass
|
|
172
|
+
// full paths.
|
|
173
|
+
const rootDir = nodePath.resolve(options.directory || process.cwd());
|
|
174
|
+
|
|
175
|
+
// openFiles maps a sqlite3_file pointer to the state we keep for it. The
|
|
176
|
+
// pointer is the only handle SQLite hands back to the io methods.
|
|
177
|
+
const openFiles = new Map();
|
|
178
|
+
|
|
179
|
+
// heldLocks maps a lock file path to the descriptor holding it, so xClose
|
|
180
|
+
// can release exactly what xOpen took.
|
|
181
|
+
const heldLocks = new Map();
|
|
182
|
+
|
|
183
|
+
// lastError carries the most recent failure so xGetLastError can report
|
|
184
|
+
// something better than a bare result code.
|
|
185
|
+
let lastError = null;
|
|
186
|
+
|
|
187
|
+
function storeErr(error, rc) {
|
|
188
|
+
if (error !== undefined) {
|
|
189
|
+
lastError = error;
|
|
190
|
+
}
|
|
191
|
+
return rc === undefined ? 0 : rc;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function popErr() {
|
|
195
|
+
const error = lastError;
|
|
196
|
+
lastError = null;
|
|
197
|
+
return error;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolvePath(name) {
|
|
201
|
+
return nodePath.resolve(rootDir, name);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function fileFor(pFile) {
|
|
205
|
+
const file = openFiles.get(Number(pFile));
|
|
206
|
+
if (!file) {
|
|
207
|
+
throw new Error(`no open node:fs file for handle ${pFile}`);
|
|
208
|
+
}
|
|
209
|
+
return file;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// syncDir flushes a directory entry, so that a file's creation or removal
|
|
213
|
+
// is itself durable rather than merely visible. Without it a power loss can
|
|
214
|
+
// undo the unlink of a rollback journal that SQLite treated as the commit
|
|
215
|
+
// point, resurrecting the journal and rolling a committed transaction back.
|
|
216
|
+
//
|
|
217
|
+
// Failure is deliberately swallowed. Not every platform lets a directory be
|
|
218
|
+
// opened for reading, and turning a weaker barrier into a failed delete
|
|
219
|
+
// would be the worse trade.
|
|
220
|
+
function syncDir(dirPath) {
|
|
221
|
+
let dfd;
|
|
222
|
+
try {
|
|
223
|
+
dfd = nodeFS.openSync(dirPath, "r");
|
|
224
|
+
} catch (_) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
nodeFS.fsyncSync(dfd);
|
|
230
|
+
} catch (_) {
|
|
231
|
+
} finally {
|
|
232
|
+
nodeFS.closeSync(dfd);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ioErrForWrite distinguishes a full disk from a broken one. SQLite's own
|
|
237
|
+
// unix VFS returns SQLITE_FULL for ENOSPC, and the difference is what sends
|
|
238
|
+
// an operator to `df` rather than to smartctl.
|
|
239
|
+
function ioErrForWrite(error, fallback) {
|
|
240
|
+
return error && error.code === "ENOSPC" ? capi.SQLITE_FULL : fallback;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// heapView returns a view of the wasm heap for a pointer/length pair. The
|
|
244
|
+
// view is only valid for the duration of one synchronous call, since any
|
|
245
|
+
// wasm memory growth detaches it.
|
|
246
|
+
function heapView(ptr, n) {
|
|
247
|
+
const start = Number(ptr);
|
|
248
|
+
return wasm.heap8u().subarray(start, start + n);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// readLockOwner returns the parsed contents of a lock file, or null when it
|
|
252
|
+
// is missing or unreadable. An unparseable lock file is treated as stale,
|
|
253
|
+
// since the only way to produce one is to crash midway through writing it.
|
|
254
|
+
function readLockOwner(lockPath) {
|
|
255
|
+
try {
|
|
256
|
+
return JSON.parse(nodeFS.readFileSync(lockPath, "utf8"));
|
|
257
|
+
} catch (_) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// lockedError builds the error for a database another process holds. The
|
|
263
|
+
// holder travels on the error object as well as in the message, because
|
|
264
|
+
// the worker turns it into something the caller can act on rather than the
|
|
265
|
+
// bare SQLITE_CANTOPEN that SQLite would otherwise surface.
|
|
266
|
+
function lockedError(dbPath, lockPath, owner) {
|
|
267
|
+
const pid = owner && owner.pid ? owner.pid : "unknown";
|
|
268
|
+
const error = new Error(
|
|
269
|
+
`database is locked by another process (pid ${pid}): ${dbPath}`
|
|
270
|
+
);
|
|
271
|
+
error.nodefsLockHolderPid = owner ? owner.pid : null;
|
|
272
|
+
error.nodefsLockPath = lockPath;
|
|
273
|
+
|
|
274
|
+
return error;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// writeLockFile creates the lock file and stamps it with this process's
|
|
278
|
+
// identity. It reports false when the lock already exists, and throws for
|
|
279
|
+
// every other failure.
|
|
280
|
+
function writeLockFile(lockPath) {
|
|
281
|
+
let fd;
|
|
282
|
+
try {
|
|
283
|
+
fd = nodeFS.openSync(lockPath, "wx", 0o600);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (error.code === "EEXIST") {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
nodeFS.writeSync(
|
|
293
|
+
fd,
|
|
294
|
+
JSON.stringify({
|
|
295
|
+
pid: process.pid,
|
|
296
|
+
startTicks: processStartTicks(process.pid),
|
|
297
|
+
at: Date.now(),
|
|
298
|
+
})
|
|
299
|
+
);
|
|
300
|
+
} finally {
|
|
301
|
+
nodeFS.closeSync(fd);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// withReclaimMutex runs body as the only process reclaiming this lock, and
|
|
308
|
+
// reports whether it ran at all.
|
|
309
|
+
//
|
|
310
|
+
// Reclaiming means observing a dead owner and then replacing the file, and
|
|
311
|
+
// those two steps cannot be made one with anything Node exposes. Left
|
|
312
|
+
// unserialized they interleave, and the interleavings all end the same
|
|
313
|
+
// way: while one process has the stale lock momentarily out of the way, a
|
|
314
|
+
// second finds the path empty, creates its own, and two writers now hold
|
|
315
|
+
// one database. Inspecting the file first does not help, because the
|
|
316
|
+
// inspection is itself a window.
|
|
317
|
+
//
|
|
318
|
+
// mkdir is the one exclusion primitive available here that costs nothing:
|
|
319
|
+
// it fails if the directory exists. So a marker directory beside the lock
|
|
320
|
+
// serializes reclamation, and only its holder may touch the lock file.
|
|
321
|
+
function withReclaimMutex(lockPath, body) {
|
|
322
|
+
const mutexPath = `${lockPath}.reclaim`;
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
nodeFS.mkdirSync(mutexPath);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (error.code !== "EEXIST") {
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// The marker is held across a handful of syscalls, so one older
|
|
332
|
+
// than any reclaim could take belongs to a process that died
|
|
333
|
+
// inside one. Clearing it costs a retry at worst, where leaving it
|
|
334
|
+
// would strand the database for good.
|
|
335
|
+
let age = RECLAIM_STALE_MS;
|
|
336
|
+
try {
|
|
337
|
+
age = Date.now() - nodeFS.statSync(mutexPath).mtimeMs;
|
|
338
|
+
} catch (_) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (age < RECLAIM_STALE_MS) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
nodeFS.rmdirSync(mutexPath);
|
|
348
|
+
} catch (_) {}
|
|
349
|
+
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
return body();
|
|
355
|
+
} finally {
|
|
356
|
+
try {
|
|
357
|
+
nodeFS.rmdirSync(mutexPath);
|
|
358
|
+
} catch (_) {}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// reclaimStaleLock replaces a lock whose owner is gone with one naming this
|
|
363
|
+
// process, and reports whether it now holds the lock. The replacement
|
|
364
|
+
// happens under the reclaim mutex so that no other process can slip into
|
|
365
|
+
// the moment when the lock file does not exist.
|
|
366
|
+
function reclaimStaleLock(lockPath) {
|
|
367
|
+
return withReclaimMutex(lockPath, () => {
|
|
368
|
+
// Read it again in here. A reclaimer ahead of us may have
|
|
369
|
+
// finished between our staleness check and our getting the
|
|
370
|
+
// mutex, in which case the lock is now held by a live process.
|
|
371
|
+
const owner = readLockOwner(lockPath);
|
|
372
|
+
if (ownerIsLive(owner)) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
nodeFS.unlinkSync(lockPath);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (error.code !== "ENOENT") {
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return writeLockFile(lockPath);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// acquireLock takes the exclusive open lock for a main database file. It
|
|
389
|
+
// reclaims a lock whose owning process is gone, and otherwise throws so
|
|
390
|
+
// the open fails loudly rather than racing another writer.
|
|
391
|
+
function acquireLock(dbPath) {
|
|
392
|
+
const lockPath = `${dbPath}${LOCK_SUFFIX}`;
|
|
393
|
+
|
|
394
|
+
// Two passes are all this needs: one to find the lock taken and clear
|
|
395
|
+
// a dead owner out of the way, and one to take it. A process that
|
|
396
|
+
// loses the reclaim race does not get a third try, because the winner
|
|
397
|
+
// now holds a live lock and the answer is no.
|
|
398
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
399
|
+
if (writeLockFile(lockPath)) {
|
|
400
|
+
heldLocks.set(dbPath, lockPath);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const owner = readLockOwner(lockPath);
|
|
405
|
+
if (ownerIsLive(owner)) {
|
|
406
|
+
throw lockedError(dbPath, lockPath, owner);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (reclaimStaleLock(lockPath)) {
|
|
410
|
+
heldLocks.set(dbPath, lockPath);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
throw lockedError(dbPath, lockPath, readLockOwner(lockPath));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// releaseLock drops the claim taken by acquireLock. It checks that the
|
|
419
|
+
// lock still names this process before removing it: if a reclaimer
|
|
420
|
+
// elsewhere concluded we were dead and took the lock for itself, the file
|
|
421
|
+
// on disk is now that process's claim, and deleting it would hand the
|
|
422
|
+
// database to a third.
|
|
423
|
+
function releaseLock(dbPath) {
|
|
424
|
+
const lockPath = heldLocks.get(dbPath);
|
|
425
|
+
if (!lockPath) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
heldLocks.delete(dbPath);
|
|
429
|
+
|
|
430
|
+
const owner = readLockOwner(lockPath);
|
|
431
|
+
if (owner && owner.pid !== process.pid) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
nodeFS.unlinkSync(lockPath);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
if (error.code !== "ENOENT") {
|
|
439
|
+
storeErr(error);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const ioMethods = {
|
|
445
|
+
xCheckReservedLock: function (pFile, pOut) {
|
|
446
|
+
// A writer here holds the database exclusively for its lifetime,
|
|
447
|
+
// so no other connection can be holding a reserved lock. The
|
|
448
|
+
// answer is the same, though for a weaker reason, for the one case
|
|
449
|
+
// that escapes the open lock: a read-only opener takes no lock and
|
|
450
|
+
// has no reserved lock of its own to report.
|
|
451
|
+
storeErr();
|
|
452
|
+
wasm.poke32(pOut, 0);
|
|
453
|
+
return 0;
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
xClose: function (pFile) {
|
|
457
|
+
storeErr();
|
|
458
|
+
const key = Number(pFile);
|
|
459
|
+
const file = openFiles.get(key);
|
|
460
|
+
if (!file) {
|
|
461
|
+
return 0;
|
|
462
|
+
}
|
|
463
|
+
openFiles.delete(key);
|
|
464
|
+
try {
|
|
465
|
+
nodeFS.closeSync(file.fd);
|
|
466
|
+
if (file.locked) {
|
|
467
|
+
releaseLock(file.path);
|
|
468
|
+
}
|
|
469
|
+
if (file.flags & capi.SQLITE_OPEN_DELETEONCLOSE) {
|
|
470
|
+
try {
|
|
471
|
+
nodeFS.unlinkSync(file.path);
|
|
472
|
+
} catch (error) {
|
|
473
|
+
if (error.code !== "ENOENT") {
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return 0;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
return storeErr(error, capi.SQLITE_IOERR);
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
xDeviceCharacteristics: function (pFile) {
|
|
485
|
+
// Claim nothing. A wallet database is worth more than the write
|
|
486
|
+
// batching SQLite would enable on the strength of these hints.
|
|
487
|
+
return 0;
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
xFileControl: function (pFile, opId, pArg) {
|
|
491
|
+
return capi.SQLITE_NOTFOUND;
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
xFileSize: function (pFile, pSz64) {
|
|
495
|
+
storeErr();
|
|
496
|
+
try {
|
|
497
|
+
const file = fileFor(pFile);
|
|
498
|
+
const size = nodeFS.fstatSync(file.fd).size;
|
|
499
|
+
wasm.poke64(pSz64, BigInt(size));
|
|
500
|
+
return 0;
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return storeErr(error, capi.SQLITE_IOERR);
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
xLock: function (pFile, lockType) {
|
|
507
|
+
storeErr();
|
|
508
|
+
try {
|
|
509
|
+
fileFor(pFile).lockType = lockType;
|
|
510
|
+
return 0;
|
|
511
|
+
} catch (error) {
|
|
512
|
+
return storeErr(error, capi.SQLITE_IOERR);
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
xRead: function (pFile, pDest, n, offset64) {
|
|
517
|
+
storeErr();
|
|
518
|
+
try {
|
|
519
|
+
const file = fileFor(pFile);
|
|
520
|
+
const view = heapView(pDest, n);
|
|
521
|
+
const nRead = nodeFS.readSync(
|
|
522
|
+
file.fd,
|
|
523
|
+
view,
|
|
524
|
+
0,
|
|
525
|
+
n,
|
|
526
|
+
Number(offset64)
|
|
527
|
+
);
|
|
528
|
+
if (nRead < n) {
|
|
529
|
+
// SQLite requires the tail of a short read to be zeroed.
|
|
530
|
+
view.fill(0, nRead, n);
|
|
531
|
+
return capi.SQLITE_IOERR_SHORT_READ;
|
|
532
|
+
}
|
|
533
|
+
return 0;
|
|
534
|
+
} catch (error) {
|
|
535
|
+
return storeErr(error, capi.SQLITE_IOERR_READ);
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
xSectorSize: function (pFile) {
|
|
540
|
+
return SECTOR_SIZE;
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
xSync: function (pFile, flags) {
|
|
544
|
+
storeErr();
|
|
545
|
+
try {
|
|
546
|
+
const file = fileFor(pFile);
|
|
547
|
+
// A durability barrier is the whole point of xSync, so this
|
|
548
|
+
// deliberately does not degrade to fdatasync on the DATAONLY
|
|
549
|
+
// hint: file size changes must reach the disk too.
|
|
550
|
+
nodeFS.fsyncSync(file.fd);
|
|
551
|
+
|
|
552
|
+
// The unix VFS syncs the parent directory at the first sync of
|
|
553
|
+
// a file it created, so the file's existence is durable before
|
|
554
|
+
// the database starts depending on it. SQLite cannot ask for
|
|
555
|
+
// that through the VFS interface, so it is done here.
|
|
556
|
+
if (file.needsDirSync) {
|
|
557
|
+
syncDir(nodePath.dirname(file.path));
|
|
558
|
+
file.needsDirSync = false;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return 0;
|
|
562
|
+
} catch (error) {
|
|
563
|
+
return storeErr(error, capi.SQLITE_IOERR_FSYNC);
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
xTruncate: function (pFile, sz64) {
|
|
568
|
+
storeErr();
|
|
569
|
+
try {
|
|
570
|
+
nodeFS.ftruncateSync(fileFor(pFile).fd, Number(sz64));
|
|
571
|
+
return 0;
|
|
572
|
+
} catch (error) {
|
|
573
|
+
return storeErr(
|
|
574
|
+
error,
|
|
575
|
+
ioErrForWrite(error, capi.SQLITE_IOERR_TRUNCATE)
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
|
|
580
|
+
xUnlock: function (pFile, lockType) {
|
|
581
|
+
storeErr();
|
|
582
|
+
try {
|
|
583
|
+
fileFor(pFile).lockType = lockType;
|
|
584
|
+
return 0;
|
|
585
|
+
} catch (error) {
|
|
586
|
+
return storeErr(error, capi.SQLITE_IOERR);
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
|
|
590
|
+
xWrite: function (pFile, pSrc, n, offset64) {
|
|
591
|
+
storeErr();
|
|
592
|
+
try {
|
|
593
|
+
const file = fileFor(pFile);
|
|
594
|
+
const nWrote = nodeFS.writeSync(
|
|
595
|
+
file.fd,
|
|
596
|
+
heapView(pSrc, n),
|
|
597
|
+
0,
|
|
598
|
+
n,
|
|
599
|
+
Number(offset64)
|
|
600
|
+
);
|
|
601
|
+
if (nWrote !== n) {
|
|
602
|
+
throw new Error(
|
|
603
|
+
`short write: wrote ${nWrote} of ${n} bytes`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
return 0;
|
|
607
|
+
} catch (error) {
|
|
608
|
+
return storeErr(
|
|
609
|
+
error,
|
|
610
|
+
ioErrForWrite(error, capi.SQLITE_IOERR_WRITE)
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const nodeIoMethods = new capi.sqlite3_io_methods();
|
|
617
|
+
nodeIoMethods.$iVersion = 1;
|
|
618
|
+
sqlite3.vfs.installVfs({
|
|
619
|
+
io: { struct: nodeIoMethods, methods: ioMethods },
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const vfsMethods = {
|
|
623
|
+
xAccess: function (pVfs, zName, flags, pOut) {
|
|
624
|
+
storeErr();
|
|
625
|
+
try {
|
|
626
|
+
const target = resolvePath(wasm.cstrToJs(zName));
|
|
627
|
+
let mode = nodeFS.constants.F_OK;
|
|
628
|
+
if (flags === capi.SQLITE_ACCESS_READWRITE) {
|
|
629
|
+
mode = nodeFS.constants.R_OK | nodeFS.constants.W_OK;
|
|
630
|
+
} else if (flags === capi.SQLITE_ACCESS_READ) {
|
|
631
|
+
mode = nodeFS.constants.R_OK;
|
|
632
|
+
}
|
|
633
|
+
nodeFS.accessSync(target, mode);
|
|
634
|
+
wasm.poke32(pOut, 1);
|
|
635
|
+
} catch (_) {
|
|
636
|
+
wasm.poke32(pOut, 0);
|
|
637
|
+
}
|
|
638
|
+
return 0;
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
xCurrentTime: function (pVfs, pOut) {
|
|
642
|
+
wasm.poke(
|
|
643
|
+
pOut,
|
|
644
|
+
2440587.5 + new Date().getTime() / 86400000,
|
|
645
|
+
"double"
|
|
646
|
+
);
|
|
647
|
+
return 0;
|
|
648
|
+
},
|
|
649
|
+
|
|
650
|
+
xCurrentTimeInt64: function (pVfs, pOut) {
|
|
651
|
+
wasm.poke(pOut, 2440587.5 * 86400000 + new Date().getTime(), "i64");
|
|
652
|
+
return 0;
|
|
653
|
+
},
|
|
654
|
+
|
|
655
|
+
xDelete: function (pVfs, zName, doSyncDir) {
|
|
656
|
+
storeErr();
|
|
657
|
+
const target = resolvePath(wasm.cstrToJs(zName));
|
|
658
|
+
try {
|
|
659
|
+
nodeFS.unlinkSync(target);
|
|
660
|
+
} catch (error) {
|
|
661
|
+
if (error.code !== "ENOENT") {
|
|
662
|
+
storeErr(error);
|
|
663
|
+
return capi.SQLITE_IOERR_DELETE;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// SQLite sets this under synchronous=EXTRA, where in rollback
|
|
668
|
+
// journal mode the unlink above *is* the commit. Ignoring the flag
|
|
669
|
+
// would quietly give EXTRA no more than FULL, which is the same
|
|
670
|
+
// class of silent substitution the journal-mode readback exists to
|
|
671
|
+
// refuse.
|
|
672
|
+
if (doSyncDir) {
|
|
673
|
+
syncDir(nodePath.dirname(target));
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return 0;
|
|
677
|
+
},
|
|
678
|
+
|
|
679
|
+
xFullPathname: function (pVfs, zName, nOut, pOut) {
|
|
680
|
+
// SQLite builds the journal and WAL names by appending suffixes to
|
|
681
|
+
// whatever this returns, so it must be the real absolute path.
|
|
682
|
+
// Otherwise a journal would land somewhere the database is not.
|
|
683
|
+
const full = resolvePath(wasm.cstrToJs(zName));
|
|
684
|
+
const scope = wasm.scopedAllocPush();
|
|
685
|
+
try {
|
|
686
|
+
const cFull = wasm.scopedAllocCString(full);
|
|
687
|
+
const written = wasm.cstrncpy(pOut, cFull, nOut);
|
|
688
|
+
return written < nOut ? 0 : capi.SQLITE_CANTOPEN;
|
|
689
|
+
} catch (error) {
|
|
690
|
+
storeErr(error);
|
|
691
|
+
return capi.SQLITE_CANTOPEN;
|
|
692
|
+
} finally {
|
|
693
|
+
wasm.scopedAllocPop(scope);
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
|
|
697
|
+
xGetLastError: function (pVfs, nOut, pOut) {
|
|
698
|
+
const error = popErr();
|
|
699
|
+
if (error) {
|
|
700
|
+
const scope = wasm.scopedAllocPush();
|
|
701
|
+
try {
|
|
702
|
+
const [cMsg, n] = wasm.scopedAllocCString(
|
|
703
|
+
error.message,
|
|
704
|
+
true
|
|
705
|
+
);
|
|
706
|
+
wasm.cstrncpy(pOut, cMsg, nOut);
|
|
707
|
+
if (n > nOut) {
|
|
708
|
+
wasm.poke8(wasm.ptr.add(pOut, nOut, -1), 0);
|
|
709
|
+
}
|
|
710
|
+
} catch (_) {
|
|
711
|
+
return capi.SQLITE_NOMEM;
|
|
712
|
+
} finally {
|
|
713
|
+
wasm.scopedAllocPop(scope);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return error ? error.sqlite3Rc || capi.SQLITE_IOERR : 0;
|
|
717
|
+
},
|
|
718
|
+
|
|
719
|
+
xOpen: function (pVfs, zName, pFile, flags, pOutFlags) {
|
|
720
|
+
storeErr();
|
|
721
|
+
let fd = -1;
|
|
722
|
+
let target = "";
|
|
723
|
+
let took = false;
|
|
724
|
+
let created = false;
|
|
725
|
+
try {
|
|
726
|
+
const isMainDB = !!(flags & capi.SQLITE_OPEN_MAIN_DB);
|
|
727
|
+
const create = !!(flags & capi.SQLITE_OPEN_CREATE);
|
|
728
|
+
const readOnly = !!(flags & capi.SQLITE_OPEN_READONLY);
|
|
729
|
+
|
|
730
|
+
// A null name asks for an anonymous temp file. SQLite deletes
|
|
731
|
+
// it on close, so a random name in the root directory is
|
|
732
|
+
// enough.
|
|
733
|
+
target =
|
|
734
|
+
zName && wasm.peek8(zName)
|
|
735
|
+
? resolvePath(wasm.cstrToJs(zName))
|
|
736
|
+
: nodePath.join(
|
|
737
|
+
rootDir,
|
|
738
|
+
`etilqs_${nodeCrypto
|
|
739
|
+
.randomBytes(12)
|
|
740
|
+
.toString("hex")}`
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
if (create) {
|
|
744
|
+
// The host may not have created the data directory yet.
|
|
745
|
+
// Creating it here keeps a first run from failing on a
|
|
746
|
+
// missing parent rather than on anything meaningful.
|
|
747
|
+
nodeFS.mkdirSync(nodePath.dirname(target), {
|
|
748
|
+
recursive: true,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// The claim comes before the file does. Creating first leaves a
|
|
753
|
+
// window in which a second process racing the same first-time
|
|
754
|
+
// open truncates a database this one has already begun
|
|
755
|
+
// writing, and the loser of the lock race discovers it only
|
|
756
|
+
// afterwards, with the damage done.
|
|
757
|
+
if (isMainDB && !readOnly) {
|
|
758
|
+
acquireLock(target);
|
|
759
|
+
took = true;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
let openMode;
|
|
763
|
+
if (readOnly) {
|
|
764
|
+
openMode = "r";
|
|
765
|
+
} else {
|
|
766
|
+
if (create) {
|
|
767
|
+
// "a+" would force every write to the end of the
|
|
768
|
+
// file, so create the file separately when it is
|
|
769
|
+
// missing and then open it read/write. The create is
|
|
770
|
+
// "wx" rather than "w" because an existing database
|
|
771
|
+
// must never be truncated on the way in; EEXIST here
|
|
772
|
+
// simply means someone got there first.
|
|
773
|
+
try {
|
|
774
|
+
nodeFS.closeSync(
|
|
775
|
+
nodeFS.openSync(target, "wx", 0o600)
|
|
776
|
+
);
|
|
777
|
+
created = true;
|
|
778
|
+
} catch (error) {
|
|
779
|
+
if (error.code !== "EEXIST") {
|
|
780
|
+
throw error;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
openMode = "r+";
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
fd = nodeFS.openSync(target, openMode);
|
|
788
|
+
|
|
789
|
+
openFiles.set(Number(pFile), {
|
|
790
|
+
fd,
|
|
791
|
+
path: target,
|
|
792
|
+
flags,
|
|
793
|
+
locked: took,
|
|
794
|
+
lockType: capi.SQLITE_LOCK_NONE,
|
|
795
|
+
|
|
796
|
+
// A file this VFS created is not durably present until its
|
|
797
|
+
// parent directory is synced. The unix VFS does that at the
|
|
798
|
+
// first xSync of such a file, and SQLite does not signal it
|
|
799
|
+
// through the VFS interface, so it is tracked here.
|
|
800
|
+
needsDirSync: created,
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
const sq3File = new capi.sqlite3_file(pFile);
|
|
804
|
+
sq3File.$pMethods = nodeIoMethods.pointer;
|
|
805
|
+
sq3File.dispose();
|
|
806
|
+
if (pOutFlags) {
|
|
807
|
+
wasm.poke32(pOutFlags, flags);
|
|
808
|
+
}
|
|
809
|
+
return 0;
|
|
810
|
+
} catch (error) {
|
|
811
|
+
// SQLite leaves pMethods null for a failed xOpen and so never
|
|
812
|
+
// calls xClose for it, which means anything recorded here
|
|
813
|
+
// would outlive the descriptor it describes. A later
|
|
814
|
+
// allocation landing on the same pointer would then find a
|
|
815
|
+
// stale entry naming a closed fd.
|
|
816
|
+
openFiles.delete(Number(pFile));
|
|
817
|
+
|
|
818
|
+
if (fd >= 0) {
|
|
819
|
+
try {
|
|
820
|
+
nodeFS.closeSync(fd);
|
|
821
|
+
} catch (_) {}
|
|
822
|
+
}
|
|
823
|
+
if (took) {
|
|
824
|
+
releaseLock(target);
|
|
825
|
+
}
|
|
826
|
+
storeErr(error);
|
|
827
|
+
return capi.SQLITE_CANTOPEN;
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
|
|
831
|
+
xRandomness: function (pVfs, nOut, pOut) {
|
|
832
|
+
const bytes = nodeCrypto.randomBytes(nOut);
|
|
833
|
+
heapView(pOut, nOut).set(bytes);
|
|
834
|
+
return nOut;
|
|
835
|
+
},
|
|
836
|
+
|
|
837
|
+
xSleep: function (pVfs, microseconds) {
|
|
838
|
+
// SQLite calls this from inside a busy handler, so returning
|
|
839
|
+
// without sleeping would turn a busy timeout into a tight spin
|
|
840
|
+
// that burns a core for however long the timeout is. Atomics.wait
|
|
841
|
+
// blocks this thread for real, and it is safe precisely because
|
|
842
|
+
// this VFS only ever runs inside the dedicated SQLite worker,
|
|
843
|
+
// never on a thread anyone else's event loop depends on.
|
|
844
|
+
const ms = Math.max(1, Math.round(Number(microseconds) / 1000));
|
|
845
|
+
Atomics.wait(sleepSignal, 0, 0, ms);
|
|
846
|
+
return 0;
|
|
847
|
+
},
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
const nodeVfs = new capi.sqlite3_vfs();
|
|
851
|
+
nodeVfs.$iVersion = 2;
|
|
852
|
+
nodeVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
|
|
853
|
+
nodeVfs.$mxPathname = 1024;
|
|
854
|
+
nodeVfs.addOnDispose((nodeVfs.$zName = wasm.allocCString(vfsName)));
|
|
855
|
+
sqlite3.vfs.installVfs({
|
|
856
|
+
vfs: { struct: nodeVfs, methods: vfsMethods },
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
const util = {
|
|
860
|
+
vfsName,
|
|
861
|
+
rootDir,
|
|
862
|
+
getVfs: () => nodeVfs,
|
|
863
|
+
openFileCount: () => openFiles.size,
|
|
864
|
+
|
|
865
|
+
// lockHolder names the process currently holding the open lock for a
|
|
866
|
+
// database, or null when nothing does.
|
|
867
|
+
//
|
|
868
|
+
// SQLite's core does not carry xGetLastError's message out to the
|
|
869
|
+
// caller, so a collision reaches the application as a bare
|
|
870
|
+
// SQLITE_CANTOPEN, indistinguishable from a missing directory or a
|
|
871
|
+
// permissions problem. This lets the worker say which process is
|
|
872
|
+
// holding the database instead.
|
|
873
|
+
lockHolder: (dbPath) => {
|
|
874
|
+
const owner = readLockOwner(
|
|
875
|
+
`${resolvePath(dbPath)}${LOCK_SUFFIX}`
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
return owner && ownerIsLive(owner) ? owner : null;
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
if (!installNodeFsVfs.installed) {
|
|
883
|
+
installNodeFsVfs.installed = new Map();
|
|
884
|
+
}
|
|
885
|
+
installNodeFsVfs.installed.set(vfsName, util);
|
|
886
|
+
|
|
887
|
+
return util;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
module.exports = {
|
|
891
|
+
DEFAULT_VFS_NAME,
|
|
892
|
+
installNodeFsVfs,
|
|
893
|
+
isNodeHost,
|
|
894
|
+
};
|