Simplify creation of web workers for drift

This commit is contained in:
Simon Binder 2023-04-10 22:58:15 +02:00
parent cb32b3418b
commit b4b4e350dd
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
26 changed files with 615 additions and 150 deletions

View File

@ -0,0 +1,11 @@
import 'package:drift/drift.dart';
import 'package:drift/web/worker.dart';
class Approach1 {
// #docregion approach1
Future<DatabaseConnection> connectToWorker() async {
return await connectToDriftWorker('/database_worker.dart.js',
mode: DriftWorkerMode.dedicatedInShared);
}
// #enddocregion approach1
}

View File

@ -0,0 +1,24 @@
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,6 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift/wasm.dart';
import 'package:http/http.dart' as http;
import 'package:sqlite3/wasm.dart';
QueryExecutor connect() {
@ -9,11 +8,9 @@ QueryExecutor connect() {
// IndexedDB database (named `my_app` here).
final fs = await IndexedDbFileSystem.open(dbName: 'my_app');
// Load wasm bundle for sqlite3
final response = await http.get(Uri.parse('sqlite3.wasm'));
final sqlite3 = await WasmSqlite3.load(
response.bodyBytes,
SqliteEnvironment(fileSystem: fs),
final sqlite3 = await WasmSqlite3.loadFromUrl(
Uri.parse('sqlite3.wasm'),
environment: SqliteEnvironment(fileSystem: fs),
);
// Then, open a database:

View File

@ -0,0 +1,28 @@
// #docregion worker
import 'dart:html';
import 'package:drift/drift.dart';
import 'package:drift/web.dart';
import 'package:drift/web/worker.dart';
void main() {
// Load sql.js library in the worker
WorkerGlobalScope.instance.importScripts('sql-wasm.js');
// Call drift function that will set up this worker
driftWorkerMain(() {
return WebDatabase.withStorage(DriftWebStorage.indexedDb('worker',
migrateFromLocalStorage: false, inWebWorker: true));
});
}
// #enddocregion worker
// #docregion client
DatabaseConnection connectToWorker() {
return DatabaseConnection.delayed(connectToDriftWorker(
'worker.dart.js',
// Note that SharedWorkers may not be available on all browsers and platforms.
mode: DriftWorkerMode.shared,
));
}
// #enddocregion client

View File

@ -150,49 +150,21 @@ the regular implementation.
The following example is meant to be used with a regular Dart web app, compiled using
[build_web_compilers](https://pub.dev/packages/build_web_compilers).
A Flutter port of this example is [part of the drift repository](https://github.com/simolus3/drift/tree/develop/examples/flutter_web_worker_example).
To write a web worker that will serve requests for drift, create a file called `worker.dart` in
To write a web worker that will serve requests for drift, create a file called `worker.dart` in
the `web/` folder of your app. It could have the following content:
```dart
import 'dart:html';
{% assign workers = 'package:drift_docs/snippets/engines/workers.dart.excerpt.json' | readString | json_decode %}
import 'package:drift/drift.dart';
import 'package:drift/web.dart';
import 'package:drift/remote.dart';
void main() {
final self = SharedWorkerGlobalScope.instance;
self.importScripts('sql-wasm.js');
final db = WebDatabase.withStorage(DriftWebStorage.indexedDb('worker',
migrateFromLocalStorage: false, inWebWorker: true));
final server = DriftServer(DatabaseConnection(db));
self.onConnect.listen((event) {
final msg = event as MessageEvent;
server.serve(msg.ports.first.channel());
});
}
```
{% include "blocks/snippet" snippets = workers name = "worker" %}
For more information on this api, see the [remote API](https://pub.dev/documentation/drift/latest/remote/remote-library.html).
Connecting to that worker is very simple with drift's web and remote apis. In your regular app code (outside of the worker),
you can connect like this:
```dart
import 'dart:html';
import 'package:drift/remote.dart';
import 'package:drift/web.dart';
import 'package:web_worker_example/database.dart';
DatabaseConnection connectToWorker() {
final worker = SharedWorker('worker.dart.js');
return remote(worker.port!.channel());
}
```
{% include "blocks/snippet" snippets = workers name = "client" %}
You can then open a drift database with that connection.
For more information on the `DatabaseConnection` class, see the documentation on

View File

@ -0,0 +1,157 @@
---
data:
title: Web draft
description: Draft for upcoming stable Drift web support.
hidden: true
template: layouts/docs/single
---
This draft document describes different approaches allowing drift to run on the
web.
After community feedback, this restructured page will replace the [existing web documentation]({{ 'web.md' | pageUrl }}).
## Introduction
Drift first gained its initial web support in 2019 by wrapping the sql.js JavaScript library.
This implementation, which is still supported today, relies on keeping an in-memory database that is periodically saved to local storage.
In the last years, development in web browsers and the Dart ecosystem enabled more performant approaches that are
unfortunately impossible to implement with the original drift web API.
This is the reason the original API is still considered experimental - while it will continue to be supported, it is now obvious
that there are better approaches coming up.
This page describes the fundamental challenges and required browser features used to efficiently run drift on the web.
It presents a guide on the current and most reliable approach to bring sqlite3 to the web, but older implementations
and approaches to migrate between them are still supported and documented as well.
## Setup
The recommended solution to run drift on the web is to use
- The File System Access API with an Origin-private File System (OPFS) for storing data, and
- shared web workers to share the database between multiple tabs.
Drift and the `sqlite3` Dart package provide helpers to use those OPFS and shared web workers
easily.
However, even though both web APIs are suppported in most browsers, they are still relatively new and your app
should handle them not being available. Drift provides a feature-detection API which you can use to warn your
users if persistence is unavailable - see the caveats section for details.
{% block "blocks/alert" title="Caveats" color = "warning" %}
Most browsers support both APIs today, with two notable exceptions:
- Chrome on Android does not support shared web workers.
- The stable version of Safari currently implements an older verison of the File System Access Standard.
This has been fixed in Technology Preview builds.
The File System Access API, or other persistence APIs are sometimes disabled in private or incognito tabs too.
You need to consider different fallbacks that you may want to support:
- If the File System Access API is not available, you may want to fall back to a different persistence layer like IndexedDb, silently use an in-memory database
only or warn the user about these circumstances. Note that, even in modern browsers, persistence may be blocked in private/incognito tabs.
- If shared workers are not available, you can still safely use the database, but not if multiple tabs of your web app are opened.
You could use [Web Locks](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API) to detect whether another instance of your
database is currently open and inform the user about this.
The [Flutter app example](https://github.com/simolus3/drift/tree/develop/examples/app) which is part of the Drift repository implements all
of these fallbacks.
Snippets to detect these error conditions are provided on this website, but the integration with fallbacks or user-visible warnings depends
on the structure of your app in the end.
{% endblock %}
### Ressources
First, you'll need a version of sqlite3 that has been compiled to WASM and is ready to use Dart bindings for its IO work.
You can grab this `sqlite3.wasm` file from the [GitHub releases](https://github.com/simolus3/sqlite3.dart/releases) of the sqlite3 package,
or [compile it yourself](https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3#compiling).
You can host this file on a CDN, or just put it in the `web/` folder of your Flutter app so that it is part of the final bundle.
It is important that your web server serves the file with `Content-Type: application/wasm`. Browsers will refuse to load it otherwise.
### Drift web worker
Since OPFS is only available in dedicated web workers, you need to define a worker responsible for hosting the database in its thread.
The main tab will connect to that worker to access the database with a communication protocol handled by drift.
In its `web/worker.dart` library, Drift provies a suitable entrypoint for both shared and dedicated web workers hosting a sqlite3
database. It takes a callback creating the actual database connection. Drift will be responsible for creating the worker in the
right configuration.
But since the worker depends on the way you set up the database, we can't ship a precompiled worker JavaScript file. You need to
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
database at a suitable time.
How to compile the worker depends on your build setup:
1. With regular Dart web apps, you're likely using `build_web_compilers` with `build_runner` or `webdev` already.
This build system can compile workers too.
[This build configuration](https://github.com/simolus3/drift/blob/develop/examples/web_worker_example/build.yaml) shows
how to configure `build_web_compilers` to always compile a worker with `dart2js`.
2. With Flutter wep apps, you can either use `build_web_compilers` too (since you're already using `build_runner` for
drift), or compile the worker with `dart compile js`. When using `build_web_compilers`, explicitly enable `dart2js`
or run the build with `--release`.
Make sure to always use `dart2js` (and not `dartdevc`) to compile a web worker, since modules emitted by `dartdevc` are
not directly supported in web workers.
#### Worker mode
Depending on the storage implementation you use in your app, different worker topologies can be used.
when in doubt, `DriftWorkerMode.dedicatedInShared` is a good default.
1. If you don't need support for multiple tabs accessing the database at the same time,
you can use `DriftWorkerMode.dedicated` which does not spawn a shared web worker.
2. The File System Acccess API can only be accessed in dedicated workers, which is why `DriftWorkerMode.dedicatedInShared`
is used. If you use a different file system implementation (like one based on IndexedDB), `DriftWorkerMode.shared`
is sufficient.
| Dedicated | Shared | Dedicated in shared |
|-----------|--------|---------------------|
| ![](dedicated.png) | ![](shared.png) | ![](dedicated_in_shared.png) |
| Each tab uses its own worker with an independent database. | A single worker hosting the database is used across tabs | Like "shared", except that the shared worker forwards requests to a dedicated worker. |
### Using the database
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.
## Technology challenges
Drift wraps [sqlite3](https://sqlite.org/index.html), a popular relational database written as a C library.
On native platforms, we can use `dart:ffi` to efficiently bind to C libraries. This is what a `NativeDatabase` does internally,
it gives us efficient and synchronous access to sqlite3.
On the web, C libraries can be compiled to [WebAssembly](https://webassembly.org/), a native-like low-level language.
While C code can be compiled to WebAssembly, there is no builtin support for file IO which would be required for a database.
This functionality needs to be implemented in JavaScript (or, in our case, in Dart).
For a long time, the web platform lacked a suitable persistence solution that could be used to give sqlite3 access to the
file system:
- Local storage is synchronous, but can't efficiently store binary data. Further, we can't efficiently change a portion of the
data stored in local storage. A one byte write to a 10MB database file requires writing everything again.
- IndexedDb supports binary data and could be used to store chunks of a file in rows. However, it is asynchronous and sqlite3,
being a C library, expects a synchronous IO layer.
- Finally, the newer File System Access API supports synchronous access to app data _and_ synchronous writes.
However, it is only supported in web workers.
Further, a file in this API can only be opened by one JavaScript context at a time.
While we can support asynchronous persistence APIs by keeping an in-memory cache for synchronous reads and simply not awaiting
writes, the direct File System Access API is more promising due to its synchronous nature that doesn't require caching the entire database in memory.
In addition to the persistence problem, there is an issue of concurrency when a user opens multiple tabs of your web app.
Natively, locks in the file system allow sqlite3 to guarantee that multiple processes can access the same database without causing
conflicts. On the web, no synchronous lock API exists between tabs.
## Legacy approaches
### sql.js {#sqljs}

View File

@ -25,7 +25,7 @@ This table list all supported drift implementations and on which platforms they
| `SqfliteQueryExecutor` from `package:drift_sqflite` | Android, iOS | Uses platform channels, Flutter only, no isolate support, doesn't support `flutter test`. Formerly known as `moor_flutter` |
| `NativeDatabase` from `package:drift/native.dart` | Android, iOS, Windows, Linux, macOS | No further setup is required for Flutter users. For support outside of Flutter, or in `flutter test`, see the [desktop](#desktop) section below. Usage in a [isolate]({{ 'Advanced Features/isolates.md' | pageUrl }}) is recommended. Formerly known as `package:moor/ffi.dart`. |
| `WebDatabase` from `package:drift/web.dart` | Web | Works with or without Flutter. A bit of [additional setup]({{ 'Other engines/web.md' | pageUrl }}) is required. |
| `WasmDatabase` from `package:drift/web.dart` | Web | Potentially faster than a `WebDatabase`, but still experimental and not yet production ready. See [this]({{ 'Other engines/web.md#drift-wasm' | pageUrl }}) for details. |
| `WasmDatabase` from `package:drift/web.dart` | Web | Potentially faster than a `WebDatabase`, but still experimental and not yet production ready. See [this]({{ 'Other engines/web2.md' | pageUrl }}) for details. |
To support all platforms in a shared codebase, you only need to change how you open your database, all other usages can stay the same.
[This repository](https://github.com/simolus3/drift/tree/develop/examples/app) gives an example on how to do that with conditional imports.

View File

@ -14,10 +14,10 @@ dependencies:
version: ^0.2.2
code_snippets:
hosted: https://simonbinder.eu
version: ^0.0.11
version: ^0.0.12
# used in snippets
http: ^0.13.5
sqlite3: ^1.7.2
sqlite3: ^1.11.0
# Fake path_provider for snippets
path_provider:
path: assets/path_provider

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,20 @@
import 'dart:html';
import 'package:stream_channel/stream_channel.dart';
/// Extension to transform a raw [MessagePort] from web workers into a Dart
/// [StreamChannel].
extension PortToChannel on MessagePort {
/// Converts this port to a two-way communication channel, exposed as a
/// [StreamChannel].
///
/// This can be used to implement a remote database connection over service
/// workers.
StreamChannel<Object?> channel() {
final controller = StreamChannelController();
onMessage.map((event) => event.data).pipe(controller.local.sink);
controller.local.stream.listen(postMessage, onDone: close);
return controller.foreign;
}
}

View File

@ -5,28 +5,9 @@
@experimental
library drift.web;
import 'dart:html';
import 'package:meta/meta.dart';
import 'package:stream_channel/stream_channel.dart';
export 'src/web/sql_js.dart';
export 'src/web/storage.dart' hide CustomSchemaVersionSave;
export 'src/web/web_db.dart';
/// Extension to transform a raw [MessagePort] from web workers into a Dart
/// [StreamChannel].
extension PortToChannel on MessagePort {
/// Converts this port to a two-way communication channel, exposed as a
/// [StreamChannel].
///
/// This can be used to implement a remote database connection over service
/// workers.
StreamChannel<Object?> channel() {
final controller = StreamChannelController();
onMessage.map((event) => event.data).pipe(controller.local.sink);
controller.local.stream.listen(postMessage, onDone: close);
return controller.foreign;
}
}
export 'src/web/channel.dart';

318
drift/lib/web/worker.dart Normal file
View File

@ -0,0 +1,318 @@
/// Utility functions enabling the use of web workers to accelerate drift on the
/// web.
///
/// For more details on how to use this library, see [the documentation].
///
/// [the documentation]: https://drift.simonbinder.eu/web/#using-web-workers
library drift.web.workers;
import 'dart:async';
import 'dart:html';
import 'package:async/async.dart';
import 'package:drift/drift.dart';
import 'package:drift/remote.dart';
import 'package:drift/src/web/channel.dart';
import 'package:stream_channel/stream_channel.dart';
/// Describes the topology between clients (e.g. tabs) and the drift web worker
/// when spawned with [connectToDriftWorker].
///
/// For more details on the individial modes, see the documentation on
/// [dedicated], [shared] and [dedicatedInShared].
enum DriftWorkerMode {
/// Starts a new, regular web [Worker] when [connectToDriftWorker] is called.
///
/// This worker, which we expect is a Dart program calling [driftWorkerMain]
/// in its `main` function compiled to JavaScript, will open a database
/// connection internally.
/// The connection returned by [connectToDriftWorker] will use a message
/// channel between the initiating tab and this worker to run its operations
/// on the worker, which can take load of the UI tab.
/// However, it is not possible for a worker to be used across different tabs.
/// To do that, [shared] or [dedicatedInShared] needs to be used.
dedicated,
/// Starts a [SharedWorker] that is used across several browsing contexts
/// (e.g. tabs or even a custom worker you wrote).
///
/// This shared worker, which we expect is a Dart program calling
/// [driftWorkerMain] in its `main` function compiled to JavaScript, will open
/// a database connection internally.
/// Just like for [dedicated] connections, the connection returned by
/// [connectToDriftWorker] will use a message channel between the current
/// context and the (potentially existing) shared worker.
///
/// So, while every tab uses its own connection, they all connect to the same
/// shared worker. Thus, every tab has a view of the same logical database.
/// Even stream queries are synchronized across all tabs.
///
/// Note that shared worker may not be supported in all browsers.
shared,
/// This mode generally works very similar to [shared] in the sense that a
/// shared worker is used and that all tabs calling [driftWorkerMain] get
/// a view of the same database with synchronized stream queries.
///
/// However, a technical difference is that the actual database is not opened
/// in the shared worker itself. Instead, the shared worker creates a new
/// [Worker] internally that will host the database and forwards incoming
/// connections to this worker.
/// Generally, it is recommended to use a [shared] worker. However, some
/// database connections, such as the one based on the Origin-private File
/// System web API, is only available in dedicated workers. This setup enables
/// the use of such APIs.
///
/// Note that only Firefox seems to support spawning dedicated workers in
/// shared workers, which makes this option effectively unsupported on Chrome
/// and Safari.
dedicatedInShared;
}
/// A suitable entrypoint for a web worker aiming to make a drift database
/// available to other browsing contexts.
///
/// This function will detect whether it is running in a shared or in a
/// dedicated worker. In either case, the [openConnection] callback is invoked
/// to start a [DriftServer] that will serve drift database requests to clients.
///
/// When running in a shared worker, this function listens to
/// [SharedWorkerGlobalScope.onConnect] events and establishes message channels
/// with connecting clients to share a database.
/// In a dedicated worker, a [DedicatedWorkerGlobalScope.postMessage]-construction
/// is used to establish a communication channel with clients.
/// To connect to this worker, the [connectToDriftWorker] function can be used.
///
/// As an example, a worker file could live in `web/database_worker.dart` and
/// have the following content:
///
/// ```dart
/// import 'dart:html';
///
/// import 'package:drift/drift.dart';
/// import 'package:drift/web.dart';
/// import 'package:drift/web/worker.dart';
///
/// void main() {
/// // Load sql.js library in the worker
/// WorkerGlobalScope.instance.importScripts('sql-wasm.js');
///
/// driftWorkerMain(() {
/// return WebDatabase.withStorage(DriftWebStorage.indexedDb('worker',
/// migrateFromLocalStorage: false, inWebWorker: true));
/// });
/// }
/// ```
///
/// Depending on the build system you use, you can then compile this Dart web
/// worker with `dart compile js`, `build_web_compilers` or other tools.
///
/// The [connectToDriftWorker] method can be used in the main portion of your
/// app to connect to a worker using [driftWorkerMain].
///
/// The [documentation](https://drift.simonbinder.eu/web/#using-web-workers)
/// contains additional information and an example on how to use workers with
/// Dart and Drift.
void driftWorkerMain(QueryExecutor Function() openConnection) {
final self = WorkerGlobalScope.instance;
_RunningDriftWorker worker;
if (self is SharedWorkerGlobalScope) {
worker = _RunningDriftWorker(true, openConnection);
} else if (self is DedicatedWorkerGlobalScope) {
worker = _RunningDriftWorker(false, openConnection);
} else {
throw StateError('This worker is neither a shared nor a dedicated worker');
}
worker.start();
}
/// Spawn or connect to a web worker written with [driftWorkerMain].
///
/// Depending on the [mode] option, this method creates either a regular [Worker]
/// or attaches itself to an [SharedWorker] in the current browsing context.
/// For more details on the different modes, see [DriftWorkerMode]. By default,
/// a dedicated worker will be used ([DriftWorkerMode.dedicated]).
///
/// The [workerJsUri] describes the path to the worker (e.g.
/// `/database_worker.dart.js` if the original Dart file defining the worker is
/// in `web/database_worker.dart`).
///
/// When using a shared worker, the database (including stream queries!) are
/// shared across multiple tabs in realtime.
Future<DatabaseConnection> connectToDriftWorker(String workerJsUri,
{DriftWorkerMode mode = DriftWorkerMode.dedicated}) {
StreamChannel<Object?> channel;
if (mode == DriftWorkerMode.dedicated) {
final worker = Worker(workerJsUri);
final webChannel = MessageChannel();
// Transfer first port to the channel, we'll use the second port on this side.
worker.postMessage(webChannel.port1, [webChannel.port1]);
channel = webChannel.port2.channel();
} else {
final worker = SharedWorker(workerJsUri, 'drift database');
final port = worker.port!;
var didGetInitializationResponse = false;
port.postMessage(mode.name);
channel = port.channel().transformStream(StreamTransformer.fromHandlers(
handleData: (data, sink) {
if (didGetInitializationResponse) {
sink.add(data);
} else {
didGetInitializationResponse = true;
final response = data as bool;
if (response) {
// Initialization ok, all good!
} else {
sink
..addError(StateError(
'Shared worker disagrees with desired mode $mode, is there '
'another tab using `connectToDriftWorker()` in a different '
'mode?'))
..close();
}
}
},
));
}
return connectToRemoteAndInitialize(channel, debugLog: true);
}
class _RunningDriftWorker {
final bool isShared;
final QueryExecutor Function() connectionFactory;
DriftServer? _startedServer;
DriftWorkerMode? _knownMode;
Worker? _dedicatedWorker;
_RunningDriftWorker(this.isShared, this.connectionFactory);
void start() {
if (isShared) {
const event = EventStreamProvider<MessageEvent>('connect');
event.forTarget(self).listen(_newConnection);
} else {
const event = EventStreamProvider<MessageEvent>('message');
event.forTarget(self).map((e) => e.data).listen(_handleMessage);
}
}
DriftServer _establishModeAndLaunchServer(DriftWorkerMode mode) {
_knownMode = mode;
final server = _startedServer = DriftServer(
connectionFactory(),
allowRemoteShutdown: mode == DriftWorkerMode.dedicated,
);
server.done.whenComplete(() {
// The only purpose of this worker is to start the drift server, so if the
// server is done, so is the worker.
if (isShared) {
SharedWorkerGlobalScope.instance.close();
} else {
DedicatedWorkerGlobalScope.instance.close();
}
});
return server;
}
/// Handle a new connection, which implies that this worker is shared.
void _newConnection(MessageEvent event) {
assert(isShared);
final outgoingPort = event.ports.first;
// We still don't know whether this shared worker is supposed to host the
// server itself or whether this is delegated to a dedicated worker managed
// by the shared worker. In our protocol, the client will tell us the
// expected mode in its first message.
final originalChannel = outgoingPort.channel();
StreamSubscription<Object?>? subscription;
StreamChannel<Object?> remainingChannel() {
return originalChannel
.changeStream((_) => SubscriptionStream(subscription!));
}
subscription = originalChannel.stream.listen((first) {
final expectedMode = DriftWorkerMode.values.byName(first as String);
if (_knownMode == null) {
switch (expectedMode) {
case DriftWorkerMode.dedicated:
// This is a shared worker, so this mode won't work
originalChannel.sink
..add(false)
..close();
break;
case DriftWorkerMode.shared:
// Ok, we're supposed to run a drift server in this worker. Let's do
// that then.
final server =
_establishModeAndLaunchServer(DriftWorkerMode.shared);
originalChannel.sink.add(true);
server.serve(remainingChannel());
break;
case DriftWorkerMode.dedicatedInShared:
// Instead of running a server ourselves, we're starting a dedicated
// child worker and forward the port.
_knownMode = DriftWorkerMode.dedicatedInShared;
final worker = _dedicatedWorker = Worker(Uri.base.toString());
// This will call [_handleMessage], but in the context of the
// dedicated worker we just created.
outgoingPort.postMessage(true);
worker.postMessage(outgoingPort, [outgoingPort]);
// This closes the channel, but doesn't close the port since it has
// been transferred to the child worker.
originalChannel.sink.close();
break;
}
} else if (_knownMode == expectedMode) {
outgoingPort.postMessage(true);
switch (_knownMode!) {
case DriftWorkerMode.dedicated:
// This is a shared worker, we won't ever set our mode to this.
throw AssertionError();
case DriftWorkerMode.shared:
_startedServer!.serve(remainingChannel());
break;
case DriftWorkerMode.dedicatedInShared:
_dedicatedWorker!.postMessage(outgoingPort, [outgoingPort]);
originalChannel.sink.close();
break;
}
} else {
// Unsupported mode
originalChannel.sink
..add(false)
..close();
}
});
}
/// Handle an incoming message for a dedicated worker.
void _handleMessage(Object? message) async {
assert(!isShared);
assert(_knownMode != DriftWorkerMode.shared);
if (message is MessagePort) {
final server = _startedServer ??
_establishModeAndLaunchServer(DriftWorkerMode.dedicated);
server.serve(message.channel());
} else {
throw StateError('Received unknown message $message, expected a port');
}
}
static WorkerGlobalScope get self => WorkerGlobalScope.instance;
}

View File

@ -15,7 +15,7 @@ dependencies:
js: ^0.6.3
meta: ^1.3.0
stream_channel: ^2.1.0
sqlite3: ^1.7.1
sqlite3: ^1.11.0
dev_dependencies:
archive: ^3.3.1

View File

@ -1,7 +1,6 @@
@TestOn('browser')
import 'package:drift/wasm.dart';
import 'package:drift_testcases/tests.dart';
import 'package:http/http.dart' as http;
import 'package:sqlite3/wasm.dart';
import 'package:test/test.dart';
@ -37,10 +36,10 @@ void main() {
final channel = spawnHybridUri('/test/test_utils/sqlite_server.dart');
final port = await channel.stream.first as int;
final response =
await http.get(Uri.parse('http://localhost:$port/sqlite3.wasm'));
sqlite3 = await WasmSqlite3.load(
response.bodyBytes, SqliteEnvironment(fileSystem: fs));
sqlite3 = await WasmSqlite3.loadFromUrl(
Uri.parse('http://localhost:$port/sqlite3.wasm'),
environment: SqliteEnvironment(fileSystem: fs),
);
});
runAllTests(DriftWasmExecutor(fs, () => sqlite3));

View File

@ -1,6 +1,5 @@
import 'package:drift/drift.dart';
import 'package:drift/wasm.dart';
import 'package:http/http.dart' as http;
import 'package:sqlite3/wasm.dart';
import 'package:test/scaffolding.dart';
@ -15,9 +14,8 @@ Future<WasmSqlite3> get sqlite3 {
final channel = spawnHybridUri('/test/test_utils/sqlite_server.dart');
final port = await channel.stream.first as int;
final response =
await http.get(Uri.parse('http://localhost:$port/sqlite3.wasm'));
return WasmSqlite3.load(response.bodyBytes);
return WasmSqlite3.loadFromUrl(
Uri.parse('http://localhost:$port/sqlite3.wasm'));
});
}

View File

@ -33,7 +33,6 @@
web/shared_worker.dart.js
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols

View File

@ -1,12 +1,8 @@
import 'dart:async';
// ignore: avoid_web_libraries_in_flutter
import 'dart:html';
import 'package:drift/drift.dart';
import 'package:drift/remote.dart';
import 'package:drift/web.dart';
import 'package:drift/wasm.dart';
import 'package:http/http.dart' as http;
import 'package:drift/web/worker.dart';
import 'package:sqlite3/wasm.dart';
const _useWorker = true;
@ -14,26 +10,22 @@ const _useWorker = true;
/// Obtains a database connection for running drift on the web.
DatabaseConnection connect({bool isInWebWorker = false}) {
if (_useWorker && !isInWebWorker) {
final worker = SharedWorker('shared_worker.dart.js');
return DatabaseConnection.delayed(
connectToRemoteAndInitialize(worker.port!.channel()));
return DatabaseConnection.delayed(connectToDriftWorker(
'shared_worker.dart.js',
mode: DriftWorkerMode.shared));
} else {
return DatabaseConnection.delayed(Future.sync(() async {
// We're using the experimental wasm support in Drift because this gives
// us a recent sqlite3 version with fts5 support.
// This is still experimental, so consider using the approach described in
// https://drift.simonbinder.eu/web/ instead.
return DatabaseConnection.delayed(
Future.sync(() async {
final fs = await IndexedDbFileSystem.open(dbName: 'my_app');
final sqlite3 = await WasmSqlite3.loadFromUrl(
Uri.parse('sqlite3.wasm'),
environment: SqliteEnvironment(fileSystem: fs),
);
final response = await http.get(Uri.parse('sqlite3.wasm'));
final fs = await IndexedDbFileSystem.open(dbName: 'my_app');
final sqlite3 = await WasmSqlite3.load(
response.bodyBytes,
SqliteEnvironment(fileSystem: fs),
);
final databaseImpl = WasmDatabase(sqlite3: sqlite3, path: 'app.db');
return DatabaseConnection(databaseImpl);
}));
final databaseImpl = WasmDatabase(sqlite3: sqlite3, path: 'app.db');
return DatabaseConnection(databaseImpl);
}),
);
}
}

View File

@ -19,7 +19,7 @@ dependencies:
intl: ^0.18.0
http: ^0.13.4 # used to load sqlite3 wasm files on the web
sqlite3_flutter_libs: ^0.5.5
sqlite3: ^1.7.0
sqlite3: ^1.11.0
path_provider: ^2.0.9
path: ^1.8.0
riverpod: ^2.3.0

View File

@ -1,16 +1,10 @@
// ignore: avoid_web_libraries_in_flutter
import 'dart:html';
import 'package:app/database/connection/web.dart';
import 'package:drift/web.dart';
import 'package:drift/remote.dart';
import 'package:drift/web/worker.dart';
/// This Dart program is the entrypoint of a web worker that will be compiled to
/// JavaScript by running `build_runner build`. The resulting JavaScript file
/// (`shared_worker.dart.js`) is part of the build result and will be shipped
/// with the rest of the application when running or building a Flutter web app.
void main() {
final self = SharedWorkerGlobalScope.instance;
final server = DriftServer(connect(isInWebWorker: true));
self.onConnect.listen((event) {
final msg = event as MessageEvent;
server.serve(msg.ports.first.channel());
});
return driftWorkerMain(() => connect(isInWebWorker: true));
}

View File

@ -1,22 +1,11 @@
// ignore: avoid_web_libraries_in_flutter
import 'dart:html';
import 'package:drift/drift.dart';
import 'package:drift/remote.dart';
import 'package:drift/web.dart';
import 'package:drift/web/worker.dart';
import 'package:flutter/foundation.dart';
class PlatformInterface {
static QueryExecutor createDatabaseConnection(String databaseName) {
return LazyDatabase(() async {
return _connectToWorker(databaseName).executor;
});
}
static DatabaseConnection _connectToWorker(String databaseName) {
final worker = SharedWorker(
kReleaseMode ? 'worker.dart.min.js' : 'worker.dart.js', databaseName);
return DatabaseConnection.delayed(
connectToRemoteAndInitialize(worker.port!.channel()));
return DatabaseConnection.delayed(connectToDriftWorker(
kReleaseMode ? 'worker.dart.min.js' : 'worker.dart.js',
mode: DriftWorkerMode.shared));
}
}

View File

@ -1,20 +1,14 @@
// ignore: avoid_web_libraries_in_flutter
import 'dart:html';
import 'package:drift/drift.dart';
import 'package:drift/web.dart';
import 'package:drift/remote.dart';
import 'package:drift/web/worker.dart';
void main() {
final self = SharedWorkerGlobalScope.instance;
self.importScripts('sql-wasm.js');
WorkerGlobalScope.instance.importScripts('sql-wasm.js');
final db = WebDatabase.withStorage(DriftWebStorage.indexedDb('worker',
migrateFromLocalStorage: false, inWebWorker: true));
final server = DriftServer(DatabaseConnection(db));
self.onConnect.listen((event) {
final msg = event as MessageEvent;
server.serve(msg.ports.first.channel());
driftWorkerMain(() {
return WebDatabase.withStorage(DriftWebStorage.indexedDb('worker',
migrateFromLocalStorage: false, inWebWorker: true));
});
}

View File

@ -1,12 +1,10 @@
import 'dart:html';
import 'package:drift/remote.dart';
import 'package:drift/web.dart';
import 'package:drift/web/worker.dart';
import 'package:web_worker_example/database.dart';
void main() async {
final worker = SharedWorker('worker.dart.js');
final connection = await connectToRemoteAndInitialize(worker.port!.channel());
final connection = await connectToDriftWorker('worker.dart.js');
final db = MyDatabase(connection);
final output = document.getElementById('output')!;

View File

@ -1,19 +1,13 @@
import 'dart:html';
import 'package:drift/drift.dart';
import 'package:drift/web.dart';
import 'package:drift/remote.dart';
import 'package:drift/web/worker.dart';
void main() {
final self = SharedWorkerGlobalScope.instance;
self.importScripts('sql-wasm.js');
WorkerGlobalScope.instance.importScripts('sql-wasm.js');
final db = WebDatabase.withStorage(DriftWebStorage.indexedDb('worker',
migrateFromLocalStorage: false, inWebWorker: true));
final server = DriftServer(DatabaseConnection(db));
self.onConnect.listen((event) {
final msg = event as MessageEvent;
server.serve(msg.ports.first.channel());
driftWorkerMain(() {
return WebDatabase.withStorage(DriftWebStorage.indexedDb('worker',
migrateFromLocalStorage: false, inWebWorker: true));
});
}

Binary file not shown.