@evolu/sqlite-wasm 2.2.4 → 3.53.4-build1
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/LICENSE +21 -0
- package/README.md +55 -218
- package/dist/src/CApi.d.ts +2108 -0
- package/dist/src/CApi.d.ts.map +1 -0
- package/dist/src/CApi.js +1919 -0
- package/dist/src/Constants.d.ts +868 -0
- package/dist/src/Constants.d.ts.map +1 -0
- package/dist/src/Constants.js +602 -0
- package/dist/src/Database.d.ts +641 -0
- package/dist/src/Database.d.ts.map +1 -0
- package/dist/src/Database.js +1177 -0
- package/dist/src/Memory.d.ts +119 -0
- package/dist/src/Memory.d.ts.map +1 -0
- package/dist/src/Memory.js +207 -0
- package/dist/src/Pointer.d.ts +100 -0
- package/dist/src/Pointer.d.ts.map +1 -0
- package/dist/src/Pointer.js +15 -0
- package/dist/src/SahPool.d.ts +744 -0
- package/dist/src/SahPool.d.ts.map +1 -0
- package/dist/src/SahPool.js +1985 -0
- package/dist/src/Wasm.d.ts +315 -0
- package/dist/src/Wasm.d.ts.map +1 -0
- package/dist/src/Wasm.js +756 -0
- package/dist/src/WasmUrl.d.ts +21 -0
- package/dist/src/WasmUrl.d.ts.map +1 -0
- package/dist/src/WasmUrl.js +20 -0
- package/dist/src/c-api/index.d.ts +9 -0
- package/dist/src/c-api/index.d.ts.map +1 -0
- package/dist/src/c-api/index.js +8 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +13 -0
- package/dist/wasm/sqlite3.wasm +0 -0
- package/package.json +60 -60
- package/src/CApi.test.ts +217 -0
- package/src/CApi.ts +3294 -0
- package/src/Constants.ts +803 -0
- package/src/Database.ts +1714 -0
- package/src/Memory.test.ts +319 -0
- package/src/Memory.ts +237 -0
- package/src/Pointer.ts +121 -0
- package/src/SahPool.test.ts +180 -0
- package/src/SahPool.ts +2627 -0
- package/src/Wasm.test.ts +444 -0
- package/src/Wasm.ts +1026 -0
- package/src/WasmUrl.ts +26 -0
- package/src/c-api/index.ts +9 -0
- package/src/index.ts +15 -0
- package/bin/index.js +0 -110
- package/index.d.ts +0 -8118
- package/index.mjs +0 -7
- package/node.mjs +0 -3
- package/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs +0 -13659
- package/sqlite-wasm/jswasm/sqlite3-node.mjs +0 -11671
- package/sqlite-wasm/jswasm/sqlite3-opfs-async-proxy.js +0 -691
- package/sqlite-wasm/jswasm/sqlite3-worker1-bundler-friendly.mjs +0 -35
- package/sqlite-wasm/jswasm/sqlite3-worker1-promiser.js +0 -193
- package/sqlite-wasm/jswasm/sqlite3-worker1-promiser.mjs +0 -187
- package/sqlite-wasm/jswasm/sqlite3-worker1.js +0 -46
- package/sqlite-wasm/jswasm/sqlite3.js +0 -13697
- package/sqlite-wasm/jswasm/sqlite3.mjs +0 -13661
- package/sqlite-wasm/jswasm/sqlite3.wasm +0 -0
package/src/Wasm.ts
ADDED
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite's Emscripten wasm, loaded without Emscripten's or SQLite's JavaScript.
|
|
3
|
+
*
|
|
4
|
+
* The package ships SQLite's own wasm, built with SQLite's own makefile, and
|
|
5
|
+
* replaces the generated JavaScript with a small loader and the standalone
|
|
6
|
+
* functions of `@evolu/sqlite-wasm/c-api`.
|
|
7
|
+
*
|
|
8
|
+
* ## Loading
|
|
9
|
+
*
|
|
10
|
+
* {@link createSqliteWasm} runs these steps and fails with a
|
|
11
|
+
* {@link SqliteWasmError} when one fails. Steps 1 to 3 fail before any wasm code
|
|
12
|
+
* runs, and steps 4 and 5 run only the stack setup, static constructors and
|
|
13
|
+
* `sqlite3__wasm_enum_json()`, before any SQLite API call:
|
|
14
|
+
*
|
|
15
|
+
* 1. Compile, while downloading a `Response` of type `application/wasm`.
|
|
16
|
+
* 2. Import guard: every import the binary declares must be one the loader
|
|
17
|
+
* implements, or instantiation never happens. A new Emscripten or SQLite
|
|
18
|
+
* release that adds an import is caught here, not at the first call that
|
|
19
|
+
* reaches it. The list matches the release recipe exactly.
|
|
20
|
+
* 3. A fresh `WebAssembly.Memory` of at least the size the binary declares, which
|
|
21
|
+
* the JavaScript API does not expose (256 pages covers every build so far),
|
|
22
|
+
* and a maximum of 32768 pages, so pointers stay positive i32 values. An
|
|
23
|
+
* engine that cannot allocate it, as on a device low on memory, fails
|
|
24
|
+
* loading with {@link SqliteWasmCompileError}.
|
|
25
|
+
* 4. Instantiate, then set up the C stack for Emscripten's stack check: call
|
|
26
|
+
* `emscripten_stack_init`, then `__set_stack_limits` with what
|
|
27
|
+
* `emscripten_stack_get_base` and `emscripten_stack_get_end` return. The
|
|
28
|
+
* check compares each new stack pointer with limits that start at 0, so they
|
|
29
|
+
* are set before any code moves it. Then call `__wasm_call_ctors` exactly
|
|
30
|
+
* once. A binary without one of these functions fails the build guard.
|
|
31
|
+
* 5. Build guard: the hash {@link sqliteWasmBuildHash} defines, computed from the
|
|
32
|
+
* string `sqlite3__wasm_enum_json()` returns and the names
|
|
33
|
+
* `WebAssembly.Module.exports` lists, must equal it. A match means the
|
|
34
|
+
* binary has the pinned build's export names, so it exports every function
|
|
35
|
+
* in {@link SqliteCExports}, and the generated constants, struct layouts and
|
|
36
|
+
* {@link SQLITE_WASM_DEALLOC} describe it. The hash does not cover wasm
|
|
37
|
+
* types: the generator checked the bindings against the pinned types, and
|
|
38
|
+
* `scripts/generate.mts --verify-build` checks a new binary's. A binary
|
|
39
|
+
* without `sqlite3__wasm_enum_json`, or whose call returns NULL, hashes the
|
|
40
|
+
* string as empty and fails. No JSON is parsed at runtime.
|
|
41
|
+
* 6. `sqlite3_initialize()`, whose result code is checked.
|
|
42
|
+
* 7. Register the default VFS, then run {@link initializeSqliteWasm}, which makes
|
|
43
|
+
* it the default and calls `sqlite3_randomness(0, 0)`. SQLite's random
|
|
44
|
+
* number generator seeds itself from the default VFS at its first use, and
|
|
45
|
+
* again at the first use after that call resets it. Without Emscripten's
|
|
46
|
+
* file system, SQLite's unix VFS seeds it from the time and a constant pid,
|
|
47
|
+
* so two instances seeded in the same second would share `randomblob()`
|
|
48
|
+
* values and journal nonces. Nothing uses the generator while loading, so
|
|
49
|
+
* the reset is defensive here; it matters after `sqlite3_shutdown()`. It
|
|
50
|
+
* also unregisters SQLite's kvvfs, which `sqlite3_initialize()` registers
|
|
51
|
+
* but whose storage only SQLite's JavaScript provides, so opening it, as the
|
|
52
|
+
* names `:localStorage:` and `:sessionStorage:` do on any VFS, fails with
|
|
53
|
+
* SQLITE_ERROR instead of trapping. Allocating, finding or registering the
|
|
54
|
+
* VFS fails with {@link SqliteWasmInitializeError}, and compiling the
|
|
55
|
+
* generated module that installs its methods, which a Content Security
|
|
56
|
+
* Policy without 'wasm-unsafe-eval' forbids, fails with
|
|
57
|
+
* {@link SqliteWasmCompileError}. `sqlite3_initialize()` after
|
|
58
|
+
* `sqlite3_shutdown()` makes the unix VFS the default again, so
|
|
59
|
+
* {@link initializeSqliteWasm} must run instead.
|
|
60
|
+
*
|
|
61
|
+
* ## The default VFS
|
|
62
|
+
*
|
|
63
|
+
* `evolu-memory`, of version 2, has no files, so only `:memory:` databases use
|
|
64
|
+
* it:
|
|
65
|
+
*
|
|
66
|
+
* - `xFullPathname` fails with SQLITE_CANTOPEN, so opening a file path fails
|
|
67
|
+
* before SQLite would open a file. `xOpen` is NULL: SQLite opens a file
|
|
68
|
+
* without a path only for a temporary file, which this build keeps in memory.
|
|
69
|
+
* `xAccess` finds no file, and `xDelete` returns SQLITE_IOERR_DELETE_NOENT.
|
|
70
|
+
* - `xRandomness` fills memory from {@link RandomBytes}, in chunks of at most
|
|
71
|
+
* 65536 bytes, as `crypto.getRandomValues` requires.
|
|
72
|
+
* - `xCurrentTime` and `xCurrentTimeInt64` read `Time.now` of {@link Time}.
|
|
73
|
+
* - `xSleep` returns at once, having slept 0 microseconds. Contention can only
|
|
74
|
+
* come from connections in the same thread, which sleeping cannot resolve.
|
|
75
|
+
* - `xGetLastError` reports no system error.
|
|
76
|
+
*
|
|
77
|
+
* ## Imports
|
|
78
|
+
*
|
|
79
|
+
* About ten imports do real work:
|
|
80
|
+
*
|
|
81
|
+
* - `memory`, see step 3.
|
|
82
|
+
* - `__handle_stack_overflow`, which the build's stack check
|
|
83
|
+
* (`-sSTACK_OVERFLOW_CHECK=2`) calls instead of moving the stack pointer
|
|
84
|
+
* outside the C stack, throws a `WebAssembly.RuntimeError`, as a trap does.
|
|
85
|
+
* The C stack has 512 KiB directly above SQLite's static data, and SQL that
|
|
86
|
+
* is legal but nests deeply enough, such as a chain of 600 triggers, would
|
|
87
|
+
* otherwise overwrite that data silently.
|
|
88
|
+
* - `emscripten_resize_heap` grows memory to the requested size, read as
|
|
89
|
+
* unsigned, and returns 1, or 0 when it cannot, never throwing.
|
|
90
|
+
* - The clocks read {@link Time}: `emscripten_date_now` returns `Time.now`,
|
|
91
|
+
* `emscripten_get_now` returns `Time.performance.now`, and WASI
|
|
92
|
+
* `clock_time_get` writes BigInt nanoseconds of the first for the realtime
|
|
93
|
+
* clock and of the second for the monotonic and CPU-time clocks, and returns
|
|
94
|
+
* EINVAL for any other.
|
|
95
|
+
* - `_localtime_js`, for SQL `localtime`, reads `Date` in the local time zone and
|
|
96
|
+
* fails for a time `Date` cannot represent. It fills only the `struct tm`
|
|
97
|
+
* fields SQLite reads, from `tm_sec` to `tm_year`, and zeroes the others.
|
|
98
|
+
* `_tzset_js` writes nothing: only `localtime_r` reads what it would write,
|
|
99
|
+
* to set `tm_zone`, which nothing reads.
|
|
100
|
+
* - `environ_sizes_get` and `environ_get` describe an empty environment.
|
|
101
|
+
* - `fd_write`, for C-side diagnostics, writes complete lines of stdout with
|
|
102
|
+
* {@link Console.log} and of stderr with {@link Console.error}, and fails any
|
|
103
|
+
* other descriptor with EBADF.
|
|
104
|
+
*
|
|
105
|
+
* The `__syscall_*` imports return -ENOSYS (-52), and the remaining WASI `fd_*`
|
|
106
|
+
* imports return ENOSYS (52), as WASI errors are positive: only `openat` of
|
|
107
|
+
* `/dev/urandom` is ever reached, and step 7 covers it.
|
|
108
|
+
*
|
|
109
|
+
* Every import's wasm type and the memory limits are pinned in
|
|
110
|
+
* `scripts/upstream/sqlite3-wasm-imports.json`, and `scripts/generate.mts
|
|
111
|
+
* --verify-build` checks a binary against them.
|
|
112
|
+
*
|
|
113
|
+
* ## Rules for everything that uses the instance
|
|
114
|
+
*
|
|
115
|
+
* - Synchronous only. The build has neither Asyncify nor JSPI, and nothing in the
|
|
116
|
+
* SQLite call path returns a promise.
|
|
117
|
+
* - Any call that can allocate, including a VFS callback running inside
|
|
118
|
+
* `sqlite3_step`, can grow memory, which detaches the previous `ArrayBuffer`.
|
|
119
|
+
* Take heap views with {@link SqliteWasm.getHeapU8} after such calls and never
|
|
120
|
+
* keep one across them.
|
|
121
|
+
* - No JavaScript exception may cross into wasm. Every function installed with
|
|
122
|
+
* {@link installWasmFunctions} catches what it throws, reports it as a defect
|
|
123
|
+
* and returns SQLITE_ERROR.
|
|
124
|
+
* - A trap or a C stack overflow throws a `WebAssembly.RuntimeError`, which
|
|
125
|
+
* installed functions let through. When the static constructors trap, loading
|
|
126
|
+
* fails with {@link SqliteWasmCompileError}.
|
|
127
|
+
* - Any exception or trap that escapes a wasm call is a defect. It unwound C
|
|
128
|
+
* frames without cleanup, so the C stack and SQLite's state are left
|
|
129
|
+
* inconsistent: it is rethrown, the instance refuses every later call, and
|
|
130
|
+
* its worker should end. {@link SqliteWasm.call} enforces this. The CApi
|
|
131
|
+
* functions return the exports themselves, so code that calls them directly
|
|
132
|
+
* must call through it. An installed function that returns once the instance
|
|
133
|
+
* is broken, as one that caught what a nested call rethrew, or whose
|
|
134
|
+
* {@link ReportDefect} broke it, throws that refusal instead, so SQLite never
|
|
135
|
+
* resumes on that state.
|
|
136
|
+
*
|
|
137
|
+
* @module
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
import {
|
|
141
|
+
constVoid,
|
|
142
|
+
disposable,
|
|
143
|
+
err,
|
|
144
|
+
isNonEmptyArray,
|
|
145
|
+
mapArray,
|
|
146
|
+
mapObject,
|
|
147
|
+
ok,
|
|
148
|
+
reportDefectAfterMicrotask,
|
|
149
|
+
tryAsync,
|
|
150
|
+
trySync,
|
|
151
|
+
type Console,
|
|
152
|
+
type ConsoleDep,
|
|
153
|
+
type NonEmptyReadonlyArray,
|
|
154
|
+
type RandomBytes,
|
|
155
|
+
type RandomBytesDep,
|
|
156
|
+
type ReadonlyRecord,
|
|
157
|
+
type ReportDefect,
|
|
158
|
+
type ReportDefectDep,
|
|
159
|
+
type Result,
|
|
160
|
+
type Task,
|
|
161
|
+
type Time,
|
|
162
|
+
type TimeDep,
|
|
163
|
+
type Typed,
|
|
164
|
+
zipArray,
|
|
165
|
+
} from "@evolu/common";
|
|
166
|
+
import type { SqliteCExports } from "./CApi.ts";
|
|
167
|
+
import {
|
|
168
|
+
SQLITE_CANTOPEN,
|
|
169
|
+
SQLITE_ERROR,
|
|
170
|
+
SQLITE_IOERR_DELETE_NOENT,
|
|
171
|
+
SQLITE_MISUSE,
|
|
172
|
+
SQLITE_NOMEM,
|
|
173
|
+
SQLITE_OK,
|
|
174
|
+
sqlite3_file_layout,
|
|
175
|
+
sqlite3_vfs_layout,
|
|
176
|
+
sqliteWasmBuildHash,
|
|
177
|
+
type SQLITE_WASM_DEALLOC,
|
|
178
|
+
type SqliteResultCode,
|
|
179
|
+
} from "./Constants.ts";
|
|
180
|
+
import { allocCString, allocWasm } from "./Memory.ts";
|
|
181
|
+
import type {
|
|
182
|
+
CStringPtr,
|
|
183
|
+
SqliteFunctionPtr,
|
|
184
|
+
SqliteVfsPtr,
|
|
185
|
+
WasmPtr,
|
|
186
|
+
} from "./Pointer.ts";
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A loaded SQLite wasm instance.
|
|
190
|
+
*
|
|
191
|
+
* One instance serves every database of a worker. It cannot be disposed; it
|
|
192
|
+
* lives until the worker ends.
|
|
193
|
+
*/
|
|
194
|
+
export interface SqliteWasm {
|
|
195
|
+
/**
|
|
196
|
+
* The C functions, and the build's shims of the variadic ones, as the wasm
|
|
197
|
+
* exports them.
|
|
198
|
+
*
|
|
199
|
+
* Prefer the standalone functions of `@evolu/sqlite-wasm/c-api`, which read
|
|
200
|
+
* these once per database.
|
|
201
|
+
*/
|
|
202
|
+
readonly exports: SqliteCExports;
|
|
203
|
+
|
|
204
|
+
/** The wasm function table, where C function pointers point. */
|
|
205
|
+
readonly functionTable: WebAssembly.Table;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Returns a view of the whole heap, created again when memory has grown since
|
|
209
|
+
* the previous call.
|
|
210
|
+
*/
|
|
211
|
+
readonly getHeapU8: () => Uint8Array<ArrayBuffer>;
|
|
212
|
+
|
|
213
|
+
/** Returns a `DataView` of the whole heap, like {@link SqliteWasm.getHeapU8}. */
|
|
214
|
+
readonly getHeapDataView: () => DataView<ArrayBuffer>;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Calls a function that calls into wasm, and returns its result.
|
|
218
|
+
*
|
|
219
|
+
* Anything the function throws breaks the instance, as the module
|
|
220
|
+
* documentation explains: it is rethrown, and this and every later call throw
|
|
221
|
+
* without calling the function. Databases call wasm through it, and so should
|
|
222
|
+
* code that calls the CApi functions directly.
|
|
223
|
+
*/
|
|
224
|
+
readonly call: <T>(fn: () => T) => T;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Whether an error escaped {@link SqliteWasm.call}, so the instance refuses
|
|
228
|
+
* calls.
|
|
229
|
+
*/
|
|
230
|
+
readonly isBroken: () => boolean;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Dependency wrapper for {@link SqliteWasm}. */
|
|
234
|
+
export interface SqliteWasmDep {
|
|
235
|
+
readonly sqliteWasm: SqliteWasm;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The wasm binary, compiled or not.
|
|
240
|
+
*
|
|
241
|
+
* A `Response` or a promise of one, such as `fetch` returns, compiles while it
|
|
242
|
+
* downloads when its `Content-Type` is exactly `application/wasm`, which keeps
|
|
243
|
+
* Chromium's code cache too. That is the only type every engine's
|
|
244
|
+
* `WebAssembly.compileStreaming` accepts, so any other compiles from its bytes
|
|
245
|
+
* once it has downloaded. A failed fetch fails with
|
|
246
|
+
* {@link SqliteWasmCompileError}, and so does a response whose status is not ok,
|
|
247
|
+
* such as a 404 for a wrong URL, with a `TypeError` naming the status as its
|
|
248
|
+
* cause. A `WebAssembly.Module` suits a module compiled elsewhere. Loading
|
|
249
|
+
* compiles a small generated module, so a Content Security Policy must allow
|
|
250
|
+
* 'wasm-unsafe-eval' even for a precompiled Module.
|
|
251
|
+
*/
|
|
252
|
+
export type SqliteWasmSource =
|
|
253
|
+
WebAssembly.Module | BufferSource | Response | PromiseLike<Response>;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Loads the wasm, following the steps in the module documentation.
|
|
257
|
+
*
|
|
258
|
+
* Start it as early as possible, in the worker's composition root, and pass the
|
|
259
|
+
* instance to everything else through {@link SqliteWasmDep}.
|
|
260
|
+
*/
|
|
261
|
+
export const createSqliteWasm =
|
|
262
|
+
(
|
|
263
|
+
source: SqliteWasmSource,
|
|
264
|
+
): Task<
|
|
265
|
+
SqliteWasm,
|
|
266
|
+
SqliteWasmError,
|
|
267
|
+
ConsoleDep & RandomBytesDep & ReportDefectDep & TimeDep
|
|
268
|
+
> =>
|
|
269
|
+
async (run) => {
|
|
270
|
+
const toCompileError = (cause: unknown): SqliteWasmCompileError => ({
|
|
271
|
+
type: "SqliteWasmCompileError",
|
|
272
|
+
cause,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// 1. Compile.
|
|
276
|
+
const compiled = await tryAsync(async () => {
|
|
277
|
+
if (source instanceof WebAssembly.Module) return source;
|
|
278
|
+
if (source instanceof ArrayBuffer || ArrayBuffer.isView(source))
|
|
279
|
+
return WebAssembly.compile(source);
|
|
280
|
+
const response = await source;
|
|
281
|
+
// A wrong URL's error page would otherwise fail to compile as HTML.
|
|
282
|
+
if (!response.ok)
|
|
283
|
+
throw new TypeError(
|
|
284
|
+
`Fetching the SQLite wasm failed with HTTP status ${response.status}.`,
|
|
285
|
+
);
|
|
286
|
+
// The only type every engine's compileStreaming accepts: browsers reject
|
|
287
|
+
// any parameter, as the spec says, and Node.js also rejects another
|
|
288
|
+
// case, although the spec matches the type case-insensitively.
|
|
289
|
+
return response.headers.get("Content-Type") === "application/wasm"
|
|
290
|
+
? WebAssembly.compileStreaming(response)
|
|
291
|
+
: WebAssembly.compile(await response.arrayBuffer());
|
|
292
|
+
}, toCompileError);
|
|
293
|
+
if (!compiled.ok) return compiled;
|
|
294
|
+
const module = compiled.value;
|
|
295
|
+
|
|
296
|
+
// 2. Import guard.
|
|
297
|
+
const unknownImports = WebAssembly.Module.imports(module)
|
|
298
|
+
.filter(
|
|
299
|
+
(entry) =>
|
|
300
|
+
!Object.hasOwn(sqliteWasmImports, entry.module) ||
|
|
301
|
+
!Object.hasOwn(sqliteWasmImports[entry.module], entry.name),
|
|
302
|
+
)
|
|
303
|
+
.map((entry) => `${entry.module}.${entry.name}`);
|
|
304
|
+
if (isNonEmptyArray(unknownImports))
|
|
305
|
+
return err({ type: "SqliteWasmUnknownImport", imports: unknownImports });
|
|
306
|
+
|
|
307
|
+
// 3. Memory.
|
|
308
|
+
const createdMemory = trySync(
|
|
309
|
+
() =>
|
|
310
|
+
new WebAssembly.Memory({
|
|
311
|
+
initial: initialMemoryPages,
|
|
312
|
+
maximum: maximumMemoryPages,
|
|
313
|
+
}),
|
|
314
|
+
toCompileError,
|
|
315
|
+
);
|
|
316
|
+
if (!createdMemory.ok) return createdMemory;
|
|
317
|
+
const memory = createdMemory.value;
|
|
318
|
+
let heapU8 = new Uint8Array(memory.buffer);
|
|
319
|
+
let heapDataView = new DataView(memory.buffer);
|
|
320
|
+
const getHeapU8 = (): Uint8Array<ArrayBuffer> => {
|
|
321
|
+
if (heapU8.buffer !== memory.buffer)
|
|
322
|
+
heapU8 = new Uint8Array(memory.buffer);
|
|
323
|
+
return heapU8;
|
|
324
|
+
};
|
|
325
|
+
const getHeapDataView = (): DataView<ArrayBuffer> => {
|
|
326
|
+
if (heapDataView.buffer !== memory.buffer)
|
|
327
|
+
heapDataView = new DataView(memory.buffer);
|
|
328
|
+
return heapDataView;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// 4. Instantiate, set the C stack's limits and run the static constructors.
|
|
332
|
+
const context: SqliteWasmImportContext = {
|
|
333
|
+
...run.deps,
|
|
334
|
+
memory,
|
|
335
|
+
getHeapU8,
|
|
336
|
+
getHeapDataView,
|
|
337
|
+
};
|
|
338
|
+
const imports = mapObject(sqliteWasmImports, (namespace) =>
|
|
339
|
+
mapObject(namespace, (createImport) => createImport(context)),
|
|
340
|
+
);
|
|
341
|
+
const instantiated = await tryAsync(async () => {
|
|
342
|
+
const instance = await WebAssembly.instantiate(module, imports);
|
|
343
|
+
// As Emscripten's stackCheckInit and setStackLimits do.
|
|
344
|
+
callExport(instance, "emscripten_stack_init");
|
|
345
|
+
callExport(
|
|
346
|
+
instance,
|
|
347
|
+
"__set_stack_limits",
|
|
348
|
+
callExport(instance, "emscripten_stack_get_base"),
|
|
349
|
+
callExport(instance, "emscripten_stack_get_end"),
|
|
350
|
+
);
|
|
351
|
+
callExport(instance, "__wasm_call_ctors");
|
|
352
|
+
return instance;
|
|
353
|
+
}, toCompileError);
|
|
354
|
+
if (!instantiated.ok) return instantiated;
|
|
355
|
+
const instance = instantiated.value;
|
|
356
|
+
|
|
357
|
+
// 5. Build guard.
|
|
358
|
+
let hash = 0x811c9dc5;
|
|
359
|
+
const hashBytes = (bytes: Uint8Array): void => {
|
|
360
|
+
for (const byte of bytes) hash = Math.imul(hash ^ byte, 0x01000193);
|
|
361
|
+
};
|
|
362
|
+
const enumJsonPtr = callExport(instance, "sqlite3__wasm_enum_json");
|
|
363
|
+
const heap = getHeapU8();
|
|
364
|
+
hashBytes(
|
|
365
|
+
enumJsonPtr === 0
|
|
366
|
+
? Uint8Array.of(0)
|
|
367
|
+
: heap.subarray(enumJsonPtr, heap.indexOf(0, enumJsonPtr) + 1),
|
|
368
|
+
);
|
|
369
|
+
const encoder = new TextEncoder();
|
|
370
|
+
for (const name of WebAssembly.Module.exports(module)
|
|
371
|
+
.map((entry) => entry.name)
|
|
372
|
+
.toSorted())
|
|
373
|
+
hashBytes(encoder.encode(`${name}\0`));
|
|
374
|
+
const actualHash = hash >>> 0;
|
|
375
|
+
if (actualHash !== sqliteWasmBuildHash)
|
|
376
|
+
return err({
|
|
377
|
+
type: "SqliteWasmBuildMismatch",
|
|
378
|
+
expectedHash: sqliteWasmBuildHash,
|
|
379
|
+
actualHash,
|
|
380
|
+
});
|
|
381
|
+
const exports = instance.exports as unknown as SqliteCExports;
|
|
382
|
+
|
|
383
|
+
// 6. Initialize.
|
|
384
|
+
const code = exports.sqlite3_initialize();
|
|
385
|
+
if (code !== SQLITE_OK)
|
|
386
|
+
return err({ type: "SqliteWasmInitializeError", code });
|
|
387
|
+
|
|
388
|
+
// What escaped a call and broke the instance.
|
|
389
|
+
let brokenBy: { readonly error: unknown } | null = null;
|
|
390
|
+
|
|
391
|
+
const sqliteWasm: SqliteWasm = {
|
|
392
|
+
exports,
|
|
393
|
+
functionTable: instance.exports
|
|
394
|
+
.__indirect_function_table as WebAssembly.Table,
|
|
395
|
+
getHeapU8,
|
|
396
|
+
getHeapDataView,
|
|
397
|
+
call: (fn) => {
|
|
398
|
+
if (brokenBy)
|
|
399
|
+
throw new Error(
|
|
400
|
+
"The SQLite wasm instance is broken: an exception escaped a wasm call.",
|
|
401
|
+
{ cause: brokenBy.error },
|
|
402
|
+
);
|
|
403
|
+
try {
|
|
404
|
+
return fn();
|
|
405
|
+
} catch (error) {
|
|
406
|
+
brokenBy ??= { error };
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
isBroken: () => brokenBy != null,
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// 7. Register the default VFS.
|
|
414
|
+
const vfsName = encoder.encode(`${defaultVfsName}\0`);
|
|
415
|
+
const allocated = allocWasm({ sqliteWasm })(
|
|
416
|
+
sqlite3_vfs_layout.sizeof + vfsName.length,
|
|
417
|
+
);
|
|
418
|
+
if (!allocated.ok)
|
|
419
|
+
return err({ type: "SqliteWasmInitializeError", code: SQLITE_NOMEM });
|
|
420
|
+
const vfs = allocated.value as WasmPtr as SqliteVfsPtr;
|
|
421
|
+
const zName = vfs + sqlite3_vfs_layout.sizeof;
|
|
422
|
+
const { members } = sqlite3_vfs_layout;
|
|
423
|
+
const vfsMethods: NonEmptyReadonlyArray<
|
|
424
|
+
readonly [keyof typeof members, SqliteWasmFunction["fn"]]
|
|
425
|
+
> = [
|
|
426
|
+
[
|
|
427
|
+
"xAccess",
|
|
428
|
+
(
|
|
429
|
+
_vfs: SqliteVfsPtr,
|
|
430
|
+
_zName: WasmPtr,
|
|
431
|
+
_flags: number,
|
|
432
|
+
pResOut: WasmPtr,
|
|
433
|
+
) => {
|
|
434
|
+
getHeapDataView().setInt32(pResOut, 0, true);
|
|
435
|
+
return SQLITE_OK;
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
["xDelete", () => SQLITE_IOERR_DELETE_NOENT],
|
|
439
|
+
["xFullPathname", () => SQLITE_CANTOPEN],
|
|
440
|
+
[
|
|
441
|
+
"xRandomness",
|
|
442
|
+
(_vfs: SqliteVfsPtr, byteLength: number, pOut: WasmPtr) => {
|
|
443
|
+
for (let offset = 0; offset < byteLength; offset += 65536)
|
|
444
|
+
getHeapU8().set(
|
|
445
|
+
run.deps.randomBytes.create(Math.min(65536, byteLength - offset)),
|
|
446
|
+
pOut + offset,
|
|
447
|
+
);
|
|
448
|
+
return byteLength;
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
["xSleep", () => 0],
|
|
452
|
+
["xGetLastError", () => 0],
|
|
453
|
+
[
|
|
454
|
+
"xCurrentTime",
|
|
455
|
+
(_vfs: SqliteVfsPtr, pTime: WasmPtr) => {
|
|
456
|
+
getHeapDataView().setFloat64(
|
|
457
|
+
pTime,
|
|
458
|
+
context.time.now() / 86_400_000 + 2_440_587.5,
|
|
459
|
+
true,
|
|
460
|
+
);
|
|
461
|
+
return SQLITE_OK;
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
[
|
|
465
|
+
"xCurrentTimeInt64",
|
|
466
|
+
(_vfs: SqliteVfsPtr, pTime: WasmPtr) => {
|
|
467
|
+
// Milliseconds since the Julian day epoch, noon in Greenwich on
|
|
468
|
+
// November 24, 4714 BC.
|
|
469
|
+
getHeapDataView().setBigInt64(
|
|
470
|
+
pTime,
|
|
471
|
+
BigInt(context.time.now()) + 210_866_760_000_000n,
|
|
472
|
+
true,
|
|
473
|
+
);
|
|
474
|
+
return SQLITE_OK;
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
];
|
|
478
|
+
const installed = trySync(
|
|
479
|
+
() =>
|
|
480
|
+
installWasmFunctions({ ...run.deps, sqliteWasm })(
|
|
481
|
+
mapArray(vfsMethods, ([name, fn]) => ({
|
|
482
|
+
signature: members[name].signature,
|
|
483
|
+
fn,
|
|
484
|
+
})),
|
|
485
|
+
),
|
|
486
|
+
toCompileError,
|
|
487
|
+
);
|
|
488
|
+
if (!installed.ok) return installed;
|
|
489
|
+
const { pointers } = installed.value;
|
|
490
|
+
|
|
491
|
+
getHeapU8().fill(0, vfs, zName);
|
|
492
|
+
getHeapU8().set(vfsName, zName);
|
|
493
|
+
const vfsView = getHeapDataView();
|
|
494
|
+
for (const [name, value] of [
|
|
495
|
+
["iVersion", 2],
|
|
496
|
+
["szOsFile", sqlite3_file_layout.sizeof],
|
|
497
|
+
["mxPathname", 1024],
|
|
498
|
+
["zName", zName],
|
|
499
|
+
] as const)
|
|
500
|
+
vfsView.setInt32(vfs + members[name].offset, value, true);
|
|
501
|
+
for (const [[name], pointer] of zipArray([vfsMethods, pointers]))
|
|
502
|
+
vfsView.setInt32(vfs + members[name].offset, pointer, true);
|
|
503
|
+
const registered = exports.sqlite3_vfs_register(vfs, 0);
|
|
504
|
+
if (registered !== SQLITE_OK)
|
|
505
|
+
return err({ type: "SqliteWasmInitializeError", code: registered });
|
|
506
|
+
const initialized = initializeSqliteWasm({ sqliteWasm })();
|
|
507
|
+
if (!initialized.ok) return initialized;
|
|
508
|
+
|
|
509
|
+
return ok(sqliteWasm);
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Initializes the library as loading steps 6 and 7 do, for use after
|
|
514
|
+
* `sqlite3_shutdown` and `sqlite3_config`.
|
|
515
|
+
*
|
|
516
|
+
* `sqlite3_initialize` alone, or any call that initializes the library itself,
|
|
517
|
+
* such as an open, makes SQLite's `unix-none` VFS the default again and
|
|
518
|
+
* registers kvvfs again, so opening `:localStorage:` or `:sessionStorage:`
|
|
519
|
+
* traps and breaks the instance. Initializing leaves SQLite's random number
|
|
520
|
+
* generator as it is, but while `unix-none` is the default, a generator reset
|
|
521
|
+
* with `sqlite3_randomness(0, 0)`, or not used since loading, seeds itself at
|
|
522
|
+
* its next use from the time and a constant pid instead of {@link RandomBytes},
|
|
523
|
+
* so instances seeded in the same second share `randomblob()` values and
|
|
524
|
+
* journal nonces. This function resets the generator once `evolu-memory` is the
|
|
525
|
+
* default.
|
|
526
|
+
*
|
|
527
|
+
* Fails with {@link SqliteWasmInitializeError}, with SQLITE_MISUSE when
|
|
528
|
+
* `sqlite3_vfs_unregister` removed `evolu-memory`.
|
|
529
|
+
*/
|
|
530
|
+
export const initializeSqliteWasm =
|
|
531
|
+
(deps: SqliteWasmDep) => (): Result<void, SqliteWasmInitializeError> =>
|
|
532
|
+
deps.sqliteWasm.call(() => {
|
|
533
|
+
const { exports } = deps.sqliteWasm;
|
|
534
|
+
const code = exports.sqlite3_initialize();
|
|
535
|
+
if (code !== SQLITE_OK)
|
|
536
|
+
return err({ type: "SqliteWasmInitializeError", code });
|
|
537
|
+
// Both names in one allocation, the second after the first's NUL.
|
|
538
|
+
const names = allocCString(deps)(`${defaultVfsName}\0kvvfs`);
|
|
539
|
+
if (!names.ok)
|
|
540
|
+
return err({ type: "SqliteWasmInitializeError", code: SQLITE_NOMEM });
|
|
541
|
+
const vfs = exports.sqlite3_vfs_find(names.value);
|
|
542
|
+
const kvvfs = exports.sqlite3_vfs_find(
|
|
543
|
+
(names.value + defaultVfsName.length + 1) as CStringPtr,
|
|
544
|
+
);
|
|
545
|
+
exports.sqlite3_free(names.value);
|
|
546
|
+
// Only sqlite3_vfs_unregister removes it.
|
|
547
|
+
if (vfs === 0)
|
|
548
|
+
return err({ type: "SqliteWasmInitializeError", code: SQLITE_MISUSE });
|
|
549
|
+
// sqlite3_initialize registers SQLite's kvvfs, whose storage only
|
|
550
|
+
// SQLite's JavaScript provides, so an open of it, which the names
|
|
551
|
+
// :localStorage: and :sessionStorage: are, would call NULL and trap.
|
|
552
|
+
if (kvvfs !== 0) exports.sqlite3_vfs_unregister(kvvfs);
|
|
553
|
+
const registered = exports.sqlite3_vfs_register(vfs, 1);
|
|
554
|
+
if (registered !== SQLITE_OK)
|
|
555
|
+
return err({ type: "SqliteWasmInitializeError", code: registered });
|
|
556
|
+
exports.sqlite3_randomness(0, 0);
|
|
557
|
+
return ok();
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
/** Why {@link createSqliteWasm} failed. */
|
|
561
|
+
export type SqliteWasmError =
|
|
562
|
+
| SqliteWasmCompileError
|
|
563
|
+
| SqliteWasmUnknownImportError
|
|
564
|
+
| SqliteWasmBuildMismatchError
|
|
565
|
+
| SqliteWasmInitializeError;
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* The source could not be fetched, compiled or instantiated, its memory could
|
|
569
|
+
* not be allocated, its static constructors failed, or the module that installs
|
|
570
|
+
* the default VFS's methods could not be compiled.
|
|
571
|
+
*/
|
|
572
|
+
export interface SqliteWasmCompileError extends Typed<"SqliteWasmCompileError"> {
|
|
573
|
+
readonly cause: unknown;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** The binary declares imports the loader does not implement. */
|
|
577
|
+
export interface SqliteWasmUnknownImportError extends Typed<"SqliteWasmUnknownImport"> {
|
|
578
|
+
/** Each as `module.name`. */
|
|
579
|
+
readonly imports: NonEmptyReadonlyArray<string>;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* The binary's {@link sqliteWasmBuildHash} differs, so it is not the build the
|
|
584
|
+
* generated code describes.
|
|
585
|
+
*
|
|
586
|
+
* To use this binary, pin it with `node scripts/generate.mts --pin-build
|
|
587
|
+
* <wasm>` and regenerate.
|
|
588
|
+
*/
|
|
589
|
+
export interface SqliteWasmBuildMismatchError extends Typed<"SqliteWasmBuildMismatch"> {
|
|
590
|
+
readonly expectedHash: number;
|
|
591
|
+
readonly actualHash: number;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* `sqlite3_initialize` failed, or allocating, finding or registering the
|
|
596
|
+
* default VFS did, for example when out of memory.
|
|
597
|
+
*/
|
|
598
|
+
export interface SqliteWasmInitializeError extends Typed<"SqliteWasmInitializeError"> {
|
|
599
|
+
readonly code: SqliteResultCode;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const defaultVfsName = "evolu-memory";
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Calls a function the instance exports with i32 arguments and returns its
|
|
606
|
+
* result, or returns 0 when it exports no function of that name. Loading uses
|
|
607
|
+
* it before the build guard, which fails a binary without the function.
|
|
608
|
+
*/
|
|
609
|
+
const callExport = (
|
|
610
|
+
instance: WebAssembly.Instance,
|
|
611
|
+
name: string,
|
|
612
|
+
...args: ReadonlyArray<number>
|
|
613
|
+
): number => {
|
|
614
|
+
const fn = instance.exports[name];
|
|
615
|
+
return typeof fn === "function"
|
|
616
|
+
? (fn as (...args: ReadonlyArray<number>) => number)(...args)
|
|
617
|
+
: 0;
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
interface SqliteWasmImportContext extends ConsoleDep, TimeDep {
|
|
621
|
+
readonly memory: WebAssembly.Memory;
|
|
622
|
+
readonly getHeapU8: SqliteWasm["getHeapU8"];
|
|
623
|
+
readonly getHeapDataView: SqliteWasm["getHeapDataView"];
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// The JavaScript API does not expose the minimum a binary declares for its
|
|
627
|
+
// memory import, so this covers every build so far: the pinned one declares 128.
|
|
628
|
+
const initialMemoryPages = 256;
|
|
629
|
+
|
|
630
|
+
// 2 GiB, so every address is a non-negative i32.
|
|
631
|
+
const maximumMemoryPages = 32768;
|
|
632
|
+
|
|
633
|
+
const wasmPageSize = 65536;
|
|
634
|
+
|
|
635
|
+
// WASI's ENOSYS, "function not supported". WASI functions return it, and
|
|
636
|
+
// Emscripten's syscalls return it negated, as Linux does.
|
|
637
|
+
const ENOSYS = 52;
|
|
638
|
+
|
|
639
|
+
const failSyscall = (): number => -ENOSYS;
|
|
640
|
+
|
|
641
|
+
// WASI's EINVAL, "invalid argument".
|
|
642
|
+
const EINVAL = 28;
|
|
643
|
+
|
|
644
|
+
// WASI's EBADF, "bad file descriptor".
|
|
645
|
+
const EBADF = 8;
|
|
646
|
+
|
|
647
|
+
// Every import the loader implements, by module and name, each created from
|
|
648
|
+
// the instance's context.
|
|
649
|
+
const sqliteWasmImports: ReadonlyRecord<
|
|
650
|
+
string,
|
|
651
|
+
ReadonlyRecord<
|
|
652
|
+
string,
|
|
653
|
+
(context: SqliteWasmImportContext) => WebAssembly.ImportValue
|
|
654
|
+
>
|
|
655
|
+
> = {
|
|
656
|
+
env: {
|
|
657
|
+
// Emscripten's version aborts. It must not return, because the function
|
|
658
|
+
// that called it then moves the stack pointer anyway.
|
|
659
|
+
__handle_stack_overflow: () => () => {
|
|
660
|
+
throw new WebAssembly.RuntimeError("SQLite's C stack overflowed.");
|
|
661
|
+
},
|
|
662
|
+
__syscall_chmod: () => failSyscall,
|
|
663
|
+
__syscall_faccessat: () => failSyscall,
|
|
664
|
+
__syscall_fchmod: () => failSyscall,
|
|
665
|
+
__syscall_fchown32: () => failSyscall,
|
|
666
|
+
__syscall_fcntl64: () => failSyscall,
|
|
667
|
+
__syscall_fstat64: () => failSyscall,
|
|
668
|
+
__syscall_ftruncate64: () => failSyscall,
|
|
669
|
+
__syscall_getcwd: () => failSyscall,
|
|
670
|
+
__syscall_ioctl: () => failSyscall,
|
|
671
|
+
__syscall_lstat64: () => failSyscall,
|
|
672
|
+
__syscall_mkdirat: () => failSyscall,
|
|
673
|
+
__syscall_newfstatat: () => failSyscall,
|
|
674
|
+
__syscall_openat: () => failSyscall,
|
|
675
|
+
__syscall_readlinkat: () => failSyscall,
|
|
676
|
+
__syscall_rmdir: () => failSyscall,
|
|
677
|
+
__syscall_stat64: () => failSyscall,
|
|
678
|
+
__syscall_unlinkat: () => failSyscall,
|
|
679
|
+
__syscall_utimensat: () => failSyscall,
|
|
680
|
+
_localtime_js:
|
|
681
|
+
({ getHeapDataView }) =>
|
|
682
|
+
(time: bigint, pTm: number) => {
|
|
683
|
+
const date = new Date(Number(time) * 1000);
|
|
684
|
+
if (Number.isNaN(date.getTime())) return 1;
|
|
685
|
+
const heap = getHeapDataView();
|
|
686
|
+
// The fields of struct tm from tm_sec to tm_year, each 4 bytes. They
|
|
687
|
+
// are all SQLite's toLocaltime reads. Of tm_wday, tm_yday, tm_isdst and
|
|
688
|
+
// tm_gmtoff, which follow, the pinned binary reads only tm_isdst, in
|
|
689
|
+
// Emscripten's localtime_r, to point tm_zone at a time zone name that
|
|
690
|
+
// nothing reads either, so they are zero.
|
|
691
|
+
for (const [index, value] of [
|
|
692
|
+
date.getSeconds(),
|
|
693
|
+
date.getMinutes(),
|
|
694
|
+
date.getHours(),
|
|
695
|
+
date.getDate(),
|
|
696
|
+
date.getMonth(),
|
|
697
|
+
date.getFullYear() - 1900,
|
|
698
|
+
0,
|
|
699
|
+
0,
|
|
700
|
+
0,
|
|
701
|
+
0,
|
|
702
|
+
].entries())
|
|
703
|
+
heap.setInt32(pTm + index * 4, value, true);
|
|
704
|
+
return 0;
|
|
705
|
+
},
|
|
706
|
+
// Emscripten's tzset calls it once to fill timezone, daylight and the time
|
|
707
|
+
// zone names. In the pinned binary, only localtime_r reads them, to point
|
|
708
|
+
// tm_zone at a name, which nothing reads, so they stay zero.
|
|
709
|
+
_tzset_js: () => () => {},
|
|
710
|
+
emscripten_date_now:
|
|
711
|
+
({ time }) =>
|
|
712
|
+
(): number =>
|
|
713
|
+
time.now(),
|
|
714
|
+
emscripten_get_now:
|
|
715
|
+
({ time }) =>
|
|
716
|
+
(): number =>
|
|
717
|
+
time.performance.now(),
|
|
718
|
+
emscripten_resize_heap:
|
|
719
|
+
({ memory }) =>
|
|
720
|
+
(requestedSize: number) => {
|
|
721
|
+
try {
|
|
722
|
+
memory.grow(
|
|
723
|
+
Math.ceil(
|
|
724
|
+
((requestedSize >>> 0) - memory.buffer.byteLength) / wasmPageSize,
|
|
725
|
+
),
|
|
726
|
+
);
|
|
727
|
+
return 1;
|
|
728
|
+
} catch {
|
|
729
|
+
return 0;
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
memory: ({ memory }) => memory,
|
|
733
|
+
},
|
|
734
|
+
wasi_snapshot_preview1: {
|
|
735
|
+
clock_time_get:
|
|
736
|
+
({ time, getHeapDataView }) =>
|
|
737
|
+
(clockId: number, _precision: bigint, pTime: number) => {
|
|
738
|
+
// CLOCK_REALTIME is 0; the monotonic and CPU-time clocks are 1 to 3.
|
|
739
|
+
if (clockId < 0 || clockId > 3) return EINVAL;
|
|
740
|
+
const millis = clockId === 0 ? time.now() : time.performance.now();
|
|
741
|
+
// Nanoseconds since 1970 exceed 2^53, so only microseconds, which
|
|
742
|
+
// clocks report at most anyway, are computed as a number.
|
|
743
|
+
getHeapDataView().setBigInt64(
|
|
744
|
+
pTime,
|
|
745
|
+
BigInt(Math.round(millis * 1000)) * 1000n,
|
|
746
|
+
true,
|
|
747
|
+
);
|
|
748
|
+
return 0;
|
|
749
|
+
},
|
|
750
|
+
environ_get: () => () => 0,
|
|
751
|
+
environ_sizes_get:
|
|
752
|
+
({ getHeapDataView }) =>
|
|
753
|
+
(pCount: number, pBufferSize: number) => {
|
|
754
|
+
const heap = getHeapDataView();
|
|
755
|
+
heap.setUint32(pCount, 0, true);
|
|
756
|
+
heap.setUint32(pBufferSize, 0, true);
|
|
757
|
+
return 0;
|
|
758
|
+
},
|
|
759
|
+
fd_close: () => () => ENOSYS,
|
|
760
|
+
fd_fdstat_get: () => () => ENOSYS,
|
|
761
|
+
fd_read: () => () => ENOSYS,
|
|
762
|
+
fd_seek: () => () => ENOSYS,
|
|
763
|
+
fd_sync: () => () => ENOSYS,
|
|
764
|
+
fd_write: ({ console, getHeapU8, getHeapDataView }) => {
|
|
765
|
+
// Lines of stdout and stderr, written when complete.
|
|
766
|
+
const streams = new Map(
|
|
767
|
+
[console.log, console.error].map((write, index) => [
|
|
768
|
+
index + 1,
|
|
769
|
+
{ write, decoder: new TextDecoder(), line: "" },
|
|
770
|
+
]),
|
|
771
|
+
);
|
|
772
|
+
return (
|
|
773
|
+
fd: number,
|
|
774
|
+
iovs: number,
|
|
775
|
+
iovsLength: number,
|
|
776
|
+
pWritten: number,
|
|
777
|
+
) => {
|
|
778
|
+
const stream = streams.get(fd);
|
|
779
|
+
if (stream == null) return EBADF;
|
|
780
|
+
const heap = getHeapDataView();
|
|
781
|
+
let written = 0;
|
|
782
|
+
for (let index = 0; index < iovsLength; index++) {
|
|
783
|
+
const ptr = heap.getUint32(iovs + index * 8, true);
|
|
784
|
+
const length = heap.getUint32(iovs + index * 8 + 4, true);
|
|
785
|
+
stream.line += stream.decoder.decode(
|
|
786
|
+
getHeapU8().subarray(ptr, ptr + length),
|
|
787
|
+
{ stream: true },
|
|
788
|
+
);
|
|
789
|
+
written += length;
|
|
790
|
+
}
|
|
791
|
+
const lines = stream.line.split("\n");
|
|
792
|
+
// split returns at least one string, the line still incomplete.
|
|
793
|
+
stream.line = lines.pop()!;
|
|
794
|
+
for (const line of lines) stream.write(line);
|
|
795
|
+
// A fresh view, because a Console that calls back into this instance
|
|
796
|
+
// can grow memory, which detaches the one taken before it wrote.
|
|
797
|
+
getHeapDataView().setUint32(pWritten, written, true);
|
|
798
|
+
return 0;
|
|
799
|
+
};
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* A JavaScript function to install into the function table.
|
|
806
|
+
*
|
|
807
|
+
* The signature uses the notation of {@link sqlite3_vfs_layout}, such as
|
|
808
|
+
* `i(ppij)`: the result, then the parameters in parentheses. `i`, `p` and `s`
|
|
809
|
+
* are i32, `j` is i64 (a bigint), `d` is f64, and a `v` result is no result.
|
|
810
|
+
* The result is `i` or `v`, because every callback SQLite calls in this build
|
|
811
|
+
* returns an int or nothing.
|
|
812
|
+
*/
|
|
813
|
+
export interface SqliteWasmFunction {
|
|
814
|
+
readonly signature: string;
|
|
815
|
+
readonly fn: (...args: never) => number | void;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** Functions installed by {@link installWasmFunctions}. */
|
|
819
|
+
export interface SqliteWasmFunctions extends Disposable {
|
|
820
|
+
/** The functions' pointers, in the order they were given. */
|
|
821
|
+
readonly pointers: NonEmptyReadonlyArray<SqliteFunctionPtr>;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Installs functions into the function table.
|
|
826
|
+
*
|
|
827
|
+
* No engine exposes `WebAssembly.Function`, so a JavaScript function becomes a
|
|
828
|
+
* wasm function by being imported into a generated module that re-exports it.
|
|
829
|
+
* All functions of one call share one generated module. A call through one
|
|
830
|
+
* costs a few nanoseconds more than a call to a plain import, for the wrapper
|
|
831
|
+
* that catches exceptions: 12 ns against 9 ns in Node.js 24. A signature the
|
|
832
|
+
* notation does not cover throws. Compiling the generated module needs
|
|
833
|
+
* 'wasm-unsafe-eval' under a Content Security Policy; without it, this throws a
|
|
834
|
+
* `WebAssembly.CompileError`.
|
|
835
|
+
*
|
|
836
|
+
* No exception crosses into wasm. What a function throws is reported with
|
|
837
|
+
* {@link ReportDefect}, and the call returns SQLITE_ERROR, or nothing for a `v`
|
|
838
|
+
* result. So a commit hook rolls back, an `sqlite3_exec` callback aborts, and a
|
|
839
|
+
* VFS method fails, but a busy handler retries, forever if it always throws,
|
|
840
|
+
* and a SQL function's `xFunc` returns NULL. A function whose failure must mean
|
|
841
|
+
* something else catches its own errors, such as an `xFunc` calling
|
|
842
|
+
* `sqlite3_result_error`. An `i` function that returns something other than a
|
|
843
|
+
* number, or undefined for 0, is also reported as a defect and returns
|
|
844
|
+
* SQLITE_ERROR. Two kinds pass through, because they unwound C frames without
|
|
845
|
+
* cleanup: a `WebAssembly.RuntimeError`, which comes from a trap or a C stack
|
|
846
|
+
* overflow in wasm the function called, and anything thrown once the instance
|
|
847
|
+
* is broken, such as a JavaScript stack overflow that escaped a nested
|
|
848
|
+
* {@link SqliteWasm.call}. A function that returns once the instance is broken,
|
|
849
|
+
* as one that caught what a nested call rethrew, or whose ReportDefect broke
|
|
850
|
+
* it, throws the refusal of a broken instance instead, so SQLite never resumes
|
|
851
|
+
* on that state.
|
|
852
|
+
*
|
|
853
|
+
* Disposing the result empties its slots and keeps them for later installs, so
|
|
854
|
+
* per-database callbacks, such as an update hook, do not grow the table with
|
|
855
|
+
* each open. Dispose only once SQLite can no longer call the functions, for
|
|
856
|
+
* example after closing the database that holds them or replacing a hook: a
|
|
857
|
+
* call through an emptied slot traps, and a reused slot calls another function.
|
|
858
|
+
* A VFS's methods stay installed for the lifetime of the instance.
|
|
859
|
+
*/
|
|
860
|
+
export const installWasmFunctions =
|
|
861
|
+
(deps: ReportDefectDep & SqliteWasmDep) =>
|
|
862
|
+
(
|
|
863
|
+
functions: NonEmptyReadonlyArray<SqliteWasmFunction>,
|
|
864
|
+
): SqliteWasmFunctions => {
|
|
865
|
+
const table = deps.sqliteWasm.functionTable;
|
|
866
|
+
const freeSlots = freeSlotsByTable.get(table) ?? [];
|
|
867
|
+
freeSlotsByTable.set(table, freeSlots);
|
|
868
|
+
|
|
869
|
+
// The module imports each function from "" under its index, with the type
|
|
870
|
+
// its signature describes, and exports it under the same name.
|
|
871
|
+
const types = functions.map(({ signature }) => {
|
|
872
|
+
const parsed = /^([iv])\(([ipsjd]*)\)$/u.exec(signature);
|
|
873
|
+
if (parsed == null)
|
|
874
|
+
throw new Error(`Invalid wasm function signature: ${signature}`);
|
|
875
|
+
const [, result, params] = parsed as unknown as readonly [
|
|
876
|
+
string,
|
|
877
|
+
string,
|
|
878
|
+
string,
|
|
879
|
+
];
|
|
880
|
+
return [
|
|
881
|
+
0x60,
|
|
882
|
+
...wasmVector(Array.from(params, (letter) => [wasmValueTypes[letter]])),
|
|
883
|
+
...wasmVector(result === "v" ? [] : [[wasmValueTypes.i]]),
|
|
884
|
+
];
|
|
885
|
+
});
|
|
886
|
+
const names = functions.map((_, index) => wasmName(String(index)));
|
|
887
|
+
const module = new WebAssembly.Module(
|
|
888
|
+
Uint8Array.from([
|
|
889
|
+
...wasmModuleHeader,
|
|
890
|
+
...wasmSection(wasmTypeSection, types),
|
|
891
|
+
...wasmSection(
|
|
892
|
+
wasmImportSection,
|
|
893
|
+
names.map((name, index) => [
|
|
894
|
+
...wasmName(""),
|
|
895
|
+
...name,
|
|
896
|
+
wasmFunctionKind,
|
|
897
|
+
...wasmUnsigned(index),
|
|
898
|
+
]),
|
|
899
|
+
),
|
|
900
|
+
...wasmSection(
|
|
901
|
+
wasmExportSection,
|
|
902
|
+
names.map((name, index) => [
|
|
903
|
+
...name,
|
|
904
|
+
wasmFunctionKind,
|
|
905
|
+
...wasmUnsigned(index),
|
|
906
|
+
]),
|
|
907
|
+
),
|
|
908
|
+
]),
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
const imports = functions.map(({ signature, fn }) => {
|
|
912
|
+
const call = fn as (...args: ReadonlyArray<unknown>) => unknown;
|
|
913
|
+
const hasResult = signature.startsWith("i");
|
|
914
|
+
const failure = hasResult ? SQLITE_ERROR : undefined;
|
|
915
|
+
return (...args: ReadonlyArray<unknown>): unknown => {
|
|
916
|
+
try {
|
|
917
|
+
const result = call(...args);
|
|
918
|
+
// A call the function made broke the instance, and the function
|
|
919
|
+
// caught what escaped it. Returning would resume the C frames below
|
|
920
|
+
// on the state that left inconsistent, so this throws the refusal of
|
|
921
|
+
// a broken instance, whose cause is what broke it, and the catch
|
|
922
|
+
// below lets it through.
|
|
923
|
+
if (deps.sqliteWasm.isBroken()) deps.sqliteWasm.call(constVoid);
|
|
924
|
+
if (!hasResult) return;
|
|
925
|
+
// Converting a result to i32 happens after this function returns, so
|
|
926
|
+
// a bigint, symbol or object that cannot become a number would throw
|
|
927
|
+
// into wasm, past this catch.
|
|
928
|
+
if (result === undefined || typeof result === "number") return result;
|
|
929
|
+
throw new TypeError(
|
|
930
|
+
`A wasm function with an int result returned a ${typeof result}`,
|
|
931
|
+
);
|
|
932
|
+
} catch (error) {
|
|
933
|
+
if (
|
|
934
|
+
error instanceof WebAssembly.RuntimeError ||
|
|
935
|
+
deps.sqliteWasm.isBroken()
|
|
936
|
+
)
|
|
937
|
+
throw error;
|
|
938
|
+
try {
|
|
939
|
+
deps.reportDefect(error);
|
|
940
|
+
} catch (reporterError) {
|
|
941
|
+
reportDefectAfterMicrotask(
|
|
942
|
+
new AggregateError(
|
|
943
|
+
[error, reporterError],
|
|
944
|
+
"ReportDefect failed while reporting a defect",
|
|
945
|
+
),
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
// ReportDefect can break the instance too, by a nested call, so this
|
|
949
|
+
// throws the refusal as above instead of returning into SQLite.
|
|
950
|
+
if (deps.sqliteWasm.isBroken()) deps.sqliteWasm.call(constVoid);
|
|
951
|
+
return failure;
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
});
|
|
955
|
+
const { exports } = new WebAssembly.Instance(module, {
|
|
956
|
+
"": Object.fromEntries(imports.entries()),
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
const pointers = mapArray(functions, (_, index) => {
|
|
960
|
+
const pointer = freeSlots.pop() ?? (table.grow(1) as SqliteFunctionPtr);
|
|
961
|
+
table.set(pointer, exports[index]);
|
|
962
|
+
return pointer;
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
using disposer = new DisposableStack();
|
|
966
|
+
disposer.defer(() => {
|
|
967
|
+
for (const pointer of pointers) {
|
|
968
|
+
table.set(pointer, null);
|
|
969
|
+
freeSlots.push(pointer);
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
return disposable<SqliteWasmFunctions>({ pointers }, disposer);
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
// The emptied slots of each instance's function table.
|
|
977
|
+
const freeSlotsByTable = /*#__PURE__*/ new WeakMap<
|
|
978
|
+
WebAssembly.Table,
|
|
979
|
+
Array<SqliteFunctionPtr>
|
|
980
|
+
>();
|
|
981
|
+
|
|
982
|
+
// The binary format's magic number and version 1.
|
|
983
|
+
const wasmModuleHeader = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
|
|
984
|
+
|
|
985
|
+
const wasmTypeSection = 1;
|
|
986
|
+
const wasmImportSection = 2;
|
|
987
|
+
const wasmExportSection = 7;
|
|
988
|
+
const wasmFunctionKind = 0x00;
|
|
989
|
+
|
|
990
|
+
// The encodings of i32, i64 and f64, by signature letter.
|
|
991
|
+
const wasmValueTypes: ReadonlyRecord<string, number> = {
|
|
992
|
+
i: 0x7f,
|
|
993
|
+
p: 0x7f,
|
|
994
|
+
s: 0x7f,
|
|
995
|
+
j: 0x7e,
|
|
996
|
+
d: 0x7c,
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
/** Encodes an unsigned integer as LEB128. */
|
|
1000
|
+
const wasmUnsigned = (value: number): Array<number> => {
|
|
1001
|
+
const bytes: Array<number> = [];
|
|
1002
|
+
do {
|
|
1003
|
+
const low = value & 0x7f;
|
|
1004
|
+
value >>>= 7;
|
|
1005
|
+
bytes.push(value === 0 ? low : low | 0x80);
|
|
1006
|
+
} while (value !== 0);
|
|
1007
|
+
return bytes;
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
/** Encodes items, each already encoded, as a vector: their count, then them. */
|
|
1011
|
+
const wasmVector = (
|
|
1012
|
+
items: ReadonlyArray<ReadonlyArray<number>>,
|
|
1013
|
+
): Array<number> => [...wasmUnsigned(items.length), ...items.flat()];
|
|
1014
|
+
|
|
1015
|
+
/** Encodes an ASCII name. */
|
|
1016
|
+
const wasmName = (name: string): Array<number> =>
|
|
1017
|
+
wasmVector(Array.from(name, (char) => [char.charCodeAt(0)]));
|
|
1018
|
+
|
|
1019
|
+
/** Encodes a section of encoded items. */
|
|
1020
|
+
const wasmSection = (
|
|
1021
|
+
id: number,
|
|
1022
|
+
items: ReadonlyArray<ReadonlyArray<number>>,
|
|
1023
|
+
): Array<number> => {
|
|
1024
|
+
const body = wasmVector(items);
|
|
1025
|
+
return [id, ...wasmUnsigned(body.length), ...body];
|
|
1026
|
+
};
|