browser-sqlite 1.0.0-rc.4 → 1.0.0-rc.5
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/README.md +66 -430
- package/dist/abandon.d.ts +77 -0
- package/dist/api.d.ts +62 -12
- package/dist/bulk.d.ts +15 -0
- package/dist/client.d.ts +43 -20
- package/dist/delete.d.ts +11 -5
- package/dist/epochs.d.ts +36 -6
- package/dist/errors.d.ts +44 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +96 -0
- package/dist/locks.d.ts +124 -6
- package/dist/pool.d.ts +101 -8
- package/dist/queries.d.ts +36 -7
- package/dist/scheduler.d.ts +14 -0
- package/dist/sqlite-codes.d.ts +154 -0
- package/dist/supervisor.d.ts +1 -1
- package/dist/transaction.d.ts +24 -1
- package/dist/types.d.ts +322 -28
- package/dist/utils.d.ts +42 -2
- package/dist/worker/probes.d.ts +26 -0
- package/dist/worker/sqlite-code.d.ts +9 -0
- package/dist/worker/statement-cache.d.ts +17 -3
- package/dist/worker/worker.js +1 -1
- package/dist/worker/worker.js.map +1 -1
- package/package.json +15 -7
package/README.md
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
a single browser release can move the answer.
|
|
5
|
+
A client stores its data through a wa-sqlite VFS, which decides where that data lives.
|
|
6
|
+
**[Read the VFS page](VFS.md)** to compare them, then
|
|
7
|
+
**[run the benchmarks in your own browser](https://lalexdotcom.github.io/browser-sqlite/)**.
|
|
9
8
|
|
|
10
9
|
## Install
|
|
11
10
|
|
|
@@ -17,7 +16,8 @@ pnpm add browser-sqlite
|
|
|
17
16
|
|
|
18
17
|
Requires a bundler that supports Web Workers with dynamic imports — or no bundler at all.
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
<details>
|
|
20
|
+
<summary><b>Bundler Configuration</b></summary>
|
|
21
21
|
|
|
22
22
|
Works with no configuration under **rsbuild 1+**, **rspack 1+**, **Parcel 2+**, **Vite 8+**, **webpack 5.101+** — and with no bundler at all.
|
|
23
23
|
|
|
@@ -32,254 +32,8 @@ export default defineConfig({
|
|
|
32
32
|
|
|
33
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.
|
|
34
34
|
|
|
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).
|
|
36
|
-
|
|
37
|
-
## Usage
|
|
38
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
import { createSQLiteClient } from 'browser-sqlite';
|
|
45
|
-
|
|
46
|
-
const db = createSQLiteClient('myapp.sqlite', {
|
|
47
|
-
poolSize: 2, // number of worker threads (default: 2)
|
|
48
|
-
vfs: 'OPFSAdaptiveVFS', // required — see VFS Selection
|
|
49
|
-
build: 'async', // wa-sqlite build (default: the VFS's first)
|
|
50
|
-
pragmas: { // SQLite PRAGMAs applied on open
|
|
51
|
-
journal_mode: 'WAL',
|
|
52
|
-
synchronous: 'NORMAL',
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
`createSQLiteClient` spawns `poolSize` Web Worker threads immediately. Workers reach READY state asynchronously — queries made before workers are ready are queued automatically.
|
|
58
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
type User = { id: number; name: string };
|
|
65
|
-
|
|
66
|
-
const users = await db.read<User>(
|
|
67
|
-
'SELECT id, name FROM users WHERE active = ?',
|
|
68
|
-
[1],
|
|
69
|
-
);
|
|
70
|
-
// users: User[]
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Read queries are dispatched to any available worker, enabling concurrent reads.
|
|
74
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
```typescript
|
|
85
|
-
const { affected } = await db.write(
|
|
86
|
-
'INSERT INTO users (name, email) VALUES (?, ?)',
|
|
87
|
-
['Alice', 'alice@example.com'],
|
|
88
|
-
);
|
|
89
|
-
// affected: number of rows inserted
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Write queries are serialized through a dedicated writer worker — only one write executes at a time.
|
|
93
|
-
|
|
94
|
-
| Option | Type | Default | Description |
|
|
95
|
-
|---|---|---|---|
|
|
96
|
-
| `signal` | `AbortSignal` | — | Aborts the query. Rejects with `signal.reason`. |
|
|
97
|
-
|
|
98
|
-
### *client*.stream
|
|
99
|
-
|
|
100
|
-
```typescript
|
|
101
|
-
// Worker is held for the full generator lifetime — always exhaust or break.
|
|
102
|
-
for await (const row of db.stream<User>('SELECT * FROM large_table', [])) {
|
|
103
|
-
processRow(row); // row is User
|
|
104
|
-
}
|
|
105
|
-
```
|
|
106
|
-
|
|
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. |
|
|
114
|
-
|
|
115
|
-
On `stream()`, `chunkSize` is the only lever on how many rows are in flight.
|
|
116
|
-
|
|
117
|
-
### *client*.chunk
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
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>(
|
|
140
|
-
'SELECT * FROM users WHERE id = ?',
|
|
141
|
-
[42],
|
|
142
|
-
);
|
|
143
|
-
// user: User | undefined
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
`first()` returns the first result row, or `undefined` if no rows match. Use it for lookups by primary key or unique field.
|
|
147
|
-
|
|
148
|
-
| Option | Type | Default | Description |
|
|
149
|
-
|---|---|---|---|
|
|
150
|
-
| `signal` | `AbortSignal` | — | Aborts the query. Rejects with `signal.reason`. |
|
|
151
|
-
|
|
152
|
-
`first()` stops the query after one row instead of draining the result set.
|
|
153
|
-
|
|
154
|
-
### *client*.transaction
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
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
|
-
});
|
|
163
|
-
```
|
|
164
|
-
|
|
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.
|
|
169
|
-
|
|
170
|
-
`tx` carries the same querying surface as the client — `read`, `write`, `chunk`, `stream`, `first`, `bulkWrite`, `output` — plus `commit` and `rollback`.
|
|
171
|
-
|
|
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.
|
|
173
|
-
|
|
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. |
|
|
179
|
-
|
|
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();
|
|
186
|
-
```
|
|
187
|
-
|
|
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. |
|
|
206
|
-
|
|
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();
|
|
217
|
-
```
|
|
218
|
-
|
|
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();
|
|
239
|
-
```
|
|
240
|
-
|
|
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
|
-
|
|
249
|
-
```typescript
|
|
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. |
|
|
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`](API.md#options).
|
|
36
|
+
</details>
|
|
283
37
|
|
|
284
38
|
## Browser support
|
|
285
39
|
|
|
@@ -287,212 +41,94 @@ Throws `SQLiteError` with code `BUSY` when the database is open or being opened,
|
|
|
287
41
|
|---|---|---|
|
|
288
42
|
| 92+ | 95+ | 15.4+ |
|
|
289
43
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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.
|
|
44
|
+
Cross-origin isolation is worth adding where you control your headers: it is what lets an
|
|
45
|
+
aborted call stop a running statement when using a VFS with `sync` build. See
|
|
46
|
+
[Aborting a call](#aborting-a-call).
|
|
308
47
|
|
|
309
|
-
|
|
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.
|
|
48
|
+
## Usage
|
|
389
49
|
|
|
390
|
-
|
|
50
|
+
Read the [detailed API documentation](API.md) for the full description.
|
|
391
51
|
|
|
392
|
-
|
|
52
|
+
```typescript
|
|
53
|
+
import { createSQLiteClient } from 'browser-sqlite';
|
|
393
54
|
|
|
394
|
-
|
|
395
|
-
|---|---|---|---|---|
|
|
396
|
-
| Any | Any | Any | Any | Any |
|
|
55
|
+
const db = createSQLiteClient('myapp.sqlite', { vfs: 'OPFSAdaptiveVFS' });
|
|
397
56
|
|
|
398
|
-
|
|
57
|
+
await db.write('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
|
|
58
|
+
await db.write('INSERT INTO users (name) VALUES (?)', ['Alice']);
|
|
399
59
|
|
|
400
|
-
|
|
60
|
+
const users = await db.read<{ id: number; name: string }>('SELECT id, name FROM users');
|
|
401
61
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
62
|
+
for await (const row of db.stream<{ id: number; name: string }>('SELECT * FROM users')) {
|
|
63
|
+
process(row);
|
|
64
|
+
}
|
|
405
65
|
|
|
406
|
-
|
|
66
|
+
await db.close();
|
|
67
|
+
```
|
|
407
68
|
|
|
408
|
-
|
|
69
|
+
[*client*.id](API.md#clientid) · [*client*.name](API.md#clientname) · [*client*.file](API.md#clientfile) · [*client*.vfs](API.md#clientvfs) · [*client*.build](API.md#clientbuild) · [*client*.poolSize](API.md#clientpoolsize)
|
|
409
70
|
|
|
410
|
-
|
|
411
|
-
|---|---|---|---|---|
|
|
412
|
-
| 137+ | 153+ | 27+ | Yes | 27+ |
|
|
71
|
+
[createSQLiteClient()](API.md#createsqliteclient) · [*client*.read()](API.md#clientread) · [*client*.write()](API.md#clientwrite) · [*client*.stream()](API.md#clientstream) · [*client*.chunk()](API.md#clientchunk) · [*client*.first()](API.md#clientfirst) · [*client*.transaction()](API.md#clienttransaction) · [*client*.bulkWrite()](API.md#clientbulkwrite) · [*client*.output()](API.md#clientoutput) · [*client*.inspect()](API.md#clientinspect) · [*client*.close()](API.md#clientclose) · [deleteDatabase()](API.md#deletedatabase) · [inspectDatabase()](API.md#inspectdatabase)
|
|
413
72
|
|
|
414
|
-
|
|
73
|
+
## Storage
|
|
415
74
|
|
|
75
|
+
The VFS decides *where* your database is written.
|
|
76
|
+
[See every available VFS on the dedicated page](VFS.md), with their pros, their cons, their
|
|
77
|
+
limitations and their browser compatibility.
|
|
416
78
|
|
|
417
|
-
|
|
79
|
+
`OPFSWriteAheadVFS` and `OPFSAdaptiveVFS` are the recommended options.
|
|
418
80
|
|
|
419
|
-
|
|
81
|
+
[`OPFSWriteAheadVFS`](VFS.md#opfswriteaheadvfs) · [`OPFSAdaptiveVFS`](VFS.md#opfsadaptivevfs) · [`OPFSCoopSyncVFS`](VFS.md#opfscoopsyncvfs) · [`AccessHandlePoolVFS`](VFS.md#accesshandlepoolvfs) · [`IDBBatchAtomicVFS`](VFS.md#idbbatchatomicvfs) · [`IDBMirrorVFS`](VFS.md#idbmirrorvfs) · [`OPFSAnyContextVFS`](VFS.md#opfsanycontextvfs) · [`MemoryVFS`](VFS.md#memoryvfs) · [`MemoryAsyncVFS`](VFS.md#memoryasyncvfs)
|
|
420
82
|
|
|
421
|
-
|
|
83
|
+
## Guarantees
|
|
422
84
|
|
|
423
|
-
|
|
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`. |
|
|
85
|
+
### Reads run concurrently
|
|
432
86
|
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
```
|
|
87
|
+
Every read is dispatched to whichever worker in the pool is free, so several run at once.
|
|
88
|
+
Writes take a dedicated writer worker instead, one at a time.
|
|
447
89
|
|
|
448
|
-
|
|
90
|
+
### Read-your-own-writes
|
|
449
91
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
92
|
+
It holds within a tab and across tabs. Once a write has resolved, any read issued afterwards
|
|
93
|
+
observes it — from that client, from any other client in the same tab, and from any other tab
|
|
94
|
+
on the same database, whatever the pool size. A worker that has not yet observed the latest
|
|
95
|
+
commit runs one discarded statement that opens a real read transaction before it serves the
|
|
96
|
+
query; that costs one extra worker round-trip on each worker's first statement after a write,
|
|
97
|
+
and nothing under read-only load. `poolSize: 1` and reading inside the same `transaction()`
|
|
98
|
+
remain valid, they are no longer required.
|
|
455
99
|
|
|
456
|
-
|
|
100
|
+
The one exception is [`IDBMirrorVFS`](VFS.md#idbmirrorvfs), which does not hold it across
|
|
101
|
+
tabs.
|
|
457
102
|
|
|
458
|
-
|
|
103
|
+
### Writes are serialized
|
|
459
104
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
105
|
+
A write, a write transaction, and each batch of a `bulkWrite` take one lock per database
|
|
106
|
+
across the whole origin, so a second writer **waits** rather than failing — between clients
|
|
107
|
+
and between tabs alike. The wait is unbounded and first-come-first-served: pass a `signal` if
|
|
108
|
+
you would rather fail than wait. A write transaction holds that lock for the whole of its
|
|
109
|
+
callback, so a callback that never returns blocks every other writer in the origin, not only
|
|
110
|
+
its own client. **A `bulkWrite` takes the lock per batch and commits per batch**, so another
|
|
111
|
+
client's write can land between two of its batches — use `tx.bulkWrite` where you need all or
|
|
112
|
+
nothing.
|
|
468
113
|
|
|
469
|
-
|
|
470
|
-
read in another. No bound is claimed on how long that lasts.
|
|
114
|
+
## Known Limitations
|
|
471
115
|
|
|
472
|
-
|
|
473
|
-
|
|
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.
|
|
116
|
+
Some VFS have limitations of their own — see [the detailed VFS page](VFS.md#vfs-reference).
|
|
117
|
+
What follows holds on all of them.
|
|
478
118
|
|
|
479
|
-
|
|
119
|
+
### Aborting a call
|
|
480
120
|
|
|
481
|
-
|
|
121
|
+
An abort does not always stop the work, and your hosting decides. On the `sync` build, a
|
|
122
|
+
`signal` or a `timeout` rejects your promise straight away, but the statement runs to its
|
|
123
|
+
end on its worker, which stays unavailable until it does.
|
|
482
124
|
|
|
483
|
-
|
|
125
|
+
Two ways out — serving the page cross-origin isolated, or `build: 'async'` — and what each
|
|
126
|
+
one costs are under [Interrupting a call](API.md#interrupting-a-call).
|
|
484
127
|
|
|
485
|
-
|
|
128
|
+
### Deleting a database
|
|
486
129
|
|
|
487
|
-
|
|
488
|
-
|
|
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.
|
|
130
|
+
A database that any client still holds cannot be deleted, in this tab or another, on every
|
|
131
|
+
VFS. More on the dedicated [`deleteDatabase`](API.md#deletedatabase) API entry.
|
|
496
132
|
|
|
497
133
|
## Development
|
|
498
134
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { PoolWorker } from './pool';
|
|
2
|
+
/**
|
|
3
|
+
* Whether this cleanup has already run.
|
|
4
|
+
*
|
|
5
|
+
* A plain object rather than a boolean because the held value must observe a
|
|
6
|
+
* change the generator makes after registration. Three routes reach the
|
|
7
|
+
* cleanup — the registry, the abort listener and the generator's own `finally`
|
|
8
|
+
* — and whichever arrives first closes the door on the other two.
|
|
9
|
+
*
|
|
10
|
+
* What it deliberately does NOT record is whether the query ever started. That
|
|
11
|
+
* answers "did this generator run", where the only question that matters is
|
|
12
|
+
* "is the worker still serving this query" — and the worker is the one that
|
|
13
|
+
* knows, which is why `interrupt()` is asked rather than told.
|
|
14
|
+
*/
|
|
15
|
+
export type AbandonState = {
|
|
16
|
+
done: boolean;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* What the cleanup needs, and all it may hold.
|
|
20
|
+
*
|
|
21
|
+
* **It must never refer to the registered generator.** A `FinalizationRegistry`
|
|
22
|
+
* held value that reaches its own target keeps the target alive and the
|
|
23
|
+
* callback then never fires. Every field here points downward or sideways: the
|
|
24
|
+
* worker and the transport iterator are reachable from the pool anyway, `state`
|
|
25
|
+
* is a plain flag, `detach` closes over the caller's signal and its listener,
|
|
26
|
+
* and `release` is the owning layer's teardown.
|
|
27
|
+
*/
|
|
28
|
+
export type Abandoned = {
|
|
29
|
+
worker: Pick<PoolWorker, 'interrupt'>;
|
|
30
|
+
iterator: {
|
|
31
|
+
return: (value?: undefined) => Promise<unknown>;
|
|
32
|
+
};
|
|
33
|
+
state: AbandonState;
|
|
34
|
+
/**
|
|
35
|
+
* Removes the abort listener that carries this very cleanup. Without it a
|
|
36
|
+
* listener stays armed on a signal the CALLER owns, long after the query it
|
|
37
|
+
* belonged to has ended — and fires against whatever the worker is doing
|
|
38
|
+
* then.
|
|
39
|
+
*/
|
|
40
|
+
detach: () => void;
|
|
41
|
+
release?: (() => void) | undefined;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* What the `finally` of `queries.chunk` would have done, for a generator that
|
|
45
|
+
* will never run it.
|
|
46
|
+
*
|
|
47
|
+
* The order is that `finally`'s and for its reason: `interrupt()` first, so the
|
|
48
|
+
* queued `return()` is not parked behind a `next()` that will not settle.
|
|
49
|
+
*
|
|
50
|
+
* **Nothing here may assume the worker is still ours.** This runs at a moment
|
|
51
|
+
* nobody chose — a collection, or the caller tidying up its own controller —
|
|
52
|
+
* and by then the worker may be serving a query that has nothing to do with
|
|
53
|
+
* this one. So the transport is named in both calls: `interrupt(iterator)` is a
|
|
54
|
+
* no-op unless the worker is still serving it, and `iterator.return()` resumes
|
|
55
|
+
* a transport whose own `finally` makes the same check. `return()` on a
|
|
56
|
+
* generator whose body never ran is a no-op besides, the body having never
|
|
57
|
+
* entered its `try`.
|
|
58
|
+
*
|
|
59
|
+
* `release` is the exception and runs unconditionally: it is the owning layer's
|
|
60
|
+
* resource — a lease, a timer, a merge teardown — and it is owed whatever the
|
|
61
|
+
* worker has since moved on to.
|
|
62
|
+
*/
|
|
63
|
+
export declare const reclaim: ({ worker, iterator, state, detach, release, }: Abandoned) => void;
|
|
64
|
+
export type AbandonRegistry = {
|
|
65
|
+
/** Watch `target`; `held` is what the cleanup receives, `token` unregisters. */
|
|
66
|
+
watch: (target: object, held: Abandoned, token: object) => void;
|
|
67
|
+
/** The generator ended by an ordinary route — there is nothing to reclaim. */
|
|
68
|
+
forget: (token: object) => void;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* `run` is injected so that tests drive the cleanup without a collection.
|
|
72
|
+
* Nothing else here is observable: a `FinalizationRegistry` fires when the
|
|
73
|
+
* engine decides, which is not a schedule a test can assert against.
|
|
74
|
+
*/
|
|
75
|
+
export declare const createAbandonRegistry: (run?: (held: Abandoned) => void) => AbandonRegistry;
|
|
76
|
+
/** The one this library uses. */
|
|
77
|
+
export declare const abandonRegistry: AbandonRegistry;
|