mirror of https://github.com/AMT-Cheif/drift.git
Simplify creation of web workers for drift
This commit is contained in:
parent
cb32b3418b
commit
b4b4e350dd
|
@ -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
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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}
|
|
@ -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.
|
||||
|
|
|
@ -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 |
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
web/shared_worker.dart.js
|
||||
|
||||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
|
|
@ -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);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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')!;
|
||||
|
|
|
@ -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.
Loading…
Reference in New Issue