mirror of https://github.com/AMT-Cheif/drift.git
Update new web documentation
This commit is contained in:
parent
fef35d9f20
commit
1e74aae972
|
@ -97,7 +97,7 @@ targets:
|
|||
dependencies: ['$default']
|
||||
auto_apply_builders: false
|
||||
sources:
|
||||
- "web/worker.dart"
|
||||
- "web/drift_worker.dart"
|
||||
builders:
|
||||
build_web_compilers:entrypoint:
|
||||
options:
|
||||
|
@ -121,7 +121,7 @@ targets:
|
|||
build_web_compilers:entrypoint:
|
||||
generate_for:
|
||||
exclude:
|
||||
- "web/worker.dart"
|
||||
- "web/drift_worker.dart"
|
||||
release_options:
|
||||
# Turn of null assertions for release builds, it looks like this
|
||||
# makes generated code slightly smaller.
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/wasm.dart';
|
||||
import 'package:drift/web.dart';
|
||||
import 'package:sqlite3/wasm.dart';
|
||||
|
||||
// #docregion connect
|
||||
DatabaseConnection connectOnWeb() {
|
||||
return DatabaseConnection.delayed(Future(() async {
|
||||
final result = await WasmDatabase.open(
|
||||
databaseName: 'my_app_db', // prefer to only use valid identifiers here
|
||||
sqlite3Uri: Uri.parse('/sqlite3.wasm'),
|
||||
driftWorkerUri: Uri.parse('/drift_worker.dart.js'),
|
||||
);
|
||||
|
||||
if (result.missingFeatures.isNotEmpty) {
|
||||
// Depending how central local persistence is to your app, you may want
|
||||
// to show a warning to the user if only unrealiable implemetentations
|
||||
// are available.
|
||||
print('Using ${result.chosenImplementation} due to missing browser '
|
||||
'features: ${result.missingFeatures}');
|
||||
}
|
||||
|
||||
return result.resolvedExecutor;
|
||||
}));
|
||||
}
|
||||
// #enddocregion connect
|
||||
|
||||
// #docregion migrate-wasm
|
||||
// If you've previously opened your database like this
|
||||
Future<WasmDatabase> customDatabase() async {
|
||||
final sqlite3 = await WasmSqlite3.loadFromUrl(Uri.parse('/sqlite3.wasm'));
|
||||
final fs = await IndexedDbFileSystem.open(dbName: 'my_app');
|
||||
sqlite3.registerVirtualFileSystem(fs, makeDefault: true);
|
||||
|
||||
return WasmDatabase(
|
||||
sqlite3: sqlite3,
|
||||
path: '/app.db',
|
||||
);
|
||||
}
|
||||
// #enddocregion migrate-wasm
|
||||
|
||||
DatabaseConnection migrateAndConnect() {
|
||||
return DatabaseConnection.delayed(Future(() async {
|
||||
// #docregion migrate-wasm
|
||||
// Then you can migrate like this
|
||||
final result = await WasmDatabase.open(
|
||||
databaseName: 'my_app',
|
||||
sqlite3Uri: Uri.parse('/sqlite3.wasm'),
|
||||
driftWorkerUri: Uri.parse('/drift_worker.dart.js'),
|
||||
initializeDatabase: () async {
|
||||
// Manually open the file system previously used
|
||||
final fs = await IndexedDbFileSystem.open(dbName: 'my_app');
|
||||
const oldPath = '/app.db'; // The path passed to WasmDatabase before
|
||||
|
||||
Uint8List? oldDatabase;
|
||||
|
||||
// Check if the old database exists
|
||||
if (fs.xAccess(oldPath, 0) != 0) {
|
||||
// It does, then copy the old file
|
||||
final (file: file, outFlags: _) =
|
||||
fs.xOpen(Sqlite3Filename(oldPath), 0);
|
||||
final blob = Uint8List(file.xFileSize());
|
||||
file.xRead(blob, 0);
|
||||
file.xClose();
|
||||
fs.xDelete(oldPath, 0);
|
||||
|
||||
oldDatabase = blob;
|
||||
}
|
||||
|
||||
await fs.close();
|
||||
return oldDatabase;
|
||||
},
|
||||
);
|
||||
// #enddocregion migrate-wasm
|
||||
|
||||
return result.resolvedExecutor;
|
||||
}));
|
||||
}
|
||||
|
||||
DatabaseConnection migrateFromLegacy() {
|
||||
return DatabaseConnection.delayed(Future(() async {
|
||||
// #docregion migrate-legacy
|
||||
final result = await WasmDatabase.open(
|
||||
databaseName: 'my_app',
|
||||
sqlite3Uri: Uri.parse('/sqlite3.wasm'),
|
||||
driftWorkerUri: Uri.parse('/drift_worker.dart.js'),
|
||||
initializeDatabase: () async {
|
||||
final storage = await DriftWebStorage.indexedDbIfSupported('old_db');
|
||||
await storage.open();
|
||||
|
||||
final blob = await storage.restore();
|
||||
await storage.close();
|
||||
|
||||
return blob;
|
||||
},
|
||||
);
|
||||
// #enddocregion migrate-legacy
|
||||
|
||||
return result.resolvedExecutor;
|
||||
}));
|
||||
}
|
|
@ -1,18 +1,24 @@
|
|||
---
|
||||
data:
|
||||
title: Web
|
||||
description: Experimental support for drift and webapps.
|
||||
description: Drift support in Flutter and Dart web apps.
|
||||
template: layouts/docs/single
|
||||
path: web/
|
||||
---
|
||||
|
||||
You can experimentally use drift in Dart webapps. Drift web supports
|
||||
Flutter Web, AngularDart, plain `dart:html` or any other web framework.
|
||||
{% assign snippets = "package:drift_docs/snippets/engines/web.dart.excerpt.json" | readString | json_decode %}
|
||||
|
||||
## 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.
|
||||
Using modern browser APIs such as WebAssembly and the Origin-Private File System API,
|
||||
you can use drift databases when compiling your apps to the web.
|
||||
Just like the core drift APIs, web support is platform-agnostic:
|
||||
Drift web supports Flutter Web, AngularDart, plain `dart:html` or any other Dart web framework.
|
||||
|
||||
{% block "blocks/alert" title="Compatibility check" %}
|
||||
This page includes a tiny drift database compiled to JavaScript.
|
||||
You can use it to verify drift works in the Browsers you want to target.
|
||||
Clicking on the button will start a feature detection run, so you can see which file system
|
||||
implementation drift would pick on this browser and which web APIs are missing.
|
||||
|
||||
<button class="btn btn-light" id="drift-compat-btn">Check compatibility</button>
|
||||
|
||||
|
@ -20,49 +26,75 @@ on this page. You can click on this button to see the results.
|
|||
Compatibility check not started yet
|
||||
</pre>
|
||||
|
||||
More information about these results is available [below](#storages).
|
||||
{% endblock %}
|
||||
|
||||
## Getting started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
On all platforms, drift requires access to [sqlite3](https://sqlite.org/index.html), the popular
|
||||
datase system written as a C library.
|
||||
On native platforms, drift can use the sqlite3 library from your operating system. Flutter apps
|
||||
typically include a more recent version of that library with the `sqlite3_flutter_libs` package too.
|
||||
|
||||
Web browsers don't have builtin access to the sqlite3 library, so it needs to be included with your app.
|
||||
The `sqlite3` Dart package (used by drift internally) contains a toolchain to compile sqlite3 to WebAssembly
|
||||
so that it can be used in browsers. You can grab a `sqlite3.wasm` file from [its releases page](https://github.com/simolus3/sqlite3.dart/releases).
|
||||
This file needs to be put into the `web/` directory of your app.
|
||||
|
||||
Drift on the web also requires you to include a portion of drift as a web worker. This worker will be used to
|
||||
host your database in a background thread, improving performance of your website. In some [storage implementations](#storages),
|
||||
the worker is also responsible for sharing your database between different tabs in real-time.
|
||||
You can compile this worker yourself, [grab one from drift releases](https://github.com/simolus3/drift/releases) or take
|
||||
the [latest one powering this website]({{ '/worker.dart.js' | absUrl }}).
|
||||
|
||||
In the end, your `web/` directory may look like this:
|
||||
|
||||
```
|
||||
web/
|
||||
├── favicon.png
|
||||
├── index.html
|
||||
├── manifest.json
|
||||
├── drift_worker.dart.js
|
||||
└── sqlite3.wasm
|
||||
```
|
||||
|
||||
#### Additional headers
|
||||
|
||||
On browsers that support it, drift uses the origin-private part of the [FileSystem Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API) to store databases efficiently.
|
||||
As parts of that API are asynchronous, and since sqlite3 expectes a synchronous file system, we need to
|
||||
use two workers with shared memory and `Atomics.wait`/`notify`.
|
||||
Just like the official sqlite3 port to the web, __this requires your website to be served with two special headers__:
|
||||
|
||||
- `Cross-Origin-Opener-Policy`: Needs to be set to `same-origin`.
|
||||
- `Cross-Origin-Embedder-Policy`: Needs to be set to `require-corp` or `credentialless`.
|
||||
|
||||
For more details, see the [security requirements](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) explained by MDN, and the [documentation on web.dev](https://web.dev/coop-coep/).
|
||||
Unfortunately, there's no way (that I'm aware of) to add these headers onto `flutter run`'s web server.
|
||||
Drift will fall back to a less reliable implementation in that case (see [storages](#storages)),
|
||||
but we recommend researching and enabling these headers in production if possible.
|
||||
|
||||
### Setup in Dart
|
||||
|
||||
From a perspective of the Dart code used, drift on the web is similar to drift on other platforms.
|
||||
You can follow the [getting started guide]({{ '../Getting started/index.md' | pageUrl }}) for general
|
||||
information on using drift.
|
||||
|
||||
Instead of using a `NativeDatabase` in your database classes, you can use a `WebDatabase` executor:
|
||||
Instead of using a `NativeDatabase` in your database classes, you can use `WasmDatabase` optimized for
|
||||
the web:
|
||||
|
||||
```dart
|
||||
import 'package:drift/web.dart';
|
||||
{% include "blocks/snippet" snippets = snippets name = "connect" %}
|
||||
|
||||
@DriftDatabase(tables: [Todos, Categories])
|
||||
class MyDatabase extends _$MyDatabase {
|
||||
// here, "app" is the name of the database - you can choose any name you want
|
||||
MyDatabase() : super(WebDatabase('app'));
|
||||
```
|
||||
When you call `WasmDatabase.open`, drift will automatically find a suitable persistence implementation
|
||||
supported by the current browser.
|
||||
|
||||
Drift web is built on top of the [sql.js](https://github.com/sql-js/sql.js/) library, which you need to include:
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script defer src="sql-wasm.js"></script>
|
||||
<script defer src="main.dart.js" type="application/javascript"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
```
|
||||
You can grab the latest version of `sql-wasm.js` and `sql-wasm.wasm` [here](https://github.com/sql-js/sql.js/releases)
|
||||
and copy them into your `web` folder.
|
||||
|
||||
A full example that works on the web (and all other platforms) is available
|
||||
A full example that works on the web (and all other platforms supported by drift) is available
|
||||
[here](https://github.com/simolus3/drift/tree/develop/examples/app).
|
||||
|
||||
## Gotchas
|
||||
The database implementation uses WebAssembly, which needs to be supported by your browser.
|
||||
Also, make sure that your webserver serves the `.wasm` file as `application/wasm`, browsers
|
||||
won't accept it otherwise.
|
||||
|
||||
## Sharing code between native apps and web
|
||||
|
||||
If you want to share your database code between native applications and webapps, just import the
|
||||
If you want to share your database code between native applications and web apps, just import the
|
||||
basic `drift/drift.dart` library into your database file.
|
||||
And instead of passing a `NativeDatabase` or `WebDatabase` to the `super` constructor, make the
|
||||
`QueryExecutor` customizable:
|
||||
|
@ -100,7 +132,7 @@ On the web, you can use
|
|||
import 'package:drift/web.dart';
|
||||
|
||||
SharedDatabase constructDb() {
|
||||
return SharedDatabase(WebDatabase('db'));
|
||||
return SharedDatabase(connectOnWeb());
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -124,10 +156,81 @@ export 'unsupported.dart'
|
|||
|
||||
A ready example of this construct can also be found [here](https://github.com/simolus3/drift/blob/develop/examples/app/lib/database/connection/connection.dart).
|
||||
|
||||
## Debugging
|
||||
## Supported storage implementations {#storages}
|
||||
|
||||
When opening a database, drift determines the state of a number of web APIs in the current browser.
|
||||
It then picks a suitable database storage based on what the current browser supports.
|
||||
The following implementation strategies are supported by drift (listed in order of descending preference).
|
||||
You can view the chosen strategy via the `WasmDatabaseResult.chosenImplementation` returned by
|
||||
`WasmDatabase.open`.
|
||||
|
||||
1. `opfsShared`: Uses the origin-private filesystem access API in a shared web worker.
|
||||
As this API is only available in dedicated web workers, this requires shared workers to be able to
|
||||
spawn dedicated workers. While allowed by the web standards, this is only implemented by Firefox.
|
||||
2. `opfsLocks`: Uses the origin-private filesystem access API, but without shared workers.
|
||||
This requires the [COOP and COEP headers](#additional-headers).
|
||||
3. `sharedIndexedDb`: Uses IndexedDB to store chunks of data, the database is hosted in a shared web worker.
|
||||
4. `unsafeIndexedDb`: This also uses the IndexedDB database, but without a shared worker or another
|
||||
means of synchronization between tabs. In this mode, __it is not safe for multiple tabs of your app
|
||||
to access the same database__.
|
||||
While this was the default mode used by earlier implementations of drift for the web, we now try to
|
||||
avoid it and it will not be used on modern browsers.
|
||||
5. `inMemory`: When no persistence API is available, drift will fall back to an in-memory database.
|
||||
|
||||
The missing browser APIs contributing to a specific implementation being chosen are available in
|
||||
`WasmDatabaseResult.missingFeatures`.
|
||||
There's nothing your app or drift could do about them. If persistence is important to your app
|
||||
and drift has chosen the `unsafeIndexedDb` or the `inMemory` implementation due to a lack of proper
|
||||
persistence support, you may want to show a warning to the user explaining that they have to upgrade
|
||||
their browser.
|
||||
|
||||
### Migrating from existing web databases
|
||||
|
||||
`WasmDatabase.open` is drift's stable web API, which has been built with the lessons learned from previous
|
||||
web APIs available in drift (that have always been marked as `@experimental`). You can use `WasmDatabase.open`
|
||||
to replace the following drift APIs:
|
||||
|
||||
1. The sql.js-based implementation in `package:drift/web.dart`.
|
||||
2. Custom `WasmDatabase` constructions that manually load sqlite3.
|
||||
3. Custom worker setups created with `package:drift/web/worker.dart`.
|
||||
|
||||
Let's start with the good news: After migrating to `WasmDatabase.open`, drift will manage workers for you.
|
||||
It automatically uses the best worker setup supported by the current browsers, enabling the use of shared
|
||||
and dedicated web workers where appropriate.
|
||||
So the migration from `package:drift/web/worker.dart` is to just stop using that API, since you get all of
|
||||
its features out of the box with `WasmDatabase.open`.
|
||||
|
||||
#### Migrating from custom `WasmDatabase`s
|
||||
|
||||
In older drift versios, you may have used a custom setup that loaded the WASM binary manually, created
|
||||
a `CommonSqlite3` instance with it and passed that to `WasmDatabase`.
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = "migrate-wasm" %}
|
||||
|
||||
#### Migrating from `package:drift/web.dart`
|
||||
|
||||
To migrate from a `WebDatabase` to the new setup, you can use the `initializeDatabase` callback.
|
||||
It is invoked when opening the database if no file exists yet. By loading the old database there,
|
||||
it is migrated to the new format without data loss:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = "migrate-legacy" %}
|
||||
|
||||
In that snippet, `old_db` is the name previously passed to the `WebDatabase`.
|
||||
|
||||
## Legacy web support
|
||||
|
||||
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 the new approach by `WasmDatabase.open` is sound and more efficient
|
||||
than these implementations.
|
||||
The original APIs are still documented on this page for your reference.
|
||||
|
||||
### Debugging
|
||||
You can see all queries sent from drift to the underlying database engine by enabling the `logStatements`
|
||||
parameter on the `WebDatabase` - they will appear in the console.
|
||||
When you have assertions enabled (e.g. in debug mode), drift will expose the underlying
|
||||
When you have assertions enabled (e.g. in debug mode), drift will expose the underlying
|
||||
[database](https://sql.js.org/documentation/Database.html)
|
||||
object via `window.db`. If you need to quickly run a query to check the state of the database, you can use
|
||||
`db.exec(sql)`.
|
||||
|
@ -135,7 +238,7 @@ If you need to delete your databases, there stored using local storage. You can
|
|||
|
||||
Web support is experimental at the moment, so please [report all issues](https://github.com/simolus3/drift/issues/new) you find.
|
||||
|
||||
## Using IndexedDb
|
||||
### Using IndexedDb
|
||||
|
||||
The default `WebDatabase` uses local storage to store the raw sqlite database file. On browsers that support it, you can also
|
||||
use `IndexedDb` to store that blob. In general, browsers allow a larger size for `IndexedDb`. The implementation is also more
|
||||
|
@ -149,7 +252,8 @@ WebDatabase.withStorage(await DriftWebStorage.indexedDbIfSupported(name))
|
|||
|
||||
Drift will automatically migrate data from local storage to `IndexedDb` when it is available.
|
||||
|
||||
### Using web workers
|
||||
|
||||
#### Using web workers
|
||||
|
||||
You can offload the database to a background thread by using
|
||||
[Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API).
|
||||
|
@ -184,7 +288,7 @@ For more information on the `DatabaseConnection` class, see the documentation on
|
|||
A small, but working example is available under [examples/web_worker_example](https://github.com/simolus3/drift/tree/develop/examples/web_worker_example)
|
||||
in the drift repository.
|
||||
|
||||
### Flutter
|
||||
#### Flutter
|
||||
Flutter users will have to use a different approach to compile service workers.
|
||||
Flutter web doesn't compile `.dart` files in web folder and won't use `.js` files generated by
|
||||
`build_web_compilers` either. Instead, we'll use Dart's build system to manually compile the worker to a
|
||||
|
@ -253,7 +357,7 @@ DatabaseConnection connectToWorker(String databaseName) {
|
|||
|
||||
You can pass that DatabaseConnection to your database by enabling the `generate_connect_constructor` build option.
|
||||
|
||||
## New web backend {#drift-wasm}
|
||||
### New web backend {#drift-wasm}
|
||||
|
||||
In recent versions, drift added support for a new backend exposed by the `package:drift/wasm.dart` library.
|
||||
Unlike sql.js or the official sqlite3 WASM edition which both use Emscripten, this backend does not need any
|
||||
|
|
|
@ -1,153 +0,0 @@
|
|||
---
|
||||
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 %}
|
||||
|
||||
### Resources
|
||||
|
||||
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:
|
||||
|
||||
{% 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:
|
||||
|
||||
{% 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}
|
|
@ -24,8 +24,8 @@ 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/web2.md' | pageUrl }}) for details. |
|
||||
| `WasmDatabase` from `package:drift/wasm.dart` | Web | Works with or without Flutter. A bit of [additional setup]({{ 'Other engines/web.md' | pageUrl }}) is required. |
|
||||
| `WebDatabase` from `package:drift/web.dart` | Web | Deprecated in favor of `WasmDatabase`. |
|
||||
|
||||
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.
|
||||
|
|
|
@ -3,7 +3,7 @@ description: Documentation website for the drift project.
|
|||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=2.19.0 <4.0.0'
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
drift:
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 34 KiB |
Binary file not shown.
Before Width: | Height: | Size: 49 KiB |
Binary file not shown.
Before Width: | Height: | Size: 31 KiB |
|
@ -14,7 +14,7 @@ void main() async {
|
|||
final db = await WasmDatabase.open(
|
||||
databaseName: 'test_db',
|
||||
sqlite3Uri: Uri.parse('/sqlite3.wasm'),
|
||||
driftWorkerUri: Uri.parse('/worker.dart.js'),
|
||||
driftWorkerUri: Uri.parse('/drift_worker.dart.js'),
|
||||
);
|
||||
|
||||
results.innerText += '''
|
||||
|
|
|
@ -41,7 +41,7 @@ class WasmDatabaseOpener {
|
|||
final Uri sqlite3WasmUri;
|
||||
final Uri driftWorkerUri;
|
||||
final String databaseName;
|
||||
FutureOr<Uint8List> Function()? initializeDatabase;
|
||||
FutureOr<Uint8List?> Function()? initializeDatabase;
|
||||
|
||||
final Set<MissingBrowserFeature> missingFeatures = {};
|
||||
final List<WasmStorageImplementation> availableImplementations = [
|
||||
|
@ -133,11 +133,13 @@ class WasmDatabaseOpener {
|
|||
|
||||
if (initializer != null) {
|
||||
final blob = await initializer();
|
||||
final (file: file, outFlags: _) = inMemory.xOpen(
|
||||
Sqlite3Filename('/database'), SqlFlag.SQLITE_OPEN_CREATE);
|
||||
file
|
||||
..xWrite(blob, 0)
|
||||
..xClose();
|
||||
if (blob != null) {
|
||||
final (file: file, outFlags: _) = inMemory.xOpen(
|
||||
Sqlite3Filename('/database'), SqlFlag.SQLITE_OPEN_CREATE);
|
||||
file
|
||||
..xWrite(blob, 0)
|
||||
..xClose();
|
||||
}
|
||||
}
|
||||
|
||||
return WasmDatabaseResult(
|
||||
|
|
|
@ -95,7 +95,7 @@ class WasmDatabase extends DelegatedDatabase {
|
|||
required String databaseName,
|
||||
required Uri sqlite3Uri,
|
||||
required Uri driftWorkerUri,
|
||||
FutureOr<Uint8List> Function()? initializeDatabase,
|
||||
FutureOr<Uint8List?> Function()? initializeDatabase,
|
||||
}) {
|
||||
return WasmDatabaseOpener(
|
||||
databaseName: databaseName,
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:package_config/package_config.dart';
|
|||
import 'package:path/path.dart' as p;
|
||||
import 'package:shelf/shelf_io.dart';
|
||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||
import 'package:web_wasm/initialization_mode.dart';
|
||||
import 'package:webdriver/async_io.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:drift/src/web/wasm_setup/types.dart';
|
||||
|
@ -132,10 +133,14 @@ class DriftWebDriver {
|
|||
await driver.executeAsync('wait_for_update("", arguments[0])', []);
|
||||
}
|
||||
|
||||
Future<void> enableInitialization(bool enabled) async {
|
||||
await driver.executeAsync(
|
||||
Future<void> enableInitialization(InitializationMode mode) async {
|
||||
final result = await driver.executeAsync(
|
||||
'enable_initialization(arguments[0], arguments[1])',
|
||||
[enabled.toString()],
|
||||
[mode.name],
|
||||
);
|
||||
|
||||
if (result != true) {
|
||||
throw 'Could not set initialization mode';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
enum InitializationMode {
|
||||
none,
|
||||
loadAsset,
|
||||
migrateCustomWasmDatabase,
|
||||
}
|
|
@ -17,6 +17,7 @@ dependencies:
|
|||
package_config: ^2.1.0
|
||||
async: ^2.11.0
|
||||
http: ^1.0.0
|
||||
sqlite3: ^2.0.0-dev.3
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.4.5
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
|||
import 'package:drift/src/web/wasm_setup/types.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:web_wasm/driver.dart';
|
||||
import 'package:web_wasm/initialization_mode.dart';
|
||||
import 'package:webdriver/async_io.dart';
|
||||
|
||||
enum Browser {
|
||||
|
@ -84,9 +85,9 @@ void main() {
|
|||
expect(result.storages, expectedImplementations);
|
||||
});
|
||||
|
||||
group('supports', () {
|
||||
for (final entry in browser.availableImplementations) {
|
||||
test(entry.name, () async {
|
||||
for (final entry in browser.availableImplementations) {
|
||||
group(entry.name, () {
|
||||
test('basic', () async {
|
||||
await driver.openDatabase(entry);
|
||||
|
||||
await driver.insertIntoDatabase();
|
||||
|
@ -117,24 +118,35 @@ void main() {
|
|||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'initializing ${entry.name} from blob',
|
||||
() async {
|
||||
await driver.enableInitialization(true);
|
||||
await driver.openDatabase(entry);
|
||||
group(
|
||||
'initialization from ',
|
||||
() {
|
||||
test('static blob', () async {
|
||||
await driver.enableInitialization(InitializationMode.loadAsset);
|
||||
await driver.openDatabase(entry);
|
||||
|
||||
expect(await driver.amountOfRows, 1);
|
||||
await driver.insertIntoDatabase();
|
||||
expect(await driver.amountOfRows, 2);
|
||||
|
||||
if (entry != WasmStorageImplementation.inMemory) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
await driver.driver.refresh();
|
||||
|
||||
await driver.enableInitialization(true);
|
||||
await driver.openDatabase();
|
||||
expect(await driver.amountOfRows, 1);
|
||||
await driver.insertIntoDatabase();
|
||||
expect(await driver.amountOfRows, 2);
|
||||
}
|
||||
|
||||
if (entry != WasmStorageImplementation.inMemory) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
await driver.driver.refresh();
|
||||
|
||||
await driver
|
||||
.enableInitialization(InitializationMode.loadAsset);
|
||||
await driver.openDatabase();
|
||||
expect(await driver.amountOfRows, 2);
|
||||
}
|
||||
});
|
||||
|
||||
test('custom wasmdatabase', () async {
|
||||
await driver.enableInitialization(
|
||||
InitializationMode.migrateCustomWasmDatabase);
|
||||
await driver.openDatabase(entry);
|
||||
|
||||
expect(await driver.amountOfRows, 1);
|
||||
});
|
||||
},
|
||||
skip: browser == Browser.firefox &&
|
||||
entry == WasmStorageImplementation.opfsLocks
|
||||
|
@ -142,8 +154,8 @@ void main() {
|
|||
'reproduced by manually running the steps of this test.'
|
||||
: null,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (browser.supports(WasmStorageImplementation.unsafeIndexedDb) &&
|
||||
browser.supports(WasmStorageImplementation.opfsLocks)) {
|
||||
|
|
|
@ -8,13 +8,15 @@ import 'package:drift/wasm.dart';
|
|||
// ignore: invalid_use_of_internal_member
|
||||
import 'package:drift/src/web/wasm_setup.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:web_wasm/initialization_mode.dart';
|
||||
import 'package:web_wasm/src/database.dart';
|
||||
import 'package:sqlite3/wasm.dart';
|
||||
|
||||
const dbName = 'drift_test';
|
||||
TestDatabase? openedDatabase;
|
||||
StreamQueue<void>? tableUpdates;
|
||||
|
||||
bool _loadFromInitializer = false;
|
||||
InitializationMode initializationMode = InitializationMode.none;
|
||||
|
||||
void main() {
|
||||
_addCallbackForWebDriver('detectImplementations', _detectImplementations);
|
||||
|
@ -22,8 +24,10 @@ void main() {
|
|||
_addCallbackForWebDriver('insert', _insert);
|
||||
_addCallbackForWebDriver('get_rows', _getRows);
|
||||
_addCallbackForWebDriver('wait_for_update', _waitForUpdate);
|
||||
_addCallbackForWebDriver('enable_initialization',
|
||||
(arg) async => _loadFromInitializer = bool.parse(arg!));
|
||||
_addCallbackForWebDriver('enable_initialization', (arg) async {
|
||||
initializationMode = InitializationMode.values.byName(arg!);
|
||||
return true;
|
||||
});
|
||||
|
||||
document.getElementById('selfcheck')?.onClick.listen((event) async {
|
||||
print('starting');
|
||||
|
@ -51,16 +55,50 @@ void _addCallbackForWebDriver(String name, Future Function(String?) impl) {
|
|||
}
|
||||
|
||||
WasmDatabaseOpener get _opener {
|
||||
Future<Uint8List> Function()? initializeDatabase;
|
||||
|
||||
switch (initializationMode) {
|
||||
case InitializationMode.loadAsset:
|
||||
initializeDatabase = () async {
|
||||
final response = await http.get(Uri.parse('/initial.db'));
|
||||
return response.bodyBytes;
|
||||
};
|
||||
case InitializationMode.migrateCustomWasmDatabase:
|
||||
initializeDatabase = () async {
|
||||
// Let's first open a custom WasmDatabase, the way it would have been
|
||||
// done before WasmDatabase.open.
|
||||
final sqlite3 =
|
||||
await WasmSqlite3.loadFromUrl(Uri.parse('/sqlite3.wasm'));
|
||||
final fs = await IndexedDbFileSystem.open(dbName: dbName);
|
||||
sqlite3.registerVirtualFileSystem(fs, makeDefault: true);
|
||||
|
||||
final wasmDb = WasmDatabase(sqlite3: sqlite3, path: '/app.db');
|
||||
final db = TestDatabase(wasmDb);
|
||||
await db
|
||||
.into(db.testTable)
|
||||
.insert(TestTableCompanion.insert(content: 'from old database'));
|
||||
await db.close();
|
||||
|
||||
final (file: file, outFlags: _) =
|
||||
fs.xOpen(Sqlite3Filename('/app.db'), 0);
|
||||
final blob = Uint8List(file.xFileSize());
|
||||
file.xRead(blob, 0);
|
||||
file.xClose();
|
||||
fs.xDelete('/app.db', 0);
|
||||
await fs.close();
|
||||
|
||||
return blob;
|
||||
};
|
||||
break;
|
||||
case InitializationMode.none:
|
||||
break;
|
||||
}
|
||||
|
||||
return WasmDatabaseOpener(
|
||||
databaseName: dbName,
|
||||
sqlite3WasmUri: Uri.parse('/sqlite3.wasm'),
|
||||
driftWorkerUri: Uri.parse('/worker.dart.js'),
|
||||
initializeDatabase: _loadFromInitializer
|
||||
? () async {
|
||||
final response = await http.get(Uri.parse('/initial.db'));
|
||||
return response.bodyBytes;
|
||||
}
|
||||
: null,
|
||||
initializeDatabase: initializeDatabase,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue