Add web compatibility information to web page

This commit is contained in:
Simon Binder 2023-05-31 23:48:13 +02:00
parent ff41de2a6b
commit ec5927bc2a
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
18 changed files with 188 additions and 122 deletions

View File

@ -93,6 +93,25 @@ targets:
- "lib/snippets/**"
- "tool/snippets.dart"
worker:
dependencies: ['$default']
auto_apply_builders: false
sources:
- "web/worker.dart"
builders:
build_web_compilers:entrypoint:
options:
compiler: dart2js
build_web_compilers:dart2js_archive_extractor:
enabled: false
dart2js_archives:
auto_apply_builders: false
dependencies: [":$default", ":worker"]
builders:
build_web_compilers:dart2js_archive_extractor:
enabled: true
$default:
dependencies: [":codegen", ":syntax_highlighting"]
builders:
@ -100,6 +119,9 @@ targets:
release_options:
environment: "preview"
build_web_compilers:entrypoint:
generate_for:
exclude:
- "web/worker.dart"
release_options:
# Turn of null assertions for release builds, it looks like this
# makes generated code slightly smaller.
@ -116,15 +138,18 @@ targets:
enabled: false
json_serializable:
enabled: false
build_web_compilers:dart2js_archive_extractor:
enabled: false
sources:
- "lib/**"
- "pages/**"
- "templates/**"
- "web/**"
- "$package$"
- "pubspec.yaml"
- "theme.yaml"
- "website.yaml"
include:
- "lib/**"
- "pages/**"
- "templates/**"
- "web/**"
- "$package$"
- "pubspec.yaml"
- "theme.yaml"
- "website.yaml"
# Snippets referencing public API members from this package will get dartdoc links
# embedded in them.

View File

@ -1,24 +0,0 @@
import 'package:drift/drift.dart';
import 'package:drift/wasm.dart';
import 'package:drift/web/worker.dart';
import 'package:sqlite3/wasm.dart';
void main() {
driftWorkerMain(() {
return LazyDatabase(() async {
// You can use a different OPFS path here is you need more than one
// persisted database in your app.
final fileSystem = await OpfsFileSystem.loadFromStorage('my_database');
final sqlite3 = await WasmSqlite3.loadFromUrl(
// Uri where you're hosting the wasm bundle for sqlite3
Uri.parse('/sqlite3.wasm'),
environment: SqliteEnvironment(fileSystem: fileSystem),
);
// The path here should always be `database` since that is the only file
// persisted by the OPFS file system.
return WasmDatabase(sqlite3: sqlite3, path: 'database');
});
});
}

View File

@ -1,19 +0,0 @@
import 'package:drift/drift.dart';
import 'package:drift/wasm.dart';
import 'package:sqlite3/wasm.dart';
QueryExecutor connect() {
return LazyDatabase(() async {
// Create virtual filesystem for sqlite3 implemented over blobs stored in an
// IndexedDB database (named `my_app` here).
final fs = await IndexedDbFileSystem.open(dbName: 'my_app');
final sqlite3 = await WasmSqlite3.loadFromUrl(
Uri.parse('sqlite3.wasm'),
environment: SqliteEnvironment(fileSystem: fs),
);
// Then, open a database:
return WasmDatabase(sqlite3: sqlite3, path: '/app.db');
});
}

View File

