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/dist/client.d.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { OptionsWithSignal, SQLiteChunkOptions } from './api';
|
|
2
|
+
import { SQLiteError } from './errors';
|
|
3
|
+
import { type SQLiteBuild, type SQLiteVFS } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for creating a SQLite client.
|
|
6
|
+
*/
|
|
7
|
+
export type CreateSQLiteClientOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* Database file name within the OPFS origin private file system.
|
|
10
|
+
* Each unique name maps to a distinct SQLite database file.
|
|
11
|
+
* @defaultValue `"SQLite"` prefix + auto-incremented client index
|
|
12
|
+
*/
|
|
13
|
+
name?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Number of Web Workers spawned in the pool at initialization.
|
|
16
|
+
* A larger pool allows more concurrent read operations but increases
|
|
17
|
+
* memory consumption and OPFS file handle usage.
|
|
18
|
+
* Must be `1` when using `AccessHandlePoolVFS` — any larger value throws at construction time.
|
|
19
|
+
* @defaultValue `2`
|
|
20
|
+
*/
|
|
21
|
+
poolSize?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Which VFS stores the database. Required: a VFS decides *where* the bytes
|
|
24
|
+
* live, and a database written through one VFS is not visible through
|
|
25
|
+
* another. See the README's VFS Selection guide.
|
|
26
|
+
*/
|
|
27
|
+
vfs: SQLiteVFS;
|
|
28
|
+
/**
|
|
29
|
+
* Which wa-sqlite WebAssembly build to load. Defaults to the first entry of
|
|
30
|
+
* `VFS_CAPABILITIES[vfs]` — `sync` where the VFS supports it, since it is both the
|
|
31
|
+
* fastest and the most portable, otherwise `async`. `jspi` needs engine
|
|
32
|
+
* support; see the README's Builds section for versions.
|
|
33
|
+
*
|
|
34
|
+
* @throws at construction when the build is not one the chosen VFS supports.
|
|
35
|
+
*/
|
|
36
|
+
build?: SQLiteBuild;
|
|
37
|
+
/**
|
|
38
|
+
* Where the workers fetch their `.wasm` from. **An escape hatch, not a
|
|
39
|
+
* setting**: omit it and resolution is exactly what it was before this
|
|
40
|
+
* option existed — the file is taken from beside `worker.js`, which is where
|
|
41
|
+
* the package ships it and where every bundler emits it.
|
|
42
|
+
*
|
|
43
|
+
* Reach for it only when the `.wasm` have been separated from `worker.js`:
|
|
44
|
+
* assets moved by hand with no bundler, or a build whose emitted URL is
|
|
45
|
+
* wrong at runtime.
|
|
46
|
+
*
|
|
47
|
+
* A **string is a directory**, resolved against the page — relative
|
|
48
|
+
* (`'wasm/'`), absolute (`'/static/wasm'`) or a full URL. A missing trailing
|
|
49
|
+
* slash is added. The file name comes from wa-sqlite itself, so one base
|
|
50
|
+
* serves whichever `build` is loaded.
|
|
51
|
+
*
|
|
52
|
+
* A **callback names one file** and receives the resolved `build`, for a
|
|
53
|
+
* bundler-emitted asset whose name carries a content hash:
|
|
54
|
+
* ```ts
|
|
55
|
+
* import wasmUrl from 'browser-sqlite/dist/worker/wa-sqlite.wasm?url';
|
|
56
|
+
* createSQLiteClient('app.db', { vfs, wasmUrl: () => wasmUrl });
|
|
57
|
+
* ```
|
|
58
|
+
* It is called once, at construction, and its answer is reused by every
|
|
59
|
+
* worker and every restart.
|
|
60
|
+
*
|
|
61
|
+
* Serving the `.wasm` from another origin has two requirements beyond this
|
|
62
|
+
* option, both enforced by the browser: the response needs CORS
|
|
63
|
+
* (`Access-Control-Allow-Origin`), since the glue fetches it, and it must
|
|
64
|
+
* carry `Content-Type: application/wasm` for streaming compilation.
|
|
65
|
+
*
|
|
66
|
+
* @throws at construction when the value cannot be parsed as a URL.
|
|
67
|
+
*/
|
|
68
|
+
wasmUrl?: string | ((build: SQLiteBuild) => string);
|
|
69
|
+
/**
|
|
70
|
+
* SQLite PRAGMAs applied to each worker's database connection on open.
|
|
71
|
+
* Keys are PRAGMA names, values are their string representations.
|
|
72
|
+
* Example: `{ journal_mode: 'WAL', synchronous: 'NORMAL' }`.
|
|
73
|
+
* If omitted, no PRAGMAs are applied beyond SQLite defaults.
|
|
74
|
+
*/
|
|
75
|
+
pragmas?: Record<string, string>;
|
|
76
|
+
/**
|
|
77
|
+
* How many times a worker slot may be restarted after it has died.
|
|
78
|
+
* A slot that never reached readiness is never restarted — an initial
|
|
79
|
+
* failure is deterministic, and restarting only delays the diagnostic.
|
|
80
|
+
* The counter resets once the replacement has actually served a request.
|
|
81
|
+
* @defaultValue `1`
|
|
82
|
+
*/
|
|
83
|
+
maxWorkerRestarts?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Milliseconds a worker has to post `ready` after its `open` message is sent.
|
|
86
|
+
* On expiry the slot is failed immediately — the most common cause is a
|
|
87
|
+
* database held under an exclusive lock by another tab or client.
|
|
88
|
+
* @defaultValue `30_000`
|
|
89
|
+
*/
|
|
90
|
+
openTimeout?: number;
|
|
91
|
+
/**
|
|
92
|
+
* Milliseconds the drain loop (in the query generator's `finally`) may run
|
|
93
|
+
* before the worker is presumed dead and the crash path is invoked.
|
|
94
|
+
* @defaultValue `60_000`
|
|
95
|
+
*/
|
|
96
|
+
drainTimeout?: number;
|
|
97
|
+
/**
|
|
98
|
+
* Turns on the introspection subsystem exposed as `db.debug`, and the
|
|
99
|
+
* lifecycle log. A string is used as the log prefix; `true` falls back to the
|
|
100
|
+
* client prefix (`"<name> <index>"`), which already names the workers.
|
|
101
|
+
*
|
|
102
|
+
* @defaultValue undefined — no collection, no output, `db.debug` undefined.
|
|
103
|
+
*/
|
|
104
|
+
debug?: string | boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Called whenever a worker slot is permanently lost. Receives the slot index,
|
|
107
|
+
* the number of workers still alive after the loss, the requested pool size,
|
|
108
|
+
* and the error that killed the slot.
|
|
109
|
+
*
|
|
110
|
+
* Guaranteed to be called **before** the client is failed when the last slot
|
|
111
|
+
* is lost. Wrapped in try/catch — a throwing callback is reported through
|
|
112
|
+
* `logger.always.warn` and does not break the pool.
|
|
113
|
+
*
|
|
114
|
+
* @defaultValue undefined
|
|
115
|
+
*/
|
|
116
|
+
onWorkerLost?: (event: WorkerLostEvent) => void;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* What `onWorkerLost` receives. Named and exported rather than inlined in the
|
|
120
|
+
* option: a consumer whose handler is a standalone function needs to be able
|
|
121
|
+
* to type its parameter.
|
|
122
|
+
*/
|
|
123
|
+
export type WorkerLostEvent = {
|
|
124
|
+
/** Zero-based index of the lost slot. */
|
|
125
|
+
index: number;
|
|
126
|
+
/** Number of workers still alive after this loss. */
|
|
127
|
+
live: number;
|
|
128
|
+
/** The requested pool size (`poolSize` option). */
|
|
129
|
+
size: number;
|
|
130
|
+
/** The error that killed the worker. */
|
|
131
|
+
cause: SQLiteError;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Creates a SQLite client backed by a pool of Web Workers, each running
|
|
135
|
+
* a wa-sqlite instance in a dedicated thread.
|
|
136
|
+
*
|
|
137
|
+
* @remarks
|
|
138
|
+
* **Browser requirements:** This client uses OPFS through Web Workers; no
|
|
139
|
+
* special HTTP headers are required and cross-origin isolation is not needed.
|
|
140
|
+
* The default `build` needs no browser opt-in; only `build: 'jspi'` does, and
|
|
141
|
+
* JSPI is Chromium-only — an unrelated constraint, not a header requirement.
|
|
142
|
+
*
|
|
143
|
+
* **Worker pool side effect:** Calling this function immediately spawns
|
|
144
|
+
* `poolSize` Web Worker threads and begins asynchronous database
|
|
145
|
+
* initialization. Workers become queryable once they emit a `ready` message.
|
|
146
|
+
*
|
|
147
|
+
* @param file - SQLite database file name within the OPFS origin.
|
|
148
|
+
* Each distinct name corresponds to a separate database file.
|
|
149
|
+
* @param clientOptions - Pool and VFS configuration. Required: `vfs` has no
|
|
150
|
+
* default, because a VFS decides where the database is written.
|
|
151
|
+
* See {@link CreateSQLiteClientOptions} for field defaults.
|
|
152
|
+
* @returns A {@link SQLiteDB} object providing `read`, `write`, `chunk`,
|
|
153
|
+
* `stream`, `first`, `transaction`, `bulkWrite`, `output`, and `close` methods.
|
|
154
|
+
*
|
|
155
|
+
* @throws {SQLiteError} With code `INVALID_OPTION` when `build` is not one of
|
|
156
|
+
* the builds the chosen `vfs` supports. The message names the supported
|
|
157
|
+
* builds; the pairing is declared once, in `VFS_CAPABILITIES`.
|
|
158
|
+
* @throws {SQLiteError} With code `INVALID_OPTION` when `poolSize` exceeds the
|
|
159
|
+
* `maxPoolSize` the chosen `vfs` declares. The message names the cap and the
|
|
160
|
+
* reason for it; both come from `VFS_CAPABILITIES`.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```typescript
|
|
164
|
+
* import { createSQLiteClient } from 'browser-sqlite';
|
|
165
|
+
*
|
|
166
|
+
* const db = createSQLiteClient('myapp.sqlite', {
|
|
167
|
+
* poolSize: 3,
|
|
168
|
+
* vfs: 'OPFSAdaptiveVFS',
|
|
169
|
+
* pragmas: { journal_mode: 'WAL', synchronous: 'NORMAL' },
|
|
170
|
+
* });
|
|
171
|
+
*
|
|
172
|
+
* const users = await db.read<{ id: number; name: string }>(
|
|
173
|
+
* 'SELECT id, name FROM users WHERE active = ?',
|
|
174
|
+
* [1],
|
|
175
|
+
* );
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
export declare const createSQLiteClient: (file: string, clientOptions: CreateSQLiteClientOptions) => {
|
|
179
|
+
chunk: <T extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: unknown[], options?: SQLiteChunkOptions) => AsyncGenerator<T[], void, any>;
|
|
180
|
+
read: <T extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: unknown[], options?: OptionsWithSignal) => Promise<T[]>;
|
|
181
|
+
write: <T extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: unknown[], options?: OptionsWithSignal) => Promise<{
|
|
182
|
+
result: T[];
|
|
183
|
+
affected: number;
|
|
184
|
+
}>;
|
|
185
|
+
stream: <T extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: unknown[], options?: SQLiteChunkOptions) => AsyncGenerator<T, void, any>;
|
|
186
|
+
first: <T extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: unknown[], options?: OptionsWithSignal) => Promise<T | undefined>;
|
|
187
|
+
transaction: <T = void>(callback: (db: import("./api").SQLiteTransactionDB) => Promise<T>, options?: import("./api").SQLiteTransactionOptions) => Promise<T>;
|
|
188
|
+
bulkWrite: <KEYS extends string>(table: string, keys: KEYS[], options?: import("./api").SQLiteBulkWriteOptions, before?: Promise<unknown>) => {
|
|
189
|
+
enqueue: (data: { [K in KEYS]: any; }) => Promise<void>;
|
|
190
|
+
close: () => Promise<number>;
|
|
191
|
+
};
|
|
192
|
+
output: <SCHEMA extends import("./api").Schema>(table: string, schema: SCHEMA, options?: import("./api").SQLiteOutputOptions<SCHEMA>) => {
|
|
193
|
+
enqueue: (data: import("./api").SQLiteOutputRow<SCHEMA>) => Promise<void>;
|
|
194
|
+
close: () => Promise<number>;
|
|
195
|
+
};
|
|
196
|
+
close: () => Promise<void>;
|
|
197
|
+
debug: import("./debug").ClientDebugState | undefined;
|
|
198
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The credit gate: back-pressure for chunk production, and the task turn that
|
|
3
|
+
* makes a worker reachable by `postMessage` while it is inside a query.
|
|
4
|
+
*
|
|
5
|
+
* Pure and Node-testable on purpose. B1 survived for months because the
|
|
6
|
+
* scheduler was reachable only through slow browser tests; this module has the
|
|
7
|
+
* same profile — subtle state transitions otherwise buried behind a worker, a
|
|
8
|
+
* VFS and a real database.
|
|
9
|
+
*/
|
|
10
|
+
/** One turn of the task queue. Injected so Node tests can drive it. */
|
|
11
|
+
export type Tick = () => Promise<void>;
|
|
12
|
+
/** Chunks a worker may send before waiting for a credit. Spec §3.4. */
|
|
13
|
+
export declare const DEFAULT_CREDIT_WINDOW = 2;
|
|
14
|
+
export type CreditGate = {
|
|
15
|
+
/** Begin a query: `window` credits, not stopped, credit counter cleared. */
|
|
16
|
+
reset: (callId: number, window: number) => void;
|
|
17
|
+
/** Add credits for `callId`. A stale `callId` is ignored (§5.4). */
|
|
18
|
+
grant: (callId: number, n: number) => void;
|
|
19
|
+
/** Stop the current query, waking any wait in progress (§5.1). */
|
|
20
|
+
stop: () => void;
|
|
21
|
+
/** Spend one credit. Always costs one task turn first. */
|
|
22
|
+
take: (callId: number) => Promise<'go' | 'stopped'>;
|
|
23
|
+
isStopped: () => boolean;
|
|
24
|
+
tick: Tick;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* A task turn via MessageChannel. NOT setTimeout: nested setTimeout is clamped
|
|
28
|
+
* to 4 ms, which would cost seconds over a few hundred chunks (spec §3.1).
|
|
29
|
+
*/
|
|
30
|
+
export declare const createMessageChannelTick: () => Tick;
|
|
31
|
+
export declare const createCreditGate: (tick: Tick) => CreditGate;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { CreateSQLiteClientOptions } from './client';
|
|
2
|
-
import {
|
|
2
|
+
import type { PoolWorker } from './pool';
|
|
3
3
|
import type { SQLiteVFS } from './types';
|
|
4
|
-
export declare const debugSQLQuery: (sql: string, params?:
|
|
5
|
-
export declare const statusToLabel: (status: number) => string;
|
|
4
|
+
export declare const debugSQLQuery: (sql: string, params?: unknown[]) => string;
|
|
6
5
|
type QueryDebugState = {
|
|
7
6
|
sql: string;
|
|
8
|
-
params?:
|
|
7
|
+
params?: unknown[] | undefined;
|
|
9
8
|
startTime: number;
|
|
10
9
|
firstRowTime?: number;
|
|
11
10
|
endTime?: number;
|
|
12
|
-
error?:
|
|
11
|
+
error?: unknown;
|
|
13
12
|
affectedRows: number;
|
|
13
|
+
prepared: number;
|
|
14
14
|
};
|
|
15
15
|
type RequestDebugState = {
|
|
16
16
|
startTime: number;
|
|
@@ -29,24 +29,35 @@ type WorkerDebugState = {
|
|
|
29
29
|
currentRequest?: RequestDebugState;
|
|
30
30
|
readonly status: string;
|
|
31
31
|
};
|
|
32
|
-
type ClientDebugState = {
|
|
32
|
+
export type ClientDebugState = {
|
|
33
33
|
readonly file: string;
|
|
34
34
|
readonly vfs: SQLiteVFS;
|
|
35
35
|
readonly pragmas: Record<string, string>;
|
|
36
36
|
readonly name: string;
|
|
37
37
|
readonly queue: {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
readonly read: number;
|
|
39
|
+
readonly write: number;
|
|
40
|
+
/**
|
|
41
|
+
* Callers suspended on the pool's readiness gate, waiting for the pool to
|
|
42
|
+
* exist rather than for a free worker. They sit in neither wait queue, so
|
|
43
|
+
* `read` and `write` are both 0 while they wait — during startup, and
|
|
44
|
+
* during the retry round that follows a failed open.
|
|
45
|
+
*/
|
|
46
|
+
readonly gated: number;
|
|
40
47
|
};
|
|
41
48
|
workers: WorkerDebugState[];
|
|
42
49
|
};
|
|
43
|
-
export declare const createClientDebug: (file: string,
|
|
50
|
+
export declare const createClientDebug: (file: string, pool: (PoolWorker | undefined)[], clientOptions: Required<Pick<CreateSQLiteClientOptions, 'vfs' | 'pragmas' | 'name'>>, stats: () => {
|
|
51
|
+
read: number;
|
|
52
|
+
write: number;
|
|
53
|
+
gated: number;
|
|
54
|
+
}) => {
|
|
44
55
|
readonly state: ClientDebugState;
|
|
45
56
|
readonly createWorkerDebugState: (index: number, name: string) => WorkerDebugState;
|
|
46
57
|
readonly createRequestDebugState: () => {
|
|
47
58
|
state: RequestDebugState;
|
|
48
59
|
assign: (index: number) => void;
|
|
49
60
|
};
|
|
50
|
-
readonly createQueryDebugState: (workerIndex: number, sql: string, params?:
|
|
61
|
+
readonly createQueryDebugState: (workerIndex: number, sql: string, params?: unknown[]) => QueryDebugState;
|
|
51
62
|
};
|
|
52
63
|
export {};
|
package/dist/delete.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type SQLiteBuild, type SQLiteVFS } from './types';
|
|
2
|
+
export type DeleteDatabaseOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Which VFS holds the database. Required for the same reason it is required
|
|
5
|
+
* on `createSQLiteClient`: a VFS decides where the bytes live, so deleting
|
|
6
|
+
* without naming one deletes in the wrong store — or nowhere, while
|
|
7
|
+
* reporting success.
|
|
8
|
+
*/
|
|
9
|
+
vfs: SQLiteVFS;
|
|
10
|
+
/**
|
|
11
|
+
* Which wa-sqlite build to load. It does **not** affect where the database
|
|
12
|
+
* lives; it is here only because a VFS runs solely on the builds it
|
|
13
|
+
* declares, and one of them must be loaded to instantiate the VFS at all.
|
|
14
|
+
* @defaultValue the first build the VFS declares
|
|
15
|
+
*/
|
|
16
|
+
build?: SQLiteBuild;
|
|
17
|
+
/**
|
|
18
|
+
* Where the worker fetches its `.wasm`, with the same meaning as on
|
|
19
|
+
* `createSQLiteClient`. A deployment that needs it to open a database needs
|
|
20
|
+
* it to delete one.
|
|
21
|
+
*/
|
|
22
|
+
wasmUrl?: string | ((build: SQLiteBuild) => string);
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Deletes a database and the two siblings SQLite may leave beside it.
|
|
26
|
+
*
|
|
27
|
+
* Deleting a database that is not there is success — SQLite's own `xDelete`
|
|
28
|
+
* behaves the same way, and a caller who wanted it gone has got what they
|
|
29
|
+
* asked for.
|
|
30
|
+
*
|
|
31
|
+
* Nothing a VFS keeps for itself is touched: not the IndexedDB store, which is
|
|
32
|
+
* shared by every database that VFS holds on this origin, and not the
|
|
33
|
+
* `AccessHandlePoolVFS` directory, whose files *are* its reusable capacity.
|
|
34
|
+
* The bytes of the named database are freed in both cases.
|
|
35
|
+
*
|
|
36
|
+
* @throws {SQLiteError} `INVALID_OPTION` when `vfs` is missing or the `build`
|
|
37
|
+
* is not one the VFS supports — synchronously in spirit, as a rejection here.
|
|
38
|
+
* @throws {SQLiteError} `BUSY` when the database is open or being opened, in
|
|
39
|
+
* this tab or another. A connection already holding its handles cannot be
|
|
40
|
+
* revoked from here; see the README's Known Limitations.
|
|
41
|
+
*/
|
|
42
|
+
export declare const deleteDatabase: (file: string, options: DeleteDatabaseOptions) => Promise<void>;
|
package/dist/epochs.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The commit epoch: a monotonic integer per database, counting commits
|
|
3
|
+
* performed in this realm. Its absolute value means nothing — only the
|
|
4
|
+
* comparison with a worker's `seen` does.
|
|
5
|
+
*
|
|
6
|
+
* The registry lives in the realm-wide symbol registry rather than in a module
|
|
7
|
+
* variable on purpose. A module singleton is unique only when the bundler
|
|
8
|
+
* loads one copy of the module; `Symbol.for` is unique per realm whatever the
|
|
9
|
+
* bundler did. That is what makes "two clients in one tab see each other" true
|
|
10
|
+
* by construction.
|
|
11
|
+
*
|
|
12
|
+
* The `v1` suffix separates incompatible shapes. Bump it ONLY if the shape
|
|
13
|
+
* changes — bumping it per release recreates the fragmentation it prevents.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The statement the barrier runs and discards.
|
|
17
|
+
*
|
|
18
|
+
* Measured 2026-08-20 in the forced configuration: 6/6 correct. `SELECT 1`
|
|
19
|
+
* touches no page and is 6/6 stale; `PRAGMA data_version` and
|
|
20
|
+
* `PRAGMA schema_version` are 8/8 stale; so is waiting. Only a statement that
|
|
21
|
+
* opens a real read transaction on the file refreshes the connection's cached
|
|
22
|
+
* page 1 — and it must be a SEPARATE statement, because the one that triggers
|
|
23
|
+
* the refresh still returns the stale result.
|
|
24
|
+
*/
|
|
25
|
+
export declare const BARRIER_SQL = "SELECT count(*) FROM sqlite_master";
|
|
26
|
+
export type Epochs = {
|
|
27
|
+
/** The number of commits observed in this realm for this database. */
|
|
28
|
+
current: () => number;
|
|
29
|
+
/** Records one commit and returns the new epoch. */
|
|
30
|
+
bump: () => number;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Handles onto the counter for `file`, which MUST already be normalized by
|
|
34
|
+
* `normalizeDatabaseFile`. Entries are never removed: deleting one would
|
|
35
|
+
* restart the counter at 0, and a worker still alive with `seen = 5` would
|
|
36
|
+
* then read `5 > 0`, believe itself current forever, and serve stale data.
|
|
37
|
+
*/
|
|
38
|
+
export declare const epochsFor: (file: string) => Epochs;
|
|
39
|
+
/**
|
|
40
|
+
* Where a worker's `seen` lands after the write it just served.
|
|
41
|
+
*
|
|
42
|
+
* `target` is the epoch captured when its lease was granted; `next` is the
|
|
43
|
+
* epoch its own commit produced. Advancing requires both conditions:
|
|
44
|
+
*
|
|
45
|
+
* - `seen === target`: the worker was actually observing from `target` when its
|
|
46
|
+
* lease was granted. If the worker was already behind (`seen < target`), it
|
|
47
|
+
* must not be marked current regardless of what it just committed.
|
|
48
|
+
* - `next === target + 1`: the commit is the immediate successor of `target`.
|
|
49
|
+
* If another client committed during our lease, `next` skipped; our
|
|
50
|
+
* connection never observed that commit and must stay marked behind.
|
|
51
|
+
*
|
|
52
|
+
* Marking a connection current when it is not is the only class of bug this
|
|
53
|
+
* design must make impossible.
|
|
54
|
+
*/
|
|
55
|
+
export declare const advanceSeen: (seen: number, target: number, next: number) => number;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every failure this library raises on its own behalf. A caller discriminates
|
|
3
|
+
* on `code`, or on `name` — they carry the same value, so `err.name` reads the
|
|
4
|
+
* way `'AbortError'` does on the DOMException an aborted signal throws.
|
|
5
|
+
*/
|
|
6
|
+
export type SQLiteErrorCode = 'NOT_A_READ_QUERY' | 'CLIENT_CLOSED' | 'WORKER_CRASHED' | 'TIMEOUT' | 'PROTOCOL_ERROR' | 'INVALID_IDENTIFIER' | 'INVALID_OPTION' | 'INVALID_PRAGMA' | 'BULK_WRITE_FAILED' | 'BUSY' | 'READ_ONLY_TRANSACTION';
|
|
7
|
+
export declare class SQLiteError extends Error {
|
|
8
|
+
readonly code: SQLiteErrorCode;
|
|
9
|
+
/**
|
|
10
|
+
* SQLite's own numeric result code, present only when the failure came from
|
|
11
|
+
* SQLite rather than from this library. `BUSY` covers both SQLITE_BUSY (5)
|
|
12
|
+
* and SQLITE_LOCKED (6); this is how a caller tells them apart.
|
|
13
|
+
*/
|
|
14
|
+
readonly sqliteCode?: number;
|
|
15
|
+
constructor(code: SQLiteErrorCode, message: string, options?: {
|
|
16
|
+
cause?: unknown;
|
|
17
|
+
sqliteCode?: number;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A batch failed. Raised by `bulkWrite().close()` and by `output().close()`.
|
|
22
|
+
*
|
|
23
|
+
* The counters exist because the old behaviour was silent: batches were chained
|
|
24
|
+
* on one shared promise, so after a rejection every later `.then` was skipped —
|
|
25
|
+
* while their rows had already been spliced out of the buffer (B5). A caller now
|
|
26
|
+
* learns how much of its data reached the database.
|
|
27
|
+
*/
|
|
28
|
+
export declare class SQLiteBulkWriteError extends SQLiteError {
|
|
29
|
+
readonly rowsWritten: number;
|
|
30
|
+
readonly rowsNotWritten: number;
|
|
31
|
+
constructor(message: string, counts: {
|
|
32
|
+
rowsWritten: number;
|
|
33
|
+
rowsNotWritten: number;
|
|
34
|
+
}, options?: {
|
|
35
|
+
cause?: unknown;
|
|
36
|
+
});
|
|
37
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './api';
|
|
2
|
+
export { detectFeatures, missingFeature } from './capabilities';
|
|
3
|
+
export * from './client';
|
|
4
|
+
export * from './delete';
|
|
5
|
+
export * from './errors';
|
|
6
|
+
export { defaultBuildFor, type PlatformFeature, type SQLiteBuild, type SQLiteVFS, VFS_CAPABILITIES, type VFSCapability, type VFSLayout, type VFSMemoryModel, type VFSStorage, } from './types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
let e={sync:[],async:[],jspi:["jspi"]},t={OPFSAdaptiveVFS:{builds:["async","jspi"],maxPoolSize:null,poolLimitReason:null,multiConnection:!0,persistent:!0,memoryModel:"page-cache",storage:"opfs",layout:"opfs-path",requires:["opfs"],degradesWithout:["readwrite-unsafe"]},OPFSWriteAheadVFS:{builds:["sync","async","jspi"],maxPoolSize:null,poolLimitReason:null,multiConnection:!0,persistent:!0,memoryModel:"page-cache",storage:"opfs",layout:"opfs-path",requires:["opfs"],degradesWithout:["readwrite-unsafe"]},OPFSCoopSyncVFS:{builds:["sync","async","jspi"],maxPoolSize:null,poolLimitReason:null,multiConnection:!0,persistent:!0,memoryModel:"page-cache",storage:"opfs",layout:"opfs-path",requires:["opfs"],degradesWithout:[]},AccessHandlePoolVFS:{builds:["sync","async","jspi"],maxPoolSize:1,poolLimitReason:"it cannot share access handles between connections",multiConnection:!1,persistent:!0,memoryModel:"page-cache",storage:"opfs",layout:"opfs-pool",requires:["opfs"],degradesWithout:[]},IDBBatchAtomicVFS:{builds:["async","jspi"],maxPoolSize:null,poolLimitReason:null,multiConnection:!0,persistent:!0,memoryModel:"page-cache",storage:"indexeddb",layout:"idb-store",requires:[],degradesWithout:[]},IDBMirrorVFS:{builds:["async","jspi"],maxPoolSize:1,poolLimitReason:"its pages are mirrored per worker and commits propagate asynchronously, so a larger pool reads stale data or fails outright",multiConnection:!1,persistent:!0,memoryModel:"whole-database",storage:"indexeddb",layout:"idb-store",requires:[],degradesWithout:[]},OPFSAnyContextVFS:{builds:["async","jspi"],maxPoolSize:null,poolLimitReason:null,multiConnection:!0,persistent:!0,memoryModel:"page-cache",storage:"opfs",layout:"opfs-path",requires:["opfs","writable-stream"],degradesWithout:[]},MemoryVFS:{builds:["sync","async","jspi"],maxPoolSize:1,poolLimitReason:"its pages live in the worker that opened them, so a larger pool would open independent databases that diverge silently",multiConnection:!1,persistent:!1,memoryModel:"whole-database",storage:"memory",layout:"memory",requires:[],degradesWithout:[]},MemoryAsyncVFS:{builds:["async","jspi"],maxPoolSize:1,poolLimitReason:"its pages live in the worker that opened them, so a larger pool would open independent databases that diverge silently",multiConnection:!1,persistent:!1,memoryModel:"whole-database",storage:"memory",layout:"memory",requires:[],degradesWithout:[]}},r=e=>t[e].builds[0],a="OPFSAdaptiveVFS",o={opfs:()=>"u">typeof navigator&&"function"==typeof navigator.storage?.getDirectory&&"u">typeof FileSystemFileHandle,jspi:()=>"function"==typeof WebAssembly.Suspending,"writable-stream":()=>"u">typeof FileSystemFileHandle&&"function"==typeof FileSystemFileHandle.prototype.createWritable},i=new Set(["readwrite-unsafe"]),s={opfs:"OPFS",jspi:"JSPI","writable-stream":"FileSystemWritableFileStream","readwrite-unsafe":"readwrite-unsafe access handles"};[...Object.keys(o),...i];let n=()=>{let e=new Set;for(let[t,r]of Object.entries(o))r()&&e.add(t);return e},l=(r,a,o)=>{for(let s of[...t[r].requires,...e[a]])if(!i.has(s)&&!o.has(s))return s;return null};class u extends Error{code;sqliteCode;constructor(e,t,r){super(t,r),this.code=e,this.name=e,r?.sqliteCode!==void 0&&(this.sqliteCode=r.sqliteCode)}}class d extends u{rowsWritten;rowsNotWritten;constructor(e,t,r){super("BULK_WRITE_FAILED",e,r),this.rowsWritten=t.rowsWritten,this.rowsNotWritten=t.rowsNotWritten}}let c=(e,t)=>`bsq:staging:${e}:${t}`,w={available:!1,hold:async()=>()=>{},withLock:async(e,t)=>t(),tryWithLock:async(e,t)=>(await t(),!0),heldNames:async()=>[]},f=(e=globalThis.navigator?.locks)=>e?{available:!0,hold:t=>new Promise((r,a)=>{let o,i=new Promise(e=>{o=e});e.request(t,()=>(r(o),i)).catch(a)}),withLock:(t,r)=>e.request(t,{mode:"exclusive"},()=>r()),tryWithLock:async(t,r)=>{let a=!1;return await e.request(t,{mode:"exclusive",ifAvailable:!0},async e=>{e&&(a=!0,await r())}),a},heldNames:async()=>((await e.query()).held??[]).map(e=>e.name).filter(e=>"string"==typeof e)}:w,m=/\b(INSERT|REPLACE|UPDATE|DELETE|CREATE|DROP|ALTER|VACUUM|ANALYZE|REINDEX|SAVEPOINT|RELEASE|BEGIN|COMMIT|ROLLBACK|ATTACH|DETACH|PRAGMA)\b/i,h=/^\s*PRAGMA\s+(\w+\.)?\w+\s*;?\s*$/i,p=e=>h.test(e)||/^\s*(SELECT|EXPLAIN|VALUES|WITH)\b/i.test(e)&&!m.test(e),y=(e,t)=>{if(p(e))return;let r=e.trim().split(/\s+/)[0]?.toUpperCase()??"";throw new u("NOT_A_READ_QUERY",`${t}() only accepts statements that are provably reads; "${r}" must go through write(). Note that a PRAGMA that assigns a value or takes an argument is a write.`)},g=e=>{if(!e)throw new u("INVALID_IDENTIFIER","Identifier cannot be empty");if(e.includes("\0"))throw new u("INVALID_IDENTIFIER",`Identifier contains a NUL character: ${JSON.stringify(e)}`);return`"${e.replace(/"/g,'""')}"`},b=/^[A-Za-z][A-Za-z0-9 ]*(\([0-9, ]+\))?$/,v=/^[A-Za-z_]\w*$/,R=/^-?\d+$/,S=/^'([^']|'')*'$/,E=e=>new URL(e,"file://").pathname.replace(/^\//,""),A=(e,t,r)=>{let a;if(void 0===e)return;let o="function"==typeof e,i=o?e(t):e,s=o||i.endsWith("/")?i:`${i}/`;try{a=new URL(s,r).href}catch{throw new u("INVALID_OPTION",`wasmUrl could not be parsed as a URL: ${JSON.stringify(i)}. Pass a directory (relative, absolute, or a full URL), or a callback returning the full URL of one .wasm file.`)}return o?{file:a}:{base:a}},$=Promise.resolve(),I=Symbol.for("browser-sqlite.epochs.v1"),k=Symbol("stop"),q=new Set([5,6]),L=e=>void 0!==e.sqliteCode&&q.has(e.sqliteCode)?new u("BUSY",e.message,{cause:e.cause,sqliteCode:e.sqliteCode}):void 0,T=e=>new Worker(new URL("./worker/worker.js",import.meta.url),{name:e,type:"module"}),P=e=>{let t;if(!e)return{aborted:void 0,teardown:()=>{}};let r=new Promise((r,a)=>{t=()=>a(e.reason),e.addEventListener("abort",t,{once:!0})});return r.catch(()=>{}),{aborted:r,teardown:()=>{t&&e.removeEventListener("abort",t)}}},D=async function*(e,t,r,a){let{signal:o,chunkSize:i,credits:s}=a??{};if(o?.aborted)throw o.reason;let{aborted:n,teardown:l}=P(o),u=e.query(t,r,{chunkSize:i,credits:s});try{for(;;){let e=n?await Promise.race([u.next(),n]):await u.next();if(e.done)break;"number"!=typeof e.value&&(yield e.value)}}finally{l(),e.interrupt(),u.return(void 0).catch(()=>{})}},O=async function*(e,t,r,a){for await(let o of D(e,t,r,a))for(let e of o)yield e},N=async(e,t,r,a)=>{let o=[];for await(let i of D(e,t,r,a))o.push(...i);return o},W=async(e,t,r,a)=>{for await(let o of D(e,t,r,{...a,chunkSize:1,credits:1}))return o[0]},C=async(e,t,r,a)=>{let{signal:o}=a??{};if(o?.aborted)throw o.reason;let{aborted:i,teardown:s}=P(o),n=e.query(t,r,{}),l=[],u=0;try{for(;;){let e=i?await Promise.race([n.next(),i]):await n.next();if(e.done)break;"number"==typeof e.value?u=e.value:l.push(...e.value)}}finally{s(),e.interrupt(),n.return(void 0).catch(()=>{})}return{result:l,affected:u}},x=async(e,t)=>{await N(e,t)},_=0,F=(o,i)=>{let w,m,h,q,F,j,M,z,U,V=E(o);if(!i?.vfs)throw new u("INVALID_OPTION",`vfs is required. ${a} is the recommended universal choice and was the previous default — pass it to keep reading a database created before this version. Compare VFS in the README's VFS Selection guide, and measure your own targets at https://lalexdotcom.github.io/browser-sqlite/`);let Q=++_,B=`${i.name??"SQLite"} ${Q}`,H=i.poolSize??2,G=[],K=i.vfs,Y=i.build??r(K),J=t[K];if(!J.builds.includes(Y))throw new u("INVALID_OPTION",`${K} cannot run on the '${Y}' build. Supported: ${J.builds.join(", ")}.`);let X=A(i.wasmUrl,Y,location.href);if(null!==J.maxPoolSize&&H>J.maxPoolSize)throw new u("INVALID_OPTION",`${K} does not support pool sizes greater than ${J.maxPoolSize}: ${J.poolLimitReason}. Set poolSize: ${J.maxPoolSize}.`);let Z=l(K,Y,n());if(Z)throw new u("INVALID_OPTION",((r,a,o)=>{let i=s[o];if(e[a].includes(o)){let e=t[r].builds.filter(e=>e!==a),o=e.length?` ${r} also runs on: ${e.join(", ")}.`:"";return`This browser does not support ${i}, which the '${a}' build requires.${o}`}let n=Object.keys(t).filter(e=>!t[e].requires.includes(o)),l=n.length?` Without it, these store elsewhere: ${n.join(", ")}.`:"";return`This browser does not support ${i}, which ${r} requires.${l}`})(K,Y,Z));i.pragmas&&Object.entries(i.pragmas).map(([e,t])=>{if(!v.test(e))throw new u("INVALID_PRAGMA",`Invalid pragma name ${JSON.stringify(e)}: a pragma name must match ${v}.`);let r=String(t).trim();if(R.test(r)||v.test(r)||S.test(r))return`PRAGMA ${e}=${r}`;throw new u("INVALID_PRAGMA",`Invalid value ${JSON.stringify(t)} for pragma "${e}": expected an integer, a bare word such as WAL, or a quoted literal.`)});let ee=i.__unsafeTestWriterPolicy,et="function"==typeof ee?ee:void 0,er=!0,ea=new Map,eo=((e={})=>{let t,r,a=[],o=new Set,i=new Set,s=new Set,n=new Map,l=e=>n.get(e)??0,u=new Set,d=(e.poolSize??0)===0,c=Promise.withResolvers();d&&c.resolve(),c.promise.catch(()=>{});let w=new Set,f=!1,m=0,h=(r,a)=>{if(!(d||u.has(r))&&(u.add(r),"opened"===a&&w.add(r),!(u.size<(e.poolSize??0)))){if(e.onFirstSettle&&!f){f=!0;let r=[...u].filter(e=>!w.has(e));if(e.onFirstSettle({openedCount:w.size,failedIndices:r}),u.size<(e.poolSize??0)||t)return}d=!0,c.resolve(),e.onGateOpen?.()}},p=[],y=[],g=-1,b=-1,v=e.canDesignateWriter??(()=>!0),R=e=>!!y.length&&(g===e.index||-1===g)&&(-1!==g||!!v(e.index))&&(g=e.index,b=e.index,y.shift()?.resolve(e),!0),S=()=>{r&&0===s.size&&r.resolve()},E=t=>{s.add(t.index);let r=l(t.index),a=!1;return{worker:t,release:()=>{if(!a){if(a=!0,l(t.index)!==r)return void S();s.delete(t.index),(t=>{if(!R(t)){if(g===t.index&&(g=-1),p.length)return p.shift()?.resolve(t);o.add(t.index),e.onIdle?.(t)}})(t),S()}}}};return{add:e=>{if(h(e.index,"opened"),i.delete(e.index),a[e.index]=e,!R(e)){if(p.length)return void p.shift()?.resolve(e);o.add(e.index)}},remove:e=>{h(e,"failed"),i.add(e),o.delete(e),s.delete(e),a[e]=void 0,n.set(e,l(e)+1),g===e&&(g=-1),b===e&&(b=-1),S()},shutdown:e=>{for(let a of(d||(d=!0,c.reject(e)),t??=e,r??=Promise.withResolvers(),p.splice(0)))a.reject(e);for(let t of y.splice(0))t.reject(e);return S(),r.promise},stats:()=>({read:p.length,write:y.length,available:o.size,leased:s.size,gated:m}),rearmSlot:e=>{d||u.delete(e)},acquire:async(e,r)=>{if(t)throw t;if(r?.throwIfAborted(),!d){m+=1;try{if(r){let{promise:e,reject:t}=Promise.withResolvers(),a=()=>t(r.reason);r.addEventListener("abort",a,{once:!0});try{await Promise.race([c.promise,e])}finally{r.removeEventListener("abort",a)}}else await c.promise}finally{m-=1}}if(t)throw t;let i="write"===e,s=(e=>{if(e&&g>-1){if(!o.has(g))return;return o.delete(g),a[g]}let t=a[b];if(void 0!==t&&o.has(b)&&(!e||v(b)))return o.delete(b),e&&(g=b),t;let r=a.find(t=>void 0!==t&&o.has(t.index)&&(!e||v(t.index)));if(r)return o.delete(r.index),e&&(g=r.index,b=r.index),r})(i);if(s)return E(s);let{promise:n,resolve:l,reject:u}=Promise.withResolvers(),w=i?y:p,f={resolve:l,reject:u};if(w.push(f),!r)return E(await n);let h=()=>{let e=w.indexOf(f);-1!==e&&(w.splice(e,1),u(r.reason))};r.addEventListener("abort",h,{once:!0});try{return E(await n)}finally{r.removeEventListener("abort",h)}}}})((q=e=>{if(0===e.openedCount){for(let[e,t]of(er=!1,ea))eL(e,t);ea.clear(),ek(w??new u("WORKER_CRASHED","All workers failed to open the database."));return}for(let t of e.failedIndices)eo.rearmSlot(t),eq(t)},F=()=>{let e;for(let[t,r]of(er=!1,ea))"fail-client"===eI.report(t,"lost")&&(e??=r);for(let[e,t]of ea)eL(e,t);ea.clear(),(void 0!==e||0===G.filter(Boolean).length)&&ek(e??w??new u("WORKER_CRASHED","All workers failed to open the database."))},et?{canDesignateWriter:et,poolSize:H,onFirstSettle:q,onGateOpen:F}:{poolSize:H,onFirstSettle:q,onGateOpen:F})),ei=i.debug,es=((e,t,r=console)=>{let a=t=>`[${e}] ${t}`,o={warn:e=>r.warn(a(e))};return t?{info:e=>r.debug(a(e)),warn:e=>r.warn(a(e)),error:e=>r.error(a(e)),always:o}:{info:()=>{},warn:()=>{},error:()=>{},always:o}})("string"==typeof ei?ei:B,!!ei),en=ei?((e,t,r,a)=>{let{vfs:o,pragmas:i,name:s}=r,n={file:e,vfs:o,pragmas:i,name:s,queue:{get read(){return a().read},get write(){return a().write},get gated(){return a().gated}},workers:[]};return{state:n,createWorkerDebugState:(e,r)=>{let a=new Proxy({index:e,name:r,requests:[],status:t[e]?.status??"EMPTY",creationTime:Date.now()},{get:(r,a)=>"status"===a?t[e]?.status??"EMPTY":r[a]});return n.workers[e]=a,a},createRequestDebugState:()=>{let e={queries:[],startTime:Date.now(),affectedRows:0};return{state:e,assign:t=>{let r=n.workers[t];r&&(e.acquireTime=Date.now(),r.requests.length>=50&&r.requests.shift(),r.requests.push(e),r.currentRequest=e)}}},createQueryDebugState:(e,t,r)=>{let a={sql:t,params:r,startTime:Date.now(),affectedRows:0,prepared:0},o=n.workers[e];return o?.currentRequest&&(o.currentRequest.queries.length>=50&&o.currentRequest.queries.shift(),o.currentRequest.queries.push(a),o.currentRequest.currentQuery=a),a}}})(V,G,{vfs:K,pragmas:i.pragmas??{},name:i.name??"SQLite"},()=>eo.stats()):void 0,el=en?.state,eu=(z=(M=(j=(()=>{let e=globalThis,t=e[I];if(t)return t;let r=new Map;return e[I]=r,r})()).get(V))??{value:0},M||j.set(V,z),{current:()=>z.value,bump:()=>(z.value+=1,z.value)}),ed=async e=>{let t=eu.current();if(e.epochTarget=t,e.seen>=t)return;let r=e.query("SELECT count(*) FROM sqlite_master",void 0,{noServed:!0});for(;!(await r.next()).done;);e.seen=t},ec=e=>{let t,r,a;t=e.seen,r=e.epochTarget,a=eu.bump(),e.seen=t===r&&a===r+1?a:t},ew=async(e,t)=>{let r=en.createRequestDebugState(),a=await eo.acquire(e,t);return r.assign(a.worker.index),{worker:a.worker,release:()=>{r.state.releaseTime=Date.now(),a.release()}}},ef=async(e,t)=>{let r=en?await ew(e,t):await eo.acquire(e,t);try{let{aborted:e,teardown:a}=P(t);try{let t=ed(r.worker);await (e?Promise.race([t,e]):t)}finally{a()}}catch(e){throw r.worker.quiesce().then(()=>r.release(),()=>r.release()),e}return r},em=async(e,t,r)=>{y(e,"read");let a=await ef("read",r?.signal);try{return await N(a.worker,e,t,r)}finally{a.worker.quiesce().then(()=>a.release(),()=>a.release())}},eh=async function*(e,t,r){y(e,"chunk");let a=await ef("read",r?.signal);try{yield*D(a.worker,e,t,r)}finally{a.worker.quiesce().then(()=>a.release(),()=>a.release())}},ep=async function*(e,t,r){y(e,"stream");let a=await ef("read",r?.signal);try{yield*O(a.worker,e,t,r)}finally{a.worker.quiesce().then(()=>a.release(),()=>a.release())}},ey=async(e,t,r)=>{let a=await ef("write",r?.signal);try{return await C(a.worker,e,t,r)}finally{ec(a.worker),a.worker.quiesce().then(()=>a.release(),()=>a.release())}},eg=async(e,t,r)=>{y(e,"first");let a=await ef("read",r?.signal);try{return await W(a.worker,e,t,r)}finally{a.worker.quiesce().then(()=>a.release(),()=>a.release())}},eb=(e=>{let t,{file:r,locks:a,maxVariables:o=32766,logger:i}=e;return e=>{let{read:s,write:n,transaction:l}=e,w=(e,t,r,a)=>{let i,s,l=r?.signal,u=Math.floor(o/t.length),c=Math.max(1,r?.queueSize??2*u),w=[],f=Promise.resolve(0),m=!1,h=0,p=0,y=0,b=()=>{s?.resolve(),s=void 0};l?.addEventListener("abort",b,{once:!0});let v=()=>new d(`bulkWrite into "${e}" failed after ${h} row(s); ${p} row(s) were not written.`,{rowsWritten:h,rowsNotWritten:p},{cause:i}),R=()=>{let r=[...w];w.length=0,y+=r.length;let o=async o=>{if(i||l?.aborted)return p+=r.length,o;try{a&&await a;let{affected:i}=await n(`INSERT INTO ${g(e)} (${t.map(g).join(",")}) VALUES ${r.map(()=>`(${t.map(()=>"?")})`)}`,r.flatMap(e=>t.map(t=>e[t])),{signal:l});return h+=r.length,o+i}catch(e){if(l?.aborted)return p+=r.length,o;return i=e,p+=r.length,o}};f=f.then(async e=>{try{return await o(e)}finally{(y-=r.length)<c&&b()}})},S=()=>new d(`Bulk writer for "${e}" is closed.`,{rowsWritten:h,rowsNotWritten:p});return{enqueue:e=>{let t;if(m)throw S();if(l?.throwIfAborted(),i)throw v();return(w.push(e),w.length>=u&&R(),y<c)?$:(s??={promise:new Promise(e=>{t=e}),resolve:t}).promise},close:async()=>{if(m)throw S();try{w.length&&R();let e=await f;if(l?.throwIfAborted(),i)throw v();return m=!0,e}finally{l?.removeEventListener("abort",b)}}}};return{bulkWrite:w,output:(e,o,d)=>{let f,m=(f=crypto.randomUUID(),`__bsq_staging_${f.replace(/-/g,"_")}`),h=Object.entries(o).map(([e,t])=>{let r=((e,t)=>{let r=e.trim();if(!b.test(r))throw new u("INVALID_IDENTIFIER",`Column "${t}" declares an unsupported type ${JSON.stringify(e)}. A type must be a word, optionally followed by numeric arguments, e.g. "INTEGER" or "VARCHAR(255)".`);return r})("string"==typeof t?t:t.type,e),a="object"==typeof t&&!!t.unique,o="object"==typeof t&&!!t.required,i="object"==typeof t&&t.generated?((e,t)=>{let r=e.trim();if(!r.startsWith("(")||!r.endsWith(")")||r.includes(";"))throw new u("INVALID_IDENTIFIER",`Column "${t}" declares an invalid generated expression ${JSON.stringify(e)}. It must be parenthesised and contain no ";", e.g. "(base * 2)".`);return r})(t.generated,e):void 0;return{name:e,type:r,unique:a,notnull:o,generated:i}}),p=a.hold(c(r,m)),y=(()=>a.available?t??=a.tryWithLock(`bsq:sweep:${r}`,async()=>{var e;let t,o=(await s("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '__bsq_staging_%'")).map(e=>e.name).filter(e=>"string"==typeof e);if(o.length)for(let i of(e=await a.heldNames(),t=new Set(e),o.filter(e=>!t.has(c(r,e)))))await n(`DROP TABLE IF EXISTS ${g(i)}`)}).then(()=>void 0).catch(()=>{}):(void 0===t&&(t=Promise.resolve(),i.warn("navigator.locks is unavailable; skipping the staging sweep")),t))().then(()=>n(`
|
|
2
|
+
CREATE TABLE ${g(m)}(
|
|
3
|
+
${h.map(({name:e,type:t,unique:r,notnull:a,generated:o})=>`${g(e)} ${t} ${r?"UNIQUE":""} ${a?"NOT NULL":""} ${o?`GENERATED ALWAYS AS ${o}`:""}`).join(",")}
|
|
4
|
+
)`)).then(()=>void 0),{enqueue:v,close:R}=w(m,Object.keys(o).filter(e=>"object"!=typeof o[e]||!o[e].generated),{signal:d?.signal,queueSize:d?.queueSize},y),S=async()=>{(await p)()},E=()=>Promise.race([n(`DROP TABLE IF EXISTS ${g(m)}`),new Promise(e=>setTimeout(e,5e3))]).catch(()=>{});return{enqueue:e=>v(e),close:async()=>{let t;try{await y,t=await R()}catch(e){throw await E(),await S(),e}try{await l(async t=>{for(let r of(await t.write(`DROP TABLE IF EXISTS ${g(e)}`),await t.write(`ALTER TABLE ${g(m)} RENAME TO ${g(e)}`),((e,t)=>{let r=[];for(let a of t?.indexes??[]){let t=Array.isArray(a)?a:"object"==typeof a?"column"in a?[a.column]:a.columns:[a],o=!Array.isArray(a)&&"object"==typeof a&&!!a.unique;if(!t?.length)continue;let i=t.map(String);r.push(`CREATE${o?" UNIQUE":""} INDEX IF NOT EXISTS ${g(`${e}_${i.join("_")}_${o?"U":"IDX"}`)} ON ${g(e)}(${i.map(g).join(",")})`)}return r})(e,d)))await t.write(r)})}catch(e){throw await E(),e}finally{await S()}return t}}}}}})({file:V,locks:f(),logger:es}),ev=(U={scheduler:{...eo,acquire:ef},afterWrite:ec,onPoisoned:(e,t)=>eT(e,t),bulkFor:eb},async(e,t)=>{let{readOnly:r=!1,autoCommit:a=!0,signal:o}=t??{},i=await U.scheduler.acquire(r?"read":"write",o),s=i.worker,n=e=>{if(r&&!p(e))throw new u("READ_ONLY_TRANSACTION","Cannot write in a read-only transaction.");return e},l=!1,d=!1,c=e=>{let{signal:t,release:r}=((e,t)=>{let r=()=>{};if(!e||e===t)return{signal:t,release:r};if(!t||e.aborted)return{signal:e,release:r};if(t.aborted)return{signal:t,release:r};let a=new AbortController,o=e=>()=>a.abort(e.reason),i=o(e),s=o(t);return e.addEventListener("abort",i,{once:!0}),t.addEventListener("abort",s,{once:!0}),{signal:a.signal,release:()=>{e.removeEventListener("abort",i),t.removeEventListener("abort",s)}}})(o,e?.signal);return{options:{...e,signal:t},release:r}},w=async function*(e,t){try{yield*e}finally{t()}},f=e=>()=>{throw new u("READ_ONLY_TRANSACTION",`${e}() writes, and this transaction is read-only.`)},m=r?{bulkWrite:f("bulkWrite"),output:f("output")}:U.bulkFor({read:(e,t,r)=>{let a=n(e),{options:o,release:i}=c(r);return N(s,a,t,o).finally(i)},write:(e,t,r)=>{let a=n(e),{options:o,release:i}=c(r);return C(s,a,t,o).finally(i)},transaction:e=>e(h)}),h={read:(e,t,r)=>{let a=n(e),{options:o,release:i}=c(r);return N(s,a,t,o).finally(i)},write:(e,t,r)=>{let a=n(e),{options:o,release:i}=c(r);return C(s,a,t,o).finally(i)},chunk:(e,t,r)=>{let a=n(e),{options:o,release:i}=c(r);return w(D(s,a,t,o),i)},stream:(e,t,r)=>{let a=n(e),{options:o,release:i}=c(r);return w(O(s,a,t,o),i)},first:(e,t,r)=>{let a=n(e),{options:o,release:i}=c(r);return W(s,a,t,o).finally(i)},bulkWrite:m.bulkWrite,output:m.output,commit:async()=>{o?.throwIfAborted(),await x(s,"COMMIT"),l=!0},rollback:async()=>{await x(s,"ROLLBACK"),l=!0}},{aborted:y,teardown:g}=P(o);try{o?.throwIfAborted(),await x(s,"BEGIN"),d=!0,o?.throwIfAborted();let t=e(h);t.catch(()=>{});let r=y?await Promise.race([t,y]):await t;return l||(a?await h.commit():await h.rollback()),r}catch(e){if(d&&!l)try{await h.rollback()}catch{U.onPoisoned(s.index,new u("WORKER_CRASHED",`Worker ${s.index+1} may hold an open transaction after a failed rollback.`,{cause:e}))}throw e}finally{g(),r||U.afterWrite(s),i.worker.quiesce().then(()=>i.release(),()=>i.release())}}),{bulkWrite:eR,output:eS}=eb({read:em,write:ey,transaction:ev}),eE=async(e,t)=>{let r;try{await Promise.race([e,new Promise(e=>{r=setTimeout(e,t)})])}finally{clearTimeout(r)}},eA=i.openTimeout??3e4,e$=i.drainTimeout??6e4,eI=(e=>{let{size:t,maxWorkerRestarts:r=1}=e,a=Array.from({length:t},()=>({everReady:!1,alive:!0,lost:!1,restarts:0})),o=()=>a.filter(e=>e.alive).length;return{report:(e,t)=>{let i=a[e];if(i){if("spawned"===t){if(i.lost)return;i.alive=!0;return}if("ready"===t){if(i.lost)return;i.everReady=!0,i.alive=!0;return}if("served"===t){if(!i.alive)return;i.restarts=0;return}if("lost"===t){if(!i.alive)return;return i.alive=!1,i.lost=!0,0===o()?"fail-client":"lost"}if(i.alive)return(i.alive=!1,i.everReady&&i.restarts<r)?(i.restarts+=1,"restart"):(i.lost=!0,0===o()?"fail-client":"lost")}}}})({size:H,maxWorkerRestarts:i.maxWorkerRestarts}),ek=e=>{for(let t of(h??=e,eo.shutdown(h),G))t?.terminate()},eq=e=>{eI.report(e,"spawned");let t=setTimeout(()=>{eT(e,new u("TIMEOUT",`Worker ${e+1} did not become ready within ${eA} ms. The database may be held under an exclusive lock by another tab or another client.`))},eA);(e=>{let t,r,a,o,i,{index:s,pool:n,clientPrefix:l,file:d,vfs:c,build:w,wasm:f,pragmas:m,statementCacheSize:h}=e,{createWorkerDebugState:p,createQueryDebugState:y,logger:g}=e,b=Promise.withResolvers(),v=`${l} / Worker ${s+1}`,R=Object.assign(T(v),{index:s,status:"NEW",seen:-1,epochTarget:0});n[s]=R,g.info(`worker ${s+1} created`);let S=p?.(s,v),E=0,A=!1,$=!1,I=!1,q=Promise.withResolvers();q.promise.catch(()=>{});let P=t=>{$||($=!0,R.status="DEAD",q.reject(t),b.reject(t),e.onDeath?.(s,t))};R.onerror=e=>{let t="object"==typeof e&&null!==e&&"message"in e?String(e.message??""):"",r=e.filename;g.error(`worker ${s+1} crashed: ${t}`),P(new u("WORKER_CRASHED",I?`Worker ${s+1} failed: ${t||"uncaught error"}`:`browser-sqlite could not load its worker${r?` from ${r}`:`; the client itself was loaded from ${import.meta.url}, and the worker must be emitted beside it`}. If the worker URL 404s, your bundler did not emit the worker beside your build output — see the "Bundler Configuration" section of the browser-sqlite README. ${t}`,{cause:e}))},R.addEventListener("messageerror",()=>{g.error(`worker ${s+1} sent an undeserializable message`),i?.reject(new u("PROTOCOL_ERROR",`Worker ${s+1} sent a message that could not be deserialized; the request cannot be completed.`))}),R.onmessage=({data:a})=>{let{callId:o,type:i}=a;switch(i){case"ready":0===o&&(I=!0,R.status="READY",S&&(S.initializationTime=Date.now()),g.info(`worker ${s+1} ready`),b.resolve(R));break;case"open-error":0===o&&(g.error(`worker ${s+1} failed to open: ${a.message}`),P(L(a)??new u("WORKER_CRASHED",a.message,{cause:a.cause})));break;case"closed":0===o&&(g.info(`worker ${s+1} closed`),R.status="CLOSED",r?.resolve());break;case"chunk":t&&o===E&&(S?.currentRequest?.currentQuery&&(S.currentRequest.currentQuery.firstRowTime??=Date.now()),t.resolve(a.data),t=Promise.withResolvers());break;case"done":if(t&&o===E){let r=a.affected;S?.currentRequest?.currentQuery&&(S.currentRequest.currentQuery.affectedRows=r,S.currentRequest.currentQuery.prepared=a.prepared,S.currentRequest.affectedRows+=r,S.currentRequest.currentQuery.endTime=Date.now()),t.resolve(r),t=void 0,A||e.onServed?.(s),A=!1}break;case"error":if(t&&o===E){let e=L(a)??Error(a.message,{cause:a.cause});S?.currentRequest?.currentQuery&&(S.currentRequest.currentQuery.error=e,S.currentRequest.currentQuery.endTime=Date.now()),t.reject(e),t.promise.catch(()=>{})}break;case"deleted":break;default:throw Error(`Unhandled worker message: ${JSON.stringify(a)}`)}};let D=async function*(r,n,l){try{if(t)throw console.error(`Previous query not finished on worker ${s+1}`),Error("Worker is already processing a query");if(S?.currentRequest){let e=y?.(s,r,n);S.currentRequest.currentQuery=e}let{chunkSize:e=500,credits:u=2,noServed:d=!1}=l??{};for(A=d,t=Promise.withResolvers(),(i=Promise.withResolvers()).promise.catch(()=>{}),a=Promise.withResolvers(),o=Promise.withResolvers(),R.postMessage({type:"query",callId:++E,sql:r,params:n,options:{chunkSize:e,credits:u}}),R.status="RUNNING";t;){let e=await Promise.race([t.promise,o.promise,i.promise,q.promise]);if(e===k)break;yield e,"number"!=typeof e&&R.postMessage({type:"credit",callId:E,n:1})}}finally{if(t&&!$){let r;R.status="ABORTING",R.postMessage({type:"stop",callId:E});let a=new Promise((t,a)=>{r=setTimeout(()=>a(new u("WORKER_CRASHED",`Worker ${s+1} did not answer the stop request within ${e.drainTimeout} ms; presumed dead.`)),e.drainTimeout)});try{for(;t;)await Promise.race([t.promise,a])}catch(e){e instanceof u&&"WORKER_CRASHED"===e.code&&P(e)}finally{clearTimeout(r)}}t=void 0,i=void 0,o=void 0,A=!1,R.status=$?"DEAD":"READY",a?.resolve(),a=void 0}};return Object.assign(R,{query:D,interrupt:()=>{o?.resolve(k)},quiesce:()=>a?.promise??Promise.resolve(),close:async()=>{r||(r=Promise.withResolvers(),R.postMessage({type:"close",callId:0})),await r.promise}}),R.postMessage({callId:0,type:"open",file:d,vfs:c,build:w,wasm:f,pragmas:m,statementCacheSize:h}),b.promise})({index:e,pool:G,clientPrefix:B,file:V,vfs:K,build:Y,wasm:X,pragmas:i.pragmas,statementCacheSize:32,onDeath:eT,onServed:e=>{eI.report(e,"served")},drainTimeout:e$,createWorkerDebugState:en?.createWorkerDebugState,createQueryDebugState:en?.createQueryDebugState,logger:es}).then(t=>{eI.report(e,"ready"),ea.delete(e),eo.add(t)}).catch(()=>{}).finally(()=>clearTimeout(t))},eL=(e,t)=>{let r=G.filter(Boolean).length;es.always.warn(`worker ${e+1} lost; pool is now ${r} of ${H}`);let a=i.onWorkerLost;if(a)try{a({index:e,live:r,size:H,cause:t})}catch(e){es.always.warn(`onWorkerLost callback threw: ${e instanceof Error?e.message:String(e)}`)}},eT=(e,t)=>{let r=er;if(r&&(w??=t,ea.set(e,t)),G[e]?.terminate(),G[e]=void 0,eo.remove(e),r)return;let a=eI.report(e,"died");"restart"===a?(es.warn(`restarting worker ${e+1}`),eq(e)):"lost"===a?eL(e,t):"fail-client"===a&&(eL(e,t),ek(t))};for(let e=0;e<H;e+=1)eq(e);return{chunk:eh,read:em,write:ey,stream:ep,first:eg,transaction:ev,bulkWrite:eR,output:eS,close:()=>m||(m=(async()=>{es.info("client closing");let e=eo.shutdown(new u("CLIENT_CLOSED","The SQLite client has been closed."));await eE(e,e$),await Promise.all(G.map(async e=>{e&&(await eE(e.close(),e$),e.terminate())})),G.length=0})()),debug:el}},j=async(e,o)=>{if(!o?.vfs)throw new u("INVALID_OPTION",`vfs is required. Pass the VFS the database was created with — ${a} is the recommended universal choice. A database written through one VFS is not visible through another, so deleting through the wrong one deletes nothing.`);let i=o.vfs,s=o.build??r(i),n=t[i];if(!n.builds.includes(s))throw new u("INVALID_OPTION",`${i} cannot run on the '${s}' build. Supported: ${n.builds.join(", ")}.`);if("memory"===n.layout)return;let l=E(e),d=A(o.wasmUrl,s,location.href);if(!await f().tryWithLock(`bsq:init:${l}`,()=>M({file:l,vfs:i,build:s,wasm:d})))throw new u("BUSY",`${l} is being opened or deleted elsewhere. Close every client on it, in every tab, and try again.`)},M=e=>new Promise((t,r)=>{let a=T(`SQLite delete / ${e.file}`),o=setTimeout(()=>{i(new u("TIMEOUT",`deleting ${e.file} timed out after 30000 ms. The database is most likely held open by another client or tab.`))},3e4),i=e=>{clearTimeout(o),a.terminate(),e?r(e):t()};a.onmessage=e=>{let t=e.data;return"deleted"===t.type?i():"error"===t.type?i(L(t)??new u("WORKER_CRASHED",t.message,{cause:t.cause})):void 0},a.onerror=t=>{i(new u("WORKER_CRASHED",`worker crashed while deleting ${e.file}: ${t.message??""}`))},a.postMessage({type:"delete",callId:0,...e})});export{d as SQLiteBulkWriteError,u as SQLiteError,t as VFS_CAPABILITIES,F as createSQLiteClient,r as defaultBuildFor,j as deleteDatabase,n as detectFeatures,l as missingFeature};
|
|
5
|
+
//# sourceMappingURL=index.js.map
|