mirror of https://github.com/AMT-Cheif/drift.git
Add web worker example
This commit is contained in:
parent
449d1cb2d3
commit
20d9cdf0fd
|
@ -94,3 +94,61 @@ WebDatabase.withStorage(await MoorWebStorage.indexedDbIfSupported(name))
|
|||
```
|
||||
|
||||
Moor will automatically migrate data from local storage to `IndexedDb` when it is available.
|
||||
|
||||
### Using web workers
|
||||
|
||||
Starting from moor 4.1, 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).
|
||||
Moor also supports [shared workers](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker),
|
||||
which allows you to seamlessly synchronize query-streams and updates across multiple tabs!
|
||||
|
||||
Since web workers can't use local storage, you need to use `MoorWebStorage.indexedDb` instead of
|
||||
the regular implementation.
|
||||
|
||||
To write a web worker that will serve requests for moor, create a file called `worker.dart` in
|
||||
the `web/` folder of your app. It could have the following content:
|
||||
|
||||
```dart
|
||||
import 'dart:html';
|
||||
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/moor_web.dart';
|
||||
import 'package:moor/remote.dart';
|
||||
|
||||
void main() {
|
||||
final self = SharedWorkerGlobalScope.instance;
|
||||
self.importScripts('sql-wasm.js');
|
||||
|
||||
final db = WebDatabase.withStorage(MoorWebStorage.indexedDb('worker',
|
||||
migrateFromLocalStorage: false, inWebWorker: true));
|
||||
final server = MoorServer(DatabaseConnection.fromExecutor(db));
|
||||
|
||||
self.onConnect.listen((event) {
|
||||
final msg = event as MessageEvent;
|
||||
server.serve(msg.ports.first.channel());
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
For more information on this api, see the [remote API](https://pub.dev/documentation/moor/latest/remote/remote-library.html).
|
||||
|
||||
Connecting to that worker is very simple with moor'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:moor/remote.dart';
|
||||
import 'package:moor/moor_web.dart';
|
||||
import 'package:web_worker_example/database.dart';
|
||||
|
||||
DatabaseConnection connectToWorker() {
|
||||
final worker = SharedWorker('worker.dart.js');
|
||||
return remote(worker.port!.channel());
|
||||
}
|
||||
```
|
||||
|
||||
You can pass that `DatabaseConnection` to your database by enabling the
|
||||
`generate_connect_constructor` build option.
|
||||
For more information on the `DatabaseConnection` class, see the documentation on
|
||||
[isolates]({{< relref "../Advanced Features/isolates.md" >}}).
|
|
@ -0,0 +1,7 @@
|
|||
This example demonstrates how a shared web worker can be used with moor.
|
||||
|
||||
To view this example, run
|
||||
|
||||
```
|
||||
dart run build_runner serve --release
|
||||
```
|
|
@ -0,0 +1,14 @@
|
|||
# Defines a default set of lint rules enforced for
|
||||
# projects at Google. For details and rationale,
|
||||
# see https://github.com/dart-lang/pedantic#enabled-lints.
|
||||
include: package:pedantic/analysis_options.yaml
|
||||
|
||||
# For lint rules and documentation, see http://dart-lang.github.io/linter/lints.
|
||||
# Uncomment to specify additional rules.
|
||||
# linter:
|
||||
# rules:
|
||||
# - camel_case_types
|
||||
|
||||
analyzer:
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
|
@ -0,0 +1,11 @@
|
|||
targets:
|
||||
$default:
|
||||
builders:
|
||||
moor_generator:
|
||||
options:
|
||||
generate_connect_constructor: true
|
||||
compact_query_methods: true
|
||||
apply_converters_on_variables: true
|
||||
generate_values_in_copy_with: true
|
||||
named_parameters: true
|
||||
new_sql_code_generation: true
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:moor/moor.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@UseMoor(include: {'src/tables.moor'})
|
||||
class MyDatabase extends _$MyDatabase {
|
||||
MyDatabase(DatabaseConnection conn) : super.connect(conn);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'database.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// MoorGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this
|
||||
class Entrie extends DataClass implements Insertable<Entrie> {
|
||||
final int? id;
|
||||
final String value;
|
||||
Entrie({this.id, required this.value});
|
||||
factory Entrie.fromData(Map<String, dynamic> data, GeneratedDatabase db,
|
||||
{String? prefix}) {
|
||||
final effectivePrefix = prefix ?? '';
|
||||
final intType = db.typeSystem.forDartType<int>();
|
||||
final stringType = db.typeSystem.forDartType<String>();
|
||||
return Entrie(
|
||||
id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']),
|
||||
value:
|
||||
stringType.mapFromDatabaseResponse(data['${effectivePrefix}text'])!,
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (!nullToAbsent || id != null) {
|
||||
map['id'] = Variable<int?>(id);
|
||||
}
|
||||
map['text'] = Variable<String>(value);
|
||||
return map;
|
||||
}
|
||||
|
||||
EntriesCompanion toCompanion(bool nullToAbsent) {
|
||||
return EntriesCompanion(
|
||||
id: id == null && nullToAbsent ? const Value.absent() : Value(id),
|
||||
value: Value(value),
|
||||
);
|
||||
}
|
||||
|
||||
factory Entrie.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
return Entrie(
|
||||
id: serializer.fromJson<int?>(json['id']),
|
||||
value: serializer.fromJson<String>(json['text']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= moorRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int?>(id),
|
||||
'text': serializer.toJson<String>(value),
|
||||
};
|
||||
}
|
||||
|
||||
Entrie copyWith({Value<int?> id = const Value.absent(), String? value}) =>
|
||||
Entrie(
|
||||
id: id.present ? id.value : this.id,
|
||||
value: value ?? this.value,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('Entrie(')
|
||||
..write('id: $id, ')
|
||||
..write('value: $value')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => $mrjf($mrjc(id.hashCode, value.hashCode));
|
||||
@override
|
||||
bool operator ==(dynamic other) =>
|
||||
identical(this, other) ||
|
||||
(other is Entrie && other.id == this.id && other.value == this.value);
|
||||
}
|
||||
|
||||
class EntriesCompanion extends UpdateCompanion<Entrie> {
|
||||
final Value<int?> id;
|
||||
final Value<String> value;
|
||||
const EntriesCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.value = const Value.absent(),
|
||||
});
|
||||
EntriesCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
required String value,
|
||||
}) : value = Value(value);
|
||||
static Insertable<Entrie> custom({
|
||||
Expression<int?>? id,
|
||||
Expression<String>? value,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (value != null) 'text': value,
|
||||
});
|
||||
}
|
||||
|
||||
EntriesCompanion copyWith({Value<int?>? id, Value<String>? value}) {
|
||||
return EntriesCompanion(
|
||||
id: id ?? this.id,
|
||||
value: value ?? this.value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<int?>(id.value);
|
||||
}
|
||||
if (value.present) {
|
||||
map['text'] = Variable<String>(value.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('EntriesCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('value: $value')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class Entries extends Table with TableInfo<Entries, Entrie> {
|
||||
final GeneratedDatabase _db;
|
||||
final String? _alias;
|
||||
Entries(this._db, [this._alias]);
|
||||
final VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
late final GeneratedIntColumn id = _constructId();
|
||||
GeneratedIntColumn _constructId() {
|
||||
return GeneratedIntColumn('id', $tableName, true,
|
||||
declaredAsPrimaryKey: true, $customConstraints: 'PRIMARY KEY');
|
||||
}
|
||||
|
||||
final VerificationMeta _valueMeta = const VerificationMeta('value');
|
||||
late final GeneratedTextColumn value = _constructValue();
|
||||
GeneratedTextColumn _constructValue() {
|
||||
return GeneratedTextColumn('text', $tableName, false,
|
||||
$customConstraints: 'NOT NULL');
|
||||
}
|
||||
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [id, value];
|
||||
@override
|
||||
Entries get asDslTable => this;
|
||||
@override
|
||||
String get $tableName => _alias ?? 'entries';
|
||||
@override
|
||||
final String actualTableName = 'entries';
|
||||
@override
|
||||
VerificationContext validateIntegrity(Insertable<Entrie> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
if (data.containsKey('text')) {
|
||||
context.handle(
|
||||
_valueMeta, value.isAcceptableOrUnknown(data['text']!, _valueMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_valueMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
Entrie map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null;
|
||||
return Entrie.fromData(data, _db, prefix: effectivePrefix);
|
||||
}
|
||||
|
||||
@override
|
||||
Entries createAlias(String alias) {
|
||||
return Entries(_db, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
abstract class _$MyDatabase extends GeneratedDatabase {
|
||||
_$MyDatabase(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e);
|
||||
_$MyDatabase.connect(DatabaseConnection c) : super.connect(c);
|
||||
late final Entries entries = Entries(this);
|
||||
Selectable<Entrie> allEntries() {
|
||||
return customSelect('SELECT * FROM entries',
|
||||
variables: [], readsFrom: {entries}).map(entries.mapFromRow);
|
||||
}
|
||||
|
||||
Future<int> addEntry(String var1) {
|
||||
return customInsert(
|
||||
'INSERT INTO entries (text) VALUES (?)',
|
||||
variables: [Variable<String>(var1)],
|
||||
updates: {entries},
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> clearEntries() {
|
||||
return customUpdate(
|
||||
'DELETE FROM entries',
|
||||
variables: [],
|
||||
updates: {entries},
|
||||
updateKind: UpdateKind.delete,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<TableInfo> get allTables => allSchemaEntities.whereType<TableInfo>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [entries];
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE entries(
|
||||
id INTEGER PRIMARY KEY,
|
||||
text TEXT AS value NOT NULL
|
||||
);
|
||||
|
||||
allEntries: SELECT * FROM entries;
|
||||
addEntry: INSERT INTO entries (text) VALUES (?);
|
||||
clearEntries: DELETE FROM entries;
|
|
@ -0,0 +1,20 @@
|
|||
import 'dart:html';
|
||||
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
|
||||
StreamChannel<Object?> startWorker(String script) {
|
||||
final worker = SharedWorker(script);
|
||||
worker.onError.forEach(print);
|
||||
|
||||
return worker.port!.channel();
|
||||
}
|
||||
|
||||
extension PortToChannel on MessagePort {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
name: web_worker_example
|
||||
description: An absolute bare-bones web app.
|
||||
# version: 1.0.0
|
||||
#homepage: https://www.example.com
|
||||
|
||||
environment:
|
||||
sdk: '>=2.12.0-0 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
js: ^0.6.3-nullsafety
|
||||
stream_channel: ^2.1.0-nullsafety.3
|
||||
moor:
|
||||
moor_generator:
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^1.10.0
|
||||
build_web_compilers: ^2.11.0
|
||||
pedantic: ^1.9.0
|
||||
|
||||
dependency_overrides:
|
||||
moor:
|
||||
path: ../../moor
|
||||
moor_generator:
|
||||
path: ../../moor_generator
|
||||
sqlparser:
|
||||
path: ../../sqlparser
|
||||
convert: ^3.0.0-nullsafety.0
|
||||
typed_data: ^1.3.0-nullsafety.5
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>web_worker_example</title>
|
||||
<script defer src="main.dart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<ul id="output">
|
||||
|
||||
</ul>
|
||||
|
||||
<input type="text" placeholder="New entry" id="field">
|
||||
<button id="submit">Add!</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,28 @@
|
|||
import 'dart:html';
|
||||
|
||||
import 'package:moor/remote.dart';
|
||||
import 'package:moor/moor_web.dart';
|
||||
import 'package:web_worker_example/database.dart';
|
||||
|
||||
void main() {
|
||||
final worker = SharedWorker('worker.dart.js');
|
||||
final connection = remote(worker.port!.channel());
|
||||
final db = MyDatabase(connection);
|
||||
|
||||
final output = document.getElementById('output')!;
|
||||
final input = document.getElementById('field')! as InputElement;
|
||||
final submit = document.getElementById('submit')! as ButtonElement;
|
||||
|
||||
db.allEntries().watch().listen((rows) {
|
||||
output.innerHtml = '';
|
||||
|
||||
for (final row in rows) {
|
||||
output.children.add(Element.li()..text = row.value);
|
||||
}
|
||||
});
|
||||
|
||||
submit.onClick.listen((event) {
|
||||
db.addEntry(input.value ?? '');
|
||||
input.value = null;
|
||||
});
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
../../integration_tests/web/test/sql-wasm.js
|
|
@ -0,0 +1 @@
|
|||
../../integration_tests/web/test/sql-wasm.wasm
|
|
@ -0,0 +1,19 @@
|
|||
import 'dart:html';
|
||||
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:moor/moor_web.dart';
|
||||
import 'package:moor/remote.dart';
|
||||
|
||||
void main() {
|
||||
final self = SharedWorkerGlobalScope.instance;
|
||||
self.importScripts('sql-wasm.js');
|
||||
|
||||
final db = WebDatabase.withStorage(MoorWebStorage.indexedDb('worker',
|
||||
migrateFromLocalStorage: false, inWebWorker: true));
|
||||
final server = MoorServer(DatabaseConnection.fromExecutor(db));
|
||||
|
||||
self.onConnect.listen((event) {
|
||||
final msg = event as MessageEvent;
|
||||
server.serve(msg.ports.first.channel());
|
||||
});
|
||||
}
|
|
@ -11,6 +11,7 @@ import 'dart:indexed_db';
|
|||
import 'dart:js';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
|
||||
import 'backends.dart';
|
||||
import 'moor.dart';
|
||||
|
@ -19,3 +20,19 @@ import 'src/web/sql_js.dart';
|
|||
|
||||
part 'src/web/storage.dart';
|
||||
part '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
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ library remote;
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
|
||||
import '' as self;
|
||||
//import '' as self;
|
||||
import 'moor.dart';
|
||||
|
||||
import 'src/runtime/remote/client_impl.dart';
|
||||
|
|
|
@ -89,8 +89,10 @@ abstract class _BaseExecutor extends QueryExecutor {
|
|||
@override
|
||||
Future<List<Map<String, Object?>>> runSelect(
|
||||
String statement, List<Object?> args) async {
|
||||
return (await _runRequest<List>(StatementMethod.select, statement, args))
|
||||
.cast();
|
||||
final result = await _runRequest<SelectResult>(
|
||||
StatementMethod.select, statement, args);
|
||||
|
||||
return result.rows;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ class MoorProtocol {
|
|||
static const _tag_NotifyTablesUpdated = 8;
|
||||
static const _tag_DefaultSqlTypeSystem = 9;
|
||||
static const _tag_DirectValue = 10;
|
||||
static const _tag_SelectResult = 11;
|
||||
|
||||
Object? serialize(Message message) {
|
||||
if (message is Request) {
|
||||
|
@ -115,6 +116,26 @@ class MoorProtocol {
|
|||
// assume connection uses SqlTypeSystem.defaultInstance, this can't
|
||||
// possibly be encoded.
|
||||
return _tag_DefaultSqlTypeSystem;
|
||||
} else if (payload is SelectResult) {
|
||||
// We can't necessary transport maps, so encode as list
|
||||
final rows = payload.rows;
|
||||
if (rows.isEmpty) {
|
||||
return const [_tag_SelectResult];
|
||||
} else {
|
||||
// Encode by first sending column names, followed by row data
|
||||
final result = <Object?>[_tag_SelectResult];
|
||||
|
||||
final columns = rows.first.keys.toList();
|
||||
result
|
||||
..add(columns.length)
|
||||
..addAll(columns);
|
||||
|
||||
result.add(rows.length);
|
||||
for (final row in rows) {
|
||||
result.addAll(row.values);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
return [_tag_DirectValue, payload];
|
||||
}
|
||||
|
@ -182,6 +203,26 @@ class MoorProtocol {
|
|||
);
|
||||
}
|
||||
return NotifyTablesUpdated(updates);
|
||||
case _tag_SelectResult:
|
||||
if (fullMessage!.length == 1) {
|
||||
// Empty result set, no data
|
||||
return const SelectResult([]);
|
||||
}
|
||||
|
||||
final columnCount = readInt(1);
|
||||
final columns = fullMessage.sublist(2, 2 + columnCount).cast<String>();
|
||||
final rows = readInt(2 + columnCount);
|
||||
|
||||
final result = <Map<String, Object?>>[];
|
||||
for (var i = 0; i < rows; i++) {
|
||||
final rowOffset = 3 + columnCount + i * columnCount;
|
||||
|
||||
result.add({
|
||||
for (var c = 0; c < columnCount; c++)
|
||||
columns[c]: fullMessage[rowOffset + c]
|
||||
});
|
||||
}
|
||||
return SelectResult(result);
|
||||
case _tag_DirectValue:
|
||||
return encoded[1];
|
||||
}
|
||||
|
@ -348,3 +389,9 @@ class NotifyTablesUpdated {
|
|||
|
||||
NotifyTablesUpdated(this.updates);
|
||||
}
|
||||
|
||||
class SelectResult {
|
||||
final List<Map<String, Object?>> rows;
|
||||
|
||||
const SelectResult(this.rows);
|
||||
}
|
||||
|
|
|
@ -120,7 +120,7 @@ class ServerImplementation implements MoorServer {
|
|||
case StatementMethod.insert:
|
||||
return executor.runInsert(sql, args);
|
||||
case StatementMethod.select:
|
||||
return executor.runSelect(sql, args);
|
||||
return SelectResult(await executor.runSelect(sql, args));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue