Skip to content

Commit 8e998be

Browse files
committed
New OPFS mode, sqlite3_web updates
1 parent 2acbd4c commit 8e998be

27 files changed

+1839
-587
lines changed

sqlite3/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.9.4
2+
3+
- `SimpleOpfsFileSystem`: Allow opening with `readwrite-unsafe`, which can be used to implement
4+
multi-tab OPFS databases on Chrome with an outer locking scheme.
5+
16
## 2.9.3
27

38
- Allow iterating over statements after `SQLITE_BUSY` errors.

sqlite3/lib/src/wasm/js_interop/new_file_system_access.dart

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,21 +52,27 @@ extension FileSystemDirectoryHandleApi on FileSystemDirectoryHandle {
5252
return getFileHandle(name, FileSystemGetFileOptions(create: create)).toDart;
5353
}
5454

55-
Future<FileSystemDirectoryHandle> getDirectory(String name,
56-
{bool create = false}) {
55+
Future<FileSystemDirectoryHandle> getDirectory(
56+
String name, {
57+
bool create = false,
58+
}) {
5759
return getDirectoryHandle(
58-
name, FileSystemGetDirectoryOptions(create: create))
59-
.toDart;
60+
name,
61+
FileSystemGetDirectoryOptions(create: create),
62+
).toDart;
6063
}
6164

6265
Future<void> remove(String name, {bool recursive = false}) {
63-
return removeEntry(name, FileSystemRemoveOptions(recursive: recursive))
64-
.toDart;
66+
return removeEntry(
67+
name,
68+
FileSystemRemoveOptions(recursive: recursive),
69+
).toDart;
6570
}
6671

6772
Stream<FileSystemHandle> list() {
68-
return AsyncJavaScriptIteratable<JSArray>(this)
69-
.map((data) => data.toDart[1] as FileSystemHandle);
73+
return AsyncJavaScriptIteratable<JSArray>(
74+
this,
75+
).map((data) => data.toDart[1] as FileSystemHandle);
7076
}
7177

7278
Stream<FileSystemHandle> getFilesRecursively() async* {
@@ -79,3 +85,22 @@ extension FileSystemDirectoryHandleApi on FileSystemDirectoryHandle {
7985
}
8086
}
8187
}
88+
89+
// https://github.com/whatwg/fs/blob/main/proposals/MultipleReadersWriters.md
90+
extension ProposedLockingSchemeApi on FileSystemFileHandle {
91+
external JSPromise<FileSystemSyncAccessHandle> createSyncAccessHandle(
92+
FileSystemCreateSyncAccessHandleOptions options,
93+
);
94+
}
95+
96+
// https://github.com/whatwg/fs/blob/main/proposals/MultipleReadersWriters.md#modes-of-creating-a-filesystemsyncaccesshandle
97+
@anonymous
98+
extension type FileSystemCreateSyncAccessHandleOptions._(JSObject _)
99+
implements JSObject {
100+
external factory FileSystemCreateSyncAccessHandleOptions({JSString? mode});
101+
102+
static FileSystemCreateSyncAccessHandleOptions unsafeReadWrite() {
103+
return FileSystemCreateSyncAccessHandleOptions(
104+
mode: 'readwrite-unsafe'.toJS);
105+
}
106+
}

sqlite3/lib/src/wasm/vfs/simple_opfs.dart

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ enum FileType {
2525

2626
const FileType(this.filePath);
2727

28-
static final byName = {
29-
for (final entry in values) entry.filePath: entry,
30-
};
28+
static final byName = {for (final entry in values) entry.filePath: entry};
3129
}
3230

