browser-sqlite 1.0.0-rc.3 → 1.0.0-rc.4
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/NOTICE +56 -0
- package/README.md +435 -63
- package/dist/LICENSE +21 -0
- package/dist/NOTICE +56 -0
- package/dist/api.d.ts +376 -0
- package/dist/bulk.d.ts +42 -0
- package/dist/capabilities.d.ts +23 -0
- package/dist/client.d.ts +198 -0
- package/dist/credits.d.ts +31 -0
- package/dist/{esm/src/debug.d.ts → debug.d.ts} +21 -10
- package/dist/delete.d.ts +42 -0
- package/dist/epochs.d.ts +55 -0
- package/dist/errors.d.ts +37 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/locks.d.ts +54 -0
- package/dist/logger.d.ts +24 -0
- package/dist/pool.d.ts +98 -0
- package/dist/queries.d.ts +36 -0
- package/dist/scheduler.d.ts +131 -0
- package/dist/supervisor.d.ts +17 -0
- package/dist/transaction.d.ts +38 -0
- package/dist/types.d.ts +348 -0
- package/dist/utils.d.ts +116 -0
- package/dist/worker/cloneable.d.ts +25 -0
- package/dist/worker/statement-cache.d.ts +22 -0
- package/dist/worker/wa-sqlite-async.wasm +0 -0
- package/dist/worker/wa-sqlite-jspi.wasm +0 -0
- package/dist/worker/wa-sqlite.wasm +0 -0
- package/dist/worker/worker.js +11 -0
- package/dist/worker/worker.js.map +1 -0
- package/package.json +36 -20
- package/dist/esm/index.js +0 -424
- package/dist/esm/rslib.config.d.ts +0 -2
- package/dist/esm/rstest.config.d.ts +0 -2
- package/dist/esm/src/client.d.ts +0 -332
- package/dist/esm/src/index.d.ts +0 -1
- package/dist/esm/src/orchestrator.d.ts +0 -87
- package/dist/esm/src/types.d.ts +0 -83
- package/dist/esm/src/utils.d.ts +0 -6
- /package/dist/{esm/src → worker}/worker.d.ts +0 -0
package/NOTICE
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
THIRD-PARTY NOTICES
|
|
2
|
+
===================
|
|
3
|
+
|
|
4
|
+
browser-sqlite is distributed under the MIT License; see LICENSE.
|
|
5
|
+
|
|
6
|
+
Its published worker artifact (dist/worker/worker.js) has third-party code
|
|
7
|
+
bundled into it, and ships compiled WebAssembly built from third-party
|
|
8
|
+
sources (dist/worker/wa-sqlite.wasm, wa-sqlite-async.wasm, wa-sqlite-jspi.wasm).
|
|
9
|
+
The notices below travel with those files and must be preserved in any
|
|
10
|
+
redistribution.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
-------------------------------------------------------------------------------
|
|
14
|
+
wa-sqlite — https://github.com/rhashimoto/wa-sqlite
|
|
15
|
+
-------------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
The JavaScript glue and the VFS implementations bundled into
|
|
18
|
+
dist/worker/worker.js, and the .wasm binaries beside it, are produced by
|
|
19
|
+
wa-sqlite.
|
|
20
|
+
|
|
21
|
+
MIT License
|
|
22
|
+
|
|
23
|
+
Copyright (c) 2023 Roy T. Hashimoto
|
|
24
|
+
|
|
25
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
26
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
27
|
+
in the Software without restriction, including without limitation the rights
|
|
28
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
29
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
30
|
+
furnished to do so, subject to the following conditions:
|
|
31
|
+
|
|
32
|
+
The above copyright notice and this permission notice shall be included in all
|
|
33
|
+
copies or substantial portions of the Software.
|
|
34
|
+
|
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
36
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
37
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
38
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
39
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
40
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
41
|
+
SOFTWARE.
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
-------------------------------------------------------------------------------
|
|
45
|
+
SQLite — https://sqlite.org
|
|
46
|
+
-------------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
The .wasm binaries are builds of SQLite. SQLite is in the public domain and
|
|
49
|
+
requires no attribution; the customary blessing is reproduced here.
|
|
50
|
+
|
|
51
|
+
The author disclaims copyright to this source code. In place of
|
|
52
|
+
a legal notice, here is a blessing:
|
|
53
|
+
|
|
54
|
+
May you do good and not evil.
|
|
55
|
+
May you find forgiveness for yourself and forgive others.
|
|
56
|
+
May you share freely, never taking more than you give.
|
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
A persistent SQLite database that lives in your browser — yes, for real. Powered by [wa-sqlite](https://github.com/rhashimoto/wa-sqlite) (WebAssembly), built for (read) concurrency.
|
|
4
4
|
|
|
5
|
+
**▶ [Run the benchmarks in your own browser](https://lalexdotcom.github.io/browser-sqlite/)** — every
|
|
6
|
+
VFS this library ships, put through the same conformance checks and measurements, on your device.
|
|
7
|
+
It is the honest way to choose one: which VFS wins depends on the engine, and it changes often —
|
|
8
|
+
a single browser release can move the answer.
|
|
9
|
+
|
|
5
10
|
## Install
|
|
6
11
|
|
|
7
12
|
```bash
|
|
@@ -10,34 +15,38 @@ npm install browser-sqlite
|
|
|
10
15
|
pnpm add browser-sqlite
|
|
11
16
|
```
|
|
12
17
|
|
|
13
|
-
Requires a bundler that supports Web Workers with dynamic imports
|
|
18
|
+
Requires a bundler that supports Web Workers with dynamic imports — or no bundler at all.
|
|
14
19
|
|
|
15
|
-
##
|
|
20
|
+
## Bundler Configuration
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
Works with no configuration under **rsbuild 1+**, **rspack 1+**, **Parcel 2+**, **Vite 8+**, **webpack 5.101+** — and with no bundler at all.
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
Works under **Vite 6.1 to 7** with the following config, which only the dev server needs:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// vite.config.ts
|
|
28
|
+
export default defineConfig({
|
|
29
|
+
optimizeDeps: { exclude: ['browser-sqlite'] },
|
|
30
|
+
});
|
|
31
|
+
```
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
Another bundler will likely work — the worker and its `.wasm` are reached through plain, statically analysable URLs — but may need configuration of its own.
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
The `.wasm` are read from beside `worker.js`. If a build separates them, or you move them by hand, point at them with [`wasmUrl`](#options).
|
|
30
36
|
|
|
31
37
|
## Usage
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
[createSQLiteClient](#createsqliteclient) · [*client*.read](#clientread) · [*client*.write](#clientwrite) · [*client*.stream](#clientstream) · [*client*.chunk](#clientchunk) · [*client*.first](#clientfirst) · [*client*.transaction](#clienttransaction) · [*client*.bulkWrite](#clientbulkwrite) · [*client*.output](#clientoutput) · [*client*.close](#clientclose) · [deleteDatabase](#deletedatabase)
|
|
40
|
+
|
|
41
|
+
### createSQLiteClient
|
|
34
42
|
|
|
35
43
|
```typescript
|
|
36
44
|
import { createSQLiteClient } from 'browser-sqlite';
|
|
37
45
|
|
|
38
46
|
const db = createSQLiteClient('myapp.sqlite', {
|
|
39
47
|
poolSize: 2, // number of worker threads (default: 2)
|
|
40
|
-
vfs: '
|
|
48
|
+
vfs: 'OPFSAdaptiveVFS', // required — see VFS Selection
|
|
49
|
+
build: 'async', // wa-sqlite build (default: the VFS's first)
|
|
41
50
|
pragmas: { // SQLite PRAGMAs applied on open
|
|
42
51
|
journal_mode: 'WAL',
|
|
43
52
|
synchronous: 'NORMAL',
|
|
@@ -47,7 +56,9 @@ const db = createSQLiteClient('myapp.sqlite', {
|
|
|
47
56
|
|
|
48
57
|
`createSQLiteClient` spawns `poolSize` Web Worker threads immediately. Workers reach READY state asynchronously — queries made before workers are ready are queued automatically.
|
|
49
58
|
|
|
50
|
-
|
|
59
|
+
Every option is listed under [Options](#options). `vfs` is the one with no default — [VFS Selection](#vfs-selection) is how to choose it, and a database written through one VFS is not readable through another.
|
|
60
|
+
|
|
61
|
+
### *client*.read
|
|
51
62
|
|
|
52
63
|
```typescript
|
|
53
64
|
type User = { id: number; name: string };
|
|
@@ -61,7 +72,14 @@ const users = await db.read<User>(
|
|
|
61
72
|
|
|
62
73
|
Read queries are dispatched to any available worker, enabling concurrent reads.
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
| Option | Type | Default | Description |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| `signal` | `AbortSignal` | — | Aborts the query. Rejects with `signal.reason`. |
|
|
78
|
+
| `chunkSize` | `number` | `500` | Rows per chunk crossing the worker boundary. Back-pressure grants credits per chunk with a window of 2, so the worker may run up to `2 × chunkSize` rows ahead of the consumer. |
|
|
79
|
+
|
|
80
|
+
On `read()` this is transport only — it still resolves with the whole array.
|
|
81
|
+
|
|
82
|
+
### *client*.write
|
|
65
83
|
|
|
66
84
|
```typescript
|
|
67
85
|
const { affected } = await db.write(
|
|
@@ -73,89 +91,443 @@ const { affected } = await db.write(
|
|
|
73
91
|
|
|
74
92
|
Write queries are serialized through a dedicated writer worker — only one write executes at a time.
|
|
75
93
|
|
|
76
|
-
|
|
94
|
+
| Option | Type | Default | Description |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| `signal` | `AbortSignal` | — | Aborts the query. Rejects with `signal.reason`. |
|
|
97
|
+
|
|
98
|
+
### *client*.stream
|
|
77
99
|
|
|
78
100
|
```typescript
|
|
79
101
|
// Worker is held for the full generator lifetime — always exhaust or break.
|
|
80
|
-
for await (const
|
|
81
|
-
|
|
82
|
-
[],
|
|
83
|
-
{ chunkSize: 100 },
|
|
84
|
-
)) {
|
|
85
|
-
processChunk(chunk); // chunk is User[]
|
|
102
|
+
for await (const row of db.stream<User>('SELECT * FROM large_table', [])) {
|
|
103
|
+
processRow(row); // row is User
|
|
86
104
|
}
|
|
87
105
|
```
|
|
88
106
|
|
|
89
|
-
`stream()` yields rows
|
|
107
|
+
`stream()` yields individual rows without buffering the full result set in memory.
|
|
108
|
+
Use `chunk()` to iterate in batches: `for await (const rows of db.chunk(...))`.
|
|
109
|
+
|
|
110
|
+
| Option | Type | Default | Description |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| `signal` | `AbortSignal` | — | Aborts the query. Rejects with `signal.reason`. |
|
|
113
|
+
| `chunkSize` | `number` | `500` | Rows per chunk crossing the worker boundary. Back-pressure grants credits per chunk with a window of 2, so the worker may run up to `2 × chunkSize` rows ahead of the consumer. |
|
|
90
114
|
|
|
91
|
-
|
|
115
|
+
On `stream()`, `chunkSize` is the only lever on how many rows are in flight.
|
|
116
|
+
|
|
117
|
+
### *client*.chunk
|
|
92
118
|
|
|
93
119
|
```typescript
|
|
94
|
-
|
|
120
|
+
// Worker is held for the full generator lifetime — always exhaust or break.
|
|
121
|
+
for await (const rows of db.chunk<User>('SELECT * FROM large_table', [])) {
|
|
122
|
+
processBatch(rows); // rows is User[]
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`chunk()` yields arrays instead of rows. Prefer it over `stream()` when the work
|
|
127
|
+
is per-batch — one `INSERT` per chunk rather than per row.
|
|
128
|
+
|
|
129
|
+
| Option | Type | Default | Description |
|
|
130
|
+
|---|---|---|---|
|
|
131
|
+
| `signal` | `AbortSignal` | — | Aborts the query. Rejects with `signal.reason`. |
|
|
132
|
+
| `chunkSize` | `number` | `500` | Rows per chunk crossing the worker boundary. Back-pressure grants credits per chunk with a window of 2, so the worker may run up to `2 × chunkSize` rows ahead of the consumer. |
|
|
133
|
+
|
|
134
|
+
Here `chunkSize` is the batch size the consumer sees, not only a transport detail.
|
|
135
|
+
|
|
136
|
+
### *client*.first
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const user = await db.first<User>(
|
|
95
140
|
'SELECT * FROM users WHERE id = ?',
|
|
96
141
|
[42],
|
|
97
142
|
);
|
|
98
143
|
// user: User | undefined
|
|
99
144
|
```
|
|
100
145
|
|
|
101
|
-
`
|
|
146
|
+
`first()` returns the first result row, or `undefined` if no rows match. Use it for lookups by primary key or unique field.
|
|
102
147
|
|
|
103
|
-
|
|
148
|
+
| Option | Type | Default | Description |
|
|
149
|
+
|---|---|---|---|
|
|
150
|
+
| `signal` | `AbortSignal` | — | Aborts the query. Rejects with `signal.reason`. |
|
|
104
151
|
|
|
105
|
-
|
|
106
|
-
- `db.bulkWrite(table, keys)` — batches inserts within `SQLITE_MAX_VARS` limit
|
|
107
|
-
- `db.output(table, schema, options)` — drops, recreates, and populates a table from a schema definition
|
|
108
|
-
- `db.transaction(callback, options)` — wraps operations in a SQLite transaction with auto-commit and rollback
|
|
152
|
+
`first()` stops the query after one row instead of draining the result set.
|
|
109
153
|
|
|
110
|
-
###
|
|
154
|
+
### *client*.transaction
|
|
111
155
|
|
|
112
156
|
```typescript
|
|
113
|
-
db.
|
|
157
|
+
const orders = await db.transaction(async (tx) => {
|
|
158
|
+
await tx.write('INSERT INTO orders (id, total) VALUES (?, ?)', [1, 42]);
|
|
159
|
+
await tx.write('UPDATE stock SET qty = qty - 1 WHERE id = ?', [7]);
|
|
160
|
+
const rows = await tx.read<{ n: number }>('SELECT count(*) AS n FROM orders');
|
|
161
|
+
return rows[0].n;
|
|
162
|
+
});
|
|
114
163
|
```
|
|
115
164
|
|
|
116
|
-
|
|
165
|
+
One worker is held for the callback's whole lifetime, so nothing else can run on
|
|
166
|
+
it: the transaction is genuinely isolated, not merely wrapped in `BEGIN`.
|
|
167
|
+
Returning commits, throwing rolls back and re-throws. `{ readOnly: true }`
|
|
168
|
+
rejects write statements; `{ autoCommit: false }` leaves the commit to you.
|
|
117
169
|
|
|
118
|
-
|
|
170
|
+
`tx` carries the same querying surface as the client — `read`, `write`, `chunk`, `stream`, `first`, `bulkWrite`, `output` — plus `commit` and `rollback`.
|
|
119
171
|
|
|
120
|
-
|
|
172
|
+
`{ signal }` abandons the transaction at any point, including while it waits for a worker and while your callback sits on something that is not a statement. It rolls back and rejects with `signal.reason`, and it never commits — a callback that catches its own statement's rejection cannot commit around the abort. Your callback is not interrupted, but every statement it issues afterwards rejects. `BEGIN`, `COMMIT` and `ROLLBACK` are the exception: they do not carry the signal, so an abort raised while one of them is in flight lands when it settles.
|
|
121
173
|
|
|
122
|
-
|
|
174
|
+
| Option | Type | Default | Description |
|
|
175
|
+
|---|---|---|---|
|
|
176
|
+
| `readOnly` | `boolean` | `false` | Rejects write statements with `READ_ONLY_TRANSACTION`, at the call rather than at the first flush. |
|
|
177
|
+
| `autoCommit` | `boolean` | `true` | Commits when the callback resolves. Set it false to commit or roll back yourself. |
|
|
178
|
+
| `signal` | `AbortSignal` | — | Abandons the transaction. Rolls back and rejects with `signal.reason`; never commits. |
|
|
123
179
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
180
|
+
### *client*.bulkWrite
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const rows = db.bulkWrite('events', ['id', 'kind', 'at']);
|
|
184
|
+
for (const event of events) rows.enqueue(event);
|
|
185
|
+
const affected = await rows.close();
|
|
127
186
|
```
|
|
128
187
|
|
|
129
|
-
|
|
188
|
+
Batches inserts to stay under SQLite's variable limit (`SQLITE_MAX_VARS`,
|
|
189
|
+
32 766), flushing whenever the next row would cross it. `close()` flushes the
|
|
190
|
+
remainder and resolves with the total number of rows written.
|
|
191
|
+
|
|
192
|
+
Single-use: `enqueue()` and `close()` throw once closed. A batch that fails
|
|
193
|
+
rejects with a `SQLiteBulkWriteError` carrying `rowsWritten` and `rowsNotWritten` — a
|
|
194
|
+
multi-row INSERT is statement-atomic, so the failing batch wrote nothing.
|
|
195
|
+
|
|
196
|
+
`bulkWrite()` is not atomic: batches are committed as they flush, so a failure leaves the rows already written in place. Call it on a `tx` if you need all-or-nothing.
|
|
197
|
+
|
|
198
|
+
Pass `{ signal }` to abort a load. `close()` then rejects with `signal.reason`, and the abort lands **between** batches — never inside one, because a multi-row INSERT is statement-atomic. The batches already written stay written, for the same reason a failure leaves them: an abort stops the load, it does not undo it.
|
|
199
|
+
|
|
200
|
+
Await `enqueue()` to be slowed to the speed of the database. It resolves immediately while fewer than `queueSize` rows are queued for writing, and only defers beyond that — so a producer that awaits every row never holds more than that many unwritten rows. Ignoring the returned promise is legal and loads exactly as before: the bound is an offer, not a guarantee, and only you can take it. `queueSize` counts rows, not bytes: if your columns carry blobs, set it yourself.
|
|
201
|
+
|
|
202
|
+
| Option | Type | Default | Description |
|
|
203
|
+
|---|---|---|---|
|
|
204
|
+
| `signal` | `AbortSignal` | — | Aborts the load between batches. `close()` rejects with `signal.reason`. |
|
|
205
|
+
| `queueSize` | `number` | 2 batches | Rows queued for writing above which `enqueue()` defers. A batch is `floor(32766 / columns)` rows. |
|
|
130
206
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
207
|
+
### *client*.output
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
const out = db.output(
|
|
211
|
+
'products',
|
|
212
|
+
{ id: 'INTEGER', name: 'TEXT', price: { type: 'REAL', required: true } },
|
|
213
|
+
{ indexes: ['name', { columns: ['name', 'price'], unique: true }] },
|
|
214
|
+
);
|
|
215
|
+
out.enqueue({ id: 1, name: 'widget', price: 9.99 });
|
|
216
|
+
const affected = await out.close();
|
|
135
217
|
```
|
|
136
218
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
219
|
+
Builds a table from a schema declaration and populates it. Rows land in a
|
|
220
|
+
staging table and the swap happens atomically at `close()`, so **the previous
|
|
221
|
+
table stays intact and fully populated until the new one is ready** — a reader
|
|
222
|
+
querying mid-load sees the old data, never a half-filled table. A target that
|
|
223
|
+
did not exist appears only at `close()`. Single-use, like `bulkWrite`.
|
|
224
|
+
|
|
225
|
+
`output()` takes `{ signal }` too, and an aborted one is observationally a no-op: the staging table is dropped and nothing else is touched. No rename, no partial publication — whatever was in the target before is still there, whole.
|
|
226
|
+
|
|
227
|
+
| Option | Type | Default | Description |
|
|
228
|
+
|---|---|---|---|
|
|
229
|
+
| `indexes` | `Index[]` | — | Indexes built after the swap, under their final names. A column name, an array of them, or `{ columns, unique }`. |
|
|
230
|
+
| `signal` | `AbortSignal` | — | Aborts the load between batches. `close()` rejects with `signal.reason` and the target is untouched. |
|
|
231
|
+
| `queueSize` | `number` | 2 batches | Rows queued for writing above which `enqueue()` defers. A batch is `floor(32766 / columns)` rows. |
|
|
232
|
+
|
|
233
|
+
**Inside a transaction, `output()` costs more than it looks.** On its own it loads rows outside any transaction and holds the write lock only for the final swap. Called on a `tx`, the entire load runs inside your transaction — every other write, in this tab and in others, waits for it to finish.
|
|
234
|
+
|
|
235
|
+
### *client*.close
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
await db.close();
|
|
144
239
|
```
|
|
145
240
|
|
|
146
|
-
|
|
241
|
+
Drains in-flight work, rejects queued work, closes each database connection, then terminates all workers. The returned promise settles once every worker has closed and been terminated, or once `drainTimeout` has elapsed. Calling `close()` a second time returns the same promise — the operation runs exactly once.
|
|
242
|
+
|
|
243
|
+
**Stored data is not deleted.** `close()` releases workers and connections; it removes nothing. To remove the database itself, use [`deleteDatabase`](#deletedatabase).
|
|
244
|
+
|
|
245
|
+
### deleteDatabase
|
|
246
|
+
|
|
247
|
+
Removes a database and the `-journal` / `-wal` files SQLite may have left beside it. The database must not be open, in this tab or any other.
|
|
248
|
+
|
|
147
249
|
```typescript
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
250
|
+
import { deleteDatabase } from 'browser-sqlite';
|
|
251
|
+
|
|
252
|
+
await deleteDatabase('myapp.sqlite', { vfs: 'OPFSAdaptiveVFS' });
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`vfs` is required and must be the VFS the database was created with: a database written through one VFS is not visible through another, so deleting through the wrong one deletes nothing and reports success. `build` and `wasmUrl` are accepted with the same meaning as on `createSQLiteClient`.
|
|
256
|
+
|
|
257
|
+
Deleting a database that does not exist is not an error.
|
|
258
|
+
|
|
259
|
+
What a VFS keeps for itself is left alone — the IndexedDB store shared by every database that VFS holds on this origin, and the `AccessHandlePoolVFS` directory whose files are its reusable capacity. The deleted database's own bytes are freed in both cases.
|
|
260
|
+
|
|
261
|
+
Throws `SQLiteError` with code `BUSY` when the database is open or being opened, and `TIMEOUT` when the VFS cannot answer within 30 seconds — most often the same cause.
|
|
262
|
+
|
|
263
|
+
| Option | Type | Default | Description |
|
|
264
|
+
|---|---|---|---|
|
|
265
|
+
| `vfs` | `SQLiteVFS` | — (required) | The VFS the database was created with. Deleting through another one deletes nothing and reports success. |
|
|
266
|
+
| `build` | `SQLiteBuild` | first build the VFS declares | Which wa-sqlite build to load. It does not affect where the database lives — only which builds can instantiate the VFS. |
|
|
267
|
+
| `wasmUrl` | `string \| ((build: SQLiteBuild) => string)` | `undefined` | Same meaning as on [`createSQLiteClient`](#options). A deployment that needs it to open a database needs it to delete one. |
|
|
268
|
+
|
|
269
|
+
## Options
|
|
270
|
+
|
|
271
|
+
| Option | Type | Default | Description |
|
|
272
|
+
|--------|------|---------|-------------|
|
|
273
|
+
| `poolSize` | `number` | `2` | Number of Web Workers spawned in the pool. A larger pool allows more concurrent reads but uses more memory. Must be `1` with `AccessHandlePoolVFS`. |
|
|
274
|
+
| `vfs` | `SQLiteVFS` | — (required) | VFS implementation for storage. See the [VFS Selection](#vfs-selection) table. |
|
|
275
|
+
| `build` | `SQLiteBuild` | first build the VFS declares | Which wa-sqlite WebAssembly build to load: `'sync'`, `'async'`, or `'jspi'`. Throws `INVALID_OPTION` at construction if the VFS does not support it. See [Builds](#builds). |
|
|
276
|
+
| `wasmUrl` | `string \| ((build: SQLiteBuild) => string)` | `undefined` | Where the workers fetch their `.wasm`. Omit it and resolution is unchanged: the files are read from beside `worker.js`. A string is a directory resolved against the page — relative, absolute or a full URL, trailing slash optional. A callback receives the resolved `build` and names one file, for a bundler-emitted asset carrying a content hash. Called once, at construction. Throws `INVALID_OPTION` there if the value is not a URL. Another origin needs CORS and `Content-Type: application/wasm`. |
|
|
277
|
+
| `pragmas` | `Record<string, string>` | `undefined` | SQLite PRAGMAs applied to each worker connection on open. |
|
|
278
|
+
| `maxWorkerRestarts` | `number` | `1` | How many times a slot may be restarted after it dies. The counter resets once a replacement has actually served a request. A slot that fails to open is retried once, but only if another worker did open — when none did, the failure is a configuration error and the client fails immediately rather than retrying. |
|
|
279
|
+
| `openTimeout` | `number` (ms) | `30_000` | How long a worker has to post `ready` after `open` is sent. On expiry the slot is failed — the most common cause is a database held under an exclusive lock by another tab. |
|
|
280
|
+
| `drainTimeout` | `number` (ms) | `60_000` | How long the drain loop may run in the query generator's `finally` before the worker is presumed dead and the crash path is invoked. |
|
|
281
|
+
| `debug` | `string \| boolean` | `undefined` | Enables lifecycle logging. A string value is used as the log prefix; `true` falls back to the client prefix (e.g. `"SQLite 1"`). Only lifecycle events are logged — worker created, ready, open-error, crash, restart, worker lost, close, and skipped staging sweep. No line per query. Off by default, with one exception: a permanently lost worker always warns, because a pool quietly smaller than `poolSize` is not something to discover later. When enabled, `db.debug` also exposes a live introspection state tree for query throughput and worker status. |
|
|
282
|
+
| `onWorkerLost` | `(event: WorkerLostEvent) => void` | `undefined` | Called when a worker is lost for good, with the slot index, how many workers are left, the requested `poolSize`, and the error. Fires before the client fails if it was the last one. A throwing callback is caught and warned about; it cannot break the pool. |
|
|
283
|
+
|
|
284
|
+
## Browser support
|
|
285
|
+
|
|
286
|
+
| Chrome | Firefox | Safari |
|
|
287
|
+
|---|---|---|
|
|
288
|
+
| 92+ | 95+ | 15.4+ |
|
|
289
|
+
|
|
290
|
+
## VFS Selection
|
|
291
|
+
|
|
292
|
+
browser-sqlite delegates storage to a
|
|
293
|
+
[wa-sqlite Virtual File System](https://github.com/rhashimoto/wa-sqlite/tree/master/src/examples#readme)
|
|
294
|
+
(VFS).
|
|
295
|
+
|
|
296
|
+
**`vfs` is required — there is no default.** A VFS decides *where* your database
|
|
297
|
+
is written, so a default that moved between versions would leave you reading an
|
|
298
|
+
empty database while your bytes sat in a store nothing queries.
|
|
299
|
+
|
|
300
|
+
**Pass `OPFSAdaptiveVFS` unless you have a reason not to.** Across every engine we
|
|
301
|
+
could test — Chrome, Firefox and Safari, desktop and mobile — it opened and passed
|
|
302
|
+
every conformance check without exception. It is the only VFS here of which that is
|
|
303
|
+
true.
|
|
304
|
+
|
|
305
|
+
> **Each VFS is a separate store.** A database written through one VFS is not
|
|
306
|
+
> visible through another — the bytes are still there, but nothing reads them.
|
|
307
|
+
> Changing `vfs` later does not migrate anything.
|
|
308
|
+
|
|
309
|
+
You would leave that choice when you control which browser runs your code — an
|
|
310
|
+
Electron app, a kiosk, a managed fleet — and need something it cannot give you:
|
|
311
|
+
|
|
312
|
+
| Browser you can guarantee | Concurrent reads | Write-heavy workloads |
|
|
313
|
+
|---|---|---|
|
|
314
|
+
| None — the open web | `OPFSAnyContextVFS` if you can require Safari 26+; otherwise `IDBBatchAtomicVFS` | stay on `OPFSAdaptiveVFS` |
|
|
315
|
+
| Chromium 121+ | already the case | `OPFSWriteAheadVFS` |
|
|
316
|
+
| Firefox 111+ | `OPFSAnyContextVFS` | stay |
|
|
317
|
+
| Safari 26+ / iPadOS 26+ | `OPFSAnyContextVFS` | stay |
|
|
318
|
+
| iOS (iPhone) | none measured to help | stay |
|
|
319
|
+
|
|
320
|
+
**Concurrent reads** covers both serving a read while a write transaction is open
|
|
321
|
+
and running several reads at once under a pool: a VFS holding one exclusive
|
|
322
|
+
access handle can do neither, because it is the same handle a second worker never
|
|
323
|
+
gets. For how much any of this is worth on your own targets, run
|
|
324
|
+
[the benchmark page](https://lalexdotcom.github.io/browser-sqlite/) — no timings
|
|
325
|
+
appear in this file.
|
|
326
|
+
|
|
327
|
+
<!-- BEGIN GENERATED VFS TABLE — edit VFS_CAPABILITIES in src/types.ts, then run `pnpm docs:vfs` -->
|
|
328
|
+
|
|
329
|
+
| VFS | Builds | Browser compatibility | Pool size | Shared between connections | Survives close | Memory |
|
|
330
|
+
|-----|--------|-----------------------|-----------|----------------------------|----------------|--------|
|
|
331
|
+
| `OPFSAdaptiveVFS` **(recommended)** | [`async`](#build-async), [`jspi`](#build-jspi) | Chrome 92+/137+<br>Firefox 111+/153+ [(*)](#-reduced-mode)<br>Safari 15.4+/27+ [(*)](#-reduced-mode)<br>Android 109+/?<br>iOS 15.4+/27+ [(*)](#-reduced-mode) | Any | Yes | Yes | Page cache only, bounded by `PRAGMA cache_size` |
|
|
332
|
+
| `OPFSWriteAheadVFS` | [`sync`](#build-sync), [`async`](#build-async), [`jspi`](#build-jspi) | Chrome 92+/137+<br>Firefox 111+/153+ [(*)](#-reduced-mode)<br>Safari 15.4+/27+ [(*)](#-reduced-mode)<br>Android 109+/?<br>iOS 15.4+/27+ [(*)](#-reduced-mode) | Any | Yes | Yes | Page cache only, bounded by `PRAGMA cache_size` |
|
|
333
|
+
| `OPFSCoopSyncVFS` | [`sync`](#build-sync), [`async`](#build-async), [`jspi`](#build-jspi) | Chrome 92+/137+<br>Firefox 111+/153+<br>Safari 15.4+/27+<br>Android 109+/?<br>iOS 15.4+/27+ | Any | Yes | Yes | Page cache only, bounded by `PRAGMA cache_size` |
|
|
334
|
+
| `AccessHandlePoolVFS` | [`sync`](#build-sync), [`async`](#build-async), [`jspi`](#build-jspi) | Chrome 92+/137+<br>Firefox 111+/153+<br>Safari 15.4+/27+<br>Android 109+/?<br>iOS 15.4+/27+ | **1** — it cannot share access handles between connections | No | Yes | Page cache only, bounded by `PRAGMA cache_size` |
|
|
335
|
+
| `IDBBatchAtomicVFS` | [`async`](#build-async), [`jspi`](#build-jspi) | Chrome 92+/137+<br>Firefox 95+/153+<br>Safari 15.4+/27+<br>Android 92+/?<br>iOS 15.4+/27+ | Any | Yes | Yes | Page cache only, bounded by `PRAGMA cache_size` |
|
|
336
|
+
| `IDBMirrorVFS` | [`async`](#build-async), [`jspi`](#build-jspi) | Chrome 92+/137+<br>Firefox 95+/153+<br>Safari 15.4+/27+<br>Android 92+/?<br>iOS 15.4+/27+ | **1** — its pages are mirrored per worker and commits propagate asynchronously, so a larger pool reads stale data or fails outright | No | Yes | **Whole database in RAM**, multiplied by `poolSize` |
|
|
337
|
+
| `OPFSAnyContextVFS` | [`async`](#build-async), [`jspi`](#build-jspi) | Chrome 92+/137+<br>Firefox 111+/153+<br>Safari 26+/27+<br>Android 109+/?<br>iOS 26+/27+ | Any | Yes | Yes | Page cache only, bounded by `PRAGMA cache_size` |
|
|
338
|
+
| `MemoryVFS` | [`sync`](#build-sync), [`async`](#build-async), [`jspi`](#build-jspi) | Chrome 92+/137+<br>Firefox 95+/153+<br>Safari 15.4+/27+<br>Android 92+/?<br>iOS 15.4+/27+ | **1** — its pages live in the worker that opened them, so a larger pool would open independent databases that diverge silently | No | **No — volatile** | **Whole database in RAM**, multiplied by `poolSize` |
|
|
339
|
+
| `MemoryAsyncVFS` | [`async`](#build-async), [`jspi`](#build-jspi) | Chrome 92+/137+<br>Firefox 95+/153+<br>Safari 15.4+/27+<br>Android 92+/?<br>iOS 15.4+/27+ | **1** — its pages live in the worker that opened them, so a larger pool would open independent databases that diverge silently | No | **No — volatile** | **Whole database in RAM**, multiplied by `poolSize` |
|
|
340
|
+
|
|
341
|
+
<!-- END GENERATED VFS TABLE -->
|
|
342
|
+
|
|
343
|
+
The **Browser compatibility** column is derived from documented platform support,
|
|
344
|
+
not from our own test runs. It covers where the VFS stores data; which **builds**
|
|
345
|
+
are reachable on each engine is a separate question, answered under
|
|
346
|
+
[Builds](#builds) — the `Builds` column links straight to the build it names.
|
|
347
|
+
|
|
348
|
+
#### (*) Reduced mode
|
|
349
|
+
|
|
350
|
+
The VFS runs on that engine, but without `readwrite-unsafe` access handles: one
|
|
351
|
+
exclusive handle rotated between workers instead of one held per connection. It
|
|
352
|
+
is not a partial failure — `OPFSAdaptiveVFS` passes 102 of 104 browser tests on
|
|
353
|
+
Firefox in exactly that mode.
|
|
354
|
+
|
|
355
|
+
What it costs is pool concurrency under one specific shape. **On an engine
|
|
356
|
+
without `readwrite-unsafe`, a VFS that rotates a single exclusive OPFS access
|
|
357
|
+
handle cannot serve any other worker while a write transaction holds that
|
|
358
|
+
handle** — the worker that took it does not give it back before the transaction
|
|
359
|
+
ends, and the next acquisition blocks in the scheduler, before an `AbortSignal`
|
|
360
|
+
is ever consulted. That covers `OPFSAdaptiveVFS` in reduced mode.
|
|
361
|
+
`IDBMirrorVFS`, `OPFSAnyContextVFS` and `IDBBatchAtomicVFS` hold no such handle
|
|
362
|
+
and are unaffected.
|
|
363
|
+
|
|
364
|
+
`OPFSCoopSyncVFS` has the same symptom for a different reason, and it is **not**
|
|
365
|
+
conditional on the engine — it never uses `readwrite-unsafe`, so it is never in
|
|
366
|
+
reduced mode. See [Known Limitations](#known-limitations).
|
|
367
|
+
|
|
368
|
+
**A long *read* does not produce this effect, except once per worker after a
|
|
369
|
+
write.**
|
|
370
|
+
|
|
371
|
+
#### `OPFSAnyContextVFS` and wa-sqlite
|
|
372
|
+
|
|
373
|
+
This VFS needs a patched wa-sqlite to work on Safari. browser-sqlite ships that
|
|
374
|
+
patch inside its own worker bundle — there is nothing for you to install or
|
|
375
|
+
configure.
|
|
376
|
+
|
|
377
|
+
### Builds
|
|
378
|
+
|
|
379
|
+
Each VFS runs on one or more wa-sqlite WebAssembly builds. The `build` option
|
|
380
|
+
selects one; omitted, the first build the VFS declares is used — `async` for the
|
|
381
|
+
default VFS. A pair the VFS does not support throws a `SQLiteError` with code
|
|
382
|
+
`INVALID_OPTION` at construction, naming the builds it does support. The pairing
|
|
383
|
+
is declared in one place, `VFS_CAPABILITIES`, which is also what the `SQLiteVFS`
|
|
384
|
+
type is derived from.
|
|
385
|
+
|
|
386
|
+
A build carries its own engine requirement, independent of where the VFS stores
|
|
387
|
+
data — so a VFS can be reachable in `sync` on an old browser and in `jspi` only
|
|
388
|
+
on a much newer one.
|
|
389
|
+
|
|
390
|
+
<!-- BEGIN GENERATED BUILD TABLE — edit FEATURE_SUPPORT in scripts/render-vfs-matrix.ts -->
|
|
391
|
+
|
|
392
|
+
#### Build `sync`
|
|
393
|
+
|
|
394
|
+
| Chrome / Edge | Firefox | Safari | Chrome Android | Safari iOS |
|
|
395
|
+
|---|---|---|---|---|
|
|
396
|
+
| Any | Any | Any | Any | Any |
|
|
397
|
+
|
|
398
|
+
Plain synchronous WebAssembly. Needs nothing beyond baseline WASM, so it runs anywhere — but only VFS whose file operations are all synchronous can offer it.
|
|
399
|
+
|
|
400
|
+
#### Build `async`
|
|
401
|
+
|
|
402
|
+
| Chrome / Edge | Firefox | Safari | Chrome Android | Safari iOS |
|
|
403
|
+
|---|---|---|---|---|
|
|
404
|
+
| Any | Any | Any | Any | Any |
|
|
405
|
+
|
|
406
|
+
Asyncify: the WASM stack is unwound and rewound around asynchronous file operations. Also needs nothing beyond baseline WASM. This is the default, and every VFS here can run on it.
|
|
407
|
+
|
|
408
|
+
#### Build `jspi`
|
|
409
|
+
|
|
410
|
+
| Chrome / Edge | Firefox | Safari | Chrome Android | Safari iOS |
|
|
411
|
+
|---|---|---|---|---|
|
|
412
|
+
| 137+ | 153+ | 27+ | Yes | 27+ |
|
|
413
|
+
|
|
414
|
+
JavaScript Promise Integration — the same asynchrony handled by the engine rather than by Asyncify. Opt-in, and no default uses it, so its narrower availability constrains nobody who does not ask for it.
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
<!-- END GENERATED BUILD TABLE -->
|
|
418
|
+
|
|
419
|
+
## Error handling
|
|
420
|
+
|
|
421
|
+
Errors raised by this library are instances of `SQLiteError`, exported from the package entry point. Discriminate on `error.code` or `error.name` — they carry the same value, so `err.name` reads the way `'AbortError'` does on a DOM `AbortError`.
|
|
422
|
+
|
|
423
|
+
| Code | When it is thrown |
|
|
424
|
+
|------|------------------|
|
|
425
|
+
| `NOT_A_READ_QUERY` | `read()`, `chunk()`, `stream()`, or `first()` was called with a statement that is not a provably readable query. A bare read pragma (`PRAGMA journal_mode`) is accepted; a pragma that assigns a value or takes an argument must go through `write()`. |
|
|
426
|
+
| `CLIENT_CLOSED` | A query was queued after `close()` was called. |
|
|
427
|
+
| `WORKER_CRASHED` | A pool worker died and the supervisor decided not to restart it. All queued and in-flight work on that slot is rejected. |
|
|
428
|
+
| `TIMEOUT` | A worker did not post `ready` within `openTimeout` milliseconds. The most common cause is a database held under an exclusive lock by another tab or client. |
|
|
429
|
+
| `PROTOCOL_ERROR` | A message was received from a worker that could not be deserialized (`messageerror`). The worker survives; only the in-flight request is rejected. |
|
|
430
|
+
| `BUSY` | SQLite reported a lock conflict (`SQLITE_BUSY` or `SQLITE_LOCKED`); the numeric SQLite code is on `sqliteCode`. The operation is not retried. |
|
|
431
|
+
| `READ_ONLY_TRANSACTION` | raised when a write statement, `bulkWrite()` or `output()` is used inside a transaction opened with `readOnly: true`. |
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
import { SQLiteError } from 'browser-sqlite';
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
await db.write('...');
|
|
438
|
+
} catch (err) {
|
|
439
|
+
if (err instanceof SQLiteError) {
|
|
440
|
+
switch (err.code) {
|
|
441
|
+
case 'WORKER_CRASHED': /* restart or notify */ break;
|
|
442
|
+
case 'CLIENT_CLOSED': /* client was shut down */ break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**Request timeouts.** The library adds no per-request timeout. To bound a query, pass `AbortSignal.timeout(ms)`:
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
const rows = await db.read('SELECT * FROM large_table', [], {
|
|
452
|
+
signal: AbortSignal.timeout(5_000),
|
|
453
|
+
});
|
|
155
454
|
```
|
|
156
455
|
|
|
456
|
+
**`close()` is async.** Always `await db.close()` — the returned promise settles once every worker has closed its database connection and been terminated. Discarding the promise means the caller cannot tell when teardown is complete.
|
|
457
|
+
|
|
458
|
+
**Read methods reject write statements.** `read()`, `chunk()`, `stream()`, and `first()` reject any statement that is not a provably readable query, throwing `NOT_A_READ_QUERY`. A bare read pragma (`PRAGMA journal_mode`) is accepted; a pragma that assigns a value or takes an argument must go through `write()`.
|
|
459
|
+
|
|
460
|
+
**Read-your-own-writes is guaranteed within a tab.** Once a write has resolved,
|
|
461
|
+
any read issued afterwards — from that client or from any other client in the
|
|
462
|
+
same tab on the same database — observes it, whatever the pool size. A worker
|
|
463
|
+
that has not yet observed the latest commit runs one discarded statement that
|
|
464
|
+
opens a real read transaction before it serves the query; that costs one extra
|
|
465
|
+
worker round-trip on each worker's first statement after a write, and nothing
|
|
466
|
+
under read-only load. `poolSize: 1` and reading inside the same `transaction()`
|
|
467
|
+
remain valid, they are no longer required.
|
|
468
|
+
|
|
469
|
+
**It is not guaranteed across tabs.** A write in one tab may not be visible to a
|
|
470
|
+
read in another. No bound is claimed on how long that lasts.
|
|
471
|
+
|
|
472
|
+
**Nothing serializes writes between clients.** Two clients writing to one
|
|
473
|
+
database concurrently can fail on a lock; the failure surfaces as
|
|
474
|
+
`SQLiteError` with code `BUSY` and `sqliteCode` 5 or 6, and it is **not**
|
|
475
|
+
retried — no `busy_timeout` is applied. This was true before the guarantee
|
|
476
|
+
above existed; it matters now because the guarantee makes several clients on
|
|
477
|
+
one database a reasonable thing to do.
|
|
478
|
+
|
|
479
|
+
## Requirements
|
|
480
|
+
|
|
481
|
+
browser-sqlite requires no special HTTP headers. OPFS access handles work in a plain worker context; cross-origin isolation is not needed. The default build needs no browser opt-in; only `build: 'jspi'` does, and that is an unrelated browser constraint, not a header requirement.
|
|
482
|
+
|
|
483
|
+
Note: the "Coop" in `OPFSCoopSyncVFS` stands for *cooperative*, not the `Cross-Origin-Opener-Policy` header.
|
|
484
|
+
|
|
157
485
|
## Known Limitations
|
|
158
486
|
|
|
159
487
|
- **`AccessHandlePoolVFS` requires `poolSize: 1`.** Passing `poolSize > 1` with this VFS throws synchronously at client creation time.
|
|
160
|
-
- **`
|
|
161
|
-
- **`
|
|
488
|
+
- **`build: 'jspi'` is not available everywhere.** The [`jspi` build table](#build-jspi) carries the per-engine versions; it is generated, so it is the one place that stays current. The build is opt-in and no default uses it, so this constrains nobody who does not ask for it.
|
|
489
|
+
- **`OPFSWriteAheadVFS` buys you nothing outside Chromium.** It opens access handles with `mode: 'readwrite-unsafe'`, which Firefox and Safari do not support — and which they ignore rather than reject, so it still works but falls back to the same reduced mode as `OPFSAdaptiveVFS` and serves no concurrent reads there. On **Safari 27 the `sync` build can also fail to reopen a database** — seen once in three runs, on macOS and on iPadOS. Use `OPFSAdaptiveVFS` outside Chromium.
|
|
490
|
+
- **`OPFSCoopSyncVFS` does not read concurrently, and stalls unpredictably under a pool.** Unlike the other OPFS VFS it implements its own locking and silently ignores the `lockPolicy: 'shared'` this library constructs every VFS with, holding one *exclusive* access handle and rotating it between workers instead of one per connection. A read issued while a write transaction is open is **never served** — the pool acquisition blocks before any `AbortSignal` is consulted — where `IDBBatchAtomicVFS`, `IDBMirrorVFS` and `OPFSAnyContextVFS` serve it every time. A bulk insert either finishes promptly or **exceeds 30 seconds**, with no middle ground and no consistency across runs. None of this depends on `readwrite-unsafe`: unlike the reduced mode described above, it happens on Chromium too.
|
|
491
|
+
- **Read-your-own-writes is guaranteed within a tab, not across tabs.** See the
|
|
492
|
+
caveat under [Error handling](#error-handling).
|
|
493
|
+
- **Two clients writing at once are not serialized, and what the loser gets depends on the VFS.** Nothing here orders writes between clients or between tabs — SQLite's own locking decides, and it decides differently in the two modes above. Where each connection holds its own access handle, the second writer is refused at once with `BUSY`, and retrying is the remedy; `BEGIN` is deferred, so both transactions open cleanly and it is the first write *inside* that fails. Where one exclusive handle is rotated instead, the second writer is not refused at all — it waits for the file as long as the first one holds it, then goes through. **Pass a `signal` and the two read alike**: an error inside a budget you chose, rather than a wait you did not. **A `bulkWrite` that is refused or abandoned leaves a partial load, not a failed one** — it commits per batch, so everything before is in the database, and one further batch may still land after you gave up: the one already handed to a worker, which no signal can recall. Use `tx.bulkWrite` where you need all or nothing.
|
|
494
|
+
- **A database that is open cannot be deleted**, in this tab or another. `deleteDatabase` takes the same origin-wide lock a client takes while opening, which prevents an open from interleaving with a delete, and reports `BUSY` rather than deleting under a live connection. A connection that already holds its handles cannot be revoked from this library — close every client on the database first.
|
|
495
|
+
- **`deleteDatabase` can time out outside Chromium**, on `OPFSWriteAheadVFS` and `OPFSCoopSyncVFS` — an observation rather than a measured rate. The call fails to settle rather than reporting an error; it has never reported success without deleting. Both VFS rotate a single exclusive OPFS access handle where `readwrite-unsafe` is unavailable, the same shape as the reduced mode described above.
|
|
496
|
+
|
|
497
|
+
## Development
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
pnpm install
|
|
501
|
+
pnpm build # rslib → dist/
|
|
502
|
+
pnpm test # unit (Node) + browser (Playwright/Chromium)
|
|
503
|
+
pnpm check # biome, with --write
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Two suites run on demand rather than on every change:
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
pnpm test:conformance # every declared (vfs, build) pair through six invariants
|
|
510
|
+
pnpm test:consumer # packs the tarball and drives four bundler modes
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### The benchmark page
|
|
514
|
+
|
|
515
|
+
`scripts/bench/html/index.html` is the page published above. It is one self-contained file
|
|
516
|
+
served beside a verbatim copy of `dist/`, so it exercises the library exactly as
|
|
517
|
+
a consumer would with no bundler at all.
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
pnpm bench:dev # build, serve on http://127.0.0.1:8099, rebuild on change
|
|
521
|
+
pnpm bench:serve # same without the watch
|
|
522
|
+
pnpm bench:build # assemble _site/ only
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
`http://127.0.0.1` is a secure context, so OPFS works with no certificate — no
|
|
526
|
+
TLS setup is needed to develop against it. A phone on the LAN is a different
|
|
527
|
+
matter: it is not a secure context, so OPFS is unavailable there and a tunnel
|
|
528
|
+
(or the published page) is the way to test a real device.
|
|
529
|
+
|
|
530
|
+
`node scripts/bench/check.mjs [chromium|firefox] [--all]` drives the page under
|
|
531
|
+
Playwright and asserts that it still works — it is run by hand and deliberately
|
|
532
|
+
not wired into CI. It checks the *page*, never that a VFS passes: a red cell can
|
|
533
|
+
be a correct report about the engine you are on.
|