@ -6,9 +6,20 @@ template: layouts/docs/single
path: web/
---
You can experimentally use drift in Dart webapps. Drift web supports
You can experimentally use drift in Dart webapps. Drift web supports
Flutter Web, AngularDart, plain `dart:html` or any other web framework.
## Compatibility check
To quickly check which storage implementation drift would chose, the drift worker is also embedded
on this page. You can click on this button to see the results.
<button class="btn btn-light" id="drift-compat-btn">Check compatibility</button>
<pre id="drift-compat-results">
Compatibility check not started yet
</pre>
## Getting started
From a perspective of the Dart code used, drift on the web is similar to drift on other platforms.
@ -261,9 +272,6 @@ Instead, grab a sqlite3.wasm file from the [releases](https://github.com/simolus
With this setup, sqlite3 can be used on the web without an external library:
{% assign snippets = 'package:drift_docs/snippets/engines/web_wasm.dart.excerpt.json' | readString | json_decode %}
{% include "blocks/snippet" snippets = snippets %}
This snippet also works in a service worker.
If you're running into any issues with the new backend, please post them [here](https://github.com/simolus3/sqlite3.dart/issues).

View File

@ -79,8 +79,6 @@ write the worker yourself and compile it to JavaScript.
The worker's source could be put into `web/database_worker.dart` and have a structure like the following:
{% assign worker = 'package:drift_docs/snippets/engines/new_worker.dart.excerpt.json' | readString | json_decode %}
{% include "blocks/snippet" snippets = worker %}
Drift will detect whether the worker is running as a shared or as a dedicated worker and call the callback to open the
@ -119,8 +117,6 @@ when in doubt, `DriftWorkerMode.dedicatedInShared` is a good default.
To spawn and connect to such a web worker, drift provides the `connectToDriftWorker` method:
{% assign snippets = 'package:drift_docs/snippets/engines/new_connect.dart.excerpt.json' | readString | json_decode %}
{% include "blocks/snippet" snippets = snippets name = "approach1" %}
The returned `DatabaseConnection` can be passed to the constructor of a generated database class.

View File

@ -17,7 +17,7 @@ dependencies:
version: ^0.0.12
# used in snippets
http: ^0.13.5
sqlite3: ^1.11.0
sqlite3: ^2.0.0-dev
# Fake path_provider for snippets
path_provider:
path: assets/path_provider

View File

@ -1,3 +1,6 @@
{% if page.path == '/search' %}
<link rel="stylesheet" href="/search.css">
{% endif -%}
{% if page.path == 'web/' %}
<script defer src="compatibility.dart.js"></script>
{% endif %}

1
docs/web/sqlite3.wasm Symbolic link
View File

@ -0,0 +1 @@
../../extras/assets/sqlite3.wasm

View File

@ -0,0 +1,30 @@
import 'dart:html';
import 'package:drift/wasm.dart';
void main() async {
final btn = querySelector('#drift-compat-btn')!;
final results = querySelector('#drift-compat-results')!;
await for (final click in btn.onClick) {
btn.attributes['disabled'] = 'true';
results.innerText = '';
try {
final db = await WasmDatabase.open(
databaseName: 'test_db',
sqlite3Uri: Uri.parse('/sqlite3.wasm'),
driftWorkerUri: Uri.parse('/worker.dart.js'),
);
results.innerText += '''
Chosen implementation: ${db.chosenImplementation}
Features missing: ${db.missingFeatures}
''';
} catch (e, s) {
results.innerText += 'Error: $e, Trace: \n$s';
} finally {
btn.attributes.remove('disabled');
}
}
}

5
docs/web/worker.dart Normal file
View File

@ -0,0 +1,5 @@
import 'package:drift/wasm.dart';
void main() {
WasmDatabase.workerMainForOpen();
}

View File

@ -63,15 +63,19 @@ Future<WasmDatabaseResult> openWasmDatabase({
StreamQueue(port.onMessage.map(WasmInitializationMessage.read));
// First, the shared worker will tell us which features it supports.
final sharedFeatures = await sharedMessages.next as SharedWorkerStatus;
final sharedFeatures =
await sharedMessages.nextNoError as SharedWorkerStatus;
missingFeatures.addAll(sharedFeatures.missingFeatures);
// Can we use the shared OPFS implementation?
if (sharedFeatures.canSpawnDedicatedWorkers &&
sharedFeatures.dedicatedWorkersCanUseOpfs) {
return connect(
WasmStorageImplementation.opfsShared, (msg) => msg.sendToPort(port));
} else if (sharedFeatures.canUseIndexedDb) {
return connect(WasmStorageImplementation.sharedIndexedDb,
(msg) => msg.sendToPort(port));
} else {
missingFeatures.addAll(sharedFeatures.missingFeatures);
await sharedMessages.cancel();
port.close();
}
@ -86,7 +90,7 @@ Future<WasmDatabaseResult> openWasmDatabase({
dedicatedWorker.onMessage.map(WasmInitializationMessage.read));
final status =
await workerMessages.next as DedicatedWorkerCompatibilityResult;
await workerMessages.nextNoError as DedicatedWorkerCompatibilityResult;
missingFeatures.addAll(status.missingFeatures);
if (status.supportsNestedWorkers &&
@ -112,3 +116,15 @@ Future<WasmDatabaseResult> openWasmDatabase({
);
}
}
extension on StreamQueue<WasmInitializationMessage> {
Future<WasmInitializationMessage> get nextNoError {
return next.then((value) {
if (value is WorkerError) {
throw value;
}
return value;
});
}
}

View File

@ -3,21 +3,15 @@
import 'dart:async';
import 'dart:html';
import 'package:drift/drift.dart';
import 'package:drift/remote.dart';
import 'package:drift/wasm.dart';
import 'package:js/js_util.dart';
import 'package:sqlite3/wasm.dart';
import '../channel.dart';
import 'protocol.dart';
import 'shared.dart';
class DedicatedDriftWorker {
final DedicatedWorkerGlobalScope self;
/// Running drift servers by the name of the database they're serving.
final Map<String, DriftServer> _servers = {};
final DriftServerController _servers = DriftServerController();
DedicatedDriftWorker(this.self);
@ -42,29 +36,7 @@ class DedicatedDriftWorker {
hasProperty(globalThis, 'SharedArrayBuffer'),
).sendToClient(self);
case ServeDriftDatabase():
final server = _servers.putIfAbsent(message.databaseName, () {
return DriftServer(LazyDatabase(() async {
final sqlite3 =
await WasmSqlite3.loadFromUrl(message.sqlite3WasmUri);
final vfs = await switch (message.storage) {
WasmStorageImplementation.opfsShared =>
SimpleOpfsFileSystem.loadFromStorage(
'/drift_db/${message.databaseName}'),
WasmStorageImplementation.opfsLocks => _loadLockedWasmVfs(),
WasmStorageImplementation.unsafeIndexedDb =>
IndexedDbFileSystem.open(dbName: message.databaseName),
WasmStorageImplementation.inMemory =>
Future.value(InMemoryFileSystem()),
};
sqlite3.registerVirtualFileSystem(vfs, makeDefault: true);
return WasmDatabase(sqlite3: sqlite3, path: '/database');
}));
});
server.serve(message.port.channel());
_servers.serve(message);
case StartFileSystemServer(sqlite3Options: final options):
final worker = await VfsWorker.create(options);
await worker.start();
@ -72,17 +44,4 @@ class DedicatedDriftWorker {
break;
}
}
Future<WasmVfs> _loadLockedWasmVfs() async {
// Create SharedArrayBuffers to synchronize requests
final options = WasmVfs.createOptions();
final worker = Worker(Uri.base.toString());
StartFileSystemServer(options).sendToWorker(worker);
// Wait for the server worker to report that it's ready
await worker.onMessage.first;
return WasmVfs(workerOptions: options);
}
}

View File

@ -62,10 +62,12 @@ final class SharedWorkerStatus extends WasmInitializationMessage {
final bool canSpawnDedicatedWorkers;
final bool dedicatedWorkersCanUseOpfs;
final bool canUseIndexedDb;
SharedWorkerStatus({
required this.canSpawnDedicatedWorkers,
required this.dedicatedWorkersCanUseOpfs,
required this.canUseIndexedDb,
});
factory SharedWorkerStatus.fromJsPayload(Object payload) {
@ -74,6 +76,7 @@ final class SharedWorkerStatus extends WasmInitializationMessage {
return SharedWorkerStatus(
canSpawnDedicatedWorkers: data[0],
dedicatedWorkersCanUseOpfs: data[1],
canUseIndexedDb: data[2],
);
}
@ -82,22 +85,21 @@ final class SharedWorkerStatus extends WasmInitializationMessage {
sender.sendTyped(type, [
canSpawnDedicatedWorkers,
dedicatedWorkersCanUseOpfs,
canUseIndexedDb
]);
}
Iterable<MissingBrowserFeature> get missingFeatures sync* {
if (!canSpawnDedicatedWorkers) {
yield MissingBrowserFeature.dedicatedWorkersInSharedWorkers;
}
if (!dedicatedWorkersCanUseOpfs) {
} else if (!dedicatedWorkersCanUseOpfs) {
yield MissingBrowserFeature.fileSystemAccess;
}
}
}
/// A message sent by a worker when an error occurred.
final class WorkerError extends WasmInitializationMessage {
final class WorkerError extends WasmInitializationMessage implements Exception {
static const type = 'Error';
final String error;
@ -112,6 +114,11 @@ final class WorkerError extends WasmInitializationMessage {
void _send(_PostMessage sender) {
sender.sendTyped(type, error);
}
@override
String toString() {
return 'Error in worker: $error';
}
}
/// Instructs a dedicated or shared worker to serve a drift database connection

View File

@ -1,6 +1,15 @@
import 'dart:html';
import 'package:drift/drift.dart';
import 'package:drift/remote.dart';
import 'package:drift/wasm.dart';
import 'package:js/js_util.dart';
// ignore: implementation_imports
import 'package:sqlite3/src/wasm/js_interop/file_system_access.dart';
import 'package:sqlite3/wasm.dart';
import '../channel.dart';
import 'protocol.dart';
/// Checks whether the OPFS API is likely to be correctly implemented in the
/// current browser.
@ -48,3 +57,47 @@ Future<bool> checkOpfsSupport() async {
Future<bool> checkIndexedDbSupport() async {
return true;
}
class DriftServerController {
/// Running drift servers by the name of the database they're serving.
final Map<String, DriftServer> _servers = {};
void serve(ServeDriftDatabase message) {
final server = _servers.putIfAbsent(message.databaseName, () {
return DriftServer(LazyDatabase(() async {
final sqlite3 = await WasmSqlite3.loadFromUrl(message.sqlite3WasmUri);
final vfs = await switch (message.storage) {
WasmStorageImplementation.opfsShared =>
SimpleOpfsFileSystem.loadFromStorage(
'/drift_db/${message.databaseName}'),
WasmStorageImplementation.opfsLocks => _loadLockedWasmVfs(),
WasmStorageImplementation.unsafeIndexedDb ||
WasmStorageImplementation.sharedIndexedDb =>
IndexedDbFileSystem.open(dbName: message.databaseName),
WasmStorageImplementation.inMemory =>
Future.value(InMemoryFileSystem()),
};
sqlite3.registerVirtualFileSystem(vfs, makeDefault: true);
return WasmDatabase(sqlite3: sqlite3, path: '/database');
}));
});
server.serve(message.port.channel());
}
Future<WasmVfs> _loadLockedWasmVfs() async {
// Create SharedArrayBuffers to synchronize requests
final options = WasmVfs.createOptions();
final worker = Worker(Uri.base.toString());
StartFileSystemServer(options).sendToWorker(worker);
// Wait for the server worker to report that it's ready
await worker.onMessage.first;
return WasmVfs(workerOptions: options);
}
}

View File

@ -6,6 +6,7 @@ import 'package:drift/wasm.dart';
import 'package:js/js_util.dart';
import 'protocol.dart';
import 'shared.dart';
class SharedDriftWorker {
final SharedWorkerGlobalScope self;
@ -15,6 +16,8 @@ class SharedDriftWorker {
Worker? _dedicatedWorker;
Future<SharedWorkerStatus>? _featureDetection;
final DriftServerController _servers = DriftServerController();
SharedDriftWorker(this.self);
void start() {
@ -30,8 +33,8 @@ class SharedDriftWorker {
try {
final result = await detectionFuture;
result.sendToPort(clientPort);
} catch (e) {
WorkerError(e.toString()).sendToPort(clientPort);
} catch (e, s) {
WorkerError(e.toString() + s.toString()).sendToPort(clientPort);
}
clientPort.onMessage
@ -43,6 +46,12 @@ class SharedDriftWorker {
final message = WasmInitializationMessage.read(event);
switch (message) {
case ServeDriftDatabase(
storage: WasmStorageImplementation.sharedIndexedDb
):
// The shared indexed db implementation can be hosted directly in this
// worker.
_servers.serve(message);
case ServeDriftDatabase():
// Forward the request to the worker - this will also transfer the
// port which means that the shared worker is not involved in the
@ -60,17 +69,19 @@ class SharedDriftWorker {
Future<SharedWorkerStatus> _startFeatureDetection() async {
// First, let's see if this shared worker can spawn dedicated workers.
final hasWorker = hasProperty(self, 'Worker');
final canUseIndexedDb = await checkIndexedDbSupport();
if (!hasWorker) {
return SharedWorkerStatus(
canSpawnDedicatedWorkers: false,
dedicatedWorkersCanUseOpfs: false,
canUseIndexedDb: canUseIndexedDb,
);
} else {
final worker = _dedicatedWorker = Worker(Uri.base.toString());
// Ask the worker about the storage implementations it can support.
worker.postMessage(DedicatedWorkerCompatibilityCheck());
DedicatedWorkerCompatibilityCheck().sendToWorker(worker);
final completer = Completer<SharedWorkerStatus>();
StreamSubscription? messageSubscription, errorSubscription;
@ -80,6 +91,7 @@ class SharedDriftWorker {
completer.complete(SharedWorkerStatus(
canSpawnDedicatedWorkers: true,
dedicatedWorkersCanUseOpfs: result,
canUseIndexedDb: canUseIndexedDb,
));
messageSubscription?.cancel();

View File

@ -215,6 +215,8 @@ enum WasmStorageImplementation {
/// [cross-origin isolation]: https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated
opfsLocks,
sharedIndexedDb,
/// Uses the asynchronous IndexedDB API outside of any worker to persist data.
///
/// Unlike [opfsShared] or [opfsLocks], this storage implementation can't

View File

@ -15,7 +15,7 @@ dependencies:
js: ^0.6.3
meta: ^1.3.0
stream_channel: ^2.1.0
sqlite3: ^2.0.0-dev
sqlite3: ^2.0.0-dev.1
dev_dependencies:
archive: ^3.3.1
@ -37,7 +37,3 @@ dev_dependencies:
shelf: ^1.3.0
stack_trace: ^1.10.0
test_descriptor: ^2.0.1
dependency_overrides:
sqlite3:
path: /home/simon/src/sqlite3.dart/sqlite3

View File

@ -39,7 +39,3 @@ dev_dependencies:
flutter:
uses-material-design: true
dependency_overrides:
sqlite3:
path: /home/simon/src/sqlite3.dart/sqlite3