3331
/// A [VirtualFileSystem] for the `sqlite3` wasm library based on the [file system access API].
@@ -96,15 +94,24 @@ final class SimpleOpfsFileSystem extends BaseVirtualFileSystem {
9694
/// Throws a [VfsException] if OPFS is not available - please note that
9795
/// this file system implementation requires a recent browser and only works
9896
/// in dedicated web workers.
99-
static Future<SimpleOpfsFileSystem> loadFromStorage(String path,
100-
{String vfsName = 'simple-opfs'}) async {
97+
///
98+
/// When [readWriteUnsafe] is passed, the synchronous file handles are opened
99+
/// using the [proposed lock mode](https://github.com/whatwg/fs/blob/main/proposals/MultipleReadersWriters.md).
100+
/// This mode is currently not supported across browsers, but can be used on
101+
/// Chrome for faster database access across tabs.
102+
static Future<SimpleOpfsFileSystem> loadFromStorage(
103+
String path, {
104+
String vfsName = 'simple-opfs',
105+
bool readWriteUnsafe = false,
106+
}) async {
101107
final storage = storageManager;
102108
if (storage == null) {
103109
throw VfsException(SqlError.SQLITE_ERROR);
104110
}
105111

106112
final (_, directory) = await _resolveDir(path);
107-
return inDirectory(directory, vfsName: vfsName);
113+
return inDirectory(directory,
114+
vfsName: vfsName, readWriteUnsafe: readWriteUnsafe);
108115
}
109116

110117
/// Deletes the file system directory handle that would store sqlite3
@@ -134,14 +141,27 @@ final class SimpleOpfsFileSystem extends BaseVirtualFileSystem {
134141
/// Loads an [SimpleOpfsFileSystem] in the desired [root] directory, which must be
135142
/// a Dart wrapper around a [FileSystemDirectoryHandle].
136143
///
144+
/// When [readWriteUnsafe] is passed, the synchronous file handles are opened
145+
/// using the [proposed lock mode](https://github.com/whatwg/fs/blob/main/proposals/MultipleReadersWriters.md).
146+
/// This mode is currently not supported across browsers, but can be used on
147+
/// Chrome for faster database access across tabs.
148+
///
137149
/// [FileSystemDirectoryHandle]: https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle
138150
static Future<SimpleOpfsFileSystem> inDirectory(
139151
FileSystemDirectoryHandle root, {
140152
String vfsName = 'simple-opfs',
153+
bool readWriteUnsafe = false,
141154
}) async {
142155
Future<FileSystemSyncAccessHandle> open(String name) async {
143156
final handle = await root.openFile(name, create: true);
144-
return await handle.createSyncAccessHandle().toDart;
157+
158+
final syncHandlePromise = readWriteUnsafe
159+
? ProposedLockingSchemeApi(handle).createSyncAccessHandle(
160+
FileSystemCreateSyncAccessHandleOptions.unsafeReadWrite(),
161+
)
162+
: handle.createSyncAccessHandle();
163+
164+
return await syncHandlePromise.toDart;
145165
}
146166

147167
final meta = await open('meta');

sqlite3/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: sqlite3
22
description: Provides lightweight yet convenient bindings to SQLite by using dart:ffi
3-
version: 2.9.3
3+
version: 2.9.4
44
homepage: https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3
55
issue_tracker: https://github.com/simolus3/sqlite3.dart/issues
66
resolution: workspace

sqlite3_web/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.4.0
2+
3+
- Remove `userVersion` and `setUserVersion`. Users should run the pragma statements manually.
4+
- Add `requestLock`, which can be used to make multiple database calls in a locked context.
5+
- Allow aborting requests.
6+
- Add support for a new Web FS (OPFS) access mode based on `readwrite-unsafe`.
7+
18
## 0.3.2
29

310
- Allow workers to send requests to clients. Previously, only the other

sqlite3_web/README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ And that is what this package can do for you:
2828
## Getting started
2929

3030
Note: While this package can be used by end applications, it is meant as a
31-
building block for database packages like `sqlite3_async` or `drift`. Using
31+
building block for database packages like `sqlite_async` or `drift`. Using
3232
these packages helps avoid some setup work.
3333

3434
Workers are responsible for opening databases and exposing them through message
@@ -75,6 +75,36 @@ Future<void> connectToDatabase() async {
7575
}
7676
```
7777

78+
## Lock and transaction management
79+
80+
This package provides the `select` and `execute` method returning:
81+
82+
1. The `autocommit` state of the database after running the statement.
83+
2. The last insert rowid value.
84+
3. For `select`, results.
85+
86+
Because each database typically only uses a single connection (likely hosted in a shared worker),
87+
implementing transactions requires multiple statements to run without interference from others.
88+
89+
The `requestLock` API is a helpful building block for this. In its callback, no other tab will
90+
have access to the database:
91+
92+
```dart
93+
await db.requestLock((lock) async {
94+
await db.execute('BEGIN', token: lock);
95+
await db.execute(
96+
'... other statements',
97+
token: lock,
98+
checkInTransaction: true,
99+
);
100+
await db.execute('COMMIT', token: lock, checkInTransaction: true);
101+
});
102+
```
103+
104+
Inside a lock context, the `LockToken` must be passed to `select` and `execute`.
105+
Additionally, the `checkInTransaction` flag can be used to verify that the database
106+
is not in autocommit mode before running a statement.
107+
78108
## Custom requests
79109

80110
In some cases, it may be useful to re-use the worker communication scheme
@@ -86,3 +116,10 @@ open transactions can be implemented in the worker by overriding
86116
`handleCustomRequest` in `WorkerDatabase`. You can encode requests and
87117
responses as arbitrary values exchanged as `JSAny?`.
88118
On the client side, requests can be issued with `Database.customRequest`.
119+
120+
## Testing this package
121+
122+
This package uses both regular `dart test` tests and integration tests.
123+
All tests need a compatible `sqlite3.wasm` file in `web/`.
124+
125+
Integration tests also require `geckodriver` and `chromedriver` to be installed.

sqlite3_web/example/index.html

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<meta name="viewport" content="width=device-width, initial-scale=1.0">
88
<meta name="scaffolded-by" content="https://github.com/dart-lang/sdk">
99
<title>sqlite on the web</title>
10-
<script defer src="main.dart.js"></script>
10+
<script defer src="main.js"></script>
1111
</head>
1212

1313
<body>
@@ -17,28 +17,26 @@ <h1>sqlite3 web demo</h1>
1717
This demo can be used to open databases in different storage implementations and
1818
access modes (e.g. through shared or dedicated workers).
1919

20-
To open a database, open the consule and run <code>await open('name', 'storage', 'access')</code>.
21-
Supported values for <code>storage</code> are:
20+
To open a database, open the consule and run <code>await open('name', 'implementation')</code>.
21+
Supported values for <code>implementation</code> are:
2222
<ul>
23-
<li><code>opfs</code>: Uses the <a href="https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system">Origin private file system</a></li>
24-
<li><code>indexedDb</code>: Uses a virtualized file system implemented by storing chunks in IndexedDB</li>
25-
<li><code>inMemory</code>: Only stores files in memory without persistence.</li>
26-
</ul>
27-
28-
Supported values for <code>access</code> are:
29-
30-
<ul>
31-
<li><code>throughSharedWorker</code>: Open the database in a shared worker. Different tabs will use the same sqlite3 connection</li>
32-
<li><code>throughDedicatedWorker</code>: Open the database in a dedicated worker. Requires shared array buffers when used with OPFS, and there is no synchronization for IndexedDB in dedicated workers.</li>
33-
<li><code>inCurrentContext</code>: Open the database in the current context, without a worker</li>
23+
<li><code>inMemoryLocal</code>: Only stores files in memory without persistence.</li>
24+
<li><code>inMemoryShared</code>: In-memory database, but shared across tabs with a shared worker.</li>
25+
<li><code>indexedDbUnsafeLocal</code>: Store data in IndexedDB, using each tab unisolated access to the database. Because data is just read once on startup, this is not a good file system implementation and prone to data corruption when used across multiple tabs.</li>
26+
<li><code>indexedDbUnsafeWorker</code>: Like local, but in a shared worker.</li>
27+
<li><code>indexedDbShared</code>: Safely store data in IndexedDB with a shared worker. Limited durability guarantees compared to OPFS.</li>
28+
29+
<li><code>opfsWithExternalLocks</code>: Uses the <a href="https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system">Origin private file system</a>. This uses the experimental <code>readwrite-unsafe</code> mode only available on Chrome.</li>
30+
<li><code>opfsAtomics</code>: OPFS storage, but uses a pair of dedicated workers using shared memory and atomics to syncify requests. This requires COOP + COEP headers.</li>
31+
<li><code>opfsShared</code>: OPFS storage, using a dedicated worker in a shared worker. This is only supported on Firefox.</li>
3432
</ul>
3533

3634
After opening a database, you can use <code>execute</code> in the console to run
3735
SQL on it, e.g:
3836

3937
<code>
4038
<pre>
41-
let db = await open('test', 'inMemory', 'throughSharedWorker');
39+
let db = await open('test', 'opfsWithExternalLocks');
4240
await execute(db, 'create table foo (bar);');
4341
</pre>
4442
</code>

sqlite3_web/example/main.dart

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@ void main() async {
1212
final features = await sqlite.runFeatureDetection();
1313
print('got features: $features');
1414

15-
globalContext['open'] =
16-
(JSString name, JSString storage, JSString accessMode) {
15+
globalContext['open'] = (JSString name, JSString implementation) {
1716
return Future(() async {
1817
final database = await sqlite.connect(
19-
name.toDart,
20-
StorageMode.values.byName(storage.toDart),
21-
AccessMode.values.byName(accessMode.toDart));
18+
name.toDart,
19+
DatabaseImplementation.values.byName(implementation.toDart),
20+
);
2221

2322
database.updates.listen((update) {
2423
print('Update on $name: $update');

sqlite3_web/example/main.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
(async () => {
2+
const thisScript = document.currentScript;
3+
const params = new URL(document.location.toString()).searchParams;
4+
const wasmOption = params.get("wasm");
5+
6+
function relativeURL(ref) {
7+
const base = thisScript?.src ?? document.baseURI;
8+
return new URL(ref, base).toString();
9+
}
10+
11+
if (wasmOption == "1") {
12+
let { compileStreaming } = await import("./main.mjs");
13+
14+
let app = await compileStreaming(fetch(relativeURL("main.wasm")));
15+
let module = await app.instantiate({});
16+
module.invokeMain();
17+
} else {
18+
const scriptTag = document.createElement("script");
19+
scriptTag.type = "application/javascript";
20+
scriptTag.src = relativeURL("./main.dart2js.js");
21+
document.head.append(scriptTag);
22+
}
23+
})();

sqlite3_web/lib/protocol_utils.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ List<Object?> deserializeParameters(JSArray values, JSArrayBuffer? types) {
2828
/// Serializes a [ResultSet] into a serializable [JSObject].
2929
JSObject serializeResultSet(ResultSet resultSet) {
3030
final msg = JSObject();
31-
RowsResponse(resultSet: resultSet, requestId: 0).serialize(msg, []);
31+
RowsResponse.serializeResultSet(msg, [], resultSet);
3232
return msg;
3333
}
3434

3535
/// Deserializes a result set from the format in [serializeResultSet].
3636
ResultSet deserializeResultSet(JSObject object) {
37-
return RowsResponse.deserialize(object).resultSet;
37+
return RowsResponse.deserializeResultSet(object);
3838
}

0 commit comments

Comments
 (0)