Add web worker example

This commit is contained in:
Simon Binder 2021-01-31 21:50:12 +01:00
parent 449d1cb2d3
commit 20d9cdf0fd
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
19 changed files with 516 additions and 4 deletions

View File

@ -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" >}}).

View File

@ -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
```

View File

@ -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/**

View File

@ -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

View File

@ -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;
}

View File

@ -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];
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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

View File

@ -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>

View File

@ -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;
});
}

View File

@ -0,0 +1 @@
../../integration_tests/web/test/sql-wasm.js

View File

@ -0,0 +1 @@
../../integration_tests/web/test/sql-wasm.wasm

View File

@ -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());
});
}

View File

@ -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;
}
}

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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));
}
}